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:
194
api-gateway/src/services/config.service.ts
Normal file
194
api-gateway/src/services/config.service.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user