fix(auth+pwa): resolve stuck-loading race condition and PWA build error
Auth: `getSession()` in a SvelteKit load function races with Supabase's async localStorage init and can return null for a valid session, triggering a spurious login() redirect. Moved auth redirect to (app)/+layout.svelte via $effect, which correctly waits for onAuthStateChange to fire. Also fixed collective data never loading on page refresh (INITIAL_SESSION event, not SIGNED_IN). PWA: SvelteKit blocks Workbox imports in src/service-worker.ts, preventing @vite-pwa/sveltekit from finding the compiled SW output. Switched to generateSW strategy (plugin auto-generates the SW). Custom SW deferred to Fase 2b using src/sw.ts (a filename SvelteKit does not intercept). Docs: added gotchas 6–8 to CLAUDE.md covering both root causes so they are not reintroduced. Updated plan/fase-2b with the sw.ts workaround note. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
73
README.md
73
README.md
@@ -47,8 +47,8 @@ colectivo/
|
||||
│ └── web/ # SvelteKit — web + PWA
|
||||
│ ├── src/
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── supabase.ts # cliente Supabase singleton
|
||||
│ │ │ ├── auth.ts # cliente OIDC (keycloak-js) + helpers de sesión
|
||||
│ │ │ ├── supabase.ts # cliente Supabase singleton (PKCE, GoTrue auth)
|
||||
│ │ │ ├── auth.ts # login() / logout() via supabase.auth.signInWithOAuth
|
||||
│ │ │ ├── stores/ # Svelte stores globales
|
||||
│ │ │ ├── sync/ # lógica offline + cola de operaciones
|
||||
│ │ │ └── components/ # componentes UI reutilizables
|
||||
@@ -78,7 +78,8 @@ colectivo/
|
||||
├── infra/
|
||||
│ ├── docker-compose.dev.yml # Supabase + Keycloak dev (Kong en puerto 8001)
|
||||
│ ├── docker-compose.prod.yml # producción en VPS
|
||||
│ ├── kong.yml # Kong declarativo (rutas API)
|
||||
│ ├── kong.yml # Kong declarativo (rutas API, con ${VAR} placeholders)
|
||||
│ ├── kong-start.sh # entrypoint Kong: sustituye ${VAR} vía perl antes de arrancar
|
||||
│ ├── db-init/
|
||||
│ │ └── 00-role-passwords.sh # ajusta passwords de roles internos de Supabase al arrancar
|
||||
│ └── scripts/
|
||||
@@ -103,8 +104,11 @@ colectivo/
|
||||
## Justfile — Comandos de referencia
|
||||
|
||||
```bash
|
||||
just setup # preparación del entorno local (idempotente, seguro relanzar)
|
||||
just dev # levanta docker-compose.dev.yml + SvelteKit dev server
|
||||
just dev-stop # para el entorno local
|
||||
just stop # para todos los servicios (Docker stack + servidor Vite)
|
||||
just dev-stop # para solo el stack de Docker
|
||||
just dev-clean # para Docker y elimina todos los volúmenes (reset completo)
|
||||
just db-reset # drop + migrate + seed (dev)
|
||||
just db-migrate # aplica migraciones pendientes (dev)
|
||||
just db-seed # aplica supabase/seed.sql a la BD dev en marcha
|
||||
@@ -121,15 +125,34 @@ just logs # docker compose logs -f (prod)
|
||||
|
||||
---
|
||||
|
||||
## Endpoints en desarrollo local
|
||||
## Endpoints y credenciales en desarrollo local
|
||||
|
||||
| Servicio | URL | Notas |
|
||||
|----------|-----|-------|
|
||||
| App SvelteKit | http://localhost:5173 | |
|
||||
| Supabase Studio | http://localhost:54323 | |
|
||||
| Supabase API (Kong) | http://localhost:8001 | Puerto 8000 puede estar ocupado por otros servicios |
|
||||
| Keycloak Admin | http://localhost:8080/admin | admin / admin (solo dev) |
|
||||
| PostgreSQL | localhost:5432 | postgres / postgres (solo dev) |
|
||||
| Servicio | URL | Credenciales (solo dev) |
|
||||
|----------|-----|-------------------------|
|
||||
| App SvelteKit | http://localhost:5173 | — |
|
||||
| Supabase Studio | http://localhost:54323 | — |
|
||||
| Supabase API (Kong) | http://localhost:8001 | apikey: `PUBLIC_SUPABASE_ANON_KEY` del `.env` |
|
||||
| Keycloak Admin | http://localhost:8080/admin | `admin` / `admin` |
|
||||
| PostgreSQL | localhost:5432 | `postgres` / `postgres` |
|
||||
|
||||
**Secretos dev (generados en `.env`, nunca usar en producción):**
|
||||
|
||||
| Variable | Valor dev |
|
||||
|----------|-----------|
|
||||
| `POSTGRES_PASSWORD` | `postgres` |
|
||||
| `SUPABASE_JWT_SECRET` | `super-secret-jwt-token-with-at-least-32-characters-long` |
|
||||
| `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` | `admin` / `admin` |
|
||||
| `KEYCLOAK_CLIENT_SECRET` | `gotrue-dev-secret` (secreto del cliente `colectivo-web` en GoTrue) |
|
||||
|
||||
**Usuarios de prueba** (definidos en `keycloak/realm-export.json` y `supabase/seed.sql`):
|
||||
|
||||
| Usuario | Email | Contraseña | Rol en el colectivo de prueba |
|
||||
|---------|-------|-----------|-------------------------------|
|
||||
| `ana` | ana@dev.local | `test1234` | Admin |
|
||||
| `borja` | borja@dev.local | `test1234` | Member |
|
||||
| `carmen` | carmen@dev.local | `test1234` | Member |
|
||||
| `david` | david@dev.local | `test1234` | Guest |
|
||||
| `eva` | eva@dev.local | `test1234` | (sin colectivo — para testing de onboarding) |
|
||||
|
||||
---
|
||||
|
||||
@@ -139,12 +162,13 @@ Antes de arrancar la Fase 0, confirmar:
|
||||
|
||||
| Decisión | Estado |
|
||||
|----------|--------|
|
||||
| Estrategia JWT Keycloak → Supabase (Opción A vs B) | Pendiente — **Opción A recomendada** |
|
||||
| Estrategia JWT Keycloak → Supabase (Opción A vs B) | ✅ **Opción B — GoTrue como proxy OIDC** |
|
||||
| Base de datos de Keycloak en prod | ✅ **PostgreSQL independiente** |
|
||||
| Entorno de producción dockerizado | ✅ **Ya operativo** |
|
||||
| Reverse proxy en producción | ✅ **nginx existente, gestionado externamente** |
|
||||
| Self-registration habilitado en Keycloak o invitación obligatoria | Pendiente |
|
||||
| Proveedor SMTP para Keycloak y Resend (emails de invitación) | Pendiente — **Resend recomendado** |
|
||||
| Self-registration habilitado en Keycloak o invitación obligatoria | ✅ **Self-registration habilitado en Keycloak** |
|
||||
| Invitaciones: email o link manual | ✅ **Link-only — admin genera link y lo comparte manualmente** |
|
||||
| Proveedor SMTP | ✅ **No necesario para invitaciones** — Resend queda solo para Keycloak reset-password si se activa |
|
||||
| Almacenamiento de backups | Pendiente — **Backblaze B2 recomendado** |
|
||||
| Dominio y DNS del VPS | Pendiente |
|
||||
| Subdominio para Keycloak | Pendiente — `auth.tudominio.com` recomendado |
|
||||
@@ -156,10 +180,18 @@ Antes de arrancar la Fase 0, confirmar:
|
||||
|
||||
## Advertencias Técnicas
|
||||
|
||||
**Keycloak y JWT con Supabase RLS:**
|
||||
- Supabase RLS usa `auth.uid()` que lee el claim `sub` del JWT. Verificar que el `sub` de Keycloak es un UUID v4 estándar — por defecto lo es, pero algunos realms lo configuran diferente.
|
||||
- En la Opción A (JWT directo), la clave pública de Keycloak (RS256) debe estar en `JWT_SECRET` de Supabase. Si rotas las claves de firma en Keycloak, debes actualizar Supabase también.
|
||||
- Keycloak tiene sesiones propias independientes de Supabase. Un `logout()` debe invalidar en ambos lados: llamar a `keycloak.logout()` (que invalida en Keycloak) y limpiar el cliente de Supabase localmente.
|
||||
**Keycloak y JWT con Supabase RLS (Opción B — GoTrue como proxy OIDC):**
|
||||
- GoTrue recibe el token de Keycloak, lo valida contra el discovery endpoint de Keycloak, y emite su propio JWT de Supabase. `auth.uid()` resuelve al UUID de GoTrue (mapeado desde el `sub` de Keycloak).
|
||||
- El `sub` del realm de Keycloak es un UUID v4 estándar — verificado en las pruebas de aceptación.
|
||||
- No es necesario gestionar `JWT_SECRET` manualmente con la clave pública de Keycloak. GoTrue usa el endpoint OIDC de Keycloak para validar tokens.
|
||||
- El logout (`supabase.auth.signOut()`) invalida la sesión de GoTrue. La sesión SSO de Keycloak persiste hasta que expire — comportamiento esperado para esta app.
|
||||
|
||||
**Integración GoTrue + Keycloak — requisitos no obvios (ya implementados):**
|
||||
- **Cliente `colectivo-web` confidencial:** GoTrue necesita un secreto de cliente (`KEYCLOAK_CLIENT_SECRET=gotrue-dev-secret`) para el intercambio de código. Sin esto, GoTrue falla con "missing OAuth secret".
|
||||
- **Scope `openid` personalizado en Keycloak:** GoTrue v2.158.1 envía `scope=profile email` a Keycloak ignorando `GOTRUE_EXTERNAL_KEYCLOAK_SCOPES`. El endpoint userinfo de Keycloak requiere `openid` en el token. Solución: scope de cliente personalizado llamado `openid` con `include.in.token.scope=true` configurado como scope por defecto en `colectivo-web`.
|
||||
- **`GOTRUE_DISABLE_SIGNUP: "false"`:** El flag bloquea también la creación de usuarios vía OIDC. Debe ser `false`; Keycloak es el guardián del registro.
|
||||
- **`auth.users` pre-sembrado con UUIDs de Keycloak:** `seed.sql` inserta `auth.users` (con `instance_id='00000000-0000-0000-0000-000000000000'` y campos de token como strings vacíos, no NULL) y `auth.identities`. Garantiza que `auth.uid()` coincida con las claves foráneas en tablas públicas.
|
||||
- **Kong 2.8 no interpola variables de entorno:** `infra/kong-start.sh` ejecuta `perl` para sustituir `${VAR}` en `kong.yml` antes de arrancar Kong. Sin esto, Kong carga la cadena literal y rechaza todas las peticiones con 401.
|
||||
|
||||
**Keycloak self-hosted en producción:**
|
||||
- Keycloak 24+ requiere mínimo 512MB de RAM asignados. En un VPS pequeño (2GB), monitorizar el consumo conjunto con Supabase.
|
||||
@@ -171,9 +203,10 @@ Antes de arrancar la Fase 0, confirmar:
|
||||
- Esto simplifica la configuración de Supabase pero requiere configurar los Identity Providers en Keycloak Admin Console.
|
||||
|
||||
**iOS Safari y PWA:**
|
||||
- La autenticación usa el flujo PKCE de Supabase OAuth (`signInWithOAuth({ provider: 'keycloak' })`). No hay keycloak-js ni iframes de refresco silencioso — compatible con Safari de base.
|
||||
- La sesión de GoTrue se persiste en `localStorage` y se refresca automáticamente; no requiere `updateToken()` manual.
|
||||
- Background Sync API no está soportada en Safari. Implementar fallback manual con el evento `online`.
|
||||
- Push notifications solo disponibles desde iOS 16.4 y únicamente con la PWA instalada en pantalla de inicio.
|
||||
- `keycloak-js` usa iframes para el refresco silencioso del token (`checkLoginIframe`). Deshabilitarlo en la config (`checkLoginIframe: false`) porque Safari bloquea cookies de terceros en iframes — usar `updateToken()` manual en su lugar.
|
||||
- Probar el Modo Compra en un iPhone real en paralelo al desarrollo, no solo en DevTools.
|
||||
|
||||
**Supabase Realtime self-hosted:**
|
||||
|
||||
Reference in New Issue
Block a user