From 59f57ded54e85b3128df5dd9c4a946c71fc5d487 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 8 Feb 2026 09:28:28 +0300 Subject: [PATCH] feat: add guards and decorators --- package.json | 1 + src/core/app.module.ts | 13 ++++++- src/core/config/index.ts | 1 + src/core/config/passport.config.ts | 10 +++++ src/global.d.ts | 7 ++++ .../decorators/current-user.decorator.ts | 7 ++++ src/shared/decorators/index.ts | 2 + src/shared/decorators/protected.decorator.ts | 5 +++ src/shared/guards/auth.guard.ts | 38 +++++++++++++++++++ src/shared/guards/index.ts | 1 + yarn.lock | 33 ++++++++++++++++ 11 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/core/config/passport.config.ts create mode 100644 src/global.d.ts create mode 100644 src/shared/decorators/current-user.decorator.ts create mode 100644 src/shared/decorators/index.ts create mode 100644 src/shared/decorators/protected.decorator.ts create mode 100644 src/shared/guards/auth.guard.ts create mode 100644 src/shared/guards/index.ts diff --git a/package.json b/package.json index e151eab..93ab8d2 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@nestjs/swagger": "^11.2.5", "@teacinema/common": "^1.0.0", "@teacinema/contracts": "^1.0.0", + "@teacinema/passport": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "cookie-parser": "^1.4.7", diff --git a/src/core/app.module.ts b/src/core/app.module.ts index 9cb9e2f..8039f3d 100644 --- a/src/core/app.module.ts +++ b/src/core/app.module.ts @@ -1,13 +1,22 @@ import { Module } from '@nestjs/common' -import { ConfigModule } from '@nestjs/config' +import { ConfigModule, ConfigService } from '@nestjs/config' +import { PassportModule } from '@teacinema/passport' import { AuthModule } from '../modules/auth/auth.module' import { AppController } from './app.controller' import { AppService } from './app.service' +import { getPassportConfig } from './config' @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true }), AuthModule], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + AuthModule, + PassportModule.registerAsync({ + useFactory: getPassportConfig, + inject: [ConfigService] + }) + ], controllers: [AppController], providers: [AppService] }) diff --git a/src/core/config/index.ts b/src/core/config/index.ts index 635ba52..5c73ea0 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -1,3 +1,4 @@ export * from './cors.config' export * from './swagger.config' export * from './validation-pipe.config' +export * from './passport.config' diff --git a/src/core/config/passport.config.ts b/src/core/config/passport.config.ts new file mode 100644 index 0000000..cd99cc4 --- /dev/null +++ b/src/core/config/passport.config.ts @@ -0,0 +1,10 @@ +import { ConfigService } from '@nestjs/config' +import { PassportOptions } from '@teacinema/passport' + +export function getPassportConfig( + configService: ConfigService +): PassportOptions { + return { + secretKey: configService.getOrThrow('PASSPORT_SECRET_KEY') + } +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..3771d09 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,7 @@ +declare namespace Express { + export interface Request { + user?: { + id: string + } + } +} diff --git a/src/shared/decorators/current-user.decorator.ts b/src/shared/decorators/current-user.decorator.ts new file mode 100644 index 0000000..69e7b27 --- /dev/null +++ b/src/shared/decorators/current-user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' +import type { Request } from 'express' + +export const CurrentUser = createParamDecorator((_, ctx) => { + const req = ctx.switchToHttp().getRequest() + return req.user?.id ?? null +}) diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..75670ec --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './protected.decorator' +export * from './current-user.decorator' diff --git a/src/shared/decorators/protected.decorator.ts b/src/shared/decorators/protected.decorator.ts new file mode 100644 index 0000000..1b67175 --- /dev/null +++ b/src/shared/decorators/protected.decorator.ts @@ -0,0 +1,5 @@ +import { applyDecorators, UseGuards } from '@nestjs/common' + +import { AuthGuard } from '../guards' + +export const Protected = () => applyDecorators(UseGuards(AuthGuard)) diff --git a/src/shared/guards/auth.guard.ts b/src/shared/guards/auth.guard.ts new file mode 100644 index 0000000..3fca1ec --- /dev/null +++ b/src/shared/guards/auth.guard.ts @@ -0,0 +1,38 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException +} from '@nestjs/common' +import { PassportService } from '@teacinema/passport' +import { Request } from 'express' + +@Injectable() +export class AuthGuard implements CanActivate { + public constructor(private readonly passportService: PassportService) {} + public canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + const token = this.extractToken(request) + const result = this.passportService.verify(token) + + if (!result.valid) { + throw new UnauthorizedException(result.reason) + } + + request.user = { + id: result.userId + } + + return true + } + + private extractToken(request: Request) { + const [type, token] = request.headers.authorization?.split(' ') ?? [] + + if (!token || type !== 'Bearer') { + throw new UnauthorizedException('Authorization header is missing') + } + + return token + } +} diff --git a/src/shared/guards/index.ts b/src/shared/guards/index.ts new file mode 100644 index 0000000..524c9d8 --- /dev/null +++ b/src/shared/guards/index.ts @@ -0,0 +1 @@ +export * from './auth.guard' diff --git a/yarn.lock b/yarn.lock index 52a22c0..4c31bd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,6 +1011,17 @@ load-esm "1.0.3" tslib "2.8.1" +"@nestjs/common@^11.1.12": + version "11.1.13" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.13.tgz#3d4bcc2a593236546bc62f7801b0d4450b26a9ac" + integrity sha512-ieqWtipT+VlyDWLz5Rvz0f3E5rXcVAnaAi+D53DEHLjc1kmFxCgZ62qVfTX2vwkywwqNkTNXvBgGR72hYqV//Q== + dependencies: + uid "2.0.2" + file-type "21.3.0" + iterare "1.2.1" + load-esm "1.0.3" + tslib "2.8.1" + "@nestjs/config@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.2.tgz#a2777a1fd2d0d594bab3953f50fbca95c14cce52" @@ -1032,6 +1043,18 @@ path-to-regexp "8.3.0" tslib "2.8.1" +"@nestjs/core@^11.1.12": + version "11.1.13" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.13.tgz#4444e124d255759d545a631f63e9d84fe1a1e3e5" + integrity sha512-Tq9EIKiC30EBL8hLK93tNqaToy0hzbuVGYt29V8NhkVJUsDzlmiVf6c3hSPtzx2krIUVbTgQ2KFeaxr72rEyzQ== + dependencies: + uid "2.0.2" + "@nuxt/opencollective" "0.4.1" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + path-to-regexp "8.3.0" + tslib "2.8.1" + "@nestjs/mapped-types@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz#b9b536b7c3571567aa1d0223db8baa1a51505a19" @@ -1214,6 +1237,16 @@ dependencies: "@trivago/prettier-plugin-sort-imports" "^5.2.2" +"@teacinema/passport@^1.0.0": + version "1.0.1" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fpassport/-/1.0.1/passport-1.0.1.tgz#83a8a605534b23929d3b1090d28023626fd5cc9f" + integrity sha512-hgFHXtWgXOemAf9InBLuhHbtF8wS01HKmot0nSaUqGlQCtu74ArMMCG8mup1SR50UGz5DEpHPEL8G5CzVnTTSQ== + dependencies: + "@nestjs/common" "^11.1.12" + "@nestjs/core" "^11.1.12" + reflect-metadata "^0.2.2" + rxjs "^7.8.2" + "@tokenizer/inflate@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.4.1.tgz#fa6cdb8366151b3cc8426bf9755c1ea03a2fba08"