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
+77
View File
@@ -0,0 +1,77 @@
# =====================================================================
# Portal interno INSTA — despliegue en Coolify
# ---------------------------------------------------------------------
# Reglas de la instancia que este archivo cumple:
# · La app llega a MySQL por el NOMBRE DEL SERVICIO (mysql), no localhost.
# · No se publica ningún puerto del host: Traefik enruta al 4180 interno.
# · SERVICE_FQDN_WEB_4180 le dice a Coolify qué servicio y puerto exponer.
# · Los datos de MySQL viven en un volumen con nombre, así que sobreviven
# a un redespliegue.
# · Ninguna contraseña está aquí: todas llegan como variables de entorno.
#
# El compose de desarrollo es docker-compose.yml (solo MySQL). Este es el
# de producción y levanta también el portal.
# =====================================================================
services:
mysql:
image: mysql:8.0
restart: unless-stopped
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME:-insta_portal}
MYSQL_USER: ${DB_USER:-insta}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- 'insta_mysql_data:/var/lib/mysql'
healthcheck:
test:
- CMD-SHELL
- 'mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD} --silent'
interval: 10s
timeout: 5s
retries: 30
start_period: 40s
web:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
environment:
# Coolify sustituye esto por el dominio real y arma las rutas de Traefik.
- SERVICE_FQDN_WEB_4180
- PORT=4180
# A MySQL por nombre de servicio, nunca localhost.
- DB_HOST=mysql
- DB_PORT=3306
- 'DB_NAME=${DB_NAME:-insta_portal}'
- 'DB_USER=${DB_USER:-insta}'
- 'DB_PASSWORD=${DB_PASSWORD}'
# Detrás de Traefik: cookie Secure y la IP real para la bitácora.
- COOKIE_SEGURA=true
- CONFIAR_PROXY=true
# La demostración muestra el enlace de acceso en pantalla a propósito,
# para que cualquiera pueda entrar a probarla con un clic.
- 'MOSTRAR_ENLACE=${MOSTRAR_ENLACE:-true}'
# true en un redespliegue devuelve la demo a su estado inicial.
- 'SEMBRAR_SIEMPRE=${SEMBRAR_SIEMPRE:-false}'
- 'ENLACES_POR_VENTANA=${ENLACES_POR_VENTANA:-60}'
depends_on:
mysql:
condition: service_healthy
healthcheck:
test:
- CMD
- curl
- '-fsS'
- 'http://127.0.0.1:4180/salud'
interval: 30s
timeout: 10s
start_period: 45s
retries: 5
volumes:
insta_mysql_data: null