Files
FRASA-CAS/api-gateway/src/services/config.service.ts
Consult AS Technology b2634f97ab 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
2026-09-06 09:46:19 +00:00

195 lines
6.5 KiB
TypeScript

/**
* Servicio de configuración del sistema.
*
* Lee/escribe las tablas ConfiguracionSistema y RolConfiguracion, y opera
* sobre usuarios (rol, asignación, activo). Singleton con cache de 30 s en
* `obtenerConfig` para no golpear la DB en cada request.
*/
import { prisma } from './prisma.js'
import { accionesPorRol, type AccionClave } from '../config/acciones.js'
import { MATRIZ } from '../config/roles.js'
import type { Modulo } from '../config/roles.js'
// ── Cache de 30 s para obtenerConfig ────────────────────────────────────
const CACHE_TTL_MS = 30_000
let cacheMap: Record<string, string> | null = null
let cacheExpiresAt = 0
function invalidarCache(): void {
cacheMap = null
cacheExpiresAt = 0
}
// ── ConfiguracionSistema ───────────────────────────────────────────────
/** Todas las configs como Record<clave, valor> */
export async function obtenerConfigs(): Promise<Record<string, string>> {
const rows = await prisma.configuracionSistema.findMany()
const result: Record<string, string> = {}
for (const r of rows) result[r.clave] = r.valor
return result
}
/** Upsert parcial de configs (solo las claves enviadas) */
export async function actualizarConfigs(parcial: Record<string, string>): Promise<void> {
await prisma.$transaction(
Object.entries(parcial).map(([clave, valor]) =>
prisma.configuracionSistema.upsert({
where: { clave },
create: { clave, valor },
update: { valor },
}),
),
)
await refrescarCache()
}
/** Una config individual (cache 30 s). Devuelve null si la clave no existe. */
export async function obtenerConfig(clave: string): Promise<string | null> {
const now = Date.now()
if (cacheMap && now < cacheExpiresAt) return cacheMap[clave] ?? null
cacheMap = await obtenerConfigs()
cacheExpiresAt = now + CACHE_TTL_MS
return cacheMap[clave] ?? null
}
/**
* Versión síncrona de obtenerConfig: lee del cache en memoria.
* Devuelve undefined si el cache no ha sido inicializado aún (el caller
* debe usar el fallback apropiado, p. ej. process.env).
* Se alimenta con `refrescarCache()` (llamar en startup y al cambiar configs).
*/
export function obtenerConfigSync(clave: string): string | undefined {
if (!cacheMap) return undefined
return cacheMap[clave]
}
/** Fuerza la recarga del cache desde la DB (llamar en startup y al guardar configs) */
export async function refrescarCache(): Promise<void> {
cacheMap = await obtenerConfigs()
cacheExpiresAt = Date.now() + CACHE_TTL_MS
}
/** Resetea el cache (para tests) */
export function resetCacheForTesting(): void {
cacheMap = null
cacheExpiresAt = 0
}
// ── RolConfiguracion ───────────────────────────────────────────────────
export interface RolConfigData {
modulos: string[]
acciones: Record<string, boolean>
}
/** Todas las configs de rol (sin parsear JSON) */
export async function obtenerRolesConfig() {
return prisma.rolConfiguracion.findMany({ orderBy: { rol: 'asc' } })
}
/** Config de un rol (JSON parseado) */
export async function obtenerRolConfig(rol: string): Promise<RolConfigData | null> {
const row = await prisma.rolConfiguracion.findUnique({ where: { rol } })
if (!row) return null
return {
modulos: JSON.parse(row.modulos) as string[],
acciones: JSON.parse(row.acciones) as Record<string, boolean>,
}
}
/** Upsert de módulos y acciones de un rol */
export async function actualizarRolConfig(
rol: string,
data: { modulos: string[]; acciones: Record<string, boolean> },
): Promise<void> {
await prisma.rolConfiguracion.upsert({
where: { rol },
create: {
rol,
modulos: JSON.stringify(data.modulos),
acciones: JSON.stringify(data.acciones),
},
update: {
modulos: JSON.stringify(data.modulos),
acciones: JSON.stringify(data.acciones),
},
})
}
/** Borra todas las RolConfiguracion y re-siembra desde MATRIZ + ACCIONES_POR_ROL_DEFAULT */
export async function resetRoles(): Promise<void> {
await prisma.rolConfiguracion.deleteMany()
const entries = Object.entries(MATRIZ) as [string, readonly Modulo[]][]
await prisma.$transaction(
entries.map(([rol, modulos]) => {
const acciones = accionesPorRol(rol)
return prisma.rolConfiguracion.create({
data: {
rol,
modulos: JSON.stringify([...modulos]),
acciones: JSON.stringify(acciones),
},
})
}),
)
}
// ── Acciones por rol ───────────────────────────────────────────────────
/** Las acciones booleanas de un rol (lee de DB; fallback a defaults) */
export async function obtenerAcciones(rol: string): Promise<Record<AccionClave, boolean>> {
const row = await prisma.rolConfiguracion.findUnique({ where: { rol } })
if (!row) return accionesPorRol(rol)
try {
return JSON.parse(row.acciones) as Record<AccionClave, boolean>
} catch {
return accionesPorRol(rol)
}
}
// ── Usuarios ───────────────────────────────────────────────────────────
export interface UsuarioListado {
id: string
username: string
nombre: string
rol: string
asignacion: string | null
activo: boolean
mondayUserId: number | null
}
/** Lista de usuarios sin passwordHash */
export async function obtenerUsuarios(): Promise<UsuarioListado[]> {
const rows = await prisma.usuario.findMany({
select: {
id: true,
username: true,
nombre: true,
rol: true,
asignacion: true,
activo: true,
mondayUserId: true,
},
orderBy: { nombre: 'asc' },
})
return rows
}
/** Actualiza rol, asignación y/o activo de un usuario */
export async function actualizarUsuario(
id: string,
data: { rol?: string; asignacion?: string | null; activo?: boolean },
): Promise<void> {
// Filtra undefined para que Prisma solo actualice los campos enviados
const clean: Record<string, unknown> = {}
if (data.rol !== undefined) clean.rol = data.rol
if (data.asignacion !== undefined) clean.asignacion = data.asignacion
if (data.activo !== undefined) clean.activo = data.activo
await prisma.usuario.update({ where: { id }, data: clean as never })
}