Initial commit - Horux Despachos NL
This commit is contained in:
273
apps/api/src/controllers/usuarios.controller.ts
Normal file
273
apps/api/src/controllers/usuarios.controller.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
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';
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
async function isGlobalAdmin(req: Request): Promise<boolean> {
|
||||
return checkGlobalAdmin(req.user!.tenantId, req.user!.role);
|
||||
}
|
||||
|
||||
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],
|
||||
);
|
||||
res.json({ supervisorUserId: rows[0]?.supervisor_user_id ?? null });
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user