Initial commit - Horux Despachos NL
This commit is contained in:
187
apps/api/src/controllers/platform-staff.controller.ts
Normal file
187
apps/api/src/controllers/platform-staff.controller.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
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<boolean> {
|
||||
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<string, any>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user