diff --git a/docs/adr/adr-004-madurez-operativa-saas.md b/docs/adr/adr-004-madurez-operativa-saas.md new file mode 100644 index 0000000..ac35c41 --- /dev/null +++ b/docs/adr/adr-004-madurez-operativa-saas.md @@ -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` diff --git a/docs/manual/img/01-login.png b/docs/manual/img/01-login.png index f5617e0..47bf3b4 100644 Binary files a/docs/manual/img/01-login.png and b/docs/manual/img/01-login.png differ diff --git a/docs/manual/img/14-recuperar.png b/docs/manual/img/14-recuperar.png new file mode 100644 index 0000000..72ad186 Binary files /dev/null and b/docs/manual/img/14-recuperar.png differ diff --git a/docs/manual/manual-usuario.md b/docs/manual/manual-usuario.md index 7289ae5..454fdd1 100644 --- a/docs/manual/manual-usuario.md +++ b/docs/manual/manual-usuario.md @@ -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. diff --git a/docs/manual/manual-usuario.pdf b/docs/manual/manual-usuario.pdf index feb64f3..1aea7fb 100644 Binary files a/docs/manual/manual-usuario.pdf and b/docs/manual/manual-usuario.pdf differ