Add 5 auth controllers (device, MFA, permission, role, session), 18 construction entities, 5 storage services, 2 document services. Enhance auth middleware, fix budget/construction controllers. Addresses gaps from TASK-2026-02-05 analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
331 lines
9.9 KiB
TypeScript
331 lines
9.9 KiB
TypeScript
/**
|
|
* PermissionController - Controlador de Permisos
|
|
*
|
|
* Endpoints REST para gestión de permisos del sistema.
|
|
* Implementa CRUD completo con filtros y búsqueda.
|
|
*
|
|
* @module Auth
|
|
*/
|
|
|
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { DataSource } from 'typeorm';
|
|
import { PermissionService, CreatePermissionDto, UpdatePermissionDto, PermissionFilters } from '../services/permission.service';
|
|
import { AuthMiddleware } from '../middleware/auth.middleware';
|
|
import { AuthService } from '../services/auth.service';
|
|
import { Permission, Role, UserRole } from '../entities';
|
|
import { User } from '../../core/entities/user.entity';
|
|
import { Tenant } from '../../core/entities/tenant.entity';
|
|
import { RefreshToken } from '../entities/refresh-token.entity';
|
|
|
|
/**
|
|
* Crear router de permisos
|
|
* @param dataSource - DataSource de TypeORM
|
|
* @returns Router de Express configurado
|
|
*/
|
|
export function createPermissionController(dataSource: DataSource): Router {
|
|
const router = Router();
|
|
|
|
// Inicializar repositorios
|
|
const permissionRepository = dataSource.getRepository(Permission);
|
|
const userRepository = dataSource.getRepository(User);
|
|
const tenantRepository = dataSource.getRepository(Tenant);
|
|
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
|
|
|
// Inicializar servicios
|
|
const permissionService = new PermissionService(permissionRepository);
|
|
const authService = new AuthService(
|
|
userRepository,
|
|
tenantRepository,
|
|
refreshTokenRepository as any
|
|
);
|
|
|
|
// Inicializar middleware
|
|
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
|
|
|
/**
|
|
* GET /permissions
|
|
* Listar permisos con filtros y paginación
|
|
*/
|
|
router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
const filters: PermissionFilters = {
|
|
code: req.query.code as string | undefined,
|
|
module: req.query.module as string | undefined,
|
|
action: req.query.action as string | undefined,
|
|
isActive: req.query.isActive !== undefined ? req.query.isActive === 'true' : undefined,
|
|
search: req.query.search as string | undefined,
|
|
};
|
|
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
|
|
|
const result = await permissionService.findWithFilters(
|
|
{ tenantId, userId },
|
|
filters,
|
|
page,
|
|
limit
|
|
);
|
|
|
|
res.status(200).json({ success: true, data: result });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /permissions/modules
|
|
* Listar módulos disponibles
|
|
*/
|
|
router.get('/modules', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
const modules = await permissionService.getModules({ tenantId, userId });
|
|
|
|
res.status(200).json({ success: true, data: modules });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /permissions/by-role/:roleId
|
|
* Obtener permisos asignados a un rol
|
|
*/
|
|
router.get('/by-role/:roleId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const { roleId } = req.params;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!roleId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Role ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Obtener rol con sus permisos
|
|
const roleRepository = dataSource.getRepository(Role);
|
|
const role = await roleRepository.findOne({
|
|
where: { id: roleId, tenantId },
|
|
relations: ['permissions'],
|
|
});
|
|
|
|
if (!role) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Role not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: {
|
|
roleId: role.id,
|
|
roleName: role.name,
|
|
permissions: role.permissions || [],
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /permissions/:id
|
|
* Obtener detalle de un permiso
|
|
*/
|
|
router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const { id } = req.params;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
const permission = await permissionService.findById({ tenantId, userId }, id);
|
|
|
|
if (!permission) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Permission not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({ success: true, data: permission });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /permissions
|
|
* Crear nuevo permiso
|
|
*/
|
|
router.post('/', authMiddleware.authenticate, authMiddleware.requireAdmin, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const dto: CreatePermissionDto = req.body;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!dto.code || !dto.name) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Code and name are required' });
|
|
return;
|
|
}
|
|
|
|
// Validar formato de código (snake_case)
|
|
const codeRegex = /^[a-z][a-z0-9_]*$/;
|
|
if (!codeRegex.test(dto.code)) {
|
|
res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Code must be in snake_case format (lowercase letters, numbers, and underscores)',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const permission = await permissionService.create({ tenantId, userId }, dto);
|
|
|
|
res.status(201).json({ success: true, data: permission });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('already exists')) {
|
|
res.status(409).json({ error: 'Conflict', message: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /permissions/:id
|
|
* Actualizar permiso
|
|
*/
|
|
router.put('/:id', authMiddleware.authenticate, authMiddleware.requireAdmin, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const { id } = req.params;
|
|
const dto: UpdatePermissionDto = req.body;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
// Validar formato de código si se está actualizando
|
|
if (dto.code) {
|
|
const codeRegex = /^[a-z][a-z0-9_]*$/;
|
|
if (!codeRegex.test(dto.code)) {
|
|
res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Code must be in snake_case format',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const permission = await permissionService.update({ tenantId, userId }, id, dto);
|
|
|
|
if (!permission) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Permission not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({ success: true, data: permission });
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('already exists')) {
|
|
res.status(409).json({ error: 'Conflict', message: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /permissions/:id
|
|
* Eliminar permiso (soft delete)
|
|
*/
|
|
router.delete('/:id', authMiddleware.authenticate, authMiddleware.requireAdmin, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const { id } = req.params;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
const deleted = await permissionService.delete({ tenantId, userId }, id);
|
|
|
|
if (!deleted) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Permission not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({ success: true, message: 'Permission deleted successfully' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /permissions/bulk
|
|
* Crear múltiples permisos
|
|
*/
|
|
router.post('/bulk', authMiddleware.authenticate, authMiddleware.requireAdmin, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
const userId = req.user?.sub;
|
|
const { permissions } = req.body;
|
|
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID is required' });
|
|
return;
|
|
}
|
|
|
|
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Permissions array is required' });
|
|
return;
|
|
}
|
|
|
|
const created = await permissionService.bulkCreate({ tenantId, userId }, permissions);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
created: created.length,
|
|
permissions: created,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
export default createPermissionController;
|