Files
collective-lists/plan/fase-1-auth-colectivo.md
Oier Bravo Urtasun f197a81a42 Record Fase 1 design decisions before implementation
- JWT strategy: Option B (GoTrue as OIDC proxy, not direct Keycloak JWT)
- Self-registration enabled in Keycloak realm
- Multiple collectives per user with sidebar switcher
- Invitations are link-only (no email/Resend integration)
- Avatar upload uses in-browser crop (cropperjs) at 1:1 ratio
- Removed Edge Functions from plan; invitation logic handled in SvelteKit server actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 14:39:11 +02:00

90 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### Fase 1 — Autenticación y Colectivo
**Duración estimada: 23 semanas**
**Objetivo: la base del dominio. Sin esto no existe nada más.**
#### 1.1 Base de datos
- [ ] Migración: tabla `users` — espejo de `auth.users` de Supabase, poblada por trigger al primer login OIDC:
- `display_name`, `email`
- `language: enum(en | es)` — default `en`, sobrescrito por preferencia de navegador en primer login
- `avatar_type: enum(initials | emoji | upload)` — default `initials`
- `avatar_emoji: text | null`
- `avatar_url: text | null` — URL firmada de Supabase Storage
- [ ] Migración: tabla `collectives` con campos `id`, `name`, `emoji`, `created_by`, `created_at`
- [ ] Migración: tabla `collective_members` con `role: enum(admin | member | guest)`
- [ ] Migración: tabla `collective_invitations` con token, expiración y estado
- [ ] **RLS completo** — políticas críticas:
- Un usuario solo puede leer colectivos a los que pertenece
- Solo administradores pueden insertar en `collective_invitations`
- Solo administradores pueden actualizar roles en `collective_members`
- Un usuario solo puede actualizar su propia fila en `users`
- [ ] Trigger: `on_auth_user_created` — sincroniza el perfil de Keycloak (display_name, email) a `users` en cada login
- [ ] Trigger: promoción automática al miembro más antiguo si el último admin elimina su cuenta (RN-10)
- [ ] Función SQL: `is_active_member(collective_id, user_id)` — usada en todas las políticas RLS
- [ ] Bucket Supabase Storage: `avatars` — privado, acceso vía signed URLs, un fichero por usuario (`{user_id}/avatar`)
#### 1.2 Integración Keycloak → Supabase
> **✅ DECISIÓN: Opción B — GoTrue como proxy OIDC.**
> GoTrue recibe el token de Keycloak, lo valida y emite su propio JWT de Supabase.
> `auth.uid()` resuelve al UUID de GoTrue (que mapea al `sub` de Keycloak).
> Esto mantiene `auth.users` de Supabase poblado y permite usar todas las helpers de GoTrue.
- [ ] Configurar Keycloak como OIDC Identity Provider en GoTrue (`supabase/config.toml`)
- [ ] Flujo: app obtiene token de Keycloak → lo intercambia por token de GoTrue → usa token de GoTrue para Supabase
- [ ] Verificar que `auth.uid()` en las políticas RLS resuelve correctamente
- [ ] Test de integración: token de Keycloak → exchange con GoTrue → query a Supabase con RLS → respuesta correcta
#### 1.3 Invitaciones (sin email)
> **✅ DECISIÓN: sin email.** Las invitaciones se crean como un link que el admin copia manualmente y comparte por cualquier canal (WhatsApp, etc.). No se integra Resend ni se envían emails.
- [ ] `collective_invitations` tiene `token` único (UUID), `expires_at`, `role`, y `accepted_at`
- [ ] Pantalla `/collective/manage` genera el link de invitación y muestra un botón "Copy link"
- [ ] `/invitation/[token]` — el invitado abre el link, se autentica si no lo está, y acepta
- [ ] Lógica de aceptación en un server action de SvelteKit (no Edge Function): validar token + expiración, insertar en `collective_members`, marcar `accepted_at`
#### 1.4 Internacionalización (i18n)
Librería: **Paraglide JS** (`@inlang/paraglide-sveltekit`). Compile-time, tree-shaken por idioma, sin overhead en runtime.
- [ ] Instalar y configurar `@inlang/paraglide-sveltekit` con el plugin de Vite
- [ ] Crear ficheros de mensajes: `messages/en.json` y `messages/es.json`
- [ ] Configurar detección de idioma: `Accept-Language` header en primer load → guardado en `users.language` tras login
- [ ] Cambio de idioma en runtime: actualizar `users.language` en Supabase + llamar a `setLanguageTag()` de Paraglide — sin recarga de página
- [ ] Traducir todas las cadenas UI de la Fase 1 (onboarding, gestión de colectivo, settings)
> Las cadenas de UI van en `messages/`. Nunca hardcodear texto visible al usuario directamente en componentes.
#### 1.5 Frontend
- [ ] Integración de `keycloak-js` en SvelteKit:
- Inicialización en `+layout.ts` con `checkLoginIframe: false` (compatibilidad PWA/Safari)
- Store Svelte `auth` — expone `user`, `token`, `isAuthenticated`, `login()`, `logout()`
- `login()` redirige al login page de Keycloak; el callback vuelve a la app con el token
- Refresco silencioso del token antes de expiración (`updateToken(60)`)
- [ ] Rutas protegidas: hook `+layout.server.ts` que verifica sesión y redirige si no hay token válido
- [ ] Flujo de onboarding post-primer-login:
- Detectar si el usuario no pertenece a ningún colectivo → redirigir a `/onboarding`
- Opción A: Crear nuevo colectivo (nombre + emoji)
- Opción B: Pegar link de invitación recibido
- [ ] Pantalla `/collective/manage` — miembros, roles, botón "Generate invite link" (copia al clipboard), expulsar
- [ ] Pantalla `/invitation/[token]` — aceptar invitación (el usuario debe estar autenticado)
- [ ] **Selector de colectivo activo** en la barra lateral — un usuario puede pertenecer a varios colectivos y cambiar entre ellos
- [ ] Store Svelte `collective` — colectivo activo, lista de miembros, rol del usuario actual, lista de todos los colectivos del usuario
- [ ] Pantalla `/settings` — configuración de usuario:
- Campo display name (input de texto, guardado automático con debounce)
- Selector de avatar con tres modos:
- *Initials* — preview generado en tiempo real desde el display name
- *Emoji* — grid de ~40 emoji curados, tap para seleccionar
- *Upload* — input de fichero; **recorte en el navegador en proporción 1:1** (usar `cropperjs` o similar); subida a Supabase Storage bucket `avatars`
- Selector de idioma (English / Español) con efecto inmediato
- Enlace externo a Keycloak Account Console para cambio de email/contraseña
- [ ] Componente `Avatar.svelte` — renderiza según `avatar_type`: initials (letra sobre fondo slate), emoji, o `<img>` con fallback a initials si la URL falla
> **Auto-registro:** Self-registration está habilitado en Keycloak. Un usuario sin cuenta que recibe un link de invitación puede registrarse en la pantalla de Keycloak y volver al callback de invitación automáticamente.
**Criterio de aceptación:** los 5 usuarios de prueba pueden autenticarse contra Keycloak, llegar a la app con sesión activa, y `auth.uid()` en Supabase resuelve correctamente para las políticas RLS.
---