Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/.env.dev.local
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.

DATABASE_URL="postgresql://root:123@localhost:5432/nestjs?schema=public"
JWT_SECRET="local-jwt-test-secret"
JWT_REFRESH_SECRET="local-jwt-refresh-test-secret"
1 change: 0 additions & 1 deletion api/oauth-mock-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ app.post('/token', (req: any, res: any) => {
exp: now + 3600,
},
privateKey,
{ algorithm: 'RS256' },
);

res.json({
Expand Down
47 changes: 47 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
"@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.2.2",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^6.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"graphql": "^16.11.0",
"jsonwebtoken": "^9.0.2",
"passport-google-oauth": "^2.0.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
Expand Down
12 changes: 10 additions & 2 deletions api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@ import { AuthResolver } from './auth.resolver';
import { PrismaService } from '../prisma/prisma.service';
import { UserModule } from '../user/user.module';
import { SessionModule } from '../session/session.module';
import { AccessTokenStrategy } from './strategy/access-token.strategy';
import { RefreshTokenStrategy } from './strategy/refresh-token.strategy';

@Module({
imports: [
forwardRef(() => UserModule),
forwardRef(() => SessionModule),
JwtModule.register({
secret: process.env.JWT_SECRET || 'defaultSecretKey',
secret: process.env.JWT_SECRET || 'local-jwt-test-secret',
signOptions: { expiresIn: '1h' },
global: true,
}),
],
providers: [AuthResolver, AuthService, PrismaService],
providers: [
AuthResolver,
AuthService,
PrismaService,
AccessTokenStrategy,
RefreshTokenStrategy,
],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
70 changes: 55 additions & 15 deletions api/src/auth/auth.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthResolver } from './auth.resolver';
import { AuthService } from './auth.service';
import { JwtService } from '@nestjs/jwt';

describe('AuthResolver', () => {
let resolver: AuthResolver;
Expand All @@ -12,12 +13,15 @@ describe('AuthResolver', () => {
findOneByEmail: jest.fn(),
refreshToken: jest.fn(),
remove: jest.fn(),
login: jest.fn(),
logout: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
AuthResolver,
{ provide: AuthService, useValue: authServiceMock },
JwtService,
],
}).compile();

Expand All @@ -32,8 +36,9 @@ describe('AuthResolver', () => {
it('should call authService.create and return AuthPayload', async () => {
const input = { email: 'test@example.com', name: 'Test' };
const payload = {
user: { id: 1, email: 'test@example.com' },
refreshToken: 'token',
user: { id: '1', email: 'test@example.com', name: 'Test' },
accessToken: 'access',
refreshToken: 'refresh',
};
authServiceMock.create.mockResolvedValue(payload);

Expand All @@ -45,7 +50,7 @@ describe('AuthResolver', () => {

describe('findOneByEmail', () => {
it('should call authService.findOneByEmail and return User', async () => {
const user = { id: 1, email: 'test@example.com' };
const user = { id: '1', email: 'test@example.com', name: 'Test' };
authServiceMock.findOneByEmail.mockResolvedValue(user);

const result = await resolver.findOneByEmail('test@example.com');
Expand All @@ -57,26 +62,61 @@ describe('AuthResolver', () => {
});

describe('refreshToken', () => {
it('should call authService.refreshToken and return AuthPayload', async () => {
const payload = {
user: { id: 1, email: 'test@example.com' },
it('should call authService.refreshToken with userId and refreshToken', async () => {
const payload = { userId: '1', email: 'test@example.com', name: 'Test' };
const refreshToken = 'oldtoken';
const returnedPayload = {
user: { id: '1', email: 'test@example.com', name: 'Test' },
refreshToken: 'newtoken',
accessToken: 'newAccess',
};
authServiceMock.refreshToken.mockResolvedValue(payload);
authServiceMock.refreshToken.mockResolvedValue(returnedPayload);

const result = await resolver.refreshToken('oldtoken');
expect(authServiceMock.refreshToken).toHaveBeenCalledWith('oldtoken');
expect(result).toBe(payload);
const result = await resolver.refreshToken(payload as any, refreshToken);
expect(authServiceMock.refreshToken).toHaveBeenCalledWith(
payload.userId,
refreshToken,
);
expect(result).toBe(returnedPayload);
});
});

describe('removeAuth', () => {
it('should call authService.remove and return true', async () => {
authServiceMock.remove.mockResolvedValue({ id: 'abc' });
it('should call authService.remove with userId from payload', async () => {
const payload = { userId: '1', email: 'test@example.com', name: 'Test' };
authServiceMock.remove.mockResolvedValue(true);

const result = await resolver.removeAuth(payload as any);
expect(authServiceMock.remove).toHaveBeenCalledWith(payload.userId);
expect(result).toBe(true);
});
});

describe('login', () => {
it('should call authService.login with email', async () => {
const email = 'test@example.com';
const returnedPayload = {
user: { id: '1', email, name: 'Test' },
accessToken: 'access',
refreshToken: 'refresh',
};
authServiceMock.login.mockResolvedValue(returnedPayload);

const result = await resolver.login(email);
expect(authServiceMock.login).toHaveBeenCalledWith(email);
expect(result).toBe(returnedPayload);
});
});

describe('logout', () => {
it('should call authService.logout with refreshToken', async () => {
const refreshToken = 'refresh';
authServiceMock.logout.mockResolvedValue(true);

const result = await resolver.logout(refreshToken);

const result = await resolver.removeAuth('abc');
expect(authServiceMock.remove).toHaveBeenCalledWith('abc');
expect(result).toBeTruthy();
expect(authServiceMock.logout).toHaveBeenCalledWith(refreshToken);
expect(result).toBe(true);
});
});
});
25 changes: 18 additions & 7 deletions api/src/auth/auth.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { Auth } from './entities/auth.entity';
import { User } from '../user/entities/user.entity';
import { CreateAuthInput } from './dto/create-auth.input';
import { AuthPayload } from './entities/authPayload.entity';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from './guards/auth.guard';
import { RefreshTokenGuard } from './guards/refresh-token.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { JwtPayload } from './types/jwt-payload.type';

@Resolver(() => Auth)
export class AuthResolver {
Expand All @@ -20,14 +25,19 @@ export class AuthResolver {
}

// refresh token
@Mutation(() => AuthPayload, { name: 'refreshToken' })
refreshToken(@Args('refreshToken') refreshToken: string) {
return this.authService.refreshToken(refreshToken);
@Mutation(() => AuthPayload, { name: 'payload' })
@UseGuards(RefreshTokenGuard)
refreshToken(
@CurrentUser('payload') payload: JwtPayload,
@Args('refreshToken', { type: () => String }) refreshToken: string,
) {
return this.authService.refreshToken(payload.userId, refreshToken);
}

@Mutation(() => Boolean, { name: 'removeAuth' })
removeAuth(@Args('id', { type: () => String }) id: string) {
return this.authService.remove(id);
@UseGuards(GqlAuthGuard)
removeAuth(@CurrentUser() payload: JwtPayload) {
return this.authService.remove(payload.userId);
}

@Mutation(() => AuthPayload, { name: 'login' })
Expand All @@ -36,7 +46,8 @@ export class AuthResolver {
}

@Mutation(() => Boolean, { name: 'logout' })
async logout(@Args('refreshToken') refreshToken: string) {
return this.authService.logout(refreshToken);
@UseGuards(GqlAuthGuard)
async logout(@CurrentUser() payload: JwtPayload) {
return this.authService.logoutByUserId(payload.userId);
}
}
Loading