Files
motor-de-encuestas/sprints/sprint-4/ETAPA_AUTH_PROMPT.md

6.2 KiB

ETAPA AUTH — Supabase Auth + protección /admin

1. Objetivo

Implementar autenticación email/password con Supabase Auth y proteger la ruta /admin/*. El dashboard y las vistas de admin quedan detrás de login. Sin esta etapa no se avanza a vista admin mejorada ni vista detallada.

2. Contexto

  • current_app_role() y current_org_id() ya leen del JWT — el esquema está listo.
  • No hay middleware.ts ni sistema de auth hoy — /admin/responses está abierto.
  • Organizations existentes: Empresa Demo (…0001), InQuality (…0002), Fundación Solidaridad (…0003).
  • No hay usuarios en auth.users — hay que crear el primero como parte de esta etapa.
  • La app usa anon key para el formulario público — eso NO cambia.
  • /admin/* usa service_role hoy — eso SÍ cambia: pasa a usar sesión del usuario.

3. DECISIONES YA TOMADAS — no consultar

  • Supabase Auth nativo, email/password. Sin OAuth por ahora.
  • Dos roles: platform_admin (acceso total) y org_admin (acceso a su org).
  • Roles van como custom claims en el JWT: app_role y org_id.
  • Middleware Next.js protege /admin/* — redirige a /login si no hay sesión válida.
  • Página de login en /login — simple, solo email/password, sin registro público.
  • El formulario público (/encuesta/*) NO se toca — sigue con anon key.
  • Las RPCs del dashboard usan current_app_role() y current_org_id() del JWT — cuando el usuario esté autenticado, RLS funciona automáticamente.

4. Tareas

4.1 Diagnóstico previo — mecanismo de custom claims

ANTES de implementar, reportar qué mecanismo de JWT customization está disponible en esta versión de Supabase self-hosted. Puede ser:

  • Hook SQL en auth.hooks
  • Override de función auth.jwt()
  • Edge Function hook No implementar nada hasta confirmar el mecanismo y reportarlo.

4.2 Migración — tabla user_roles + custom claims hook

Migración 20260627000001_auth_user_roles.sql:

-- Tabla de roles
CREATE TABLE public.user_roles (
  user_id  uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  app_role text NOT NULL CHECK (app_role IN ('platform_admin','org_admin')),
  org_id   uuid REFERENCES public.organizations(id)
);

-- RLS: solo el propio usuario puede ver su rol (y platform_admin ve todos)
ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;

Implementar el hook de custom claims según el mecanismo disponible confirmado en 4.1. El JWT resultante debe incluir:

{ "app_role": "platform_admin", "org_id": "10000000-0000-0000-0000-000000000002" }

4.3 Script de setup — primer usuario admin

Script operativo (NO migración) en scripts/setup_first_admin.sql:

  • Crea el usuario en auth.users vía INSERT directo o Supabase admin API
  • Inserta en user_roles con app_role='platform_admin' y org_id de InQuality
  • NO hardcodear credenciales — usar variables :email y :password con instrucciones de cómo ejecutarlo:
    docker exec -i supabase-db psql -U postgres -d postgres \
      -v email='admin@ejemplo.com' -v password='CAMBIAR' \
      < scripts/setup_first_admin.sql
    
  • Documentar en el script cómo cambiar la contraseña después del primer login

4.4 Middleware Next.js

middleware.ts en la raíz del proyecto:

  • Protege /admin/* y /dashboard/*
  • Si no hay sesión válida → redirect a /login
  • Si hay sesión → deja pasar
  • /, /encuesta/*, /login — sin restricción

4.5 Página de login

app/login/page.tsx — Client Component:

  • Formulario email/password
  • Llama supabase.auth.signInWithPassword()
  • En éxito: redirect a /admin/responses
  • En error: mensaje claro ("Email o contraseña incorrectos"), nunca trace crudo
  • Sin registro público
  • Usar clases Tailwind existentes — sin nuevo sistema de diseño

4.6 Actualizar /admin/responses

  • Reemplazar getSupabaseAdmin() (service_role) por cliente de sesión del usuario
  • RLS de Postgres filtra según app_role y org_id del JWT automáticamente
  • Agregar botón "Cerrar sesión" (supabase.auth.signOut() → redirect a /login)

5. DoD antes de reportar

  • git log --name-only -1 — listar todos los archivos tocados
  • Diagnóstico de mecanismo de custom claims reportado (4.1) antes de implementar
  • Migración 20260627000001 aplicada en VPS — verificar con:
    docker exec -i supabase-db psql -U postgres -d postgres -c "\d user_roles"
    
  • Script scripts/setup_first_admin.sql existe y está documentado
  • Hook JWT verificado — el token incluye app_role (cómo verificarlo: reportar el método)
  • /login funciona — email/password → sesión activa → redirect a /admin/responses
  • /admin/responses sin sesión → redirect a /login
  • /admin/responses con sesión → carga datos correctamente
  • Formulario público sin cambios (verificar que /encuesta/* sigue funcionando)
  • Build sin errores: npm run build
  • Commit + push a Gitea

6. Qué reportar

Diagnóstico previo (4.1):
- Mecanismo de custom claims disponible: [cuál]
- Alternativas descartadas: [por qué]

Verificado:
- Migración user_roles aplicada: ✓ — output de \d user_roles
- Hook JWT: app_role presente en token ✓/✗ — método de verificación usado
- /login funciona: ✓/✗
- /admin sin sesión → /login: ✓/✗
- /admin con sesión → datos: ✓/✗
- Formulario público sin cambios: ✓/✗
- Build: ✓/✗
- git log --name-only -1: [lista completa de archivos]

Asumido / decisiones:
- [mecanismo de hook y razonamiento]

Hallazgos:
- [lo que aparezca]

7. Qué NO hacer

  • No tocar el formulario público ni la anon key
  • No hardcodear credenciales en ningún archivo commiteado
  • No implementar registro público de usuarios
  • No usar service_role en rutas que ya tienen sesión de usuario
  • No avanzar a vista admin mejorada — eso es la etapa siguiente
  • No hacer deploy — solo commit y push a Gitea
  • No implementar el hook sin reportar primero el diagnóstico de 4.1

8. Versionado

feat(auth): Supabase Auth email/password + middleware + login [auth]

Protege /admin/* con Supabase Auth. Tabla user_roles con custom claims
(app_role, org_id) en JWT. Middleware Next.js redirige a /login sin sesión.
Página /login con email/password. /admin/responses migrado de service_role
a sesión de usuario autenticado.