Base de conocimiento para el personal del Instituto Santiago Tapia: oferta educativa (en tarjetas y en tabla), requisitos y titulacion, becas y fechas de inscripcion, links de clases de Google Meet, preguntas frecuentes, mensajes listos y datos del instituto. Acceso con Magic Login: enlace de un solo uso, 15 minutos de vigencia, y en la base solo se guarda el SHA-256 del token. El canje es atomico, asi que dos canjes simultaneos del mismo enlace no pueden crear dos sesiones. Tres roles con 19 permisos. El catalogo lo edita direccion; los links de clases, direccion y administracion; el resto del equipo consulta. El menu se pinta con lo que el servidor autoriza y cada endpoint exige la misma clave, asi que no hay pantallas alcanzables escribiendo la direccion a mano. Toda edicion pasa por una lista blanca de campos validados y queda en la bitacora. Un precio vacio no se guarda como cero, un link que no es una direccion se rechaza, y no se puede marcar una validez oficial como confirmada sin numero de acuerdo. Pensado para leerse comodo: texto de 17 px, contraste AA, areas de toque de 44 px, tema claro y bloques plegados en telefono. La oferta, los costos, las becas, los requisitos y el contacto son los reales del instituto, tomados del CSV de oferta educativa y de la base de conocimiento. Los usuarios de acceso y los links de Meet son de demostracion. Verificado: 103 comprobaciones de API y 51 pruebas en WebKit escritorio y iPhone 13. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
46 lines
1.5 KiB
JavaScript
46 lines
1.5 KiB
JavaScript
import { exec } from './db.mjs';
|
|
import { config } from './config.mjs';
|
|
|
|
/**
|
|
* Registra una acción en la bitácora. Nunca lanza: si el registro falla,
|
|
* la petición del usuario no se cae por eso.
|
|
*/
|
|
export async function registrar(req, { accion, entidad = '', entidad_id = '', resultado = 'ok', detalle = '' }) {
|
|
const u = req.usuario ?? null;
|
|
try {
|
|
await exec(
|
|
`INSERT INTO bitacora (usuario_id, actor, rol, accion, entidad, entidad_id, resultado, detalle, ip_origen)
|
|
VALUES (?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
u?.id ?? null,
|
|
u?.nombre ?? 'anónimo',
|
|
u?.rol_clave ?? '',
|
|
accion,
|
|
String(entidad).slice(0, 48),
|
|
String(entidad_id).slice(0, 48),
|
|
resultado,
|
|
String(detalle).slice(0, 500),
|
|
ip(req),
|
|
],
|
|
);
|
|
} catch (err) {
|
|
console.error('[bitacora] no se pudo registrar:', err.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Origen de la petición. X-Forwarded-For lo pone quien llama, así que solo se
|
|
* cree cuando se declara explícitamente que hay un proxy de confianza delante
|
|
* (CONFIAR_PROXY=true). Si no, la bitácora registraría la IP que el cliente
|
|
* quiera y dejaría de servir para auditar.
|
|
*/
|
|
export function ip(req) {
|
|
let cruda = req.socket?.remoteAddress || '';
|
|
if (config.confiarProxy) {
|
|
const xff = req.headers['x-forwarded-for'];
|
|
const primera = (Array.isArray(xff) ? xff[0] : xff)?.split(',')[0]?.trim();
|
|
if (primera) cruda = primera;
|
|
}
|
|
return String(cruda).replace('::ffff:', '').slice(0, 64);
|
|
}
|