diff --git a/nestjs-BE/server/src/auth/auth.controller.spec.ts b/nestjs-BE/server/src/auth/auth.controller.spec.ts index ab53c8b8..98e69e30 100644 --- a/nestjs-BE/server/src/auth/auth.controller.spec.ts +++ b/nestjs-BE/server/src/auth/auth.controller.spec.ts @@ -3,7 +3,7 @@ import { AuthController } from './auth.controller'; import { RefreshToken, User } from '@prisma/client'; import { AuthService } from './auth.service'; import { UsersService } from '../users/users.service'; -import { RefreshTokensService } from './refresh-tokens.service'; +import { RefreshTokensService } from '../refresh-tokens/refresh-tokens.service'; import { ProfilesService } from '../profiles/profiles.service'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; diff --git a/nestjs-BE/server/src/auth/auth.controller.ts b/nestjs-BE/server/src/auth/auth.controller.ts index d995fa96..be2ac020 100644 --- a/nestjs-BE/server/src/auth/auth.controller.ts +++ b/nestjs-BE/server/src/auth/auth.controller.ts @@ -12,7 +12,7 @@ import { UsersService } from '../users/users.service'; import { RefreshTokenDto } from './dto/refresh-token.dto'; import { ProfilesService } from '../profiles/profiles.service'; import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; -import { RefreshTokensService } from './refresh-tokens.service'; +import { RefreshTokensService } from '../refresh-tokens/refresh-tokens.service'; import { ConfigService } from '@nestjs/config'; @Controller('auth') diff --git a/nestjs-BE/server/src/auth/auth.module.ts b/nestjs-BE/server/src/auth/auth.module.ts index c1c27693..134c973e 100644 --- a/nestjs-BE/server/src/auth/auth.module.ts +++ b/nestjs-BE/server/src/auth/auth.module.ts @@ -8,16 +8,21 @@ import { JwtStrategy } from './jwt.strategy'; import { APP_GUARD } from '@nestjs/core'; import { JwtAuthGuard } from './jwt-auth.guard'; import { ProfilesModule } from '../profiles/profiles.module'; -import { RefreshTokensService } from './refresh-tokens.service'; +import { RefreshTokensModule } from '../refresh-tokens/refresh-tokens.module'; @Module({ - imports: [UsersModule, PassportModule, JwtModule, ProfilesModule], + imports: [ + UsersModule, + PassportModule, + JwtModule, + ProfilesModule, + RefreshTokensModule, + ], controllers: [AuthController], providers: [ AuthService, JwtStrategy, { provide: APP_GUARD, useClass: JwtAuthGuard }, - RefreshTokensService, ], exports: [AuthService], }) diff --git a/nestjs-BE/server/src/auth/auth.service.spec.ts b/nestjs-BE/server/src/auth/auth.service.spec.ts index 1455c03a..4a7ba0f7 100644 --- a/nestjs-BE/server/src/auth/auth.service.spec.ts +++ b/nestjs-BE/server/src/auth/auth.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { RefreshToken } from '@prisma/client'; import { JwtModule, JwtService } from '@nestjs/jwt'; -import { RefreshTokensService } from './refresh-tokens.service'; +import { RefreshTokensService } from '../refresh-tokens/refresh-tokens.service'; import { ConfigModule } from '@nestjs/config'; const fetchSpy = jest.spyOn(global, 'fetch'); diff --git a/nestjs-BE/server/src/auth/auth.service.ts b/nestjs-BE/server/src/auth/auth.service.ts index 71a672dd..b2e62d63 100644 --- a/nestjs-BE/server/src/auth/auth.service.ts +++ b/nestjs-BE/server/src/auth/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { stringify } from 'qs'; -import { RefreshTokensService } from './refresh-tokens.service'; +import { RefreshTokensService } from '../refresh-tokens/refresh-tokens.service'; import { ConfigService } from '@nestjs/config'; @Injectable() diff --git a/nestjs-BE/server/src/auth/refresh-tokens.service.spec.ts b/nestjs-BE/server/src/auth/refresh-tokens.service.spec.ts deleted file mode 100644 index ed3acd19..00000000 --- a/nestjs-BE/server/src/auth/refresh-tokens.service.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RefreshTokensService } from './refresh-tokens.service'; -import { PrismaService } from '../prisma/prisma.service'; -import { JwtModule, JwtService } from '@nestjs/jwt'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; -import { ConfigModule } from '@nestjs/config'; - -jest.useFakeTimers(); - -describe('RefreshTokensService', () => { - let service: RefreshTokensService; - let prisma: PrismaService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [JwtModule, ConfigModule.forRoot()], - providers: [ - RefreshTokensService, - { - provide: PrismaService, - useValue: { - refreshToken: { - findUnique: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - }, - }, - }, - ], - }) - .overrideProvider(JwtService) - .useValue({ sign: jest.fn() }) - .compile(); - - service = module.get(RefreshTokensService); - prisma = module.get(PrismaService); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - it('getExpiryDate check time diff', () => { - const currentDate = new Date(); - const expiryDate = service.getExpiryDate(); - const twoWeeksInMilliseconds = 2 * 7 * 24 * 60 * 60 * 1000; - - const timeDiff = expiryDate.getTime() - currentDate.getTime(); - - expect(twoWeeksInMilliseconds == timeDiff).toBeTruthy(); - }); - - it('findRefreshToken found token', async () => { - const testToken = { - id: 0, - token: 'Token', - expiryDate: service.getExpiryDate(), - userUuid: 'UserId', - }; - jest.spyOn(prisma.refreshToken, 'findUnique').mockResolvedValue(testToken); - - const token = service.findRefreshToken(testToken.token); - - await expect(token).resolves.toEqual(testToken); - }); - - it('findRefreshToken not found token', async () => { - jest.spyOn(prisma.refreshToken, 'findUnique').mockResolvedValue(null); - - const token = service.findRefreshToken('Token'); - - await expect(token).resolves.toBeNull(); - }); - - it('createRefreshToken created', async () => { - const testToken = { - id: 0, - token: 'Token', - expiryDate: service.getExpiryDate(), - userUuid: 'userId', - }; - jest.spyOn(prisma.refreshToken, 'create').mockResolvedValue(testToken); - - const token = service.createRefreshToken('userId'); - - await expect(token).resolves.toEqual(testToken); - }); - - it('createUser user already exists', async () => { - jest - .spyOn(prisma.refreshToken, 'create') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'Unique constraint failed on the constraint: `RefreshToken_token_key`', - { code: 'P2025', clientVersion: '' }, - ), - ); - - const token = service.createRefreshToken('userId'); - - await expect(token).rejects.toThrow(PrismaClientKnownRequestError); - }); - - it('deleteRefreshToken deleted', async () => { - const testToken = { - id: 0, - token: 'Token', - expiryDate: service.getExpiryDate(), - userUuid: 'userId', - }; - jest.spyOn(prisma.refreshToken, 'delete').mockResolvedValue(testToken); - - const token = service.deleteRefreshToken(testToken.token); - - await expect(token).resolves.toEqual(testToken); - }); - - it('deleteRefreshToken not found', async () => { - jest - .spyOn(prisma.refreshToken, 'delete') - .mockRejectedValue( - new PrismaClientKnownRequestError( - 'An operation failed because it depends on one or more records that were required but not found. Record to delete not found.', - { code: 'P2025', clientVersion: '' }, - ), - ); - - const token = service.deleteRefreshToken('Token'); - - await expect(token).resolves.toBeNull(); - }); -}); diff --git a/nestjs-BE/server/src/refresh-tokens/refresh-tokens.module.ts b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.module.ts new file mode 100644 index 00000000..f907e43d --- /dev/null +++ b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { RefreshTokensService } from './refresh-tokens.service'; +import { JwtModule } from '@nestjs/jwt'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [JwtModule, PrismaModule], + providers: [RefreshTokensService], + exports: [RefreshTokensService], +}) +export class RefreshTokensModule {} diff --git a/nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.spec.ts b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.spec.ts new file mode 100644 index 00000000..247de400 --- /dev/null +++ b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.spec.ts @@ -0,0 +1,94 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RefreshTokensService } from './refresh-tokens.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { JwtModule } from '@nestjs/jwt'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { ConfigModule } from '@nestjs/config'; +import { getExpiryDate } from '../utils/date'; +import { v4 as uuid } from 'uuid'; + +jest.useFakeTimers(); + +describe('RefreshTokensService', () => { + let service: RefreshTokensService; + let prisma: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [JwtModule, ConfigModule.forRoot()], + providers: [ + RefreshTokensService, + { + provide: PrismaService, + useValue: { + refreshToken: { + create: jest.fn(), + delete: jest.fn(), + }, + }, + }, + ], + }).compile(); + + service = module.get(RefreshTokensService); + prisma = module.get(PrismaService); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('createRefreshToken created', async () => { + const TWO_WEEK = 14; + + const testUserUuid = uuid(); + const twoWeek = new Date(); + twoWeek.setDate(twoWeek.getDate() + TWO_WEEK); + + (prisma.refreshToken.create as jest.Mock).mockImplementation( + async ({ data }) => { + return { + id: 0, + token: data.token, + expiryDate: data.expiryDate, + userUuid: data.userUuid, + }; + }, + ); + + const token = await service.createRefreshToken(testUserUuid); + + expect(token.token).toMatch( + /^[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+$/, + ); + expect(token.expiryDate.toISOString()).toBe(twoWeek.toISOString()); + expect(token.userUuid).toBe(testUserUuid); + }); + + it('deleteRefreshToken deleted', async () => { + const testToken = { + id: 0, + token: 'Token', + expiryDate: getExpiryDate({ week: 2 }), + userUuid: 'userId', + }; + (prisma.refreshToken.delete as jest.Mock).mockResolvedValue(testToken); + + const token = service.deleteRefreshToken(testToken.token); + + await expect(token).resolves.toEqual(testToken); + }); + + it('deleteRefreshToken not found', async () => { + (prisma.refreshToken.delete as jest.Mock).mockRejectedValue( + new PrismaClientKnownRequestError( + 'An operation failed because it depends on one or more records that were required but not found. Record to delete not found.', + { code: 'P2025', clientVersion: '' }, + ), + ); + + const token = service.deleteRefreshToken('Token'); + + await expect(token).resolves.toBeNull(); + }); +}); diff --git a/nestjs-BE/server/src/auth/refresh-tokens.service.ts b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.ts similarity index 76% rename from nestjs-BE/server/src/auth/refresh-tokens.service.ts rename to nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.ts index 33295ac5..cee9d580 100644 --- a/nestjs-BE/server/src/auth/refresh-tokens.service.ts +++ b/nestjs-BE/server/src/refresh-tokens/refresh-tokens.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '../prisma/prisma.service'; -import generateUuid from '../utils/uuid'; +import { v4 as uuid } from 'uuid'; import { Prisma, RefreshToken } from '@prisma/client'; -import { REFRESH_TOKEN_EXPIRY_DAYS } from '../config/magic-number'; import { ConfigService } from '@nestjs/config'; +import { getExpiryDate } from '../utils/date'; @Injectable() export class RefreshTokensService { @@ -18,7 +18,7 @@ export class RefreshTokensService { return this.prisma.refreshToken.create({ data: { token: this.createToken(), - expiryDate: this.getExpiryDate(), + expiryDate: getExpiryDate({ week: 2 }), userUuid, }, }); @@ -44,9 +44,9 @@ export class RefreshTokensService { } } - createToken(): string { + private createToken(): string { const refreshToken = this.jwtService.sign( - { uuid: generateUuid() }, + { uuid: uuid() }, { secret: this.configService.get('JWT_REFRESH_SECRET'), expiresIn: '14d', @@ -54,11 +54,4 @@ export class RefreshTokensService { ); return refreshToken; } - - getExpiryDate(): Date { - const currentDate = new Date(); - const expiryDate = new Date(currentDate); - expiryDate.setDate(currentDate.getDate() + REFRESH_TOKEN_EXPIRY_DAYS); - return expiryDate; - } } diff --git a/nestjs-BE/server/src/utils/date.ts b/nestjs-BE/server/src/utils/date.ts new file mode 100644 index 00000000..e7dec37e --- /dev/null +++ b/nestjs-BE/server/src/utils/date.ts @@ -0,0 +1,15 @@ +const WEEK_DAY = 7; + +export function getExpiryDate({ + week = 0, + day = 0, +}: { + week?: number; + day?: number; +}): Date { + const expiryDate = new Date(); + + expiryDate.setDate(expiryDate.getDate() + week * WEEK_DAY + day); + + return expiryDate; +}