- 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
195 lines
6.5 KiB
TypeScript
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 })
|
|
}
|