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)
> 💡 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
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?**
Se conserva. No se borran tareas ni canjes anteriores.

Binary file not shown.