);
}
```
Create `src/app/admin/page.tsx` — Server Component, no client state:
```typescript
import Link from "next/link";
import { getAllClientsWithPayments } from "@/lib/admin-queries";
import { ClientRow } from "@/components/admin/ClientRow";
import { Button } from "@/components/ui/button";
export const revalidate = 0; // always fresh — admin needs real-time data
export default async function AdminDashboard() {
const clients = await getAllClientsWithPayments();
return (
Clienti
{clients.length === 0 ? (
Nessun cliente ancora.
Crea il primo cliente
) : (
Cliente
Totale
Acconto
Saldo
Link
{clients.map((client) => (
))}
)}
);
}
```
Create `src/app/admin/clients/new/actions.ts` — Server Action (per D-05):
```typescript
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db } from "@/db";
import { clients, payments } from "@/db/schema";
const createClientSchema = z.object({
name: z.string().min(1, "Nome richiesto"),
brand_name: z.string().min(1, "Nome brand richiesto"),
brief: z.string().min(1, "Brief richiesto"),
});
export async function createClient(formData: FormData) {
const raw = {
name: formData.get("name") as string,
brand_name: formData.get("brand_name") as string,
brief: formData.get("brief") as string,
};
const parsed = createClientSchema.safeParse(raw);
if (!parsed.success) {
// In v1 return errors as thrown string — form displays validation inline
throw new Error(parsed.error.issues.map((i) => i.message).join(", "));
}
// Insert client — token and id are auto-generated by $defaultFn(() => nanoid())
const [newClient] = await db
.insert(clients)
.values({
name: parsed.data.name,
brand_name: parsed.data.brand_name,
brief: parsed.data.brief,
})
.returning({ id: clients.id, token: clients.token });
// Always create two payment stubs per client — Acconto 50% and Saldo 50%
// Amounts default to 0 until admin sets accepted_total; admin updates separately
await db.insert(payments).values([
{
client_id: newClient.id,
label: "Acconto 50%",
amount: "0",
status: "da_saldare",
},
{
client_id: newClient.id,
label: "Saldo 50%",
amount: "0",
status: "da_saldare",
},
]);
revalidatePath("/admin");
redirect(`/admin/clients/${newClient.id}`);
}
```
Create `src/app/admin/clients/new/page.tsx` — form using the Server Action:
```typescript
import { createClient } from "./actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
export default function NewClientPage() {
return (
← Clienti
Nuovo cliente
);
}
```
test -f src/app/admin/page.tsx && grep -q "getAllClientsWithPayments" src/app/admin/page.tsx && echo "Admin page fetches clients"grep -q "ClientRow" src/app/admin/page.tsx && echo "ClientRow used in table"test -f src/app/admin/clients/new/actions.ts && grep -q '"use server"' src/app/admin/clients/new/actions.ts && echo "Server Action directive present"grep -q "db.insert(clients)" src/app/admin/clients/new/actions.ts && echo "client insert present"grep -q "Acconto 50%" src/app/admin/clients/new/actions.ts && grep -q "Saldo 50%" src/app/admin/clients/new/actions.ts && echo "both payment stubs inserted"grep -q "createClientSchema" src/app/admin/clients/new/actions.ts && echo "Zod validation present"test -f src/app/admin/clients/new/page.tsx && grep -q "action={createClient}" src/app/admin/clients/new/page.tsx && echo "form wired to Server Action"npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK"
- /admin shows table of clients with name, brand, totale, acconto badge, saldo badge, client link
- Empty state shows "Nessun cliente ancora" with link to create
- /admin/clients/new shows form with name, brand_name, brief fields
- Submitting the form inserts client row (token auto-generated by nanoid) + 2 payment stubs
- After creation, admin is redirected to /admin/clients/[id] (detail page — stub until Plan 03)
- npm run build passes
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Admin browser → Server Action | createClient() runs server-side; input validated with Zod before any DB write |
| Admin browser → /admin/* | Middleware session guard (02-01) prevents unauthenticated access to all admin pages and Server Actions |
| Client token → DB | Token generated server-side by nanoid(), never user-supplied; cannot be guessed |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-06 | Tampering | createClient Server Action | mitigate | Zod validates all input fields before DB insert; malformed input throws, no partial writes |
| T-02-07 | Information Disclosure | Client list page | mitigate | /admin/* protected by middleware session guard from 02-01; unauthenticated requests never reach this Server Component |
| T-02-08 | Tampering | token generation | mitigate | token is $defaultFn(() => nanoid()) — server-generated, cryptographically random, never derived from user input |
| T-02-09 | Information Disclosure | ClientRow renders full token | accept | Token is shown truncated in UI for usability; full token accessible via /c/[token] link only — acceptable since admin has session auth |
After plan execution:
1. `npm run build` — no errors
2. Log in as admin, visit /admin — client table renders (empty or with seeded data)
3. Click "+ Nuovo cliente" → /admin/clients/new loads with form
4. Submit form with valid data → redirects to /admin/clients/[id] (stub page acceptable at this point)
5. Return to /admin → new client appears in table with "Da saldare" badges for Acconto and Saldo
6. Click the /c/[token] link in the table → opens client dashboard (Phase 1 output)
- /admin shows all clients with payment status badges; empty state handled gracefully
- New client can be created via form; token is auto-generated server-side
- Two payment stubs (Acconto 50% / Saldo 50%) are created automatically on client creation
- Admin can immediately share /c/[token] after creating a client
- npm run build passes cleanly