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:
+194
@@ -0,0 +1,194 @@
|
||||
// =====================================================================
|
||||
// Reconstruye la base del portal desde cero: esquema + datos.
|
||||
// node db/seed.mjs (o npm run db:reset)
|
||||
// =====================================================================
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { config } from '../server/config.mjs';
|
||||
import * as D from './data.mjs';
|
||||
|
||||
const raiz = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function log(paso, detalle = '') {
|
||||
console.log(` ${paso.padEnd(34, '.')} ${detalle}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cn = await mysql.createConnection({ ...config.db, multipleStatements: true, charset: 'utf8mb4' });
|
||||
console.log(`\n▸ Portal INSTA · siembra de "${config.db.database}" en ${config.db.host}:${config.db.port}\n`);
|
||||
|
||||
const schema = await readFile(path.join(raiz, 'schema.sql'), 'utf8');
|
||||
await cn.query(schema);
|
||||
log('esquema reconstruido', `${schema.split('CREATE TABLE').length - 1} tablas`);
|
||||
|
||||
// ---- roles, permisos y usuarios -------------------------------------
|
||||
const rolId = {};
|
||||
for (const r of D.roles) {
|
||||
const [res] = await cn.execute(
|
||||
'INSERT INTO roles (clave, nombre, nivel, color, descripcion) VALUES (?,?,?,?,?)',
|
||||
[r.clave, r.nombre, r.nivel, r.color, r.descripcion],
|
||||
);
|
||||
rolId[r.clave] = res.insertId;
|
||||
}
|
||||
|
||||
const permisoId = {};
|
||||
for (const [clave, seccion, descripcion] of D.permisos) {
|
||||
const [res] = await cn.execute(
|
||||
'INSERT INTO permisos (clave, seccion, descripcion) VALUES (?,?,?)',
|
||||
[clave, seccion, descripcion],
|
||||
);
|
||||
permisoId[clave] = res.insertId;
|
||||
}
|
||||
|
||||
let asignaciones = 0;
|
||||
for (const [rolClave, lista] of Object.entries(D.rolesPermisos)) {
|
||||
const claves = lista === '*' ? Object.keys(permisoId) : lista;
|
||||
for (const c of claves) {
|
||||
if (!permisoId[c]) throw new Error(`Permiso desconocido en rolesPermisos.${rolClave}: ${c}`);
|
||||
await cn.execute('INSERT INTO roles_permisos (rol_id, permiso_id) VALUES (?,?)', [rolId[rolClave], permisoId[c]]);
|
||||
asignaciones++;
|
||||
}
|
||||
}
|
||||
log('roles y permisos', `${D.roles.length} roles · ${D.permisos.length} permisos · ${asignaciones} asignaciones`);
|
||||
|
||||
const usuarioId = {};
|
||||
for (const u of D.usuarios) {
|
||||
const [res] = await cn.execute(
|
||||
'INSERT INTO usuarios (nombre, email, puesto, rol_id, iniciales, extension, es_demo) VALUES (?,?,?,?,?,?,?)',
|
||||
[u.nombre, u.email, u.puesto, rolId[u.rol], u.iniciales, u.extension, u.es_demo],
|
||||
);
|
||||
usuarioId[u.email] = res.insertId;
|
||||
}
|
||||
log('usuarios', `${Object.keys(usuarioId).length} (3 en el acceso rápido)`);
|
||||
|
||||
// ---- oferta educativa -----------------------------------------------
|
||||
const nivelId = {};
|
||||
for (const n of D.niveles) {
|
||||
const [res] = await cn.execute('INSERT INTO niveles (clave, nombre, orden) VALUES (?,?,?)', [n.clave, n.nombre, n.orden]);
|
||||
nivelId[n.clave] = res.insertId;
|
||||
}
|
||||
|
||||
const modalidadId = {};
|
||||
for (const m of D.modalidades) {
|
||||
const [res] = await cn.execute(
|
||||
'INSERT INTO modalidades (clave, nombre, color, detalle) VALUES (?,?,?,?)',
|
||||
[m.clave, m.nombre, m.color, m.detalle],
|
||||
);
|
||||
modalidadId[m.clave] = res.insertId;
|
||||
}
|
||||
|
||||
const programaId = {};
|
||||
let horarios = 0;
|
||||
for (const p of D.programas) {
|
||||
const [res] = await cn.execute(
|
||||
`INSERT INTO programas
|
||||
(nivel_id, clave, nombre, nombre_corto, duracion, fecha_inicio,
|
||||
inscripcion_lista, inscripcion_beca, mensualidad_lista, mensualidad_beca, ciclo_semanas,
|
||||
rvoe_acuerdo, rvoe_fecha, rvoe_autoridad, rvoe_estado, rvoe_nota, beneficios, color, orden)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[nivelId[p.nivel], p.clave, p.nombre, p.nombre_corto, p.duracion, p.fecha_inicio,
|
||||
p.inscripcion_lista, p.inscripcion_beca, p.mensualidad_lista, p.mensualidad_beca, p.ciclo_semanas,
|
||||
p.rvoe_acuerdo, p.rvoe_fecha, p.rvoe_autoridad, p.rvoe_estado, p.rvoe_nota, p.beneficios, p.color, p.orden],
|
||||
);
|
||||
programaId[p.clave] = res.insertId;
|
||||
for (const h of p.horarios) {
|
||||
await cn.execute(
|
||||
'INSERT INTO programas_modalidades (programa_id, modalidad_id, dias, hora_inicio, hora_fin, nota) VALUES (?,?,?,?,?,?)',
|
||||
[res.insertId, modalidadId[h.modalidad], h.dias, h.hora_inicio, h.hora_fin, h.nota],
|
||||
);
|
||||
horarios++;
|
||||
}
|
||||
}
|
||||
log('programas', `${Object.keys(programaId).length} programas · ${horarios} horarios`);
|
||||
|
||||
let reqs = 0;
|
||||
for (const [nivelClave, lista] of Object.entries(D.requisitos)) {
|
||||
let orden = 1;
|
||||
for (const [texto, nota] of lista) {
|
||||
await cn.execute('INSERT INTO requisitos (nivel_id, texto, nota, orden) VALUES (?,?,?,?)', [nivelId[nivelClave], texto, nota, orden++]);
|
||||
reqs++;
|
||||
}
|
||||
}
|
||||
let ordenTit = 1;
|
||||
for (const [nombre, descripcion] of D.titulacionOpciones) {
|
||||
await cn.execute('INSERT INTO titulacion_opciones (nombre, descripcion, orden) VALUES (?,?,?)', [nombre, descripcion, ordenTit++]);
|
||||
}
|
||||
log('requisitos y titulación', `${reqs} requisitos · ${D.titulacionOpciones.length} formas de titulación`);
|
||||
|
||||
// ---- becas y promociones --------------------------------------------
|
||||
for (const p of D.promociones) {
|
||||
await cn.execute(
|
||||
'INSERT INTO promociones (clave, nombre, resumen, aplica_a, vigencia, acumulable, condiciones, orden) VALUES (?,?,?,?,?,?,?,?)',
|
||||
[p.clave, p.nombre, p.resumen, p.aplica_a, p.vigencia, p.acumulable, p.condiciones, p.orden],
|
||||
);
|
||||
}
|
||||
log('becas y promociones', `${D.promociones.length}`);
|
||||
|
||||
// ---- links de clases -------------------------------------------------
|
||||
let ordenClase = 1;
|
||||
for (const c of D.clases) {
|
||||
if (!programaId[c.programa]) throw new Error(`Programa desconocido en clases: ${c.programa}`);
|
||||
await cn.execute(
|
||||
`INSERT INTO clases (programa_id, materia, docente, grupo, dias, hora_inicio, hora_fin, enlace, notas, orden)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
[programaId[c.programa], c.materia, c.docente, c.grupo, c.dias, c.hora_inicio, c.hora_fin, c.enlace, c.notas, ordenClase++],
|
||||
);
|
||||
}
|
||||
log('links de clases', `${D.clases.length} en ${new Set(D.clases.map((c) => c.programa)).size} programas`);
|
||||
|
||||
// ---- conocimiento y contacto -----------------------------------------
|
||||
for (const g of D.guiones) {
|
||||
await cn.execute(
|
||||
'INSERT INTO guiones (categoria, titulo, cuerpo, canal, orden) VALUES (?,?,?,?,?)',
|
||||
[g.categoria, g.titulo, g.cuerpo, g.canal, g.orden],
|
||||
);
|
||||
}
|
||||
for (const a of D.kbArticulos) {
|
||||
await cn.execute(
|
||||
'INSERT INTO kb_articulos (seccion, pregunta, respuesta, palabras_clave, orden) VALUES (?,?,?,?,?)',
|
||||
[a.seccion, a.pregunta, a.respuesta, a.palabras_clave, a.orden],
|
||||
);
|
||||
}
|
||||
for (const i of D.institucional) {
|
||||
await cn.execute(
|
||||
'INSERT INTO institucional (grupo, clave, etiqueta, valor, orden) VALUES (?,?,?,?,?)',
|
||||
[i.grupo, i.clave, i.etiqueta, i.valor, i.orden],
|
||||
);
|
||||
}
|
||||
for (const d of D.discrepancias) {
|
||||
await cn.execute(
|
||||
'INSERT INTO discrepancias (titulo, detalle, ambito, prioridad, estado, responsable, orden) VALUES (?,?,?,?,?,?,?)',
|
||||
[d.titulo, d.detalle, d.ambito, d.prioridad, d.estado, d.responsable, d.orden],
|
||||
);
|
||||
}
|
||||
log('conocimiento y contacto', `${D.guiones.length} mensajes · ${D.kbArticulos.length} preguntas · ${D.institucional.length} datos · ${D.discrepancias.length} por confirmar`);
|
||||
|
||||
await cn.execute(
|
||||
"INSERT INTO bitacora (actor, rol, accion, entidad, detalle) VALUES ('sistema','','db.siembra','base','Base reconstruida desde la base de conocimiento INSTA')",
|
||||
);
|
||||
|
||||
// ---- verificación ----------------------------------------------------
|
||||
const [tablas] = await cn.query(
|
||||
'SELECT TABLE_NAME AS t FROM information_schema.tables WHERE table_schema = ? ORDER BY t',
|
||||
[config.db.database],
|
||||
);
|
||||
let total = 0;
|
||||
const vacias = [];
|
||||
for (const { t } of tablas) {
|
||||
const [[{ n }]] = await cn.query(`SELECT COUNT(*) AS n FROM \`${t}\``);
|
||||
total += n;
|
||||
if (n === 0) vacias.push(t);
|
||||
}
|
||||
console.log(`\n ✔ ${tablas.length} tablas · ${total} filas`);
|
||||
console.log(` ✔ vacías por diseño: ${vacias.join(', ') || 'ninguna'}`);
|
||||
console.log('\n Accesos demo: [email protected] · [email protected] · [email protected]\n');
|
||||
|
||||
await cn.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('\n✖ La siembra falló:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user