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:
Uriel Jareth
2026-07-29 23:20:34 -06:00
co-authored by Claude Opus 5
commit 0b9c9c1dc2
34 changed files with 8307 additions and 0 deletions
+67
View File
@@ -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