From 6e54efe5e43835a72a962cee24b287b1fc921681 Mon Sep 17 00:00:00 2001 From: Horux Dev Date: Fri, 29 May 2026 21:32:12 +0000 Subject: [PATCH] feat(usuarios): supervisor puede invitar usuarios cliente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend inviteUsuario: permite owner, cfo y supervisor - Backend valida que supervisor solo pueda invitar rol cliente - Backend addClienteAcceso: supervisor solo puede asignar contribuyentes que tenga visibles (getEntidadesVisibles) - Frontend: supervisor ve botón Invitar Usuario y solo puede seleccionar rol Cliente en el dropdown --- apps/api/src/controllers/contribuyente.controller.ts | 9 +++++++++ apps/api/src/controllers/usuarios.controller.ts | 9 +++++++-- apps/web/app/(dashboard)/usuarios/page.tsx | 8 ++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/api/src/controllers/contribuyente.controller.ts b/apps/api/src/controllers/contribuyente.controller.ts index 487ed5a..343a985 100644 --- a/apps/api/src/controllers/contribuyente.controller.ts +++ b/apps/api/src/controllers/contribuyente.controller.ts @@ -170,6 +170,15 @@ export async function addClienteAcceso(req: Request, res: Response, next: NextFu const { userId } = req.body; if (!userId || typeof userId !== 'string') return next(new AppError(400, 'userId requerido')); const entidadId = String(req.params.id); + + // Seguridad: supervisor solo puede asignar contribuyentes que supervise + if (req.user!.role === 'supervisor') { + const visibleIds = await getEntidadesVisibles(req.tenantPool!, req.user!.userId, req.user!.role); + if (!visibleIds.includes(entidadId)) { + return next(new AppError(403, 'No tienes acceso a este contribuyente')); + } + } + await req.tenantPool!.query( 'INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [userId, entidadId], diff --git a/apps/api/src/controllers/usuarios.controller.ts b/apps/api/src/controllers/usuarios.controller.ts index b3d98ae..51d0137 100644 --- a/apps/api/src/controllers/usuarios.controller.ts +++ b/apps/api/src/controllers/usuarios.controller.ts @@ -65,11 +65,16 @@ export async function getAllUsuarios(req: Request, res: Response, next: NextFunc export async function inviteUsuario(req: Request, res: Response, next: NextFunction) { try { - if (req.user!.role !== 'owner') { - throw new AppError(403, 'Solo los dueños pueden invitar usuarios'); + if (!['owner', 'cfo', 'supervisor'].includes(req.user!.role)) { + throw new AppError(403, 'No autorizado para invitar usuarios'); } const data = inviteSchema.parse(req.body); + // Los supervisores solo pueden invitar clientes + if (req.user!.role === 'supervisor' && data.role !== 'cliente') { + throw new AppError(403, 'Los supervisores solo pueden invitar clientes'); + } + // Validate: auxiliar requires a supervisor if (data.role === 'auxiliar' && !data.supervisorUserId) { throw new AppError(400, 'Debes asignar un supervisor al auxiliar'); diff --git a/apps/web/app/(dashboard)/usuarios/page.tsx b/apps/web/app/(dashboard)/usuarios/page.tsx index 0a9c44e..ce6aa38 100644 --- a/apps/web/app/(dashboard)/usuarios/page.tsx +++ b/apps/web/app/(dashboard)/usuarios/page.tsx @@ -77,10 +77,14 @@ export default function UsuariosPage() { const deleteUsuario = useDeleteUsuario(); const isDespacho = isDespachoTenant(currentUser?.tenantRfc); - const inviteRoles = isDespacho ? despachoInviteRoles : legacyInviteRoles; + const inviteRoles = isDespacho + ? (currentUser?.role === 'supervisor' + ? despachoInviteRoles.filter(r => r.value === 'cliente') + : despachoInviteRoles) + : legacyInviteRoles; const defaultInviteRole = isDespacho ? 'auxiliar' : 'visor'; - const isAdmin = currentUser?.role === 'owner' || currentUser?.role === 'cfo'; + const isAdmin = currentUser?.role === 'owner' || currentUser?.role === 'cfo' || currentUser?.role === 'supervisor'; const [showInvite, setShowInvite] = useState(false); const [inviteForm, setInviteForm] = useState<{ email: string; nombre: string; role: UserInvite['role']; supervisorUserId?: string }>({