import type { Request, Response, NextFunction } from 'express'; import { prisma } from '../config/database.js'; import { hasPlatformRole, invalidatePlatformRolesCache, type PlatformRole } from '../utils/platform-admin.js'; import { auditFromReq } from '../utils/audit.js'; const VALID_ROLES: PlatformRole[] = ['platform_admin', 'platform_ti', 'platform_support', 'platform_sales', 'platform_finance']; const SUPERSET_ROLES: PlatformRole[] = ['platform_admin', 'platform_ti']; async function requirePlatformAdmin(req: Request, res: Response): Promise { const ok = await hasPlatformRole(req.user?.userId, 'platform_admin'); if (!ok) { res.status(403).json({ message: 'Solo platform_admin puede gestionar staff' }); } return ok; } /** * Lista users que tienen al menos un platform role + users candidatos a serlo. * Admin global (platform_admin) only. */ export async function listStaff(req: Request, res: Response, next: NextFunction) { try { if (!(await requirePlatformAdmin(req, res))) return; // Todos los users con al menos un platform role const roles = await prisma.userPlatformRole.findMany({ include: { user: { select: { id: true, email: true, nombre: true, active: true, // Tenant principal del staff: el primer membership owner por joinedAt // ASC. Se incluye solo para mostrar contexto en la UI admin. memberships: { where: { active: true, isOwner: true }, orderBy: { joinedAt: 'asc' }, take: 1, include: { tenant: { select: { id: true, nombre: true, rfc: true } } }, }, }, }, }, orderBy: { createdAt: 'desc' }, }); // Agrupa por user const byUser = new Map(); for (const r of roles) { const existing = byUser.get(r.userId); if (existing) { existing.roles.push(r.role); } else { const { memberships, ...userBase } = r.user; byUser.set(r.userId, { ...userBase, tenant: memberships[0]?.tenant ?? null, roles: [r.role], }); } } res.json(Array.from(byUser.values())); } catch (error) { next(error); } } /** * Busca users por email (para agregar nuevos staff). Admin global only. */ export async function searchUsers(req: Request, res: Response, next: NextFunction) { try { if (!(await requirePlatformAdmin(req, res))) return; const q = String(req.query.q || '').trim(); if (q.length < 2) return res.json([]); const users = await prisma.user.findMany({ where: { OR: [ { email: { contains: q, mode: 'insensitive' } }, { nombre: { contains: q, mode: 'insensitive' } }, ], }, select: { id: true, email: true, nombre: true, active: true, memberships: { where: { active: true, isOwner: true }, orderBy: { joinedAt: 'asc' }, take: 1, include: { tenant: { select: { id: true, nombre: true, rfc: true } } }, }, }, take: 10, }); res.json(users.map(u => { const { memberships, ...rest } = u; return { ...rest, tenant: memberships[0]?.tenant ?? null }; })); } catch (error) { next(error); } } /** * Asigna un rol a un user. Idempotente (si ya existe, no duplica). */ export async function grantRole(req: Request, res: Response, next: NextFunction) { try { if (!(await requirePlatformAdmin(req, res))) return; const { userId, role } = req.body; if (!userId || typeof userId !== 'string') { return res.status(400).json({ message: 'userId requerido' }); } if (!VALID_ROLES.includes(role)) { return res.status(400).json({ message: `role inválido. Valores: ${VALID_ROLES.join(', ')}` }); } const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true } }); if (!user) return res.status(404).json({ message: 'Usuario no encontrado' }); await prisma.userPlatformRole.upsert({ where: { userId_role: { userId, role } }, create: { userId, role, createdBy: req.user!.userId }, update: {}, }); invalidatePlatformRolesCache(userId); auditFromReq(req, 'platform_role.granted', { entityType: 'User', entityId: userId, metadata: { role, targetEmail: user.email }, }); res.json({ ok: true }); } catch (error) { next(error); } } /** * Quita un rol a un user. Protección: no puedes quitarte tu propio `platform_admin` * si eres el último admin (evita bootstrap problem — nadie queda con acceso). */ export async function revokeRole(req: Request, res: Response, next: NextFunction) { try { if (!(await requirePlatformAdmin(req, res))) return; const { userId, role } = req.body; if (!userId || typeof userId !== 'string') { return res.status(400).json({ message: 'userId requerido' }); } if (!VALID_ROLES.includes(role)) { return res.status(400).json({ message: 'role inválido' }); } // Protección: no quitar tu último rol superset (admin o TI) — evita bootstrap problem if (SUPERSET_ROLES.includes(role) && userId === req.user!.userId) { const supersetCount = await prisma.userPlatformRole.count({ where: { role: { in: SUPERSET_ROLES } }, }); if (supersetCount <= 1) { return res.status(400).json({ message: 'No puedes quitar tu propio rol superset — serías el último con acceso transversal. Asigna platform_admin o platform_ti a otro usuario primero.', }); } } const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } }); await prisma.userPlatformRole.deleteMany({ where: { userId, role }, }); invalidatePlatformRolesCache(userId); auditFromReq(req, 'platform_role.revoked', { entityType: 'User', entityId: userId, metadata: { role, targetEmail: user?.email }, }); res.json({ ok: true }); } catch (error) { next(error); } }