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:
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user