feat: add role decorator

This commit is contained in:
Sergey Krylov 2026-02-08 10:54:40 +03:00
parent 59f57ded54
commit 07e84f910b
10 changed files with 132 additions and 6 deletions

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config' import { ConfigModule, ConfigService } from '@nestjs/config'
import { PassportModule } from '@teacinema/passport' import { PassportModule } from '@teacinema/passport'
import { AccountModule } from '../modules/account/account.module'
import { AuthModule } from '../modules/auth/auth.module' import { AuthModule } from '../modules/auth/auth.module'
import { AppController } from './app.controller' import { AppController } from './app.controller'
@ -12,6 +13,7 @@ import { getPassportConfig } from './config'
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
AuthModule, AuthModule,
AccountModule,
PassportModule.registerAsync({ PassportModule.registerAsync({
useFactory: getPassportConfig, useFactory: getPassportConfig,
inject: [ConfigService] inject: [ConfigService]

View File

@ -0,0 +1,22 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common'
import type { ClientGrpc } from '@nestjs/microservices'
import {
AccountServiceClient,
GetAccountRequest
} from '@teacinema/contracts/gen/account'
@Injectable()
export class AccountClientGrpc implements OnModuleInit {
private accountService: AccountServiceClient
constructor(@Inject('ACCOUNT_PACKAGE') private readonly client: ClientGrpc) {}
onModuleInit() {
this.accountService =
this.client.getService<AccountServiceClient>('AccountService')
}
public getAccount(request: GetAccountRequest) {
return this.accountService.getAccount(request)
}
}

View File

@ -0,0 +1,28 @@
import { Module } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { ClientsModule, Transport } from '@nestjs/microservices'
import { PROTO_PATHS } from '@teacinema/contracts'
import { AccountClientGrpc } from './account.grpc'
@Module({
imports: [
ClientsModule.registerAsync([
{
name: 'ACCOUNT_PACKAGE',
useFactory: (configService: ConfigService) => ({
transport: Transport.GRPC,
options: {
package: 'account.v1',
protoPath: PROTO_PATHS.ACCOUNT,
url: configService.getOrThrow<string>('AUTH_GRPC_URL')
}
}),
inject: [ConfigService]
}
])
],
providers: [AccountClientGrpc],
exports: [AccountClientGrpc]
})
export class AccountModule {}

View File

@ -3,6 +3,8 @@ import { ConfigService } from '@nestjs/config'
import { ClientsModule, Transport } from '@nestjs/microservices' import { ClientsModule, Transport } from '@nestjs/microservices'
import { PROTO_PATHS } from '@teacinema/contracts' import { PROTO_PATHS } from '@teacinema/contracts'
import { AccountModule } from '../account/account.module'
import { AuthController } from './auth.controller' import { AuthController } from './auth.controller'
import { AuthClientGrpc } from './auth.grpc' import { AuthClientGrpc } from './auth.grpc'
@ -21,7 +23,8 @@ import { AuthClientGrpc } from './auth.grpc'
}), }),
inject: [ConfigService] inject: [ConfigService]
} }
]) ]),
AccountModule
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthClientGrpc] providers: [AuthClientGrpc]

View File

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

View File

@ -1,5 +1,14 @@
import { applyDecorators, UseGuards } from '@nestjs/common' import { applyDecorators, UseGuards } from '@nestjs/common'
import { Role } from '@teacinema/contracts/gen/account'
import { AuthGuard } from '../guards' import { AuthGuard, RolesGuard } from '../guards'
export const Protected = () => applyDecorators(UseGuards(AuthGuard)) import { Roles } from './roles.decorator'
export const Protected = (...roles: Role[]) => {
if (roles.length === 0) {
return applyDecorators(UseGuards(AuthGuard))
}
return applyDecorators(Roles(...roles), UseGuards(AuthGuard, RolesGuard))
}

View File

@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common'
import { Role } from '@teacinema/contracts/gen/account'
export const ROLES_KEY = 'required_roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

View File

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

View File

@ -0,0 +1,54 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
NotFoundException
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Role } from '@teacinema/contracts/gen/account'
import { Request } from 'express'
import { lastValueFrom } from 'rxjs'
import { AccountClientGrpc } from '../../modules/account/account.grpc'
import { ROLES_KEY } from '../decorators'
@Injectable()
export class RolesGuard implements CanActivate {
public constructor(
private readonly reflector: Reflector,
private readonly accountClient: AccountClientGrpc
) {}
public async canActivate(context: ExecutionContext) {
const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass()
])
if (!required || required.length === 0) {
return true
}
const request = context.switchToHttp().getRequest<Request>()
const user = request.user
if (!user) {
throw new ForbiddenException('User context missing')
}
const account = await lastValueFrom(
this.accountClient.getAccount({ id: user.id })
)
if (!account) {
throw new NotFoundException('Account not found')
}
if (!required.includes(account.role)) {
throw new ForbiddenException('Forbidden')
}
return true
}
}

View File

@ -1221,9 +1221,9 @@
integrity sha512-L006mySvhRpUlRHf3ONbXA0RG42gbQ9QAnmQuZx4zejHEsWB9702xe18MSYcBilv+lmYkQpiX/g3p8P7suLfqQ== integrity sha512-L006mySvhRpUlRHf3ONbXA0RG42gbQ9QAnmQuZx4zejHEsWB9702xe18MSYcBilv+lmYkQpiX/g3p8P7suLfqQ==
"@teacinema/contracts@^1.0.0": "@teacinema/contracts@^1.0.0":
version "1.0.5" version "1.0.6"
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.5/contracts-1.0.5.tgz#f6db65f58ba3621e81e886f54d6ec9e674c4785b" resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.6/contracts-1.0.6.tgz#7aa97d4c3081b791908c90d2a12d771b5fb79bd5"
integrity sha512-paFa/zVWKzDNuH04EyGPlG981YEX7Xt42gRwpfZwACGa5CCM5lehT7xKU4h9p/l9IebrOtTPDwJdQn/D41o3Gw== integrity sha512-2IU2S9hWtKeMpn4zRS9pJAaSryIODrRjS1q9QezQOXF6554vtQlMVr0fLVTGhNmXlbRfuje0f6HggApqPDFMaA==
dependencies: dependencies:
"@nestjs/microservices" "^11.1.12" "@nestjs/microservices" "^11.1.12"
protoc "33.4.0" protoc "33.4.0"