feat: add global user administration for admin users

Backend:
- Add getAllUsuarios() to get users from all tenants
- Add updateUsuarioGlobal() to edit users and change their tenant
- Add deleteUsuarioGlobal() for global user deletion
- Add global admin check based on tenant RFC
- Add new API routes: /usuarios/global/*

Frontend:
- Add UserListItem.tenantId and tenantName fields
- Add /admin/usuarios page with full user management
- Support filtering by tenant and search
- Inline editing for name, role, and tenant assignment
- Group users by company for better organization
- Add "Admin Usuarios" menu item for admin navigation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-01-25 01:22:34 +00:00
parent 2655a51a99
commit 69efb585d3
8 changed files with 522 additions and 3 deletions

View File

@@ -1,6 +1,21 @@
import { Request, Response, NextFunction } from 'express';
import * as usuariosService from '../services/usuarios.service.js';
import { AppError } from '../utils/errors.js';
import { prisma } from '../config/database.js';
// RFC del tenant administrador global
const ADMIN_TENANT_RFC = 'AASI940812GM6';
async function isGlobalAdmin(req: Request): Promise<boolean> {
if (req.user!.role !== 'admin') return false;
const tenant = await prisma.tenant.findUnique({
where: { id: req.user!.tenantId },
select: { rfc: true },
});
return tenant?.rfc === ADMIN_TENANT_RFC;
}
export async function getUsuarios(req: Request, res: Response, next: NextFunction) {
try {
@@ -11,6 +26,21 @@ export async function getUsuarios(req: Request, res: Response, next: NextFunctio
}
}
/**
* Obtiene todos los usuarios de todas las empresas (solo admin global)
*/
export async function getAllUsuarios(req: Request, res: Response, next: NextFunction) {
try {
if (!(await isGlobalAdmin(req))) {
throw new AppError(403, 'Solo el administrador global puede ver todos los usuarios');
}
const usuarios = await usuariosService.getAllUsuarios();
res.json(usuarios);
} catch (error) {
next(error);
}
}
export async function inviteUsuario(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'admin') {
@@ -28,7 +58,8 @@ export async function updateUsuario(req: Request, res: Response, next: NextFunct
if (req.user!.role !== 'admin') {
throw new AppError(403, 'Solo administradores pueden modificar usuarios');
}
const usuario = await usuariosService.updateUsuario(req.user!.tenantId, req.params.id, req.body);
const userId = req.params.id as string;
const usuario = await usuariosService.updateUsuario(req.user!.tenantId, userId, req.body);
res.json(usuario);
} catch (error) {
next(error);
@@ -40,10 +71,49 @@ export async function deleteUsuario(req: Request, res: Response, next: NextFunct
if (req.user!.role !== 'admin') {
throw new AppError(403, 'Solo administradores pueden eliminar usuarios');
}
if (req.params.id === req.user!.id) {
const userId = req.params.id as string;
if (userId === req.user!.userId) {
throw new AppError(400, 'No puedes eliminar tu propia cuenta');
}
await usuariosService.deleteUsuario(req.user!.tenantId, req.params.id);
await usuariosService.deleteUsuario(req.user!.tenantId, userId);
res.status(204).send();
} catch (error) {
next(error);
}
}
/**
* Actualiza un usuario globalmente (puede cambiar de empresa)
*/
export async function updateUsuarioGlobal(req: Request, res: Response, next: NextFunction) {
try {
if (!(await isGlobalAdmin(req))) {
throw new AppError(403, 'Solo el administrador global puede modificar usuarios globalmente');
}
const userId = req.params.id as string;
if (userId === req.user!.userId && req.body.tenantId) {
throw new AppError(400, 'No puedes cambiar tu propia empresa');
}
const usuario = await usuariosService.updateUsuarioGlobal(userId, req.body);
res.json(usuario);
} catch (error) {
next(error);
}
}
/**
* Elimina un usuario globalmente
*/
export async function deleteUsuarioGlobal(req: Request, res: Response, next: NextFunction) {
try {
if (!(await isGlobalAdmin(req))) {
throw new AppError(403, 'Solo el administrador global puede eliminar usuarios globalmente');
}
const userId = req.params.id as string;
if (userId === req.user!.userId) {
throw new AppError(400, 'No puedes eliminar tu propia cuenta');
}
await usuariosService.deleteUsuarioGlobal(userId);
res.status(204).send();
} catch (error) {
next(error);

View File

@@ -6,9 +6,15 @@ const router = Router();
router.use(authenticate);
// Rutas por tenant
router.get('/', usuariosController.getUsuarios);
router.post('/invite', usuariosController.inviteUsuario);
router.patch('/:id', usuariosController.updateUsuario);
router.delete('/:id', usuariosController.deleteUsuario);
// Rutas globales (solo admin global)
router.get('/global/all', usuariosController.getAllUsuarios);
router.patch('/global/:id', usuariosController.updateUsuarioGlobal);
router.delete('/global/:id', usuariosController.deleteUsuarioGlobal);
export { router as usuariosRoutes };

View File

@@ -105,3 +105,93 @@ export async function deleteUsuario(tenantId: string, userId: string): Promise<v
where: { id: userId, tenantId },
});
}
/**
* Obtiene todos los usuarios de todas las empresas (solo admin global)
*/
export async function getAllUsuarios(): Promise<UserListItem[]> {
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
nombre: true,
role: true,
active: true,
lastLogin: true,
createdAt: true,
tenantId: true,
tenant: {
select: {
nombre: true,
},
},
},
orderBy: [{ tenant: { nombre: 'asc' } }, { createdAt: 'desc' }],
});
return users.map(u => ({
id: u.id,
email: u.email,
nombre: u.nombre,
role: u.role,
active: u.active,
lastLogin: u.lastLogin?.toISOString() || null,
createdAt: u.createdAt.toISOString(),
tenantId: u.tenantId,
tenantName: u.tenant.nombre,
}));
}
/**
* Actualiza un usuario globalmente (puede cambiar de tenant)
*/
export async function updateUsuarioGlobal(
userId: string,
data: UserUpdate & { tenantId?: string }
): Promise<UserListItem> {
const user = await prisma.user.update({
where: { id: userId },
data: {
...(data.nombre && { nombre: data.nombre }),
...(data.role && { role: data.role }),
...(data.active !== undefined && { active: data.active }),
...(data.tenantId && { tenantId: data.tenantId }),
},
select: {
id: true,
email: true,
nombre: true,
role: true,
active: true,
lastLogin: true,
createdAt: true,
tenantId: true,
tenant: {
select: {
nombre: true,
},
},
},
});
return {
id: user.id,
email: user.email,
nombre: user.nombre,
role: user.role,
active: user.active,
lastLogin: user.lastLogin?.toISOString() || null,
createdAt: user.createdAt.toISOString(),
tenantId: user.tenantId,
tenantName: user.tenant.nombre,
};
}
/**
* Elimina un usuario globalmente
*/
export async function deleteUsuarioGlobal(userId: string): Promise<void> {
await prisma.user.delete({
where: { id: userId },
});
}