Portal interno INSTA: base de conocimiento con Magic Login y permisos por rol
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]>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user