feat(config): modulo Configuraciones admin + permisos por rol

- Tablas ConfiguracionSistema (7 configs) + RolConfiguracion (6 roles)
- Endpoints: config/sistema GET/PATCH, config/roles GET/PATCH,
  config/usuarios GET/PATCH, config/reset-roles POST, config/acciones GET
- modulosDeRol() async con fallback a MATRIZ estatica
- auth/me devuelve acciones[] segun rol del usuario
- saludMonday.estaDisponible() lee modo_local_forzado de DB (cache 30s)
- Landing: vista Configuraciones con 4 tabs (Modo, Roles, Usuarios, Sistema)
- Mi-dia: boton Descartar se deshabilita si !descartar_lead
- 630 API tests + 24 landing tests + 20 mi-dia tests verdes

Seed: npm run seed:config
This commit is contained in:
2026-09-06 09:46:19 +00:00
parent 50e6eca7da
commit b2634f97ab
35 changed files with 2572 additions and 33 deletions

View File

@@ -0,0 +1,111 @@
/**
* Endpoints de administración de configuración.
*
* Protegidos con requireAdmin (rol === 'admin') excepto /acciones que
* cualquiera autenticado puede consultar.
*/
import type { NextFunction, Request, Response } from 'express'
import { z } from 'zod'
import * as configService from '../services/config.service.js'
import { AppError, BadRequestError } from '../utils/errors.js'
// ── Middleware admin ────────────────────────────────────────────────────
export function requireAdmin(req: Request, _res: Response, next: NextFunction): void {
if (req.usuarioToken?.rol !== 'admin') {
throw new AppError(403, 'Se requiere rol admin')
}
next()
}
// ── GET /api/config/sistema ────────────────────────────────────────────
export async function obtenerConfigsEndpoint(_req: Request, res: Response): Promise<void> {
const configs = await configService.obtenerConfigs()
res.json(configs)
}
// ── PATCH /api/config/sistema ──────────────────────────────────────────
const actualizarConfigsSchema = z.record(z.string(), z.string())
export async function actualizarConfigsEndpoint(req: Request, res: Response): Promise<void> {
const parcial = actualizarConfigsSchema.parse(req.body)
if (Object.keys(parcial).length === 0) throw new BadRequestError('Body vacío')
await configService.actualizarConfigs(parcial)
res.status(204).end()
}
// ── GET /api/config/roles ──────────────────────────────────────────────
export async function obtenerRolesConfigEndpoint(_req: Request, res: Response): Promise<void> {
const roles = await configService.obtenerRolesConfig()
// Parsear JSON para que el frontend reciba objetos
res.json(
roles.map((r) => ({
rol: r.rol,
modulos: JSON.parse(r.modulos) as string[],
acciones: JSON.parse(r.acciones) as Record<string, boolean>,
updatedAt: r.updatedAt,
})),
)
}
// ── PATCH /api/config/roles/:rol ───────────────────────────────────────
const actualizarRolSchema = z.object({
modulos: z.array(z.string()),
acciones: z.record(z.string(), z.boolean()),
})
export async function actualizarRolConfigEndpoint(req: Request, res: Response): Promise<void> {
const rol = req.params.rol as string
if (!rol) throw new BadRequestError('Falta el parámetro :rol')
const body = actualizarRolSchema.parse(req.body)
await configService.actualizarRolConfig(rol, body)
res.status(204).end()
}
// ── GET /api/config/usuarios ───────────────────────────────────────────
export async function obtenerUsuariosEndpoint(_req: Request, res: Response): Promise<void> {
const usuarios = await configService.obtenerUsuarios()
res.json(usuarios)
}
// ── PATCH /api/config/usuarios/:id ─────────────────────────────────────
const actualizarUsuarioSchema = z.object({
rol: z.string().optional(),
asignacion: z.string().nullable().optional(),
activo: z.boolean().optional(),
})
export async function actualizarUsuarioEndpoint(req: Request, res: Response): Promise<void> {
const id = req.params.id as string
if (!id) throw new BadRequestError('Falta el parámetro :id')
const parsed = actualizarUsuarioSchema.parse(req.body)
// Filtra undefined para enviar solo campos con valor
const body: { rol?: string; asignacion?: string | null; activo?: boolean } = {}
if (parsed.rol !== undefined) body.rol = parsed.rol
if (parsed.asignacion !== undefined) body.asignacion = parsed.asignacion
if (parsed.activo !== undefined) body.activo = parsed.activo
if (Object.keys(body).length === 0) throw new BadRequestError('Body vacío')
await configService.actualizarUsuario(id, body)
res.status(204).end()
}
// ── POST /api/config/reset-roles ───────────────────────────────────────
export async function resetRolesEndpoint(_req: Request, res: Response): Promise<void> {
await configService.resetRoles()
res.status(204).end()
}
// ── GET /api/config/acciones ───────────────────────────────────────────
export async function obtenerAccionesEndpoint(req: Request, res: Response): Promise<void> {
if (!req.usuarioToken) throw new BadRequestError('Falta el token verificado')
const acciones = await configService.obtenerAcciones(req.usuarioToken.rol)
res.json(acciones)
}