8158038145
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>
8.0 KiB
8.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ClientHub
Portale clienti per consulente di personal branding. Admin area + dashboard cliente via link segreto.
Stack
Next.js 16 App Router · Neon Postgres · Drizzle ORM · Auth.js v4 · Tailwind v4 · shadcn/ui · Zod · nanoid
Commands
npm run dev·npm run build·npm run lint(lint script is bareeslint, notnext lint)- There is no test suite — no vitest/jest/playwright, no
testscript. Don't go looking for one and don't invent test commands.npm run buildis the verification of record (it typechecks). - One-off scripts:
npx tsx scripts/<name>.tswithDATABASE_URLin the env (tsxis not a devDependency — it must go throughnpx).scripts/holds three reusable utilities —seed.tsand the two Notion importers — and nothing there is part of the runtime.
Architecture
src/proxy.tsis the middleware. Next 16 names itproxy, notmiddleware— searching formiddleware.tsfinds nothing. Matcher:/admin/*,/client/*,/quote/*.- Admin auth is a double gate.
proxy.tsredirects unauthenticated/admintraffic and stamps two headers (x-admin-pathname+x-admin-gate, a digest derived fromNEXTAUTH_SECRET).src/app/admin/layout.tsxis a second, independent gate: it verifies that digest withsafeEqualand fails closed by rendering (not redirecting — that would loop) when the proxy never ran. Shared helper:src/lib/admin-gate.ts, on Web Crypto so it works in both the edge and node runtimes. Reuse those helpers; don't bypass or reimplement either gate. - Client access resolution.
/client/<x>: per-IP rate limit (src/lib/rate-limit.ts, 20/min), then an HTTP fetch tolocalhost:$PORTagainst/api/internal/validate-slug, falling back to/api/internal/validate-token. Those internal routes are guarded byINTERNAL_SECRET. The localhost base URL is deliberate (hairpin NAT inside Docker), not a leftover. - The query layers are split, and that split is what enforces LOCKED constraint #2.
src/lib/client-view.tsexposes only client-safe projections — it deliberately omitsquote_items, service prices, and payment amounts.src/lib/admin-queries.tsand the other*-queries.tsare admin-only. New client-facing queries go inclient-view.ts; never importadmin-queriesfrom a/client/*route. - Two distinct commercial artifacts, easy to confuse:
/quote/[token]→quotestable, single tier, 21-char nanoid token, served bysrc/lib/quote-service.ts/preventivo/[slug]→proposalstable, an A/B/C tier deck generated by the AI agent insrc/lib/proposal/(agent.tscalls the Anthropic SDK, output is Zod-validated byschema.ts, thenassembleProposalinassemble.tsmerges it with offer data). States:draft|published|accepted|rejected.
- Offer model:
offer_macros→offer_micros(tiers A/B/C) →services, wired through join tables (offer_tier_services,offer_phase_services, …).importOfferIntoProjectinsrc/app/admin/projects/project-actions.tsturns an offer into phases/tasks by grouping services onservices.fase. - Auth: a single admin credential from env (
ADMIN_EMAIL/ADMIN_PASSWORD) — no users table — with a stateless JWT session (src/lib/auth.ts).
Architecture Constraints (LOCKED)
clients.token= campo separato rotatable, MAI primary keyquote_itemsMAI esposti via client API — soloaccepted_totalal clientedeliverables.approved_atimmutable once set- Auth:
/client/[token]/*→ middleware token check |/admin/*→ Auth.js session - No file hosting v1 — documenti come URL esterni
Conventions
- Mutations are Server Actions, colocated as
actions.ts(or*-actions.ts) inside the route folder. There is no REST API for admin:src/app/api/holds only NextAuth, the two internal validation routes, and two client endpoints. - Migrations are hand-written SQL in
src/db/migrations/NNNN_name.sql, with gaps in the numbering (0002 doesn't exist — that's expected).drizzle.config.tsis present butdrizzle-kit generateis broken: editsrc/db/schema.tsand write the SQL by hand, keeping the two in sync. Applying a migration goes exclusively through the SSH/docker-exec procedure below — never through a throwaway script. - Slugs and tokens are bearer credentials. Never commit their values (migration files included). They're generated with
customAlphabet(CSPRNG) insrc/app/admin/clients/new/actions.ts— neverMath.random(). - Language: code and comments mix English and Italian; all user-facing UI and error messages are Italian.
- AI-generated HTML: never
dangerouslySetInnerHTMLon model output — usesrc/components/public/proposal/RichText.tsx, which whitelists bold/emphasis only.
Design System
Single source of truth: design-reference/DESIGN-SYSTEM.md ("Quiet Luxury" v1.0).
- Cardinal rule: semantic tokens only (
bg-card,text-muted-foreground,border-border) — never raw Tailwind palette classes or hex literals. That's what makes dual light/dark work off the single.darkclass toggle (FOUC guard insrc/app/layout.tsx, tokens insrc/app/globals.css). - Fonts: Plus Jakarta Sans for UI, Geist Mono for numeric/tabular cells (prices, counts, dates).
- Per-page HTML mocks live in
design-reference/pagina-*/— replicate them faithfully. - Reuse the existing primitives before building new ones:
StatusBadge,SearchInput,SegmentedToggle,editable-cell,option-select/option-multi-selectinsrc/components/ui/, and the shell insrc/components/admin/AdminShell.tsx. .planning/UI-RULES.mdand.planning/DESIGN-SYSTEM.mdare SUPERSEDED — they mandate hex literals and forbid semantic tokens, the exact inverse of the current rule. Don't follow them.
Other docs: STATUS.md (current project status + backlog) · .planning/STATE.md (GSD state, milestone v2.3) · .planning/SECURITY-*.md (2026-07 audit).
GSD Workflow
Planning in .planning/. Use /gsd-plan-phase N → /gsd-execute-phase N. State in .planning/STATE.md.
Memory Discipline
@.claude/rules/memory-discipline.md
Data Safety (LOCKED)
- Any migration, refactor, or deploy MUST NOT delete or truncate
clients,projects,payments, orphasesrows - Before running any migration: verify it only adds columns/tables — never drops or truncates production data
- Confirm explicitly before any schema change that removes a column or table used by these entities
Deploy & DB Access (procedure)
- Environments: local → Gitea (remote is named
gitea, NOTorigin) → Coolify (prod, auto-deploys on push tomain) - Prod Postgres is NOT publicly exposed. Claude has working key-based SSH to
root@178.104.27.55and applies migrations directly via docker exec — no SSH tunnel needed from the user:cat src/db/migrations/NNNN.sql | ssh root@178.104.27.55 "docker exec -i xwkk0040w0kk0gsgcgog8owk psql -U clienthub -d clienthub -v ON_ERROR_STOP=1 --single-transaction"(container =xwkk0040w0kk0gsgcgog8owk, db/user =clienthub; if the container hash changes, find it by scanningdocker psfor the one whose db has thepaymentstable). A tunnel-L 54321:localhost:54321is only needed to point local tooling at prod. - Migrations are hand-written SQL in
src/db/migrations/(drizzle-kit generate is broken). - Ordering: apply an additive migration to prod BEFORE pushing the schema-dependent code, so the live portal never queries a missing column.
Security
- Confirm before any destructive command (rm -rf, reset --hard, force push, DROP TABLE / drop-column, truncate, infra changes)
- Never print .env contents or credentials in plaintext output; using them internally to connect is fine
- Don't install packages without showing name + registry + version first
- Pushing to
mainis allowed automatically (standard local → Gitea → Coolify flow); never force-push tomain - Any change to this section: propose full new version, get approval before applying