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,67 @@
|
||||
# =====================================================================
|
||||
# Reinicia el portal en limpio: mata lo que ocupe el puerto, siembra la
|
||||
# base y arranca el servidor. Sembrar mientras un servidor viejo sigue
|
||||
# vivo deja errores raros ("Table doesn't exist"), así que primero se
|
||||
# libera el puerto de verdad, por PID y no por nombre de proceso.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File scripts\reiniciar.ps1
|
||||
# powershell -ExecutionPolicy Bypass -File scripts\reiniciar.ps1 -SinSembrar
|
||||
# =====================================================================
|
||||
param(
|
||||
[int]$Puerto = 4180,
|
||||
[switch]$SinSembrar
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$raiz = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $raiz
|
||||
|
||||
Write-Host "Liberando el puerto $Puerto..."
|
||||
$ocupantes = @()
|
||||
try {
|
||||
$ocupantes = Get-NetTCPConnection -LocalPort $Puerto -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty OwningProcess -Unique
|
||||
} catch {}
|
||||
# $PID es de solo lectura en PowerShell, así que la variable va con otro nombre.
|
||||
foreach ($idProceso in $ocupantes) {
|
||||
try {
|
||||
$p = Get-Process -Id $idProceso -ErrorAction Stop
|
||||
Write-Host " cerrando $($p.ProcessName) (PID $idProceso)"
|
||||
Stop-Process -Id $idProceso -Force
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# Espera a que el puerto quede realmente libre antes de seguir.
|
||||
for ($i = 0; $i -lt 20; $i++) {
|
||||
$sigue = Get-NetTCPConnection -LocalPort $Puerto -State Listen -ErrorAction SilentlyContinue
|
||||
if (-not $sigue) { break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
$sigue = Get-NetTCPConnection -LocalPort $Puerto -State Listen -ErrorAction SilentlyContinue
|
||||
if ($sigue) { throw "El puerto $Puerto sigue ocupado. Ciérralo a mano antes de seguir." }
|
||||
|
||||
if (-not $SinSembrar) {
|
||||
Write-Host "Sembrando la base..."
|
||||
& node db/seed.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "La siembra falló." }
|
||||
}
|
||||
|
||||
Write-Host "Arrancando el servidor..."
|
||||
Start-Process -FilePath 'node' -ArgumentList 'server/index.mjs' `
|
||||
-RedirectStandardOutput 'server.log' -RedirectStandardError 'server.err.log' `
|
||||
-WindowStyle Hidden | Out-Null
|
||||
|
||||
for ($i = 0; $i -lt 40; $i++) {
|
||||
Start-Sleep -Milliseconds 250
|
||||
try {
|
||||
$r = Invoke-WebRequest -Uri "http://localhost:$Puerto/api/auth/demo" -UseBasicParsing -TimeoutSec 2
|
||||
if ($r.StatusCode -eq 200) {
|
||||
Write-Host "`nListo: http://localhost:$Puerto`n" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Write-Host "`nEl servidor no respondió. Revisa server.err.log:`n" -ForegroundColor Red
|
||||
Get-Content server.err.log -ErrorAction SilentlyContinue | Select-Object -First 20
|
||||
exit 1
|
||||
@@ -0,0 +1,52 @@
|
||||
// =====================================================================
|
||||
// Detecta mojibake (UTF-8 leído como Latin-1) en los archivos del proyecto.
|
||||
// node scripts/revisar-codificacion.mjs
|
||||
// Se corre después de cualquier edición masiva: reemplazar con expresiones
|
||||
// regulares desde PowerShell rompe los acentos y el daño es difícil de ver.
|
||||
// =====================================================================
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const RAIZ = path.resolve(import.meta.dirname, '..');
|
||||
const EXTENSIONES = new Set(['.mjs', '.js', '.css', '.html', '.sql', '.md', '.json', '.yml', '.ps1']);
|
||||
const IGNORAR = new Set(['node_modules', 'tests', '.git', 'capturas', 'test-results', 'reporte']);
|
||||
|
||||
// Secuencias típicas del doble encodeo: Ã seguida de otra letra acentuada, etc.
|
||||
const SOSPECHAS = [/Ã[-¿]/, /Â[ -¿]/, /â€/, /Â/];
|
||||
|
||||
async function archivos(dir) {
|
||||
const salida = [];
|
||||
for (const e of await readdir(dir, { withFileTypes: true })) {
|
||||
if (IGNORAR.has(e.name)) continue;
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) salida.push(...await archivos(p));
|
||||
else if (EXTENSIONES.has(path.extname(e.name))) salida.push(p);
|
||||
}
|
||||
return salida;
|
||||
}
|
||||
|
||||
const sospechosos = [];
|
||||
for (const p of await archivos(RAIZ)) {
|
||||
if (p === import.meta.filename) continue; // este archivo contiene los patrones a propósito
|
||||
const texto = await readFile(p, 'utf8');
|
||||
const lineas = texto.split('\n');
|
||||
const malas = [];
|
||||
lineas.forEach((l, i) => {
|
||||
if (SOSPECHAS.some((re) => re.test(l))) malas.push({ n: i + 1, l: l.trim().slice(0, 90) });
|
||||
});
|
||||
if (malas.length) sospechosos.push({ archivo: path.relative(RAIZ, p), malas });
|
||||
}
|
||||
|
||||
if (!sospechosos.length) {
|
||||
console.log('\n ✔ Sin mojibake: los acentos están bien en todos los archivos.\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('\n ✖ Se encontró texto mal codificado:\n');
|
||||
for (const s of sospechosos) {
|
||||
console.log(` ${s.archivo}`);
|
||||
for (const m of s.malas.slice(0, 6)) console.log(` línea ${m.n}: ${m.l}`);
|
||||
if (s.malas.length > 6) console.log(` …y ${s.malas.length - 6} líneas más`);
|
||||
}
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
Reference in New Issue
Block a user