feat: cuentas de familia (multi-tenant), registro/login y preferencias

Convierte recordaLexia de una sola familia a multi-familia, con cuentas
propias y persistencia de preferencias.

Backend:
- Tenant Family (email único + contraseña BCrypt + PIN + prefs de cuenta);
  family_id en child/activity/material_item/reward; aislamiento por familia
  (acceso cruzado responde 404).
- Auth propia (sin Keycloak): registro/login email+contraseña, sesiones de
  familia persistidas en BD (sobreviven a reinicios), panel de padres tras PIN.
- Liquibase 002-multitenant; seeder crea una familia demo.
- Tests de aislamiento entre familias, registro/login y gate del panel.

Frontend:
- Login, registro y pantalla de cuenta; guards (sesion + PIN) e interceptor
  de sesion global; perfiles scopeados a la familia.

Preferencias:
- OpenDyslexic persistida por nino (child.dyslexiaFont) y default de cuenta.

Decisiones en docs/adr/0004.
This commit is contained in:
Jaume Garriga Maestre
2026-06-21 13:11:34 +02:00
parent 52e559a159
commit 24a0c8a0dd
72 changed files with 1959 additions and 647 deletions

View File

@@ -0,0 +1,42 @@
# ADR 0004 — Multi-tenant + registro/login propio (email/contraseña)
- **Estado:** aceptada
- **Fecha:** 2026-06-21
- **Supersede (parcialmente):** ADR 0002/0003 en lo relativo a "auth ligera sin cuentas".
## Contexto
El contrato original definía la app como **una sola familia**, con auth ligera
(PIN de padres) y datos globales. El usuario pide convertirla en **multi-familia
(multi-tenant)** con **registro/login propio** y persistencia de preferencias.
## Decisión
1. **Tenant = `Family`** (cuenta con email único + contraseña BCrypt + PIN + prefs).
Las entidades raíz (`child`, `activity`, `material_item`, `reward`) llevan
`family_id`. El resto cuelga del niño.
2. **Aislamiento**: toda consulta raíz se filtra por la familia de la sesión; las
operaciones por id verifican pertenencia y, si no, responden **404** (no 403,
para no filtrar existencia).
3. **Sesión de familia ligada al dispositivo**, persistida en BD (`family_session`)
para sobrevivir a reinicios (clave para el kiosko). Cabecera `X-Auth-Session`.
Toda la API exige sesión válida; el niño NO se loguea (el adulto deja la sesión
abierta en la tablet).
4. **Panel de padres**: además de la sesión, exige **desbloqueo con PIN**
(`POST /api/parents/unlock`), que concede el rol PARENT durante 30 min.
5. **Sin Keycloak**: auth propia encapsulada en el paquete `security`
(`AuthService`, `SessionAuthService`, `SessionAuthFilter`).
6. **Preferencia OpenDyslexic** pasa a campo por niño (`child.dyslexia_font`) + un
default de cuenta (`family.default_dyslexia_font`).
## Consecuencias
- Registro abierto (cualquiera crea una familia). Rate-limiting y verificación de
email quedan como mejora futura (homelab).
- La migración del esquema (`002-multitenant.yaml`) añade `family_id` NOT NULL; en
BD ya poblada habría que hacer backfill (en este proyecto se parte de BD limpia
con `docker compose down -v`).
- La sesión de 30 días en el dispositivo es un compromiso UX/seguridad razonable
para un kiosko doméstico; revocable borrando la fila de `family_session`.
- Sustituir la auth por un IdP externo (Keycloak) solo afectaría al paquete
`security`.