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).
This commit is contained in:
Jaume Garriga Maestre
2026-07-13 22:41:47 +02:00
parent 6b655634ca
commit 3dfe0218a4
5 changed files with 132 additions and 1 deletions

View File

@@ -0,0 +1,105 @@
# 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`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -23,7 +23,25 @@ volver a entrar.
![Pantalla de inicio de sesión](img/01-login.png) ![Pantalla de inicio de sesión](img/01-login.png)
> 💡 Cuenta de demostración: `demo@recordalexia.local` / `demo1234`. Si aún no tienes > 💡 Cuenta de demostración: `demo@recordalexia.local` / `demo1234`. Si aún no tienes
> cuenta, pulsa **«Crear una»**. > cuenta, pulsa **«Crear una»** (te pedirá aceptar la política de privacidad —
> puedes leerla desde el propio enlace).
### Instalar como app en la tablet 📲
recordaLexia se instala **sin tienda de aplicaciones**: abre la web en el navegador
de la tablet y usa **«Añadir a pantalla de inicio»** (Chrome/Android: menú ⋮ →
«Añadir a pantalla de inicio»; Safari/iPad: botón compartir → «Añadir a pantalla
de inicio»). Aparece el icono de la mascota 🦊 y, al abrirla desde ahí, ocupa la
pantalla completa sin la barra del navegador — perfecta para el modo kiosko.
### ¿Contraseña olvidada? 🔑
En la pantalla de entrada, pulsa **«¿Has olvidado la contraseña?»**, escribe tu
email y recibirás un enlace para crear una nueva. El enlace **caduca en 30 minutos
y solo sirve una vez**; por seguridad, al cambiarla se cierran las sesiones
abiertas y hay que volver a entrar en cada dispositivo.
![Recuperar contraseña](img/14-recuperar.png)
--- ---
@@ -175,6 +193,14 @@ No. Defines la rutina una vez en el catálogo y la asignas a los días que quier
Sí, con el botón «Mañana ›» de su pantalla. Es solo de consulta (no se pueden marcar Sí, con el botón «Mañana ›» de su pantalla. Es solo de consulta (no se pueden marcar
tareas por adelantado) y siempre refleja los últimos cambios del panel de padres. tareas por adelantado) y siempre refleja los últimos cambios del panel de padres.
**¿Y si olvido la contraseña?**
Usa «¿Has olvidado la contraseña?» en la pantalla de entrada: te llegará un enlace
por email (caduca en 30 minutos y es de un solo uso).
**¿Qué pasa con mis datos si borro la cuenta?**
Se elimina TODO de forma inmediata e irreversible: niños, tareas, historial y
monedas. Los detalles están en la política de privacidad, enlazada en el registro.
**¿Qué pasa con el histórico al cambiar de día?** **¿Qué pasa con el histórico al cambiar de día?**
Se conserva. No se borran tareas ni canjes anteriores. Se conserva. No se borran tareas ni canjes anteriores.

Binary file not shown.