From 820cc3f29a49e56f7dbd8e78d970a631a2717f08 Mon Sep 17 00:00:00 2001 From: soorq Date: Wed, 22 Apr 2026 22:30:46 +0300 Subject: [PATCH] fix(auth): handle auth exceptions and fix internal server error 500 --- .../auth/controller/auth.controller.ts | 4 +-- src/modules/auth/services/auth.service.ts | 5 ++-- src/modules/auth/services/token.service.ts | 15 ++++++----- .../auth/strategies/bearer.strategy.ts | 11 +++++--- .../auth/strategies/cookie.strategy.ts | 4 ++- src/modules/user/entities/user.entity.ts | 1 - src/modules/user/index.ts | 1 + src/shared/guards/cookie.guard.ts | 26 +++++++++++++++++-- 8 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 7deeb74..0e1ae9c 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -69,7 +69,7 @@ export class AuthController { @UseGuards(BearerAuthGuard) @PostLogoutSwagger() async logout(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { - const session = req.cookies['refresh']; + const session = req.cookies?.['refresh']; const response = await this.facade.signOut(session); res.clearCookie('refresh', { path: '/' }); @@ -83,7 +83,7 @@ export class AuthController { @HttpCode(200) async refresh(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { const meta = getDeviceMeta(req); - const session = req.cookies['refresh']; + const session = req.cookies?.['refresh']; const { tokens, ...response } = await this.facade.refresh(session, meta); res.setCookie('refresh', tokens.refresh, { diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 6da50ff..488ace6 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -151,9 +151,9 @@ export class AuthService { }; public signIn = async (dto: SignInDto, meta: DeviceMetadata) => { - const { user, security } = await this.findUserCommand.execute({ email: dto.email }); + const entities = await this.findUserCommand.execute({ email: dto.email }); - if (!user || !security) { + if (!entities?.user || !entities?.security) { throw new BaseException( { code: 'INVALID_CREDENTIALS', @@ -163,6 +163,7 @@ export class AuthService { ); } + const { security, user } = entities; const isPasswordValid = await argon.verify(security.passwordHash, dto.password); if (!isPasswordValid) { diff --git a/src/modules/auth/services/token.service.ts b/src/modules/auth/services/token.service.ts index 43d61fe..72930b1 100644 --- a/src/modules/auth/services/token.service.ts +++ b/src/modules/auth/services/token.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import type { JwtPayload } from '@shared/types'; +import type { User } from '@core/modules/user'; @Injectable() export class TokenService { @@ -10,16 +11,16 @@ export class TokenService { private readonly cfg: ConfigService, ) {} - async generateTokens(user: any, sessionId: string) { + async generateTokens(user: User, sessionId: string) { const domain = this.cfg.get('DOMAIN'); + const audConstraint = this.cfg.getOrThrow('JWT_AUDIENCE'); const payload = { jti: sessionId, sub: user.id, email: user.email, iss: btoa(domain), - aud: btoa(this.cfg.getOrThrow('JWT_AUDIENCE')), - role: user.role, + aud: btoa(audConstraint), }; const [access, refresh] = await Promise.all([ @@ -38,10 +39,10 @@ export class TokenService { async validateToken(token: string, type: 'access' | 'refresh'): Promise { try { - const secret = - type === 'access' - ? this.cfg.get('JWT_ACCESS_SECRET') - : this.cfg.get('JWT_REFRESH_SECRET'); + const accessSecret = this.cfg.get('JWT_ACCESS_SECRET'); + const refreshSecret = this.cfg.get('JWT_REFRESH_SECRET'); + + const secret = type === 'access' ? accessSecret : refreshSecret; return this.jwtService.verifyAsync(token, { secret }); } catch (e) { diff --git a/src/modules/auth/strategies/bearer.strategy.ts b/src/modules/auth/strategies/bearer.strategy.ts index a7ccdfc..c14ed7d 100644 --- a/src/modules/auth/strategies/bearer.strategy.ts +++ b/src/modules/auth/strategies/bearer.strategy.ts @@ -6,12 +6,15 @@ import { Strategy, ExtractJwt } from 'passport-jwt'; @Injectable() export class BearerStrategy extends PassportStrategy(Strategy, 'bearer') { - constructor(configService: ConfigService) { + constructor(cfg: ConfigService) { + const audConstraint = cfg.getOrThrow('JWT_AUDIENCE'); + const audience = btoa(audConstraint); + super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: configService.get('JWT_ACCESS_SECRET'), - issuer: configService.get('JWT_ISSUER'), - audience: configService.get('JWT_AUDIENCE'), + secretOrKey: cfg.get('JWT_ACCESS_SECRET'), + issuer: cfg.get('JWT_ISSUER'), + audience, }); } diff --git a/src/modules/auth/strategies/cookie.strategy.ts b/src/modules/auth/strategies/cookie.strategy.ts index 4411361..7255ee7 100644 --- a/src/modules/auth/strategies/cookie.strategy.ts +++ b/src/modules/auth/strategies/cookie.strategy.ts @@ -12,7 +12,8 @@ export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { super({ jwtFromRequest: ExtractJwt.fromExtractors([ (request: FastifyRequest) => { - return request?.cookies?.['refresh']; + const token = request?.cookies?.['refresh']; + return token; }, ]), secretOrKey: configService.get('JWT_REFRESH_SECRET'), @@ -21,6 +22,7 @@ export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { } validate(_req: FastifyRequest, payload: JwtPayload) { + console.log(_req, payload); if (!payload || !payload.jti) { throw new BaseException( { diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index b77ec74..4078a21 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -10,7 +10,6 @@ export const users = baseSchema.table('users', { firstName: varchar('first_name', { length: 50 }).notNull(), lastName: varchar('last_name', { length: 50 }).notNull(), middleName: varchar('middle_name', { length: 50 }), - email: varchar('email', { length: 255 }).notNull().unique(), bio: text('bio'), avatarUrl: varchar('avatar_url', { length: 512 }), diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts index 3b9d53d..9871038 100644 --- a/src/modules/user/index.ts +++ b/src/modules/user/index.ts @@ -1,3 +1,4 @@ export { UserModule } from './user.module'; export { UserRepository } from './repository/user.repository'; export { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; +export { User } from './entities/user.domain'; diff --git a/src/shared/guards/cookie.guard.ts b/src/shared/guards/cookie.guard.ts index 9ae8936..89300ab 100644 --- a/src/shared/guards/cookie.guard.ts +++ b/src/shared/guards/cookie.guard.ts @@ -1,5 +1,27 @@ -import { Injectable } from '@nestjs/common'; +import { HttpStatus, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { BaseException } from '@shared/error'; +import type { JwtPayload } from '@shared/types'; @Injectable() -export class CookieAuthGuard extends AuthGuard('cookie') {} +export class CookieAuthGuard extends AuthGuard('cookie') { + handleRequest(err: unknown, user: TUser, info: any): TUser { + if (err || !user) { + throw new BaseException( + { + code: 'INVALID_REFRESH_TOKEN', + message: 'Refresh токен невалиден или отсутствует', + details: [ + { + target: 'auth', + reason: info?.message || 'Token verification failed', + }, + ], + }, + HttpStatus.UNAUTHORIZED, + ); + } + + return user; + } +}