feat(client): anteprima admin in sola lettura del portale cliente
Quando un cliente segnalava "non trovo una cosa" non c'era modo di
guardare il portale con i suoi occhi: il gate OTP lascia entrare solo
lui. Dall'elenco clienti ora un'icona apre /client/<slug>?preview=1.
getClientGate() accetta { previewRequested } e salta il gate solo se il
query param c'è E getServerSession(authOptions) è valida. Senza param
anche un admin vede il gate OTP, così il gate resta testabile dal vivo.
Ritorna preview: true senza sintetizzare una ClientSession: un admin in
anteprima non è un cliente autenticato, e confondere i due stati li
renderebbe indistinguibili proprio dove serve distinguerli.
Sola lettura perché il portale scrive davvero: /api/client/approve e
/api/client/comment autenticano sul token nel body, non sulla sessione,
e deliverables.approved_at è immutabile una volta impostato (LOCKED #3).
La protezione è a livello di UI, non di API — impedisce l'incidente, non
difende da sé stessi. Il flag passa da PreviewProvider e non per prop
drilling: ApproveButton sta quattro livelli sotto la dashboard.
Deviazione consapevole dal vincolo LOCKED #4: una route client ora legge
anche la sessione Auth.js. CLAUDE.md non è aggiornato, la sezione LOCKED
richiede approvazione esplicita.
Verificato col build di produzione contro il DB reale (sole letture):
gate OTP senza sessione admin, con cookie contraffatto e con preview=0/
abc/vuoto; portale con banner e composer disattivato con sessione valida,
sia a progetto singolo sia a due progetti. Il ramo ApproveButton non è
esercitabile dal vivo: in produzione deliverables è vuota.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+27
-7
@@ -3,9 +3,9 @@ gsd_state_version: 1.0
|
||||
milestone: v2.3
|
||||
milestone_name: Email & Accesso
|
||||
status: executing
|
||||
stopped_at: "v2.3 deployata in produzione; resta da popolare la whitelist dei 3 clienti reali"
|
||||
last_updated: "2026-07-29T21:20:00.000Z"
|
||||
last_activity: 2026-07-29 -- dominio Resend verificato, gate OTP deployato in produzione
|
||||
stopped_at: "anteprima admin del portale + occhiolino login: scritti, buildati e verificati in locale, NON ancora committati né deployati. Resta da popolare la whitelist dei 3 clienti reali"
|
||||
last_updated: "2026-08-08T12:20:00.000Z"
|
||||
last_activity: 2026-08-08 -- anteprima admin in sola lettura del portale cliente + toggle password sul login
|
||||
progress:
|
||||
total_phases: 3
|
||||
completed_phases: 3
|
||||
@@ -26,10 +26,30 @@ See: .planning/PROJECT.md (updated 2026-06-21)
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 23/24/25 completate e deployate
|
||||
Plan: `~/.claude/plans/si-ma-abbiamo-un-jaunty-micali.md`
|
||||
Status: in produzione. Resta da popolare la whitelist dei 3 clienti reali.
|
||||
Last activity: 2026-07-29 — dominio Resend verificato, deploy su `hub.iamcavalli.net`
|
||||
Phase: v2.3 (23/24/25) e v2.4 Phase 13 in produzione. Sopra, lavoro **non ancora committato**: anteprima admin del portale + occhiolino sul login.
|
||||
Plan: `~/.claude/plans/per-fare-l-accesso-all-admin-elegant-teapot.md`
|
||||
Status: scritto, buildato, verificato in locale contro il DB di produzione. **Non committato, non deployato.**
|
||||
Last activity: 2026-08-08 — anteprima admin in sola lettura del portale cliente
|
||||
|
||||
### 2026-08-08 — Anteprima admin del portale + occhiolino sul login
|
||||
|
||||
**Non è in produzione.** Codice scritto e verificato in locale (`npm run build` verde, nessun nuovo problema di lint), nessun commit e nessun push fatti.
|
||||
|
||||
**Cosa risolve.** Quando un cliente diceva "non trovo una cosa" non c'era modo di guardare il portale con i suoi occhi: il gate OTP lascia entrare solo lui. Ora l'elenco clienti (`ClientRow`) ha un'icona che apre `/client/<slug>?preview=1` in una scheda nuova.
|
||||
|
||||
**Come funziona.** `getClientGate()` accetta `{ previewRequested }` e salta il gate solo se il query param c'è **e** `getServerSession(authOptions)` è valida. Ritorna `preview: true` senza sintetizzare una `ClientSession`: un admin in anteprima non è un cliente autenticato, e confondere i due stati renderebbe i casi indistinguibili a valle. La sola lettura passa da `PreviewProvider` (`usePreview()`), consumato da `ApproveButton` e dal composer di `ChatPanel` — context e non prop drilling, perché `ApproveButton` sta quattro livelli sotto la dashboard.
|
||||
|
||||
**Perché sola lettura e non anteprima interattiva.** Il portale scrive davvero: `/api/client/approve` e `/api/client/comment` autenticano sul token nel body, non sulla sessione. Un click distratto approverebbe un deliverable, e `approved_at` è immutabile una volta impostato (vincolo LOCKED #3). La protezione è a livello di UI, non di API: un admin può ancora chiamare le route a mano. È voluto — l'obiettivo è impedire l'incidente, non difendersi da sé stessi.
|
||||
|
||||
**Deviazione consapevole dal vincolo LOCKED #4** (`/client/*` → token, `/admin/*` → Auth.js): ora una route client legge anche la sessione Auth.js. Non indebolisce nulla — per i clienti il gate OTP è identico — ma `CLAUDE.md` **non è stato ancora aggiornato**: serve l'ok dell'utente per toccare la sezione LOCKED.
|
||||
|
||||
**Verificato in locale** (build di produzione su :3100, DB di prod via tunnel SSH, sole letture): `?preview=1` **senza** sessione admin → gate OTP; con cookie di sessione contraffatto → gate OTP; `?preview=0`, `?preview=abc`, `?preview=` → gate OTP; senza query param e **con** sessione admin → gate OTP (nessuna regressione); con sessione admin valida + `?preview=1` → portale con banner e composer disattivato, sia sul cliente a progetto singolo sia su quello a due progetti (tab).
|
||||
|
||||
**Non verificato dal vivo:** il ramo `ApproveButton` — in produzione la tabella `deliverables` è **vuota** (0 righe), quindi quel pulsante oggi non si renderizza mai. Wiring controllato solo a livello di codice. Il click dell'occhiolino sul login è verificato solo nel markup renderizzato (`type="button"`, `tabindex="-1"`, `aria-label`): manca la prova a schermo, Playwright non ha browser in cache.
|
||||
|
||||
**Perché la password di `.env.local` non funzionava.** Non era un bug: `ADMIN_PASSWORD` è stata ruotata il 2026-07-28 **solo su Coolify** (vedi `SECURITY-REMEDIATION-PLAN.md`), e `.env.local` è rimasto alla precedente. Vale anche per `NEXTAUTH_SECRET`. **`.env.local` non è allineato a produzione** — non trattarlo come fonte di verità per le credenziali.
|
||||
|
||||
**Nota su `.env.local`:** contiene due righe `DATABASE_URL`; vince l'ultima (porta 54321) e punta al DB di produzione. Il DB non è raggiungibile dall'esterno: per puntarci in locale serve `ssh -f -N -L 54321:localhost:54321 root@178.104.27.55` **e** sostituire l'host con `localhost` nella stringa di connessione.
|
||||
|
||||
### ✅ Prerequisiti email risolti (2026-07-29)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user