chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PhaseADataModule } from '../core-data/phase-a-data.module';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
import { PermissionResolverService } from './permission-resolver.service';
|
||||
|
||||
@Module({
|
||||
imports: [PhaseADataModule],
|
||||
providers: [PermissionResolverService, PermissionsGuard],
|
||||
exports: [PermissionResolverService, PermissionsGuard],
|
||||
})
|
||||
export class AuthorizationModule {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const REQUIRED_PERMISSIONS_KEY = 'requiredPermissions';
|
||||
|
||||
export function RequirePermissions(...permissions: string[]) {
|
||||
const normalized = [
|
||||
...new Set(permissions.map((permission) => permission.trim()).filter(Boolean)),
|
||||
];
|
||||
|
||||
if (normalized.length === 0) {
|
||||
throw new Error('RequirePermissions needs at least one permission code');
|
||||
}
|
||||
|
||||
return SetMetadata(REQUIRED_PERMISSIONS_KEY, normalized);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_KEY } from '../../auth/decorators/public.decorator';
|
||||
import type { RequestWithContext } from '../../common/http/request-context';
|
||||
import { REQUIRED_PERMISSIONS_KEY } from '../decorators/require-permissions.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) return true;
|
||||
|
||||
const required =
|
||||
this.reflector.getAllAndMerge<string[]>(REQUIRED_PERMISSIONS_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]) ?? [];
|
||||
|
||||
const uniqueRequired = [...new Set(required)];
|
||||
if (uniqueRequired.length === 0) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest<RequestWithContext>();
|
||||
if (!request.auth) {
|
||||
throw new UnauthorizedException({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Autenticación requerida',
|
||||
});
|
||||
}
|
||||
|
||||
if (request.auth.mustChangePassword) {
|
||||
throw new ForbiddenException({
|
||||
code: 'PASSWORD_CHANGE_REQUIRED',
|
||||
message: 'Debe cambiar la contraseña temporal antes de continuar',
|
||||
});
|
||||
}
|
||||
|
||||
const granted = new Set(request.auth.permissions);
|
||||
const missing = uniqueRequired.filter((permission) => !granted.has(permission));
|
||||
if (missing.length > 0) {
|
||||
throw new ForbiddenException({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Permisos insuficientes',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RolesRepository } from '../core-data/repositories/roles.repository';
|
||||
|
||||
export interface ResolvedPermissions {
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PermissionResolverService {
|
||||
constructor(private readonly roles: RolesRepository) {}
|
||||
|
||||
async resolveForUser(userId: string): Promise<ResolvedPermissions> {
|
||||
const [roles, permissions] = await Promise.all([
|
||||
this.roles.findRoleCodesForUser(userId),
|
||||
this.roles.findPermissionCodesForUser(userId),
|
||||
]);
|
||||
|
||||
return { roles, permissions };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user