feat(auth): gate OTP email sul portale cliente (v2.3 Phases 23-25)
Il portale /client/<slug> era protetto dal solo token in URL: chiunque ricevesse o intercettasse il link entrava, per sempre, senza identificarsi. Ora l'admin registra le email autorizzate per cliente e il cliente si identifica con un codice usa-e-getta prima di vedere qualsiasi dato. - Resend 6.18.1 + src/lib/mailer.ts (Result tipizzato, mai catch silenzioso) - migration 0015 (gia applicata a prod): client_emails, otp_codes, clients.sessions_valid_from. Additiva pura, conteggi verificati pre/post - admin: sezione "Accessi al portale" in /admin/clients/[id] con whitelist e revoca sessioni in blocco - gate: codice 6 cifre CSPRNG, hash SHA-256 (mai il codice in chiaro), TTL 15 min, max 5 tentativi, rate limit su entrambi gli endpoint, risposta identica per email in whitelist e non (no enumeration) - sessione: cookie HMAC per-cliente, 90 giorni, httpOnly/secure/lax Il gate sta in cima alla page, NON nel layout: nell'App Router il segmento page viene renderizzato in parallelo al layout, quindi gattare nel layout nascondeva la dashboard a schermo ma lasciava fasi, task e pagamenti nel payload RSC dell'HTML (46907 byte -> 17594 dopo il fix). Verificato. Verifica: build OK, 9/9 test E2E in locale contro il DB di produzione. NON DEPLOYARE prima di: RESEND_API_KEY+RESEND_FROM su Coolify e whitelist popolata per i 3 clienti reali (oggi vuota) - altrimenti il gate li chiude fuori dal loro portale. Checklist in .planning/STATE.md. SEND-01/02 (invio preventivo via email) rinviati a v2.4 su richiesta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+28
-27
@@ -7,22 +7,21 @@
|
||||
|
||||
### Email OTP Gate (AUTH-OTP-01)
|
||||
|
||||
Portale cliente blindato da email OTP. Nuovo `client_emails` table (whitelist) + `otp_codes` table (codice, email, expires_at, consumed). Resend come provider email. Sessione 30gg con cookie dopo verifica.
|
||||
Portale cliente blindato da email OTP. Nuovo `client_emails` table (whitelist) + `otp_codes` table (codice, email, expires_at, consumed). Resend come provider email. Sessione **90 giorni** con cookie dopo verifica, revocabile dall'admin.
|
||||
|
||||
- [ ] **OTP-01**: Admin può aggiungere e rimuovere email dalla whitelist di ogni cliente nell'admin UI
|
||||
- [ ] **OTP-02**: Cliente senza sessione OTP vede una schermata "inserisci email" invece della dashboard
|
||||
- [ ] **OTP-03**: Sistema invia OTP via Resend solo se l'email inserita è nella whitelist di quel cliente
|
||||
- [ ] **OTP-04**: Cliente inserisce il codice OTP ricevuto e ottiene sessione autenticata (cookie 30 giorni)
|
||||
- [ ] **OTP-05**: Codici OTP scadono dopo 15 minuti dall'invio
|
||||
- [ ] **OTP-06**: Endpoint OTP è rate-limited per prevenire brute force
|
||||
- [ ] **OTP-07**: Messaggi di errore OTP non rivelano se l'email è in whitelist o no (no enumeration)
|
||||
- [x] **OTP-01**: Admin può aggiungere e rimuovere email dalla whitelist di ogni cliente nell'admin UI
|
||||
- [x] **OTP-02**: Cliente senza sessione OTP vede una schermata "inserisci email" invece della dashboard
|
||||
- [x] **OTP-03**: Sistema invia OTP via Resend solo se l'email inserita è nella whitelist di quel cliente
|
||||
- [x] **OTP-04**: Cliente inserisce il codice OTP ricevuto e ottiene sessione autenticata (cookie **90 giorni**)
|
||||
- [x] **OTP-05**: Codici OTP scadono dopo 15 minuti dall'invio
|
||||
- [x] **OTP-06**: Endpoint OTP è rate-limited per prevenire brute force
|
||||
- [x] **OTP-07**: Messaggi di errore OTP non rivelano se l'email è in whitelist o no (no enumeration)
|
||||
- [x] **OTP-08**: Admin può revocare in blocco tutte le sessioni attive di un cliente
|
||||
|
||||
### Invio Link Preventivo via Email (PUB-03)
|
||||
|
||||
Admin invia link deck pubblico al lead direttamente dall'admin UI. Usa stessa infrastruttura Resend del OTP gate.
|
||||
|
||||
- [ ] **SEND-01**: Admin può inviare link `/preventivo/[slug]` via email al lead con un'azione dall'admin UI
|
||||
- [ ] **SEND-02**: Email inviata via Resend include link deck + nome cliente, template minimale in italiano
|
||||
> **[2026-07-28] Modifiche alla spec del 2026-06-21**, decise in sessione:
|
||||
> - Sessione **90 giorni** invece di 30 (rientro più fluido), compensata da OTP-08.
|
||||
> - **SEND-01/SEND-02 spostati al backlog v2.4**: il preventivo si invia a mano, l'automazione non serve ora. Phase 23 si è ridotta alla sola infrastruttura Resend, che l'OTP usa comunque.
|
||||
> - **Il gate NON sta nel layout** ma in cima a ogni page sotto `/client/[token]/`. Nell'App Router il segmento `page` viene renderizzato in parallelo al layout: gattare nel layout nascondeva la dashboard a schermo ma lasciava fasi, task e pagamenti nel payload RSC dell'HTML (verificato: 46.907 byte con i dati → 17.594 dopo il fix). Helper: `src/lib/client-gate.ts`.
|
||||
|
||||
## v2.4+ Backlog
|
||||
|
||||
@@ -30,6 +29,7 @@ Admin invia link deck pubblico al lead direttamente dall'admin UI. Usa stessa in
|
||||
|
||||
- **PROP-03**: Stripe Payment Link su deck pubblico `/preventivo/[slug]`
|
||||
- **PROP-04**: Auto-provisioning cliente/progetto/fasi al "Vinto" nel CRM
|
||||
- **SEND-01/SEND-02**: invio del link `/preventivo/[slug]` via email dall'admin UI — *rinviato da v2.3 il 2026-07-28, l'invio si fa a mano. L'infrastruttura Resend (`src/lib/mailer.ts`) è già pronta, manca solo l'azione e il pulsante.*
|
||||
|
||||
### Post-Vendita
|
||||
|
||||
@@ -48,21 +48,22 @@ Admin invia link deck pubblico al lead direttamente dall'admin UI. Usa stessa in
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| OTP-01 | Phase 24 | Pending |
|
||||
| OTP-02 | Phase 25 | Pending |
|
||||
| OTP-03 | Phase 25 | Pending |
|
||||
| OTP-04 | Phase 25 | Pending |
|
||||
| OTP-05 | Phase 25 | Pending |
|
||||
| OTP-06 | Phase 25 | Pending |
|
||||
| OTP-07 | Phase 25 | Pending |
|
||||
| SEND-01 | Phase 23 | Pending |
|
||||
| SEND-02 | Phase 23 | Pending |
|
||||
| OTP-01 | Phase 24 | ✅ Done (2026-07-28) |
|
||||
| OTP-02 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-03 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-04 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-05 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-06 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-07 | Phase 25 | ✅ Done (2026-07-28) |
|
||||
| OTP-08 | Phase 24 | ✅ Done (2026-07-28) |
|
||||
| SEND-01 | — | ⏭️ Rinviato a v2.4 |
|
||||
| SEND-02 | — | ⏭️ Rinviato a v2.4 |
|
||||
|
||||
**Coverage:**
|
||||
- v2.3 requirements: 9 total
|
||||
- Mapped to phases: 9
|
||||
- Unmapped: 0 ✓
|
||||
- v2.3 requirements: 8 in scope (OTP-01…08) + 2 rinviati
|
||||
- Implementati: 8/8 ✓ — verificati con 9 test E2E in locale contro il DB di produzione
|
||||
- **Non ancora in produzione**: il codice è scritto e testato ma NON pushato. Vedi i blocchi in `STATE.md`.
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-06-21*
|
||||
*Last updated: 2026-06-21 — traceability filled after roadmap creation*
|
||||
*Last updated: 2026-07-28 — sessione 90gg, OTP-08 aggiunto, SEND-01/02 rinviati, OTP-01…08 implementati*
|
||||
|
||||
@@ -36,9 +36,11 @@ Archivio completo: [milestones/v2.2-ROADMAP.md](milestones/v2.2-ROADMAP.md)
|
||||
|
||||
### 🔨 v2.3 — Email & Accesso (Phases 23–25)
|
||||
|
||||
- [ ] **Phase 23: Resend Setup + Invio Preventivo** — Integrazione Resend e invio link deck dall'admin
|
||||
- [ ] **Phase 24: Schema + Whitelist Admin** — Tabelle `client_emails` e `otp_codes`, admin UI gestione whitelist
|
||||
- [ ] **Phase 25: OTP Gate + Sessione** — Gate OTP completo, sessione 30gg, rate limiting, no enumeration
|
||||
- [x] **Phase 23: Resend Setup** — SDK Resend + `src/lib/mailer.ts` + template OTP *(l'invio preventivo è stato rinviato a v2.4 il 2026-07-28)*
|
||||
- [x] **Phase 24: Schema + Whitelist Admin** — Tabelle `client_emails` e `otp_codes`, admin UI gestione whitelist + revoca sessioni
|
||||
- [x] **Phase 25: OTP Gate + Sessione** — Gate OTP completo, sessione **90gg**, rate limiting, no enumeration
|
||||
|
||||
> ⚠️ **Codice completo e testato, NON ancora in produzione** (2026-07-28). Il deploy è bloccato da: `RESEND_API_KEY` assente su Coolify e whitelist vuota per 3 clienti su 4. Dettagli e checklist in `STATE.md`.
|
||||
|
||||
## Phase Details
|
||||
|
||||
@@ -95,9 +97,9 @@ Archivio completo: [milestones/v2.2-ROADMAP.md](milestones/v2.2-ROADMAP.md)
|
||||
| 20. Knowledge Base Cliente | v2.2 | 3/3 | ✅ Done | 2026-06-20 |
|
||||
| 21. Agente AI Preventivo | v2.2 | 1/1 | ✅ Done | 2026-06-20 |
|
||||
| 22. Pagina Pubblica + Deck | v2.2 | 1/1 | ✅ Done | 2026-06-20 |
|
||||
| 23. Resend Setup + Invio Preventivo | v2.3 | 0/? | Not started | — |
|
||||
| 24. Schema + Whitelist Admin | v2.3 | 0/? | Not started | — |
|
||||
| 25. OTP Gate + Sessione | v2.3 | 0/? | Not started | — |
|
||||
| 23. Resend Setup | v2.3 | 1/1 | ✅ Done | 2026-07-28 |
|
||||
| 24. Schema + Whitelist Admin | v2.3 | 1/1 | ✅ Done | 2026-07-28 |
|
||||
| 25. OTP Gate + Sessione | v2.3 | 1/1 | ✅ Done (non deployato) | 2026-07-28 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+48
-22
@@ -2,16 +2,16 @@
|
||||
gsd_state_version: 1.0
|
||||
milestone: v2.3
|
||||
milestone_name: Email & Accesso
|
||||
status: planning
|
||||
stopped_at: ""
|
||||
last_updated: "2026-06-22T09:00:00.000Z"
|
||||
last_activity: 2026-06-22 -- Lead→Cliente (A+B) in prod; cleanup tab progetto + offerta→fasi in corso
|
||||
status: executing
|
||||
stopped_at: "v2.3 implementata e testata in locale; deploy BLOCCATO su RESEND_API_KEY + whitelist clienti"
|
||||
last_updated: "2026-07-28T20:45:00.000Z"
|
||||
last_activity: 2026-07-28 -- Phases 23/24/25 (OTP gate) implementate e verificate E2E; non ancora pushate
|
||||
progress:
|
||||
total_phases: 3
|
||||
completed_phases: 0
|
||||
total_plans: 0
|
||||
completed_plans: 0
|
||||
percent: 0
|
||||
completed_phases: 3
|
||||
total_plans: 3
|
||||
completed_plans: 3
|
||||
percent: 100
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -22,20 +22,41 @@ See: .planning/PROJECT.md (updated 2026-06-21)
|
||||
|
||||
**Core value:** Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti.
|
||||
|
||||
**Current focus:** Milestone **v2.3 "Email & Accesso"** (started 2026-06-21). North-star: OTP gate per il portale cliente + invio link preventivo via email — un'unica integrazione Resend condivisa. Roadmap: `.planning/ROADMAP.md` (Phases 23–25). Prossimo passo: `/gsd-plan-phase 23`.
|
||||
**Current focus:** Milestone **v2.3 "Email & Accesso"**. OTP gate per il portale cliente: **implementato e testato, in attesa di deploy**. L'invio del preventivo via email (SEND-01/02) è stato rinviato a v2.4 — si manda a mano.
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: Pre-23 fixes & flow wiring (fuori roadmap formale)
|
||||
Plan: `.claude/plans/te-li-scrivo-tutti-lucky-hearth.md`
|
||||
Status: In esecuzione — cleanup tab progetto + collegamento offerta→fasi/task
|
||||
Last activity: 2026-06-22 — Lead→Cliente consegnato in prod
|
||||
Phase: 23/24/25 completate — v2.3 pronta ma NON in produzione
|
||||
Plan: `~/.claude/plans/si-ma-abbiamo-un-jaunty-micali.md`
|
||||
Status: **Bloccata sul deploy**, non sul codice. Vedi "Blocchi al deploy" qui sotto.
|
||||
Last activity: 2026-07-28 — gate OTP completo, 9/9 test E2E passati contro il DB di produzione
|
||||
|
||||
### Lavoro recente (pre-fase-23, in prod)
|
||||
### ⛔ Blocchi al deploy (2026-07-28)
|
||||
|
||||
- **Tassonomie**: gestione centralizzata categorie/tag in Impostazioni (modello Notion, pool persistenti, `src/lib/taxonomy.ts`).
|
||||
- **Lead → Cliente (A+B)**: campi `clients.email/phone` + `leads.archived` (migrazione 0011 applicata a prod); `convertLeadToClient` (riusa `createClientCore`, porta i transcript, archivia il lead mantenendo "won"); tasto Converti/Convertito; lead archiviati nascosti da lista/kanban.
|
||||
- **In corso**: rimozione tab Preventivo dal progetto, riordino sidebar, tab Offerte rifatta, import offerta→fasi/task per `services.fase`.
|
||||
Pushare così com'è **chiude fuori i clienti reali dal loro portale**. Prima del push servono, in quest'ordine:
|
||||
|
||||
1. **`RESEND_API_KEY` + `RESEND_FROM` su Coolify** (production). Senza, nessuno riceve il codice e il portale è inaccessibile a tutti. Serve un account Resend con dominio verificato. In `.env.local` i due nomi ci sono ma il valore della key è vuoto.
|
||||
2. **Whitelist popolata per i 3 clienti reali.** La migration ha seedato `client_emails` da `clients.email`, ma solo 1 cliente su 4 aveva quel campo valorizzato (`mario@test.it`, cliente di test "Rossi Inc"). George Vlad / Protocollo Estetico, Gian Luca Caruso / Caruso Speaker e Gianfranco Barban / Teckell hanno **whitelist vuota** → gate senza via d'uscita. Da fare da `/admin/clients/<id>` → sezione "Accessi al portale".
|
||||
3. Solo dopo: push su `main` → auto-deploy Coolify → riverifica su `hub.iamcavalli.net`.
|
||||
|
||||
### Cosa è stato consegnato in v2.3 (codice locale, buildato e testato)
|
||||
|
||||
- **Resend**: `resend@6.18.1`, `src/lib/mailer.ts` (Result tipizzato, mai catch silenzioso) + template OTP in italiano.
|
||||
- **Schema**: migration `0015_otp_access.sql` **già applicata a prod** — `client_emails` (whitelist, unique case-insensitive), `otp_codes` (hash del codice, mai il codice), `clients.sessions_valid_from` (revoca). Additiva pura: conteggi pre/post identici su clients 4 / projects 5 / payments 11 / phases 10.
|
||||
- **Admin**: sezione "Accessi al portale" in `/admin/clients/[id]` — aggiungi/rimuovi email + "Revoca sessioni attive". Server actions in `clients/[id]/actions.ts`. Scritta a token semantici benché la pagina attorno sia ancora a palette vecchia.
|
||||
- **Gate**: `src/lib/otp.ts` (codice 6 cifre CSPRNG, hash SHA-256 con `NEXTAUTH_SECRET`+clientId, TTL 15 min, max 5 tentativi), `src/lib/client-session.ts` (cookie HMAC per-cliente `ch_sess_<id>`, 90 giorni, httpOnly/secure/lax, path=/client), `src/lib/client-gate.ts`, route `/api/client/otp/request|verify`, componente `OtpGate`.
|
||||
|
||||
### ⚠️ Lezione: il gate NON va nel layout
|
||||
|
||||
Prima implementazione: gate in `client/[token]/layout.tsx` che rendeva `<OtpGate/>` al posto di `{children}`. **Non funziona come protezione.** Nell'App Router il segmento `page` viene renderizzato in parallelo al layout: la dashboard spariva a schermo ma fasi, task e pagamenti restavano leggibili nel payload RSC dell'HTML (46.907 byte → 17.594 dopo il fix). Il gate è ora in cima alla `page`, prima di ogni query, via `getClientGate()`. **Ogni nuova route sotto `/client/[token]/` deve fare lo stesso** — il layout porta un commento che lo ricorda.
|
||||
|
||||
### Lavoro recente precedente (in prod)
|
||||
|
||||
- **[2026-07-27/28] Audit di sicurezza**: 4 vulnerabilità chiuse e deployate (secondo gate admin, hardening slug, XSS, CSP/HSTS); slug clienti deboli ruotati a 12 char CSPRNG; `INTERNAL_SECRET` e `ADMIN_PASSWORD` configurati su Coolify. Finding #1 (password Postgres committata) declassato CRITICO→BASSO: verificata inattiva, già ruotata. Report in `.planning/SECURITY-*.md`.
|
||||
- **[2026-07-28] Riorganizzazione cartella**: fasi di planning consolidate, script one-off archiviati in `cestino/`, `CLAUDE.md` arricchito.
|
||||
- **Design system "Quiet Luxury"**: dashboard, liste (Clienti/Offerte/Catalogo/Preventivi/Progetti), Conversazioni, Impostazioni, Pipeline+Kanban, dettaglio Lead e portale cliente base sono a token e dual-theme. **Ancora a design vecchio** (funzionanti, solo estetica): `/admin/offers/[id]/edit`, `/admin/projects/[id]` (il cluster peggiore, ~140 occorrenze fra i suoi tab), `/admin/projects/new`, `/admin/clients/[id]`, `/admin/clients/[id]/edit`, `/admin/login`, badge in `/admin/preventivi/[id]`, chat portale cliente — più, non censiti prima: **tutto `/quote/[token]`** (~40 occorrenze, pagina rivolta al cliente) e `ui/dialog.tsx`, che propaga la palette vecchia a ogni modale.
|
||||
- **Tassonomie**: gestione centralizzata categorie/tag in Impostazioni (`src/lib/taxonomy.ts`).
|
||||
- **Lead → Cliente (A+B)**: `clients.email/phone` + `leads.archived` (migration 0011); `convertLeadToClient`.
|
||||
|
||||
### Fasi completate (v2.2, storico)
|
||||
|
||||
@@ -98,8 +119,10 @@ None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
- **Migrations (sempre valido)**: ogni fase con schema (Phase 24: `client_emails` + `otp_codes`) DEVE avere la migration applicata a prod via tunnel SSH (`ssh -L 54321:localhost:54321 root@178.104.27.55`, `DATABASE_URL` riscritto a `127.0.0.1:54321`) PRIMA di pushare il codice dipendente. `drizzle-kit generate` rotto → SQL a mano.
|
||||
- **Resend env vars**: `RESEND_API_KEY` e `RESEND_FROM` devono essere aggiunti a Coolify prima di testare Phase 23 in prod. Stessa procedura di `ANTHROPIC_API_KEY` (2026-06-20).
|
||||
- **[BLOCCANTE] `RESEND_API_KEY` + `RESEND_FROM` su Coolify** — senza, il gate OTP deployato rende il portale inaccessibile a tutti i clienti. Stessa procedura di `ANTHROPIC_API_KEY` (2026-06-20). Serve un account Resend con dominio verificato.
|
||||
- **[BLOCCANTE] Whitelist vuota per 3 clienti su 4** — vedi "Blocchi al deploy". Da popolare da `/admin/clients/<id>` prima del push.
|
||||
- **Migrations (sempre valido)**: ogni fase con schema DEVE avere la migration applicata a prod PRIMA di pushare il codice dipendente. `drizzle-kit generate` rotto → SQL a mano. La 0015 è già applicata. Due strade: `cat migration.sql | ssh root@178.104.27.55 "docker exec -i xwkk0040w0kk0gsgcgog8owk psql -U clienthub -d clienthub -v ON_ERROR_STOP=1 --single-transaction"` (autoritativa, nessun tunnel), oppure tunnel `ssh -f -N -L 54321:localhost:54321 root@178.104.27.55` con `DATABASE_URL` riscritto a `127.0.0.1:54321` se serve puntarci il tooling locale.
|
||||
- **`.env.local` punta al DB di PRODUZIONE** (178.104.27.55:54321, richiede il tunnel). Non esiste un DB di sviluppo separato: qualsiasi test in locale scrive su dati reali. Verificare sempre i conteggi delle tabelle protette prima e dopo.
|
||||
- **Debito tecnico (non bloccante)**: tabelle legacy `service_catalog`/`offer_services`/`offer_micro_services` restano come deadweight; `createService`/`serviceSchema` dead code in `catalog/actions.ts`.
|
||||
|
||||
## Deferred Items
|
||||
@@ -113,9 +136,12 @@ Items acknowledged and carried forward from previous milestone close:
|
||||
| v2+ | Phase 13 — Servizi attivi/ricorrenti post-vendita | Congelata | v2.1 kickoff |
|
||||
| v2 | OFFER-14 — Sezioni analitiche stile Notion | Backlog | v2.1 kickoff |
|
||||
| v2 | ARCH-01 — Split modulo "compartimento stagno" in deploy separato | Backlog (only if module grows) | v2.1 kickoff |
|
||||
| v2.4 | SEND-01/02 — Invio link preventivo via email dall'admin | Backlog (mailer già pronto) | 2026-07-28 |
|
||||
| Design | 11 pagine ancora a palette vecchia — vedi elenco in "Lavoro recente" | Backlog | 2026-07-28 |
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-21T11:05:00.000Z
|
||||
Stopped at: Roadmap v2.3 created — Phases 23–25, 9/9 requirements mapped. Next: `/gsd-plan-phase 23`
|
||||
Resume file: .planning/ROADMAP.md
|
||||
Last session: 2026-07-28T20:45:00.000Z
|
||||
Stopped at: v2.3 implementata e verificata E2E in locale (9/9). **Non pushata**: mancano `RESEND_API_KEY` su Coolify e la whitelist dei 3 clienti reali.
|
||||
Next: configurare Resend → popolare le whitelist → push su `main` → riverifica su `hub.iamcavalli.net`.
|
||||
Resume file: .planning/STATE.md
|
||||
|
||||
Reference in New Issue
Block a user