);
}
```
Per `ClientDashboardView`: leggere il file attuale di /c/[token]/page.tsx per capire come è strutturata la dashboard corrente. Il componente `ClientDashboardView` è probabilmente già esistente o il rendering è inline. Adattare seguendo ESATTAMENTE la struttura attuale:
- Se il file corrente ha un componente separato (es. ClientDashboard o simile) → riutilizzarlo, passando `view` invece di `clientView`
- Se il rendering è inline → estrarlo in una funzione helper `ClientDashboardView` nello stesso file
- I dati che `ClientDashboardView` riceve vengono ora da `ProjectView` invece di `ClientView` — adattare le prop references
CRITICO: verificare che `ClientDashboardView` NON abbia accesso a quote_items — deve usare solo i dati di `ProjectView` (phases, payments con solo status, documents, notes, comments).
Il campo `accepted_total` da mostrare viene da `view.project.accepted_total` (non dal client-level).
**B. Aggiornare src/app/admin/clients/[id]/edit/page.tsx**
Aggiungere il campo slug con:
1. Input field con label "Slug personalizzato"
2. Validazione Zod: `slug: z.string().regex(/^[a-z0-9-]{3,50}$/).optional().or(z.literal(""))` — stringa vuota = nessuno slug
3. Preview del link risultante: `/{slug || client.token}`
4. Testo help: "Solo lettere minuscole, numeri e trattini (es. mario-rossi). Min 3, max 50 caratteri."
Leggere il file per trovare il form attuale e aggiungere il campo slug nel form esistente. L'action di salvataggio deve aggiornare `clients.slug` oltre ai campi esistenti.
Schema Zod da aggiungere/aggiornare per il campo slug:
```typescript
const updateClientSchema = z.object({
// ... existing fields ...
slug: z.string().regex(/^[a-z0-9-]{3,50}$/).optional().or(z.literal("")).transform(v => v === "" ? null : v),
});
```
Nel form HTML:
```html
Solo lettere minuscole, numeri e trattini (es. mario-rossi). Min 3, max 50 caratteri.
{/* Link preview */}
Link cliente: /c/{client.slug || client.token}
```
Nella server action che salva, aggiungere l'update di `clients.slug`:
```typescript
// Se slug è stringa vuota, settarlo a null (rimuove lo slug)
await db.update(clients).set({
// ...existing fields...
slug: parsed.slug ?? null,
}).where(eq(clients.id, clientId));
```
Aggiungere anche gestione errore per unique constraint violation (se lo slug è già usato da un altro cliente), mostrando un messaggio user-friendly.
npm run build 2>&1 | tail -20
- src/app/c/[token]/page.tsx contains `getClientWithProjectsByToken` (grep)
- src/app/c/[token]/page.tsx contains `projects.length === 1` (grep — single project direct view logic)
- src/app/c/[token]/page.tsx contains `Tabs` import (grep — multi-project tabs)
- src/app/c/[token]/page.tsx does NOT contain `quote_items` anywhere (grep)
- src/app/admin/clients/[id]/edit/page.tsx contains `slug` input field (grep: `grep "name=\"slug\"" src/app/admin/clients/\[id\]/edit/page.tsx`)
- src/app/admin/clients/[id]/edit/page.tsx contains `/^[a-z0-9-]{3,50}$/` validation pattern (grep)
- `npm run build` completa senza errori TypeScript
Dashboard cliente funziona con singolo progetto (vista diretta) e multi-progetto (tabs); slug impostabile dall'admin
Funzionalità complete di Phase 04:
1. Schema multi-project con FK migrate (04-01)
2. Admin projects list + create + client detail con project cards (04-02)
3. Admin project workspace con timer project-scoped e analytics profittabilità (04-03)
4. Slug resolution middleware + dashboard cliente multi-project + slug edit (questo piano)
Eseguire `npm run dev` e verificare manualmente:
**Test 1 — Admin projects list (/admin/projects)**
- Aprire /admin/projects
- Verificare che la pagina carichi senza errori
- Verificare colonne: Progetto (con nome cliente sotto), Valore, Acconto, Saldo, Timer, €/h
**Test 2 — Creazione progetto**
- Aprire /admin e cliccare su un cliente
- Verificare che /admin/clients/[id] mostri project cards (non più il workspace tab)
- Cliccare "+ Nuovo Progetto" e creare un progetto
- Verificare che il redirect vada a /admin/projects/[id]
**Test 3 — Workspace progetto (/admin/projects/[id])**
- Aprire /admin/projects/[id] per il progetto appena creato
- Verificare tutti i tabs: Fasi & Task, Pagamenti, Documenti, Note, Commenti, Preventivo, Timer
- Nel tab Timer: verificare play/stop funziona, ProfitabilityCard mostra ore lavorate, €/h, costo ideale, delta
**Test 4 — Impostazioni (/admin/impostazioni)**
- Aprire /admin/impostazioni
- Verificare form con campo tariffa oraria target (default 50.00)
- Cambiare il valore, salvare, ricaricare — verificare che il nuovo valore sia persistito
- Aprire /admin/projects/[id] → tab Timer → verificare che la tariffa target aggiornata appaia nella ProfitabilityCard
**Test 5 — Slug cliente**
- Aprire /admin/clients/[id]/edit per un cliente
- Impostare slug "mario-rossi" (o simile)
- Salvare e verificare che non ci siano errori
- Aprire /c/mario-rossi → verificare che carichi la dashboard del cliente corretto
**Test 6 — Fallback token**
- Con lo stesso cliente che ha lo slug impostato, aprire /c/[token-originale]
- Verificare che carichi correttamente (fallback token deve funzionare)
**Test 7 — Dashboard multi-progetto**
- Per il cliente di test, creare un secondo progetto
- Aprire /c/[token-o-slug] del cliente
- Verificare che appaiano le tabs con i nomi dei due progetti
- Cliccare tra i tabs e verificare che i dati siano scoped al progetto corretto
**Test 8 — Dashboard singolo progetto**
- Per un cliente con 1 solo progetto, aprire /c/[token]
- Verificare che NON appaiano tabs — la dashboard si apre direttamente sul progetto
Digitare "approvato" se tutti i test passano, oppure descrivere gli errori trovati per correzione.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Public internet → /c/[slug-or-token] | Chiunque con il link accede alla dashboard; il middleware valida prima slug poi token — accesso bloccato se entrambi falliscono |
| Client dashboard → DB | getProjectView NON espone quote_items né payment amounts — invarianti CLAUDE.md + DASH-07 |
| Admin edit → clients.slug | Il campo slug è validato con regex e aggiornato solo in sessione admin autenticata |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-14 | Information Disclosure | getProjectView — payments | mitigate | SELECT include solo id, label, status — amount escluso esplicitamente. Commento nel codice documenta il motivo (DASH-07 + CLAUDE.md). grep di test in acceptance criteria verifica l'assenza di amount |
| T-04-15 | Information Disclosure | getProjectView — quote_items | mitigate | quote_items NON importato in client-view.ts. Acceptance criteria include grep check `grep "quote_items" src/lib/client-view.ts` → deve essere assente |
| T-04-16 | Tampering | clients.slug — unique constraint | mitigate | DB unique constraint su clients.slug previene slug duplicati; server action cattura unique violation e mostra errore user-friendly |
| T-04-17 | Spoofing | Slug collisione con token esistente | accept | Slug regex [a-z0-9-]{3,50} non può collidere con nanoid tokens (che usano anche maiuscole e caratteri speciali); middleware prova prima slug poi token nell'ordine corretto (D-06) |
| T-04-18 | Information Disclosure | Dashboard multi-project tabs — dati cross-project | mitigate | Ogni getProjectView(projectId) è scoped con WHERE eq(phases.project_id, projectId) — un cliente non può vedere dati di un altro cliente perché l'accesso è gate-kept dal client.id risolto dal token |
```bash
# 1. Slug API route exists
ls src/app/api/internal/validate-slug/route.ts
# 2. Middleware has slug-first
grep -n "validate-slug\|validate-token" src/proxy.ts
# 3. client-view.ts has new functions
grep "export async function" src/lib/client-view.ts
# 4. client-view.ts security invariants
grep "quote_items" src/lib/client-view.ts # must be empty
grep "amount" src/lib/client-view.ts # must not appear in payments select
# 5. Dashboard has tabs logic
grep "projects.length === 1" src/app/c/\[token\]/page.tsx
# 6. Edit page has slug field
grep "name=\"slug\"" src/app/admin/clients/\[id\]/edit/page.tsx
# 7. Build clean
npm run build
```
- /c/[slug] risolve correttamente alla dashboard del cliente → stesso comportamento di /c/[token]
- /c/[token] continua a funzionare come fallback per i link esistenti
- Dashboard con 1 progetto → nessun selettore/tabs, vista diretta
- Dashboard con 2+ progetti → shadcn Tabs con nomi brand, switch funziona
- /admin/impostazioni persiste il target_hourly_rate e la ProfitabilityCard nel workspace progetto lo usa
- `npm run build` → 0 errori TypeScript
- `grep "quote_items" src/lib/client-view.ts` → nessun output (security invariant verificato)