Files
HoruxDespachosNuevo/apps/api/src/controllers/usuarios.controller.ts

321 lines
11 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import * as usuariosService from '../services/usuarios.service.js';
import { AppError } from '../middlewares/error.middleware.js';
import { isGlobalAdmin as checkGlobalAdmin } from '../utils/global-admin.js';
import { prisma } from '../config/database.js';
const inviteSchema = z.object({
email: z.string().email('email inválido'),
nombre: z.string().min(2).max(100),
// Legacy Horux360 roles + Despacho-specific roles
role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']),
supervisorUserId: z.string().uuid().optional(), // Required when role=auxiliar
});
const updateSchema = z.object({
nombre: z.string().min(2).max(100).optional(),
// Legacy Horux360 roles + Despacho-specific roles
role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']).optional(),
active: z.boolean().optional(),
});
const updateGlobalSchema = z.object({
nombre: z.string().min(2).max(100).optional(),
role: z.enum(['owner', 'cfo', 'contador', 'visor', 'auxiliar', 'supervisor', 'cliente']).optional(),
active: z.boolean().optional(),
tenantId: z.string().uuid().optional(),
});
const createGlobalSchema = z.object({
email: z.string().email('email inválido'),
nombre: z.string().min(2).max(100),
role: z.enum(['contador', 'visor', 'auxiliar', 'supervisor', 'cliente']),
tenantId: z.string().uuid('tenantId inválido'),
supervisorUserId: z.string().uuid().optional(),
});
async function isGlobalAdmin(req: Request): Promise<boolean> {
return checkGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
}
export async function getUsuarios(req: Request, res: Response, next: NextFunction) {
try {
const usuarios = await usuariosService.getUsuarios(req.user!.tenantId);
res.json(usuarios);
} catch (error) {
next(error);
}
}
/**
* 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 !== 'owner') {
throw new AppError(403, 'Solo los dueños pueden invitar usuarios');
}
const data = inviteSchema.parse(req.body);
// Validate: auxiliar requires a supervisor
if (data.role === 'auxiliar' && !data.supervisorUserId) {
throw new AppError(400, 'Debes asignar un supervisor al auxiliar');
}
const usuario = await usuariosService.inviteUsuario(req.user!.tenantId, data);
// Store auxiliar→supervisor relationship in tenant DB
if (data.role === 'auxiliar' && data.supervisorUserId && req.tenantPool) {
await req.tenantPool.query(
`INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id)
VALUES ($1, $2) ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = $2`,
[usuario.id, data.supervisorUserId],
);
}
res.status(201).json(usuario);
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
next(error);
}
}
export async function updateUsuario(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'owner') {
throw new AppError(403, 'Solo los dueños pueden modificar usuarios');
}
const userId = req.params.id as string;
const data = updateSchema.parse(req.body);
const usuario = await usuariosService.updateUsuario(req.user!.tenantId, userId, data);
res.json(usuario);
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
next(error);
}
}
/**
* Lee el supervisor actualmente asignado a un auxiliar. Resuelve desde 3
* fuentes (en orden de prioridad):
* 1. `auxiliar_supervisores` (override explícito del owner desde /usuarios).
* 2. Cartera donde el user es `auxiliar_user_id` y la misma tiene supervisor.
* 3. Subcartera donde el user es `auxiliar_user_id`; el supervisor viene
* del cartera padre.
*
* Devuelve `null` si no aparece en ninguna.
*/
export async function getSupervisor(req: Request, res: Response, next: NextFunction) {
try {
if (!req.tenantPool) throw new AppError(500, 'Tenant pool no disponible');
const userId = String(req.params.id);
const { rows } = await req.tenantPool.query<{ supervisor_user_id: string }>(
`SELECT supervisor_user_id FROM (
SELECT supervisor_user_id, 1 AS prio FROM auxiliar_supervisores
WHERE auxiliar_user_id = $1
UNION ALL
SELECT supervisor_user_id, 2 AS prio FROM carteras
WHERE auxiliar_user_id = $1 AND supervisor_user_id IS NOT NULL
UNION ALL
SELECT p.supervisor_user_id, 3 AS prio
FROM carteras sub
JOIN carteras p ON p.id = sub.parent_id
WHERE sub.auxiliar_user_id = $1 AND p.supervisor_user_id IS NOT NULL
) t
WHERE supervisor_user_id IS NOT NULL
ORDER BY prio
LIMIT 1`,
[userId],
);
const supervisorUserId = rows[0]?.supervisor_user_id ?? null;
let supervisorNombre: string | null = null;
if (supervisorUserId) {
const u = await prisma.user.findUnique({
where: { id: supervisorUserId },
select: { nombre: true },
});
supervisorNombre = u?.nombre ?? null;
}
res.json({ supervisorUserId, supervisorNombre });
} catch (error) {
next(error);
}
}
const supervisorSchema = z.object({
supervisorUserId: z.string().uuid().nullable(),
});
/**
* Asigna o elimina el supervisor de un auxiliar (BD tenant).
* Solo owner/cfo. Pasar `null` borra la asignación.
*/
export async function updateSupervisor(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'owner' && req.user!.role !== 'cfo') {
throw new AppError(403, 'Solo el owner puede asignar supervisores');
}
if (!req.tenantPool) throw new AppError(500, 'Tenant pool no disponible');
const userId = String(req.params.id);
const { supervisorUserId } = supervisorSchema.parse(req.body);
if (supervisorUserId === null) {
await req.tenantPool.query(
`DELETE FROM auxiliar_supervisores WHERE auxiliar_user_id = $1`,
[userId],
);
} else {
await req.tenantPool.query(
`INSERT INTO auxiliar_supervisores (auxiliar_user_id, supervisor_user_id)
VALUES ($1, $2)
ON CONFLICT (auxiliar_user_id) DO UPDATE SET supervisor_user_id = $2`,
[userId, supervisorUserId],
);
}
res.json({ supervisorUserId });
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
next(error);
}
}
export async function deleteUsuario(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'owner') {
throw new AppError(403, 'Solo los dueños pueden eliminar usuarios');
}
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, userId);
res.status(204).send();
} catch (error) {
next(error);
}
}
/**
* Crea un usuario globalmente (solo admin global)
*/
export async function createUsuarioGlobal(req: Request, res: Response, next: NextFunction) {
try {
if (!(await isGlobalAdmin(req))) {
throw new AppError(403, 'Solo el administrador global puede crear usuarios');
}
const data = createGlobalSchema.parse(req.body);
if (data.role === 'auxiliar' && !data.supervisorUserId) {
throw new AppError(400, 'Debes asignar un supervisor al auxiliar');
}
const usuario = await usuariosService.createUsuarioGlobal(data.tenantId, {
email: data.email,
nombre: data.nombre,
role: data.role,
supervisorUserId: data.supervisorUserId,
});
res.status(201).json(usuario);
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
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;
const data = updateGlobalSchema.parse(req.body);
if (userId === req.user!.userId && data.tenantId) {
throw new AppError(400, 'No puedes cambiar tu propia empresa');
}
const usuario = await usuariosService.updateUsuarioGlobal(userId, data);
res.json(usuario);
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
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);
}
}
/**
* Get cliente accesos (which contribuyentes a client user can access)
*/
export async function getClienteAccesos(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'owner') throw new AppError(403, 'No autorizado');
const userId = req.params.id as string;
const { rows } = await req.tenantPool!.query(
'SELECT entidad_id AS "entidadId" FROM cliente_accesos WHERE user_id = $1',
[userId],
);
res.json({ data: rows.map(r => r.entidadId) });
} catch (error) { next(error); }
}
/**
* Set cliente accesos (replace all accesos for a client user)
*/
export async function setClienteAccesos(req: Request, res: Response, next: NextFunction) {
try {
if (req.user!.role !== 'owner') throw new AppError(403, 'No autorizado');
const userId = req.params.id as string;
const { entidadIds } = z.object({
entidadIds: z.array(z.string().uuid()),
}).parse(req.body);
// Replace all accesos
await req.tenantPool!.query('DELETE FROM cliente_accesos WHERE user_id = $1', [userId]);
for (const entidadId of entidadIds) {
await req.tenantPool!.query(
'INSERT INTO cliente_accesos (user_id, entidad_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[userId, entidadId],
);
}
res.json({ data: entidadIds });
} catch (error) {
if (error instanceof z.ZodError) return next(new AppError(400, error.errors[0].message));
next(error);
}
}