feat: add guards and decorators

This commit is contained in:
Sergey Krylov 2026-02-08 09:28:28 +03:00
parent cce2d83101
commit 59f57ded54
11 changed files with 116 additions and 2 deletions

View File

@ -30,6 +30,7 @@
"@nestjs/swagger": "^11.2.5", "@nestjs/swagger": "^11.2.5",
"@teacinema/common": "^1.0.0", "@teacinema/common": "^1.0.0",
"@teacinema/contracts": "^1.0.0", "@teacinema/contracts": "^1.0.0",
"@teacinema/passport": "^1.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.3", "class-validator": "^0.14.3",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",

View File

@ -1,13 +1,22 @@
import { Module } from '@nestjs/common' 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 { AuthModule } from '../modules/auth/auth.module'
import { AppController } from './app.controller' import { AppController } from './app.controller'
import { AppService } from './app.service' import { AppService } from './app.service'
import { getPassportConfig } from './config'
@Module({ @Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), AuthModule], imports: [
ConfigModule.forRoot({ isGlobal: true }),
AuthModule,
PassportModule.registerAsync({
useFactory: getPassportConfig,
inject: [ConfigService]
})
],
controllers: [AppController], controllers: [AppController],
providers: [AppService] providers: [AppService]
}) })

View File

@ -1,3 +1,4 @@
export * from './cors.config' export * from './cors.config'
export * from './swagger.config' export * from './swagger.config'
export * from './validation-pipe.config' export * from './validation-pipe.config'
export * from './passport.config'

View File

@ -0,0 +1,10 @@
import { ConfigService } from '@nestjs/config'
import { PassportOptions } from '@teacinema/passport'
export function getPassportConfig(
configService: ConfigService
): PassportOptions {
return {
secretKey: configService.getOrThrow<string>('PASSPORT_SECRET_KEY')
}
}

7
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare namespace Express {
export interface Request {
user?: {
id: string
}
}
}

View File

@ -0,0 +1,7 @@
import { createParamDecorator } from '@nestjs/common'
import type { Request } from 'express'
export const CurrentUser = createParamDecorator((_, ctx) => {
const req = ctx.switchToHttp().getRequest<Request>()
return req.user?.id ?? null
})

View File

@ -0,0 +1,2 @@
export * from './protected.decorator'
export * from './current-user.decorator'

View File

@ -0,0 +1,5 @@
import { applyDecorators, UseGuards } from '@nestjs/common'
import { AuthGuard } from '../guards'
export const Protected = () => applyDecorators(UseGuards(AuthGuard))

View File

@ -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<Request>()
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
}
}

View File

@ -0,0 +1 @@
export * from './auth.guard'

View File

@ -1011,6 +1011,17 @@
load-esm "1.0.3" load-esm "1.0.3"
tslib "2.8.1" 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": "@nestjs/config@^4.0.2":
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.2.tgz#a2777a1fd2d0d594bab3953f50fbca95c14cce52" resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.2.tgz#a2777a1fd2d0d594bab3953f50fbca95c14cce52"
@ -1032,6 +1043,18 @@
path-to-regexp "8.3.0" path-to-regexp "8.3.0"
tslib "2.8.1" 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": "@nestjs/mapped-types@2.1.0":
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz#b9b536b7c3571567aa1d0223db8baa1a51505a19" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz#b9b536b7c3571567aa1d0223db8baa1a51505a19"
@ -1214,6 +1237,16 @@
dependencies: dependencies:
"@trivago/prettier-plugin-sort-imports" "^5.2.2" "@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": "@tokenizer/inflate@^0.4.1":
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.4.1.tgz#fa6cdb8366151b3cc8426bf9755c1ea03a2fba08" resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.4.1.tgz#fa6cdb8366151b3cc8426bf9755c1ea03a2fba08"