diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index e7d582d..38497c6 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -10,6 +10,7 @@ describe('AuthController', () => { const mockJwtService = { sign: jest.fn().mockReturnValue('mock-jwt-token'), + expiresIn: '7d', }; const mockPrismaService = { @@ -88,6 +89,7 @@ describe('AuthController', () => { expect(result.success).toBe(true); expect(result.data.token).toBe('mock-jwt-token'); expect(result.data.walletAddress).toBe(walletAddress); + expect(result.data.expiresIn).toBe(mockJwtService.expiresIn); expect(mockPrismaService.authChallenge.delete).toHaveBeenCalledWith({ where: { walletAddress }, }); @@ -162,6 +164,8 @@ describe('AuthController', () => { message: nonce, }), ).rejects.toThrow(UnauthorizedException); + + expect(mockPrismaService.authChallenge.delete).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a639a4c..1c7c9ea 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -106,11 +106,6 @@ export class AuthController { throw new UnauthorizedException('Invalid challenge message'); } - // Invalidate the nonce immediately after use (one-time use) - await this.prisma.authChallenge.delete({ where: { walletAddress } }).catch((err) => { - this.logger.warn(`Failed to delete challenge for ${walletAddress}: ${err.message}`); - }); - try { const keypair = Keypair.fromPublicKey(walletAddress); const messageBytes = Buffer.from(message, 'utf8'); @@ -127,6 +122,11 @@ export class AuthController { throw new UnauthorizedException('Signature verification failed'); } + // Invalidate the nonce only after successful verification (one-time use). + await this.prisma.authChallenge.delete({ where: { walletAddress } }).catch((err) => { + this.logger.warn(`Failed to delete challenge for ${walletAddress}: ${err.message}`); + }); + const token = this.jwtService.sign(walletAddress); this.logger.log(`Login successful: wallet=${walletAddress}`); @@ -135,7 +135,7 @@ export class AuthController { data: { token, walletAddress, - expiresIn: '7d', + expiresIn: this.jwtService.expiresIn, }, }; } diff --git a/src/auth/jwt.service.spec.ts b/src/auth/jwt.service.spec.ts index 60e593a..52169ea 100644 --- a/src/auth/jwt.service.spec.ts +++ b/src/auth/jwt.service.spec.ts @@ -38,6 +38,10 @@ describe('JwtService', () => { expect(decoded.walletAddress).toBe(walletAddress); }); + it('should expose the configured token expiry', () => { + expect(service.expiresIn).toBe('7d'); + }); + it('should throw UnauthorizedException for an invalid token', () => { expect(() => { service.verify('invalid.token.value'); diff --git a/src/auth/jwt.service.ts b/src/auth/jwt.service.ts index d89eec8..ae4042a 100644 --- a/src/auth/jwt.service.ts +++ b/src/auth/jwt.service.ts @@ -21,6 +21,7 @@ export interface JwtPayload { export class JwtService { private readonly logger = new Logger(JwtService.name); private readonly secret: string; + private readonly tokenExpiry = '7d'; constructor(private readonly config: ConfigService) { const secret = config.get('JWT_SECRET'); @@ -31,13 +32,17 @@ export class JwtService { this.secret = secret; } + get expiresIn(): string { + return this.tokenExpiry; + } + /** * Sign a JWT for the given wallet address. * Token expires in 7 days. */ sign(walletAddress: string): string { const payload: JwtPayload = { walletAddress }; - const token = jwt.sign(payload, this.secret, { expiresIn: '7d' }); + const token = jwt.sign(payload, this.secret, { expiresIn: this.expiresIn }); this.logger.log(`JWT issued for wallet: ${walletAddress}`); return token; }