# Archivio milestone v2.3 — Email & Accesso **Fasi:** 23–25 · **Aperta:** 2026-06-21 · **Shipped:** 2026-07-29 (commit `27da969`) **Requisiti:** [v2.3-REQUIREMENTS.md](v2.3-REQUIREMENTS.md) > **Nota di archivio.** v2.3 è stata eseguita **fuori dal ciclo GSD**: non sono mai > esistite cartelle `phases/23`, `24`, `25` con PLAN/SUMMARY. Questo file *è* > la documentazione della milestone — non cercare altrove. ## Obiettivo Aggiungere uno strato email all'app: gate OTP per il portale cliente e invio del link preventivo dall'admin, con un'unica integrazione Resend condivisa. Il portale non doveva più essere apribile col solo link: chiunque avesse l'URL vedeva il progetto del cliente. ## Fasi ### Phase 23 — Resend Setup ✅ 2026-07-28 **Goal:** infrastruttura email condivisa. **Requisiti:** SEND-01, SEND-02 (poi ridotti — vedi sotto). Consegnato: `resend@6.18.1`, `src/lib/mailer.ts` (Result tipizzato, mai un catch silenzioso), template OTP in italiano. `RESEND_API_KEY` e `RESEND_FROM` configurate su Coolify (production **e** preview). **Riduzione di scope del 2026-07-28:** SEND-01/SEND-02 (invio del preventivo via email dall'admin) spostati al backlog. Il preventivo si manda a mano; l'automazione non serviva subito. Phase 23 si è ridotta alla sola infrastruttura Resend, che il gate OTP usa comunque. ### Phase 24 — Schema + Whitelist Admin ✅ 2026-07-28 **Goal:** l'admin gestisce la whitelist email di ogni cliente; tabelle pronte per il gate. **Requisiti:** OTP-01. **Dipende da:** Phase 23. Migration `0015_otp_access.sql`, **additiva pura**, applicata a prod via SSH prima del codice dipendente: `client_emails` (whitelist, unique case-insensitive), `otp_codes` (hash del codice, mai il codice in chiaro), `clients.sessions_valid_from` (revoca in blocco). Conteggi pre/post identici sulle tabelle protette — clients 4 / projects 5 / payments 11 / phases 10. UI: sezione "Accessi al portale" in `/admin/clients/[id]` — aggiungi/rimuovi email, "Revoca sessioni attive". Server actions in `clients/[id]/actions.ts`. ### Phase 25 — OTP Gate + Sessione ✅ 2026-07-29 **Goal:** `/client/[token]/*` richiede verifica OTP prima di mostrare la dashboard. **Requisiti:** OTP-02..OTP-07. **Dipende da:** Phase 24. Consegnato: `src/lib/otp.ts` (codice 6 cifre CSPRNG, hash SHA-256 con `NEXTAUTH_SECRET`+clientId, TTL 15 minuti, monouso, max 5 tentativi), `src/lib/client-session.ts` (cookie HMAC per-cliente `ch_sess_`, httpOnly + secure + SameSite=lax, `path=/client`), `src/lib/client-gate.ts`, le route `/api/client/otp/request|verify`, il componente `OtpGate`. **Scostamento dalla spec del 21/06:** sessione **90 giorni** invece di 30 — rientro più fluido, compensato da OTP-08 (revoca in blocco lato admin). ## Catena di dipendenze ``` Phase 23 (Resend SDK + env) └── Phase 24 (schema additivo: client_emails + otp_codes) └── Phase 25 (gate OTP + sessione cookie 90gg) ``` ## Copertura requisiti | Requisito | Fase | Esito | |---|---|---| | SEND-01, SEND-02 | 23 | ⏭ Rinviati al backlog il 2026-07-28 | | OTP-01 | 24 | ✅ | | OTP-02 … OTP-07 | 25 | ✅ | | OTP-08 (revoca) | 24 | ✅ | ## Verifica in produzione (2026-07-29, `hub.iamcavalli.net`) Gate mostrato senza cookie e **zero dati di progetto nell'HTML** (12.487 byte); email fuori e dentro whitelist danno risposta identica e solo la seconda genera un OTP; codice sbagliato rifiutato, corretto accettato; cookie `ch_sess_` con `Secure` + `HttpOnly` + `SameSite=lax` + `Max-Age=7776000`; rientro col cookie mostra la dashboard; la sessione di un cliente sull'URL di un altro mostra il gate. Nessun errore d'invio nei log del container. Dati di test rimossi, tabelle protette invariate. ## Lezioni Le due lezioni operative di questa milestone (il gate non va nel layout App Router; ricreare il dominio su Resend rigenera la chiave DKIM) sono in `STATUS.md`, sezione "Lezioni operative" — è lì che si vanno a cercare. ## Strascico alla chiusura La whitelist è stata seedata solo con `mario@test.it` (cliente di test). Tre clienti reali su quattro hanno whitelist vuota e finché lo è **il loro portale non è accessibile**. Voce aperta in `STATUS.md`.