Skip to content

0039. Panel de plataforma en repo propio + enforcement server-side de mustChangePassword

Estado

Aceptada.

Contexto

Tarea platform-frontend: construir el panel web que consume la capa de plataforma (superadmin) descrita en ADR-0038. Dos decisiones no obvias surgieron durante el review del plan.

Decisión

1. Repo nuevo camaroneras_platform, no una sección del admin

Se evaluó meter la plataforma como rutas protegidas dentro de camaroneras_admin (menos setup, reusa auth/HTTP/UI) contra un proyecto separado. Se optó por repo nuevo, espejando el stack y convenciones del admin (React 19 + Vite + TS + Shadcn + TanStack Query + Zustand + React Router), para no mezclar la identidad de superadmin (god-mode cross-tenant) con el panel de tenant en el mismo bundle/sesión de navegador. Puerto dev fijo 5174.

2. mustChangePassword se enforza también en el backend, no solo en la UI

El gate de cambio de contraseña forzado inicialmente se planeó solo como UX (ProtectedRoute redirige en el cliente). Al revisar el plan se identificó el riesgo: la clave temporal del bootstrap sale de una env var (SUPERADMIN_PASSWORD) y da acceso god-mode cross-tenant; un cliente ad-hoc pegándole directo al API podría saltarse el gate de UI indefinidamente.

Se agregó PlatformPasswordChangeGuard (src/modules/auth/guards/), aplicado a nivel de clase en PlatformController (después de PlatformJwtGuard): mientras mustChangePassword=true, todo /platform/* responde 403. El chequeo hace lookup en BD (no lee el claim del JWT) para que, apenas change-password limpia el flag, el mismo access token ya pase — sin reemitir sesión. /auth/platform/me, /signout y /change-password quedan fuera del guard (si no, el usuario no podría ni ver su propio estado ni cambiar la clave).

3. Puertos de dev fijos (strictPort) para los tres frontends

Se detectó que camaroneras_admin no fijaba puerto (dependía del default de Vite + autoincremento silencioso si estaba ocupado). Sumado al nuevo camaroneras_platform, el orden de arranque determinaba qué app terminaba en qué puerto — rompiendo CORS sin aviso claro (un 403/CORS opaco en vez de un error de arranque). Se fijó puerto + strictPort: true en los tres (admin 5173, platform 5174, docs 5175, vía vite.server / vitepress.vite.server): si el puerto está tomado, el server falla al arrancar en vez de saltar a otro en silencio.

Consecuencias

  • test/platform.e2e-spec.ts se reordenó: el refresh y el change-password del owner recién sembrado (mustChangePassword=true) deben correr antes que las pruebas de supervisión (que ahora requieren la clave ya cambiada) — de lo contrario el nuevo guard las bloquea con 403. Se agregaron 3 casos: bloqueo con clave temporal, que el gate no bloquea la propia superficie de auth, y que el mismo token pasa tras cambiar la clave.
  • Cierra el ítem 💡 "mustChangePassword no se enforza en el backend" del backlog.
  • CLAUDE.md raíz documenta la tabla de puertos fijos y el 5º repo.