321 lines
11 KiB
TypeScript
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);
|
|
}
|
|
}
|