# Especificacion Tecnica: RBAC Backend ## Identificacion | Campo | Valor | |-------|-------| | **Modulo** | MGN-003 Roles/RBAC | | **Componente** | Backend API | | **Framework** | NestJS | | **Version** | 1.0 | | **Fecha** | 2025-12-05 | --- ## Arquitectura ``` src/ ├── modules/ │ └── rbac/ │ ├── rbac.module.ts │ ├── controllers/ │ │ ├── roles.controller.ts │ │ └── permissions.controller.ts │ ├── services/ │ │ ├── roles.service.ts │ │ ├── permissions.service.ts │ │ └── rbac-cache.service.ts │ ├── guards/ │ │ ├── rbac.guard.ts │ │ └── owner.guard.ts │ ├── decorators/ │ │ ├── permissions.decorator.ts │ │ ├── roles.decorator.ts │ │ └── public.decorator.ts │ ├── dto/ │ │ ├── create-role.dto.ts │ │ ├── update-role.dto.ts │ │ ├── assign-roles.dto.ts │ │ └── ... │ ├── entities/ │ │ ├── role.entity.ts │ │ ├── permission.entity.ts │ │ ├── role-permission.entity.ts │ │ └── user-role.entity.ts │ └── interfaces/ │ └── permission-metadata.interface.ts ``` --- ## Entidades ### Role Entity ```typescript // entities/role.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, OneToMany, ManyToOne, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, } from 'typeorm'; import { Permission } from './permission.entity'; import { UserRole } from './user-role.entity'; import { User } from '../../users/entities/user.entity'; import { Tenant } from '../../tenants/entities/tenant.entity'; @Entity({ schema: 'core_rbac', name: 'roles' }) export class Role { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id' }) tenantId: string; @ManyToOne(() => Tenant) tenant: Tenant; @Column({ length: 50 }) name: string; @Column({ length: 50 }) slug: string; @Column({ length: 500, nullable: true }) description: string; @Column({ name: 'is_built_in', default: false }) isBuiltIn: boolean; @Column({ name: 'is_active', default: true }) isActive: boolean; @ManyToMany(() => Permission) @JoinTable({ name: 'role_permissions', schema: 'core_rbac', joinColumn: { name: 'role_id' }, inverseJoinColumn: { name: 'permission_id' }, }) permissions: Permission[]; @OneToMany(() => UserRole, (userRole) => userRole.role) userRoles: UserRole[]; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; @Column({ name: 'created_by', nullable: true }) createdBy: string; @ManyToOne(() => User) createdByUser: User; @Column({ name: 'updated_by', nullable: true }) updatedBy: string; @DeleteDateColumn({ name: 'deleted_at' }) deletedAt: Date; @Column({ name: 'deleted_by', nullable: true }) deletedBy: string; // Virtual: count of users usersCount?: number; } ``` ### Permission Entity ```typescript // entities/permission.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, } from 'typeorm'; @Entity({ schema: 'core_rbac', name: 'permissions' }) export class Permission { @PrimaryGeneratedColumn('uuid') id: string; @Column({ length: 100, unique: true }) code: string; @Column({ length: 100 }) name: string; @Column({ length: 500, nullable: true }) description: string; @Column({ length: 50 }) module: string; @Column({ name: 'parent_code', nullable: true }) parentCode: string; @ManyToOne(() => Permission) parent: Permission; @Column({ name: 'is_deprecated', default: false }) isDeprecated: boolean; @Column({ name: 'sort_order', default: 0 }) sortOrder: number; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; } ``` ### UserRole Entity ```typescript // entities/user-role.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, } from 'typeorm'; import { User } from '../../users/entities/user.entity'; import { Role } from './role.entity'; @Entity({ schema: 'core_rbac', name: 'user_roles' }) export class UserRole { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'user_id' }) userId: string; @ManyToOne(() => User) user: User; @Column({ name: 'role_id' }) roleId: string; @ManyToOne(() => Role) role: Role; @CreateDateColumn({ name: 'assigned_at' }) assignedAt: Date; @Column({ name: 'assigned_by', nullable: true }) assignedBy: string; @Column({ name: 'expires_at', nullable: true }) expiresAt: Date; } ``` --- ## DTOs ### Create Role DTO ```typescript // dto/create-role.dto.ts import { IsString, IsOptional, IsArray, IsUUID, MinLength, MaxLength, Matches, ArrayMinSize, } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateRoleDto { @ApiProperty({ description: 'Nombre del rol', example: 'Vendedor', minLength: 3, maxLength: 50, }) @IsString() @MinLength(3) @MaxLength(50) name: string; @ApiPropertyOptional({ description: 'Descripcion del rol', example: 'Equipo de ventas', maxLength: 500, }) @IsOptional() @IsString() @MaxLength(500) description?: string; @ApiProperty({ description: 'IDs de permisos a asignar', example: ['uuid-1', 'uuid-2'], type: [String], }) @IsArray() @IsUUID('4', { each: true }) @ArrayMinSize(1) permissionIds: string[]; } ``` ### Update Role DTO ```typescript // dto/update-role.dto.ts import { PartialType, OmitType } from '@nestjs/swagger'; import { CreateRoleDto } from './create-role.dto'; import { IsOptional, IsBoolean } from 'class-validator'; export class UpdateRoleDto extends PartialType(CreateRoleDto) { @IsOptional() @IsBoolean() isActive?: boolean; } ``` ### Assign Roles DTO ```typescript // dto/assign-roles.dto.ts import { IsArray, IsUUID, ArrayMinSize } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AssignRolesDto { @ApiProperty({ description: 'IDs de roles a asignar', example: ['role-uuid-1', 'role-uuid-2'], type: [String], }) @IsArray() @IsUUID('4', { each: true }) @ArrayMinSize(1) roleIds: string[]; } ``` ### Assign Users to Role DTO ```typescript // dto/assign-users.dto.ts import { IsArray, IsUUID, ArrayMinSize } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AssignUsersToRoleDto { @ApiProperty({ description: 'IDs de usuarios a asignar al rol', example: ['user-uuid-1', 'user-uuid-2'], type: [String], }) @IsArray() @IsUUID('4', { each: true }) @ArrayMinSize(1) userIds: string[]; } ``` ### Role Query DTO ```typescript // dto/role-query.dto.ts import { IsOptional, IsString, IsEnum, IsBoolean } from 'class-validator'; import { Transform } from 'class-transformer'; import { PaginationDto } from '../../../common/dto/pagination.dto'; export enum RoleType { ALL = 'all', BUILT_IN = 'builtin', CUSTOM = 'custom', } export class RoleQueryDto extends PaginationDto { @IsOptional() @IsString() search?: string; @IsOptional() @IsEnum(RoleType) type?: RoleType = RoleType.ALL; @IsOptional() @Transform(({ value }) => value === 'true') @IsBoolean() includeInactive?: boolean = false; } ``` --- ## Response DTOs ### Role Response DTO ```typescript // dto/role-response.dto.ts import { ApiProperty } from '@nestjs/swagger'; export class RoleResponseDto { @ApiProperty() id: string; @ApiProperty() name: string; @ApiProperty() slug: string; @ApiProperty() description: string; @ApiProperty() isBuiltIn: boolean; @ApiProperty() isActive: boolean; @ApiProperty() usersCount: number; @ApiProperty() permissionsCount: number; @ApiProperty() createdAt: Date; } export class RoleDetailResponseDto extends RoleResponseDto { @ApiProperty({ type: [PermissionGroupDto] }) permissions: PermissionGroupDto[]; } export class PermissionGroupDto { @ApiProperty() module: string; @ApiProperty() moduleName: string; @ApiProperty({ type: [PermissionDto] }) permissions: PermissionDto[]; } export class PermissionDto { @ApiProperty() id: string; @ApiProperty() code: string; @ApiProperty() name: string; @ApiProperty() description: string; } ``` ### Effective Permissions Response ```typescript // dto/effective-permissions.dto.ts export class EffectivePermissionsDto { @ApiProperty({ description: 'Roles del usuario' }) roles: string[]; @ApiProperty({ description: 'Permisos directos de los roles' }) direct: string[]; @ApiProperty({ description: 'Permisos heredados via wildcards' }) inherited: string[]; @ApiProperty({ description: 'Todos los permisos efectivos' }) all: string[]; } ``` --- ## Servicios ### Roles Service ```typescript // services/roles.service.ts import { Injectable, BadRequestException, ConflictException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In, Like, Not, IsNull } from 'typeorm'; import { Role } from '../entities/role.entity'; import { Permission } from '../entities/permission.entity'; import { UserRole } from '../entities/user-role.entity'; import { CreateRoleDto, UpdateRoleDto, RoleQueryDto, RoleType } from '../dto'; import { RbacCacheService } from './rbac-cache.service'; import { slugify } from '../../../common/utils/slugify'; @Injectable() export class RolesService { constructor( @InjectRepository(Role) private roleRepository: Repository, @InjectRepository(Permission) private permissionRepository: Repository, @InjectRepository(UserRole) private userRoleRepository: Repository, private cacheService: RbacCacheService, ) {} async create(tenantId: string, dto: CreateRoleDto, userId: string): Promise { // Validar nombre unico const existing = await this.roleRepository.findOne({ where: { tenantId, name: dto.name }, }); if (existing) { throw new ConflictException('Ya existe un rol con este nombre'); } // Validar limite de roles por tenant const count = await this.roleRepository.count({ where: { tenantId, isBuiltIn: false }, }); if (count >= 50) { throw new BadRequestException('Limite de roles alcanzado (50)'); } // Validar permisos existen const permissions = await this.permissionRepository.findBy({ id: In(dto.permissionIds), }); if (permissions.length !== dto.permissionIds.length) { throw new BadRequestException('Algunos permisos no existen'); } // Crear rol const role = this.roleRepository.create({ tenantId, name: dto.name, slug: slugify(dto.name), description: dto.description, permissions, createdBy: userId, }); return this.roleRepository.save(role); } async findAll(tenantId: string, query: RoleQueryDto) { const qb = this.roleRepository .createQueryBuilder('role') .leftJoin('role.userRoles', 'ur') .leftJoin('role.permissions', 'p') .select([ 'role.id', 'role.name', 'role.slug', 'role.description', 'role.isBuiltIn', 'role.isActive', 'role.createdAt', ]) .addSelect('COUNT(DISTINCT ur.userId)', 'usersCount') .addSelect('COUNT(DISTINCT p.id)', 'permissionsCount') .where('role.tenantId = :tenantId', { tenantId }) .groupBy('role.id'); // Filtro por tipo if (query.type === RoleType.BUILT_IN) { qb.andWhere('role.isBuiltIn = true'); } else if (query.type === RoleType.CUSTOM) { qb.andWhere('role.isBuiltIn = false'); } // Filtro por activo if (!query.includeInactive) { qb.andWhere('role.isActive = true'); } // Busqueda if (query.search) { qb.andWhere('(role.name ILIKE :search OR role.description ILIKE :search)', { search: `%${query.search}%`, }); } // Ordenar: built-in primero, luego por nombre qb.orderBy('role.isBuiltIn', 'DESC').addOrderBy('role.name', 'ASC'); // Paginacion const total = await qb.getCount(); const data = await qb .offset((query.page - 1) * query.limit) .limit(query.limit) .getRawAndEntities(); return { data: data.entities.map((role, i) => ({ ...role, usersCount: parseInt(data.raw[i].usersCount), permissionsCount: parseInt(data.raw[i].permissionsCount), })), meta: { total, page: query.page, limit: query.limit, totalPages: Math.ceil(total / query.limit), hasNext: query.page * query.limit < total, hasPrev: query.page > 1, }, }; } async findOne(tenantId: string, id: string): Promise { const role = await this.roleRepository.findOne({ where: { id, tenantId }, relations: ['permissions'], }); if (!role) { throw new NotFoundException('Rol no encontrado'); } // Agregar conteo de usuarios const usersCount = await this.userRoleRepository.count({ where: { roleId: id }, }); role.usersCount = usersCount; return role; } async update( tenantId: string, id: string, dto: UpdateRoleDto, userId: string, ): Promise { const role = await this.findOne(tenantId, id); // Validar si es built-in if (role.isBuiltIn) { // Solo permitir agregar permisos, no quitar ni cambiar nombre if (dto.name && dto.name !== role.name) { throw new BadRequestException('No se puede cambiar el nombre de roles del sistema'); } if (dto.permissionIds) { // Validar que solo se agregan, no se quitan const currentPermIds = role.permissions.map(p => p.id); const removedPerms = currentPermIds.filter(id => !dto.permissionIds.includes(id)); if (removedPerms.length > 0) { throw new BadRequestException('No se pueden quitar permisos de roles del sistema'); } } } // Validar nombre unico si cambio if (dto.name && dto.name !== role.name) { const existing = await this.roleRepository.findOne({ where: { tenantId, name: dto.name, id: Not(id) }, }); if (existing) { throw new ConflictException('Ya existe un rol con este nombre'); } role.name = dto.name; role.slug = slugify(dto.name); } if (dto.description !== undefined) { role.description = dto.description; } if (dto.isActive !== undefined && !role.isBuiltIn) { role.isActive = dto.isActive; } if (dto.permissionIds) { const permissions = await this.permissionRepository.findBy({ id: In(dto.permissionIds), }); role.permissions = permissions; } role.updatedBy = userId; const updated = await this.roleRepository.save(role); // Invalidar cache de usuarios con este rol await this.invalidateCacheForRole(id); return updated; } async remove( tenantId: string, id: string, userId: string, reassignTo?: string, ): Promise { const role = await this.findOne(tenantId, id); if (role.isBuiltIn) { throw new BadRequestException('No se pueden eliminar roles del sistema'); } // Reasignar usuarios si se especifico if (reassignTo && role.usersCount > 0) { const targetRole = await this.findOne(tenantId, reassignTo); await this.userRoleRepository.update( { roleId: id }, { roleId: targetRole.id }, ); } else if (role.usersCount > 0) { // Eliminar asignaciones await this.userRoleRepository.delete({ roleId: id }); } // Soft delete role.deletedBy = userId; await this.roleRepository.save(role); await this.roleRepository.softDelete(id); // Invalidar cache await this.invalidateCacheForRole(id); } private async invalidateCacheForRole(roleId: string): Promise { const userRoles = await this.userRoleRepository.find({ where: { roleId }, select: ['userId'], }); for (const ur of userRoles) { await this.cacheService.invalidateUserPermissions(ur.userId); } } } ``` ### Permissions Service ```typescript // services/permissions.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import { Permission } from '../entities/permission.entity'; import { UserRole } from '../entities/user-role.entity'; import { RbacCacheService } from './rbac-cache.service'; @Injectable() export class PermissionsService { private moduleNames: Record = { auth: 'Autenticacion', users: 'Gestion de Usuarios', roles: 'Roles y Permisos', tenants: 'Multi-Tenancy', settings: 'Configuracion', audit: 'Auditoria', reports: 'Reportes', financial: 'Modulo Financiero', inventory: 'Inventario', }; constructor( @InjectRepository(Permission) private permissionRepository: Repository, @InjectRepository(UserRole) private userRoleRepository: Repository, private cacheService: RbacCacheService, ) {} async findAll(search?: string): Promise { let qb = this.permissionRepository .createQueryBuilder('p') .where('p.isDeprecated = false') .orderBy('p.module', 'ASC') .addOrderBy('p.sortOrder', 'ASC'); if (search) { qb = qb.andWhere( '(p.code ILIKE :search OR p.name ILIKE :search)', { search: `%${search}%` }, ); } const permissions = await qb.getMany(); // Agrupar por modulo const grouped = new Map(); for (const perm of permissions) { if (!grouped.has(perm.module)) { grouped.set(perm.module, []); } grouped.get(perm.module).push(perm); } return Array.from(grouped.entries()).map(([module, perms]) => ({ module, moduleName: this.moduleNames[module] || module, permissions: perms.map(p => ({ id: p.id, code: p.code, name: p.name, description: p.description, })), })); } async getEffectivePermissions(userId: string): Promise { // Intentar desde cache const cached = await this.cacheService.getUserPermissions(userId); if (cached) { return cached; } // Obtener roles del usuario const userRoles = await this.userRoleRepository.find({ where: { userId }, relations: ['role', 'role.permissions'], }); const roles = userRoles .filter(ur => !ur.expiresAt || ur.expiresAt > new Date()) .filter(ur => ur.role.isActive) .map(ur => ur.role.slug); // Recolectar permisos directos const directPerms = new Set(); for (const ur of userRoles) { if (ur.role.isActive) { for (const perm of ur.role.permissions) { directPerms.add(perm.code); } } } // Expandir wildcards const allPerms = await this.expandWildcards(Array.from(directPerms)); // Calcular heredados const inherited = allPerms.filter(p => !directPerms.has(p)); const result: EffectivePermissionsDto = { roles, direct: Array.from(directPerms), inherited, all: allPerms, }; // Guardar en cache await this.cacheService.setUserPermissions(userId, result); return result; } async userHasPermission(userId: string, permission: string): Promise { const effective = await this.getEffectivePermissions(userId); return effective.all.includes(permission); } async userHasAnyPermission(userId: string, permissions: string[]): Promise { const effective = await this.getEffectivePermissions(userId); return permissions.some(p => effective.all.includes(p)); } async userHasAllPermissions(userId: string, permissions: string[]): Promise { const effective = await this.getEffectivePermissions(userId); return permissions.every(p => effective.all.includes(p)); } private async expandWildcards(permissions: string[]): Promise { const result = new Set(permissions); // Encontrar wildcards const wildcards = permissions.filter(p => p.endsWith(':*')); if (wildcards.length === 0) { return Array.from(result); } // Obtener permisos hijos de wildcards for (const wildcard of wildcards) { const module = wildcard.replace(':*', ''); const children = await this.permissionRepository.find({ where: { module, isDeprecated: false }, }); children.forEach(c => result.add(c.code)); } return Array.from(result); } } ``` ### RBAC Cache Service ```typescript // services/rbac-cache.service.ts import { Injectable, Inject } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { EffectivePermissionsDto } from '../dto'; @Injectable() export class RbacCacheService { private readonly CACHE_TTL = 300000; // 5 minutos private readonly PREFIX = 'rbac:permissions:'; constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} async getUserPermissions(userId: string): Promise { const key = `${this.PREFIX}${userId}`; return this.cache.get(key); } async setUserPermissions( userId: string, permissions: EffectivePermissionsDto, ): Promise { const key = `${this.PREFIX}${userId}`; await this.cache.set(key, permissions, this.CACHE_TTL); } async invalidateUserPermissions(userId: string): Promise { const key = `${this.PREFIX}${userId}`; await this.cache.del(key); } async invalidateAll(): Promise { // En produccion: usar patron de keys o pub/sub // await this.cache.reset(); } } ``` --- ## Guards y Decoradores ### Decoradores ```typescript // decorators/permissions.decorator.ts import { SetMetadata } from '@nestjs/common'; export const PERMISSIONS_KEY = 'permissions'; export const PERMISSION_MODE_KEY = 'permissionMode'; export interface PermissionMetadata { permissions: string[]; mode: 'AND' | 'OR'; } export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'AND' } as PermissionMetadata); export const AnyPermission = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'OR' } as PermissionMetadata); // decorators/roles.decorator.ts export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); // decorators/public.decorator.ts export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); // decorators/owner-or-permission.decorator.ts export const OWNER_OR_PERMISSION_KEY = 'ownerOrPermission'; export const OwnerOrPermission = (permission: string) => SetMetadata(OWNER_OR_PERMISSION_KEY, permission); ``` ### RBAC Guard ```typescript // guards/rbac.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { PermissionsService } from '../services/permissions.service'; import { IS_PUBLIC_KEY, PERMISSIONS_KEY, ROLES_KEY, PermissionMetadata, } from '../decorators'; @Injectable() export class RbacGuard implements CanActivate { private readonly logger = new Logger(RbacGuard.name); constructor( private reflector: Reflector, private permissionsService: PermissionsService, ) {} async canActivate(context: ExecutionContext): Promise { // Verificar si es ruta publica const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; const request = context.switchToHttp().getRequest(); const user = request.user; if (!user) { throw new ForbiddenException('No autenticado'); } // Super Admin bypass const effectivePerms = await this.permissionsService.getEffectivePermissions(user.id); if (effectivePerms.roles.includes('super_admin')) { return true; } // Verificar roles requeridos const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (requiredRoles && requiredRoles.length > 0) { const hasRole = requiredRoles.some(role => effectivePerms.roles.includes(role)); if (!hasRole) { this.logDenied(user.id, request, 'roles', requiredRoles); throw new ForbiddenException('No tienes permiso para realizar esta accion'); } } // Verificar permisos requeridos const permMetadata = this.reflector.getAllAndOverride( PERMISSIONS_KEY, [context.getHandler(), context.getClass()], ); if (!permMetadata) return true; const { permissions, mode } = permMetadata; let hasPermission: boolean; if (mode === 'AND') { hasPermission = await this.permissionsService.userHasAllPermissions( user.id, permissions, ); } else { hasPermission = await this.permissionsService.userHasAnyPermission( user.id, permissions, ); } if (!hasPermission) { this.logDenied(user.id, request, 'permissions', permissions); throw new ForbiddenException('No tienes permiso para realizar esta accion'); } // Adjuntar permisos al request para uso posterior request.userPermissions = effectivePerms; return true; } private logDenied( userId: string, request: any, type: string, required: string[], ): void { this.logger.warn({ message: 'Access denied', userId, endpoint: `${request.method} ${request.url}`, requiredType: type, required, timestamp: new Date().toISOString(), }); } } ``` ### Owner Guard ```typescript // guards/owner.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { OWNER_OR_PERMISSION_KEY } from '../decorators'; import { PermissionsService } from '../services/permissions.service'; @Injectable() export class OwnerGuard implements CanActivate { constructor( private reflector: Reflector, private permissionsService: PermissionsService, ) {} async canActivate(context: ExecutionContext): Promise { const permission = this.reflector.get( OWNER_OR_PERMISSION_KEY, context.getHandler(), ); if (!permission) return true; const request = context.switchToHttp().getRequest(); const user = request.user; const resourceId = request.params.id; // Verificar si tiene permiso const hasPermission = await this.permissionsService.userHasPermission( user.id, permission, ); if (hasPermission) return true; // Verificar si es owner return user.id === resourceId; } } ``` --- ## Controllers ### Roles Controller ```typescript // controllers/roles.controller.ts import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, ParseUUIDPipe, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; import { TenantGuard } from '../../tenants/guards/tenant.guard'; import { RbacGuard } from '../guards/rbac.guard'; import { Permissions } from '../decorators'; import { CurrentUser } from '../../auth/decorators/current-user.decorator'; import { CurrentTenant } from '../../tenants/decorators/current-tenant.decorator'; import { RolesService } from '../services/roles.service'; import { CreateRoleDto, UpdateRoleDto, RoleQueryDto } from '../dto'; @ApiTags('Roles') @ApiBearerAuth() @Controller('api/v1/roles') @UseGuards(JwtAuthGuard, TenantGuard, RbacGuard) export class RolesController { constructor(private rolesService: RolesService) {} @Post() @Permissions('roles:create') @ApiOperation({ summary: 'Crear un nuevo rol' }) create( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Body() dto: CreateRoleDto, ) { return this.rolesService.create(tenantId, dto, userId); } @Get() @Permissions('roles:read') @ApiOperation({ summary: 'Listar roles' }) findAll( @CurrentTenant() tenantId: string, @Query() query: RoleQueryDto, ) { return this.rolesService.findAll(tenantId, query); } @Get(':id') @Permissions('roles:read') @ApiOperation({ summary: 'Obtener detalle de un rol' }) findOne( @CurrentTenant() tenantId: string, @Param('id', ParseUUIDPipe) id: string, ) { return this.rolesService.findOne(tenantId, id); } @Patch(':id') @Permissions('roles:update') @ApiOperation({ summary: 'Actualizar un rol' }) update( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRoleDto, ) { return this.rolesService.update(tenantId, id, dto, userId); } @Delete(':id') @Permissions('roles:delete') @ApiOperation({ summary: 'Eliminar un rol' }) remove( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Param('id', ParseUUIDPipe) id: string, @Query('reassignTo') reassignTo?: string, ) { return this.rolesService.remove(tenantId, id, userId, reassignTo); } @Get(':id/users') @Permissions('roles:read') @ApiOperation({ summary: 'Listar usuarios de un rol' }) findUsers( @CurrentTenant() tenantId: string, @Param('id', ParseUUIDPipe) id: string, @Query() query: PaginationDto, ) { return this.rolesService.findUsers(tenantId, id, query); } @Post(':id/users') @Permissions('roles:assign') @ApiOperation({ summary: 'Asignar rol a usuarios' }) assignUsers( @CurrentTenant() tenantId: string, @CurrentUser('id') userId: string, @Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignUsersToRoleDto, ) { return this.rolesService.assignUsers(tenantId, id, dto.userIds, userId); } } ``` ### Permissions Controller ```typescript // controllers/permissions.controller.ts import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; import { RbacGuard } from '../guards/rbac.guard'; import { Permissions } from '../decorators'; import { PermissionsService } from '../services/permissions.service'; @ApiTags('Permissions') @ApiBearerAuth() @Controller('api/v1/permissions') @UseGuards(JwtAuthGuard, RbacGuard) export class PermissionsController { constructor(private permissionsService: PermissionsService) {} @Get() @Permissions('permissions:read') @ApiOperation({ summary: 'Listar permisos agrupados por modulo' }) findAll(@Query('search') search?: string) { return this.permissionsService.findAll(search); } } ``` --- ## Endpoints Summary | Metodo | Endpoint | Permiso | Descripcion | |--------|----------|---------|-------------| | POST | /api/v1/roles | roles:create | Crear rol | | GET | /api/v1/roles | roles:read | Listar roles | | GET | /api/v1/roles/:id | roles:read | Detalle de rol | | PATCH | /api/v1/roles/:id | roles:update | Actualizar rol | | DELETE | /api/v1/roles/:id | roles:delete | Eliminar rol | | GET | /api/v1/roles/:id/users | roles:read | Usuarios del rol | | POST | /api/v1/roles/:id/users | roles:assign | Asignar usuarios | | GET | /api/v1/permissions | permissions:read | Listar permisos | | PUT | /api/v1/users/:id/roles | roles:assign | Asignar roles a usuario | | GET | /api/v1/users/:id/permissions | - | Permisos efectivos | **Total: 10 endpoints** --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | System | Creacion inicial |