Files
recordalexia/docs/adr/adr-004-madurez-operativa-saas.md
Jaume Garriga Maestre 3dfe0218a4 docs: manual con PWA y recuperación de contraseña + ADR-004
Manual: instalación como app (añadir a pantalla de inicio), recuperación
de contraseña con captura real, login actualizado y FAQ ampliado (olvido
de contraseña, borrado de datos). PDF regenerado.
ADR-004: decisiones de madurez operativa SaaS (rate-limiter propio, código
de recuperación hasheado y anti-enumeración, seeder fail-safe, RGPD
verificable, PWA sin stores, producción de superficie mínima).
2026-07-13 22:41:47 +02:00

106 lines
4.9 KiB
Markdown
Raw Permalink 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.
# ADR-004: Madurez operativa para la instancia pública (SaaS Fase 1)
| Metadata | Valor |
|----------|-------|
| **Estado** | Aceptado (implementado; despliegue pendiente) |
| **Fecha** | 2026-07-13 |
| **Autor(es)** | Jaume Garriga Maestre |
| **Supersede** | — |
## Contexto
Se decide ofrecer recordaLexia como **servicio hospedado** para familias sin
conocimientos técnicos (abrir URL, registrarse, «añadir a pantalla de inicio»),
en lugar de distribuir software instalable. El multi-tenant ya existía (ADR-001);
lo que faltaba era madurez operativa para exponer públicamente una app que guarda
datos de menores: recuperación de contraseña, freno de fuerza bruta, RGPD mínimo,
PWA y despliegue reproducible con backups. Este ADR fija las decisiones técnicas
de esa transformación (cambio SDD `saas-fase-1`; artefactos en engram).
## Decisiones y trade-offs
### 1. Rate-limiter propio en memoria (no bucket4j/Redis)
`LoginAttemptService`: mapa concurrente por clave (`login:email:ip`,
`unlock:sesión`, `forgot:email:ip`), 5 fallos → bloqueo 30 s con backoff x2 (tope
1 h), comprobado **antes** de evaluar credenciales (429 + Retry-After).
- **Optimizamos**: cero dependencias e infraestructura; testeable con el `Clock`
inyectable del proyecto. El PIN de 4 dígitos pasa de fuerza-brutable en minutos
a inviable (días).
- **Sacrificamos**: el estado vive en memoria de UNA instancia.
- **Asunción que lo invalida**: si algún día hay múltiples réplicas del backend,
migrar el estado a Redis o similar.
### 2. Recuperación por código de un solo uso, hasheado, anti-enumeración
Tabla `password_reset_token` (Liquibase 005): solo el **SHA-256** del código
(el valor real viaja únicamente en el enlace del email), caducidad 30 min, un solo
uso, y emitir uno nuevo invalida los anteriores. La solicitud responde **siempre
202 con el mismo cuerpo** y el email sale `@Async` (ni el contenido ni el tiempo
de respuesta delatan si una cuenta existe). Restablecer cierra todas las sesiones
de la familia. Sin SMTP configurado, `MailService` cae a modo log (dev/CI
funcionan sin correo).
- **Optimizamos**: patrón OWASP; un volcado de BD no permite tomar cuentas.
- **Sacrificamos**: el enlace del email es el único factor — aceptable porque el
email ya es el factor de recuperación universal en consumo.
### 3. Seeder demo fail-safe
`@ConditionalOnProperty(... matchIfMissing = false)`: sin la propiedad, **no se
siembra**. El compose local y los tests que lo necesitan la activan explícitamente.
- **Optimizamos**: un despliegue olvidadizo deja la instancia pública SIN la
familia demo y su PIN público. En seguridad, el olvido debe cerrar, no abrir.
- **Sacrificamos**: el `bootRun` local necesita la variable para tener demo.
### 4. RGPD mínimo verificable
Consentimiento persistido (`family.privacy_accepted_at`, casilla no premarcada),
política de privacidad pública ES/CA, y borrado de cuenta real
(`POST /api/account/delete`, confirmado con contraseña) que elimina todos los
datos de la familia — con test que cuenta filas.
### 5. PWA en lugar de apps nativas
`@angular/pwa` con el service worker limitado a **assets** (el `/api` nunca se
cachea: el día del niño siempre fresco). Instalación vía «añadir a pantalla de
inicio»: experiencia de app sin stores, sin cuentas de desarrollador ni revisiones.
- **Asunción que lo invalida**: si hiciera falta notificación push nativa fiable
u offline profundo, reevaluar Capacitor.
### 6. Producción con superficie mínima en la VM existente
`docker-compose.prod.yml`: Postgres y backend sin puertos publicados; el frontend
solo en `127.0.0.1:8089`; el nginx nativo de la VM es el único punto público (TLS
con certbot, cabeceras de seguridad, `X-Forwarded-For` para el rate-limiter).
Backups: `pg_dump` diario con retención y copia off-VM (rclone), con la regla
operativa de que **un restore no ensayado no cuenta como backup** (runbook
`docs/operacion-saas.md`).
## Consecuencias
- (+) Una familia se da de alta y usa la app sin instalar nada; el onboarding lo
resuelve la provisión automática (ADR-003).
- (+) 44 tests backend (incl. migración contra Postgres real) fijan el
comportamiento de seguridad: anti-enumeración, caducidad, un solo uso, 429.
- (−) El operador (una persona) asume backups, certificados y SMTP → mitigado con
runbook, cron y monitorización ya existente en la VM.
- (→) CI/CD con Gitea Actions queda como cambio hermano (`saas-cicd`).
## Criterios de éxito
- [x] Flujo completo de recuperación verificado E2E (incl. sesiones invalidadas).
- [x] Fuerza bruta frenada con test (login y PIN).
- [x] Instancia sin demo por defecto (test).
- [x] PWA instalable con service worker activo.
- [ ] Despliegue público con restore ensayado (pendiente: SMTP + DNS del usuario).
## Referencias
- Artefactos SDD en engram: `sdd/saas-fase-1/{proposal,spec,design,tasks,apply-progress}`
- ADR-001 (auth propia), ADR-003 (provisión inicial)
- `backend/.../security/`, `deploy/`, `docs/operacion-saas.md`