56dd18b0c2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1160 lines
53 KiB
Markdown
1160 lines
53 KiB
Markdown
# Phase 2: Admin Area & Interactive Features - Research
|
|
|
|
**Researched:** 2026-05-15
|
|
**Domain:** Next.js authentication, admin CRUD, form handling, route protection
|
|
**Confidence:** MEDIUM-HIGH
|
|
|
|
## Summary
|
|
|
|
Phase 2 requires implementing Next.js 16 authentication for admin area (`/admin`), form-based CRUD operations for clients/phases/tasks/deliverables/payments, and client-facing interactive features (approval, comments). The research reveals a critical version decision: **next-auth v4 has unresolved peer dependency issues with Next.js 16**, requiring workarounds (`--legacy-peer-deps` flag). The CONTEXT.md decision to use "next-auth@4" is technically viable but carries technical debt. The phase architecture is sound: Edge middleware protection (proxy.ts) for admin routes, Server Actions for mutations, and API routes for client interactions (separate auth path). Key dependencies are not yet installed.
|
|
|
|
**Primary recommendation:** Proceed with next-auth v4 as locked in CONTEXT.md, but document the workaround requirement. All other patterns (Server Actions, revalidatePath, Zod+RHF forms, tabs) are stable and production-ready on Next.js 16.
|
|
|
|
## User Constraints (from CONTEXT.md)
|
|
|
|
### Locked Decisions
|
|
|
|
**D-01: Auth.js v4 (`next-auth@4`)** — versione stabile. Installare `next-auth@4` + credentials via env vars (ADMIN_EMAIL + ADMIN_PASSWORD). No DB table per utenti.
|
|
|
|
**D-02: Credenziali admin via env vars** — Singolo admin hardcoded in `.env.local`. CredentialsProvider di Auth.js valida contro le env vars.
|
|
|
|
**D-03: Sessione JWT** — Nessuna tabella session nel DB. JWT stateless firmato, scadenza 30 giorni.
|
|
|
|
**D-04: Protezione route admin** — `src/proxy.ts` gestisce `/c/[token]`. Aggiungere matcher `/admin/:path*` → redirect a `/admin/login` se no session. Check sessione nel middleware via `getToken` da next-auth/jwt.
|
|
|
|
**D-05: Server Actions** — Next.js 15/16 nativo. Nessuna API route separata per CRUD admin. Form → Server Action → Drizzle → `revalidatePath`.
|
|
|
|
**D-06: API route solo per client interactions** — Approvazioni e commenti del cliente su API routes (non Server Actions). Route pattern: `POST /api/client/approve` e `POST /api/client/comment`. Validate token via header/cookie, non Auth.js.
|
|
|
|
**D-07: Lista → dettaglio** — `/admin` → lista clienti con badge stato pagamenti. Click → `/admin/clients/[id]` dettaglio completo.
|
|
|
|
**D-08: Tabs per sezioni cliente** — Nella pagina dettaglio cliente, tabs: Panoramica | Fasi & Task | Documenti | Pagamenti | Commenti. Aggiungere `@radix-ui/react-tabs`.
|
|
|
|
**D-09: Sidebar assente in v1** — Nessuna sidebar globale. Nav minimal: logo + link "Clienti" + pulsante logout.
|
|
|
|
**D-10: Approvazione deliverable** — Pulsante "Approva" visibile solo se `approved_at` è null. POST a `/api/client/approve` con `{ deliverableId }`.
|
|
|
|
**D-11: Commenti inline** — Textarea + pulsante "Invia" sotto task/deliverable. POST a `/api/client/comment`. Lista commenti sopra il form, ordinati per `created_at` asc.
|
|
|
|
### Claude's Discretion
|
|
|
|
- Struttura esatta delle Server Actions (file naming)
|
|
- Ordine campi nei form admin
|
|
- Icone lucide-react per stati
|
|
- Colori badge admin
|
|
|
|
### Deferred Ideas (OUT OF SCOPE)
|
|
|
|
- Brand customization panel (Phase 3+)
|
|
- Sidebar globale (v1 non necessaria)
|
|
- Real-time commenti (WebSocket/polling → v2)
|
|
- Multi-admin / ruoli
|
|
- Password change UI
|
|
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|------------------|
|
|
| ADMIN-01 | Vista di tutti i clienti con stato sintetico | Server Component + Drizzle query per lista con badge pagamenti |
|
|
| ADMIN-02 | Gestione completa di ogni cliente: fasi, task, documenti, pagamenti | Server Actions per CRUD su tutte le tabelle; proxy.ts protezione route `/admin/[*]`; revalidatePath aggiorna UI |
|
|
| DASH-05 | Il cliente può approvare i deliverable dalla sua area | API route `POST /api/client/approve` — token validation — UPDATE `approved_at` immutabile |
|
|
| DASH-06 | Il cliente può lasciare commenti su task e deliverable | API route `POST /api/client/comment` — INSERT comments table — lista commenti con author (client/admin) |
|
|
|
|
## Architectural Responsibility Map
|
|
|
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
|
|------------|-------------|----------------|-----------|
|
|
| Admin authentication & session | API / Backend (proxy.ts) | Frontend Server (layout redirect) | JWT validation happens at Edge; session unavailable → redirect to login before rendering |
|
|
| Admin CRUD mutations | API / Backend (Server Actions) | Frontend Server (forms) | Server Actions run in Node context; Drizzle mutations; revalidatePath triggers UI refresh |
|
|
| Admin UI rendering | Frontend Server (Server Components) | Browser (Client Components for interactivity) | List/detail pages are Server Components with data from Drizzle; tabs/forms use Client Components for state |
|
|
| Client approval/comments | API / Backend (route handlers) | Browser (client tokens in URL/cookie) | Separate auth path from admin; token validation in route handlers; mutations persist to DB |
|
|
| Client-facing UI (approval, comments) | Browser / Client | Frontend Server (token validation via middleware) | Approval button, comment form are interactive client components; middleware validates token before rendering |
|
|
| Payment status display | API / Backend (Drizzle query) | Frontend Server (Server Component) | Payments table joins with clients; status badge computed server-side; client API returns `accepted_total` only |
|
|
|
|
## Standard Stack
|
|
|
|
### Core Authentication & Middleware
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| next-auth | 4.24.14 | JWT-based admin session, credentials provider | Industry standard for Next.js; stateless JWT avoids DB session lookups at Edge [VERIFIED: npm registry] |
|
|
| next-auth/jwt | bundled with next-auth | Token signing/verification in middleware | Required for getToken() calls in proxy.ts [VERIFIED: next-auth docs] |
|
|
|
|
### Form Handling & Validation
|
|
|
|
| Library | Version | Purpose | When to Use |
|
|
|---------|---------|---------|-------------|
|
|
| react-hook-form | 7.75.0 | Client-side form state + server submission | Already installed; lightweight; pairs with Zod for validation [VERIFIED: package.json] |
|
|
| @hookform/resolvers | 5.2.2 | Adapter for Zod → RHF integration | Already installed; official resolver [VERIFIED: package.json] |
|
|
| zod | 4.4.3 | Schema validation (client & server) | Already installed; enables validation reuse across RHF forms and Server Actions [VERIFIED: package.json] |
|
|
|
|
### UI Components
|
|
|
|
| Library | Version | Purpose | When to Use |
|
|
|---------|---------|---------|-------------|
|
|
| @radix-ui/react-tabs | 1.0.x | Tabs primitive (Radix-based) | Not yet installed; needed for admin detail page tabs per D-08 [CITED: shadcn/ui docs] |
|
|
| @radix-ui/react-dialog | 1.1.x | Modal/dialog primitive | Optional; can defer if confirm modals not needed in Wave 1 |
|
|
| shadcn/ui (tabs component) | — | Pre-configured Tabs built on Radix | Install via `npx shadcn@latest add tabs` after @radix-ui/react-tabs added [CITED: shadcn/ui] |
|
|
|
|
### Existing, Reusable Components (from Phase 1)
|
|
|
|
| Library | Version | Purpose | Status |
|
|
|---------|---------|---------|--------|
|
|
| next | 16.2.6 | App Router, Server Components, Server Actions | ✅ Installed, using in Phase 2 |
|
|
| drizzle-orm | 0.45.2 | Type-safe ORM + postgres-js connection | ✅ Installed, patterns established Phase 1 |
|
|
| postgres | 3.4.9 | Postgres client (Node.js only, not Edge-compatible) | ✅ Installed; used in API routes (not middleware) |
|
|
| nanoid | 5.1.11 | Cryptographic token generation | ✅ Installed; schema uses for `token` field |
|
|
| tailwindcss | 4.x | Styling | ✅ Installed v4 in Phase 1 |
|
|
| shadcn/ui (installed) | — | badge, button, card, input, label, select, table, textarea | ✅ All used in Phase 1; reuse for admin UI |
|
|
| lucide-react | 1.14.0 | Icons | ✅ Installed Phase 1; use for payment status icons |
|
|
|
|
### Installation
|
|
|
|
**New packages to add:**
|
|
```bash
|
|
npm install next-auth@4.24.14 @radix-ui/react-tabs
|
|
npx shadcn@latest add tabs
|
|
```
|
|
|
|
**Note:** next-auth v4 with Next.js 16 requires `--legacy-peer-deps` flag due to unresolved peer dependency issue:
|
|
```bash
|
|
npm install next-auth@4.24.14 --legacy-peer-deps
|
|
```
|
|
|
|
**Version verification:**
|
|
```bash
|
|
npm view next-auth@4.24.14 version # Should show: 4.24.14, published ~May 2026
|
|
npm view @radix-ui/react-tabs version # Should show: 1.x.x (latest stable)
|
|
```
|
|
|
|
## Architecture Patterns
|
|
|
|
### System Architecture Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ BROWSER / CLIENT │
|
|
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
│ │ Admin Form │ │ Client Dashboard │ │
|
|
│ │ (RHF + Zod) │ │ (Approval/Comment) │
|
|
│ └────────┬─────────┘ └────────┬─────────┘ │
|
|
└───────────┼──────────────────────┼───────────────────────────┘
|
|
│ │
|
|
│ POST /api/[...] + │ POST /api/client/[...] +
|
|
│ Server Action │ client token
|
|
│ │
|
|
┌───────────▼──────────────────────▼───────────────────────────┐
|
|
│ FRONTEND SERVER (Next.js App Router) │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ Route Handler: POST /api/client/approve │ │
|
|
│ │ Route Handler: POST /api/client/comment │ │
|
|
│ │ Server Actions: createClient, updatePhase, etc. │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└───────────┬──────────────────────────────────────────────────┘
|
|
│
|
|
│ Token validation,
|
|
│ Drizzle mutation
|
|
│
|
|
┌───────────▼──────────────────────────────────────────────────┐
|
|
│ DATABASE (Hetzner Postgres) │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ clients, phases, tasks, deliverables, comments, │ │
|
|
│ │ payments, documents, notes, service_catalog, │ │
|
|
│ │ quote_items │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└───────────────────────────────────────────────────────────────┘
|
|
|
|
SEPARATELY: Edge Layer (proxy.ts)
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ EDGE (Vercel / Next.js middleware layer) │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ proxy.ts: getToken() → session exists? │ │
|
|
│ │ → Route: /admin/[*] → getToken ok? → allow : 404 │ │
|
|
│ │ → Route: /c/[token]/[*] → validateToken ok? → allow │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Route Structure & Protection
|
|
|
|
**Admin Routes (Protected by Edge middleware):**
|
|
- `/admin/login` — GET form, POST credentials (unprotected)
|
|
- `/admin` — GET list of clients (protected, Server Component)
|
|
- `/admin/clients/[id]` — GET detail + CRUD forms (protected, Server Component + Client Components)
|
|
- `/admin/clients/[id]/phases` — Nested detail (protected)
|
|
- `/admin/clients/[id]/payments` — Nested detail (protected)
|
|
|
|
**Client Routes (Already protected in Phase 1):**
|
|
- `/c/[token]/` — Client dashboard (token validation via proxy.ts)
|
|
- `/c/[token]/task/[taskId]` — Task detail with comments
|
|
|
|
**API Routes (Internal, not middleware-protected at Edge):**
|
|
- `POST /api/client/approve` — Expects client token in body/header
|
|
- `POST /api/client/comment` — Expects client token in body/header
|
|
- `POST /api/internal/validate-token` — Called by proxy.ts (no auth needed, internal only)
|
|
|
|
### Recommended Project Structure
|
|
|
|
```
|
|
src/
|
|
├── app/
|
|
│ ├── admin/
|
|
│ │ ├── login/
|
|
│ │ │ ├── page.tsx # Login form
|
|
│ │ │ └── actions.ts # signIn Server Action
|
|
│ │ ├── clients/
|
|
│ │ │ ├── page.tsx # List all clients
|
|
│ │ │ ├── actions.ts # createClient, deleteClient
|
|
│ │ │ └── [id]/
|
|
│ │ │ ├── page.tsx # Detail page with tabs
|
|
│ │ │ ├── actions.ts # updateClient, createPhase, etc.
|
|
│ │ │ └── layout.tsx # Detail layout (optional nested)
|
|
│ │ ├── layout.tsx # Admin layout (nav, logout)
|
|
│ │ └── components/
|
|
│ │ ├── ClientForm.tsx
|
|
│ │ ├── PhaseForm.tsx
|
|
│ │ └── PaymentBadge.tsx
|
|
│ ├── c/
|
|
│ │ └── [token]/
|
|
│ │ ├── components/
|
|
│ │ │ ├── ApprovalButton.tsx
|
|
│ │ │ └── CommentForm.tsx
|
|
│ │ └── ... # Phase 1 routes (reuse)
|
|
│ └── api/
|
|
│ ├── client/
|
|
│ │ ├── approve/
|
|
│ │ │ └── route.ts # POST /api/client/approve
|
|
│ │ └── comment/
|
|
│ │ └── route.ts # POST /api/client/comment
|
|
│ └── internal/
|
|
│ └── validate-token/
|
|
│ └── route.ts # (existing, Phase 1)
|
|
├── auth.ts # next-auth config (NEW)
|
|
├── proxy.ts # Edge middleware (MODIFY)
|
|
├── db/
|
|
│ └── schema.ts # (unchanged, Phase 1)
|
|
└── lib/
|
|
└── client-view.ts # (unchanged, Phase 1)
|
|
```
|
|
|
|
### Pattern 1: Server Actions for Admin CRUD
|
|
|
|
**What:** Async functions marked with `'use server'` that run on the server, handle form submissions, mutate the database via Drizzle, and revalidate the cache to refresh the UI.
|
|
|
|
**When to use:** Every admin form (create/edit/delete client, phase, task, payment). Never call from client-side mutation code directly.
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/app/admin/clients/actions.ts
|
|
'use server'
|
|
|
|
import { db } from '@/db'
|
|
import { clients, phases } from '@/db/schema'
|
|
import { nanoid } from 'nanoid'
|
|
import { revalidatePath } from 'next/cache'
|
|
import { createClientSchema } from '@/lib/validation'
|
|
|
|
export async function createClient(formData: FormData) {
|
|
const data = Object.fromEntries(formData)
|
|
const validated = createClientSchema.parse(data)
|
|
|
|
const token = nanoid() // Generate secret link token
|
|
|
|
const newClient = await db.insert(clients).values({
|
|
name: validated.name,
|
|
brand_name: validated.brand_name,
|
|
brief: validated.brief,
|
|
token: token,
|
|
accepted_total: '0',
|
|
}).returning()
|
|
|
|
revalidatePath('/admin') // Refresh client list
|
|
return { success: true, clientId: newClient[0].id, token }
|
|
}
|
|
|
|
// src/app/admin/clients/page.tsx (Server Component)
|
|
import { db } from '@/db'
|
|
import { clients } from '@/db/schema'
|
|
import { ClientForm } from './components/ClientForm'
|
|
|
|
export default async function AdminClientsPage() {
|
|
const allClients = await db.query.clients.findMany()
|
|
|
|
return (
|
|
<div>
|
|
<h1>All Clients</h1>
|
|
<ClientForm onSubmit={createClient} />
|
|
<ClientList clients={allClients} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// src/app/admin/clients/components/ClientForm.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { useFormState, useFormStatus } from 'react-dom'
|
|
import { createClient } from '../actions'
|
|
|
|
export function ClientForm() {
|
|
const [state, formAction] = useFormState(createClient, null)
|
|
|
|
return (
|
|
<form action={formAction}>
|
|
<input name="name" required />
|
|
<input name="brand_name" required />
|
|
<textarea name="brief" required />
|
|
<SubmitButton />
|
|
{state?.success && <p>Client created! Token: {state.token}</p>}
|
|
</form>
|
|
)
|
|
}
|
|
|
|
function SubmitButton() {
|
|
const { pending } = useFormStatus()
|
|
return <button disabled={pending}>{pending ? 'Creating...' : 'Create Client'}</button>
|
|
}
|
|
```
|
|
|
|
[Source: https://nextjs.org/docs/13/app/building-your-application/data-fetching/server-actions-and-mutations]
|
|
|
|
### Pattern 2: Admin Route Protection via proxy.ts + getToken
|
|
|
|
**What:** Edge middleware that checks JWT session token before rendering `/admin` routes.
|
|
|
|
**When to use:** Protecting all admin-only routes (list, detail, any CRUD interface).
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/proxy.ts (MODIFY existing file)
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getToken } from 'next-auth/jwt'
|
|
|
|
const ADMIN_MATCHER = /^\/admin/
|
|
const CLIENT_MATCHER = /^\/c\/[a-zA-Z0-9_-]+/
|
|
|
|
export async function proxy(request: NextRequest) {
|
|
const pathname = request.nextUrl.pathname
|
|
|
|
// Admin route protection
|
|
if (ADMIN_MATCHER.test(pathname)) {
|
|
// Allow /admin/login without session
|
|
if (pathname === '/admin/login') {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
const token = await getToken({
|
|
req: request,
|
|
secret: process.env.NEXTAUTH_SECRET,
|
|
})
|
|
|
|
if (!token) {
|
|
// Redirect to login if no session
|
|
return NextResponse.redirect(new URL('/admin/login', request.url))
|
|
}
|
|
|
|
return NextResponse.next()
|
|
}
|
|
|
|
// Client token validation (Phase 1 pattern — unchanged)
|
|
const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/)
|
|
if (tokenMatch) {
|
|
const token = tokenMatch[1]
|
|
try {
|
|
const validateUrl = new URL(
|
|
`/api/internal/validate-token?token=${encodeURIComponent(token)}`,
|
|
request.url
|
|
)
|
|
const res = await fetch(validateUrl.toString())
|
|
if (!res.ok) {
|
|
return NextResponse.rewrite(new URL('/not-found', request.url))
|
|
}
|
|
return NextResponse.next()
|
|
} catch {
|
|
return NextResponse.rewrite(new URL('/not-found', request.url))
|
|
}
|
|
}
|
|
|
|
return NextResponse.rewrite(new URL('/not-found', request.url))
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/admin/:path*', '/c/:path*'],
|
|
}
|
|
```
|
|
|
|
[Source: https://authjs.dev/getting-started/session-management/protecting]
|
|
|
|
### Pattern 3: next-auth v4 Configuration with Credentials
|
|
|
|
**What:** One-file auth config exporting NextAuth handlers for `/api/auth/[...nextauth]` route. Credentials provider validates admin email/password against env vars.
|
|
|
|
**When to use:** Initialize once at `src/auth.ts`. Used by all routes via `getSession()` (server) or `useSession()` (client).
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/auth.ts (NEW)
|
|
import NextAuth from 'next-auth'
|
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
|
|
|
export const { handlers, auth } = NextAuth({
|
|
providers: [
|
|
CredentialsProvider({
|
|
credentials: {
|
|
email: { label: 'Email', type: 'text' },
|
|
password: { label: 'Password', type: 'password' },
|
|
},
|
|
async authorize(credentials) {
|
|
// Validate against env vars only — no DB lookup
|
|
if (
|
|
credentials?.email === process.env.ADMIN_EMAIL &&
|
|
credentials?.password === process.env.ADMIN_PASSWORD
|
|
) {
|
|
return {
|
|
id: 'admin',
|
|
email: process.env.ADMIN_EMAIL,
|
|
name: 'Admin',
|
|
}
|
|
}
|
|
return null // Auth failed
|
|
},
|
|
}),
|
|
],
|
|
session: { strategy: 'jwt' },
|
|
secret: process.env.NEXTAUTH_SECRET,
|
|
pages: { signIn: '/admin/login' },
|
|
})
|
|
|
|
// src/app/api/auth/[...nextauth]/route.ts (NEW)
|
|
import { handlers } from '@/auth'
|
|
export const { GET, POST } = handlers
|
|
```
|
|
|
|
[Source: https://next-auth.js.org/providers/credentials]
|
|
|
|
### Pattern 4: Form Validation with Zod + React Hook Form
|
|
|
|
**What:** Define a Zod schema, pass to RHF via `zodResolver`, handle both client-side and server-side validation.
|
|
|
|
**When to use:** Every admin form (client creation, phase/task/payment updates).
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/lib/validation.ts (NEW)
|
|
import { z } from 'zod'
|
|
|
|
export const createClientSchema = z.object({
|
|
name: z.string().min(1, 'Client name required'),
|
|
brand_name: z.string().min(1, 'Brand name required'),
|
|
brief: z.string().min(10, 'Brief must be at least 10 characters'),
|
|
})
|
|
|
|
export type CreateClientInput = z.infer<typeof createClientSchema>
|
|
|
|
// src/app/admin/clients/components/ClientForm.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { createClientSchema, type CreateClientInput } from '@/lib/validation'
|
|
import { createClient } from '../actions'
|
|
|
|
export function ClientForm() {
|
|
const { register, handleSubmit, formState: { errors } } = useForm<CreateClientInput>({
|
|
resolver: zodResolver(createClientSchema),
|
|
})
|
|
|
|
async function onSubmit(data: CreateClientInput) {
|
|
const formData = new FormData()
|
|
Object.entries(data).forEach(([k, v]) => formData.append(k, v))
|
|
await createClient(formData)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<div>
|
|
<label>Client Name</label>
|
|
<input {...register('name')} />
|
|
{errors.name && <span>{errors.name.message}</span>}
|
|
</div>
|
|
<div>
|
|
<label>Brand Name</label>
|
|
<input {...register('brand_name')} />
|
|
{errors.brand_name && <span>{errors.brand_name.message}</span>}
|
|
</div>
|
|
<div>
|
|
<label>Project Brief</label>
|
|
<textarea {...register('brief')} />
|
|
{errors.brief && <span>{errors.brief.message}</span>}
|
|
</div>
|
|
<button type="submit">Create Client</button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
[Source: https://medium.com/@ctrlaltmonique/how-to-use-react-hook-form-zod-with-next-js-server-actions-437aaca3d72d]
|
|
|
|
### Pattern 5: Client API Routes (Token-Based, Separate from Admin)
|
|
|
|
**What:** Route handlers that validate client token (from request body or cookie) before allowing approval/comment mutations.
|
|
|
|
**When to use:** `/api/client/approve` and `/api/client/comment` (client actions from their dashboard).
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/app/api/client/approve/route.ts (NEW)
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { db } from '@/db'
|
|
import { deliverables } from '@/db/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const { deliverableId, clientToken } = await request.json()
|
|
|
|
// Validate client token via DB lookup
|
|
const client = await db.query.clients.findFirst({
|
|
where: (c, { eq }) => eq(c.token, clientToken),
|
|
})
|
|
|
|
if (!client) {
|
|
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
|
|
}
|
|
|
|
// Verify deliverable belongs to this client (via task → phase → client)
|
|
const deliverable = await db.query.deliverables.findFirst({
|
|
where: (d, { eq }) => eq(d.id, deliverableId),
|
|
with: {
|
|
task: {
|
|
with: {
|
|
phase: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (!deliverable || deliverable.task.phase.client_id !== client.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
|
}
|
|
|
|
if (deliverable.approved_at) {
|
|
return NextResponse.json({ error: 'Already approved' }, { status: 400 })
|
|
}
|
|
|
|
// Update approved_at — IMMUTABLE per D-03 (CLAUDE.md)
|
|
await db
|
|
.update(deliverables)
|
|
.set({ approved_at: new Date(), status: 'approved' })
|
|
.where(eq(deliverables.id, deliverableId))
|
|
|
|
return NextResponse.json({ success: true })
|
|
}
|
|
|
|
// src/app/c/[token]/components/ApprovalButton.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
export function ApprovalButton({
|
|
deliverableId,
|
|
clientToken,
|
|
isApproved,
|
|
}: {
|
|
deliverableId: string
|
|
clientToken: string
|
|
isApproved: boolean
|
|
}) {
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
async function handleApprove() {
|
|
setLoading(true)
|
|
const res = await fetch('/api/client/approve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ deliverableId, clientToken }),
|
|
})
|
|
|
|
if (res.ok) {
|
|
// Page refresh to update state — no real-time in v1
|
|
window.location.reload()
|
|
} else {
|
|
alert('Failed to approve')
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
if (isApproved) {
|
|
return <span className="text-green-600">✓ Approved</span>
|
|
}
|
|
|
|
return (
|
|
<Button onClick={handleApprove} disabled={loading}>
|
|
{loading ? 'Approving...' : 'Approve'}
|
|
</Button>
|
|
)
|
|
}
|
|
```
|
|
|
|
[Source: CONTEXT.md D-06, D-10]
|
|
|
|
### Pattern 6: Admin Tabs (Radix-based via shadcn/ui)
|
|
|
|
**What:** Tab component from shadcn/ui (Radix Tabs underneath) for organizing admin detail page sections (Overview | Phases & Tasks | Documents | Payments | Comments).
|
|
|
|
**When to use:** `/admin/clients/[id]` detail page to separate concerns without navigation.
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// src/app/admin/clients/[id]/page.tsx (Server Component)
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { ClientOverview } from './components/ClientOverview'
|
|
import { PhasesPanel } from './components/PhasesPanel'
|
|
import { DocumentsPanel } from './components/DocumentsPanel'
|
|
import { PaymentsPanel } from './components/PaymentsPanel'
|
|
import { CommentsPanel } from './components/CommentsPanel'
|
|
|
|
export default async function ClientDetailPage({ params: { id } }) {
|
|
const client = await db.query.clients.findFirst({
|
|
where: (c, { eq }) => eq(c.id, id),
|
|
with: { phases: true, payments: true, documents: true, notes: true },
|
|
})
|
|
|
|
return (
|
|
<div>
|
|
<h1>{client?.name}</h1>
|
|
<Tabs defaultValue="overview">
|
|
<TabsList>
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="phases">Phases & Tasks</TabsTrigger>
|
|
<TabsTrigger value="documents">Documents</TabsTrigger>
|
|
<TabsTrigger value="payments">Payments</TabsTrigger>
|
|
<TabsTrigger value="comments">Comments</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview">
|
|
<ClientOverview client={client} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="phases">
|
|
<PhasesPanel clientId={id} phases={client?.phases || []} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="documents">
|
|
<DocumentsPanel documents={client?.documents || []} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="payments">
|
|
<PaymentsPanel payments={client?.payments || []} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="comments">
|
|
<CommentsPanel comments={client?.notes || []} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
[Source: https://ui.shadcn.com/docs/components/radix/tabs]
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Don't use useEffect to fetch data inside admin components.** Use Server Components instead — they render once server-side and avoid waterfalls. [CITED: Phase 1 pattern]
|
|
- **Don't store session in state.** Use `getToken()` in proxy.ts (Edge) and `getSession()` in Server Components. Avoid client-side session stores which are slower and less secure.
|
|
- **Don't hand-roll JWT signing.** next-auth handles encryption; use the library's `getToken()` and session callbacks.
|
|
- **Don't expose `quote_items` in any admin API.** Client API must return only `accepted_total` field. Enforced at query layer, not UI. [CITED: CLAUDE.md constraint #2]
|
|
- **Don't allow modification of `approved_at` once set.** Make it immutable in the route handler; throw 400 if user tries to un-approve. [CITED: CLAUDE.md constraint #3]
|
|
- **Don't call postgres-js directly in proxy.ts.** postgres-js requires Node.js `net`/`tls` modules unavailable at Edge. Always use `fetch()` to internal API routes. [CITED: Phase 1 proxy.ts comment]
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Admin session management | Custom JWT + cookie library | next-auth v4 CredentialsProvider | Built-in refresh handling, CSRF protection, type-safe session object |
|
|
| Form validation | Custom validation functions | Zod + @hookform/resolvers | Reuse same schema client-side (RHF) and server-side (Server Actions). Errors map automatically. |
|
|
| Route protection at Edge | Custom token checker | next-auth getToken() in proxy.ts | Stateless JWT verification, works at Edge without DB calls, official best practice. |
|
|
| Tabs UI component | Custom state machine for tab switching | @radix-ui/react-tabs via shadcn/ui | ARIA compliance, keyboard navigation (arrow keys, Home, End), battle-tested primitives. |
|
|
| Client approval workflow | Custom approval + state flags | `deliverables.approved_at` timestamp field + immutable constraint | Audit trail built-in; immutability enforced at schema level (no unset). Single UPDATE mutation. |
|
|
| Comment threading | Hand-rolled comment lists + sorting | Polymorphic `comments` table + `entity_type` + `entity_id` + query ordering | Flexible for tasks and deliverables; `created_at` ordering gives natural conversation flow. |
|
|
|
|
**Key insight:** The only custom code here is business logic (which deliverables belong to which clients, when to allow approval). The infrastructure (auth, forms, UI) is mature and battle-tested. Avoid reimplementing.
|
|
|
|
## Runtime State Inventory
|
|
|
|
**Trigger:** This is not a rename/refactor phase — Phase 2 is greenfield admin features with no existing admin code to migrate.
|
|
|
|
**Finding:** None — no runtime state to inventory. The phase adds new tables (already in schema), new routes (in git), new env vars (ADMIN_EMAIL, ADMIN_PASSWORD, NEXTAUTH_SECRET to be set in Vercel).
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: next-auth v4 Peer Dependency Issue with Next.js 16
|
|
|
|
**What goes wrong:** Running `npm install next-auth@4.24.14` fails with "Could not resolve dependency: peer next@^12.2.5 || ^13 || ^14 || ^15".
|
|
|
|
**Why it happens:** next-auth v4's peer dependency range doesn't include Next.js 16. The issue is [unresolved on GitHub](https://github.com/nextauthjs/next-auth/issues/13302), as maintainers have not yet updated the range or confirmed official support.
|
|
|
|
**How to avoid:** Install with `npm install next-auth@4.24.14 --legacy-peer-deps`. This bypasses the peer dependency check. Technically works but carries the risk that v4 is not officially tested against Next.js 16.
|
|
|
|
**Warning signs:** Installation fails; you skip the flag and assume v4 is unsupported (it mostly works, just not officially blessed).
|
|
|
|
**Alternative (not recommended by CONTEXT.md):** Use Auth.js v5 (beta) or Better Auth. But CONTEXT.md locks v4, so use the workaround.
|
|
|
|
### Pitfall 2: Trying to Use postgres-js in proxy.ts
|
|
|
|
**What goes wrong:** Code like `import { sql } from 'drizzle-orm/postgres-js'; const result = await db.query...` fails at runtime in Edge middleware with "Error: postgres.js is not supported in the Edge runtime."
|
|
|
|
**Why it happens:** postgres-js requires Node.js `net` and `tls` modules. Vercel Edge (Cloudflare Workers-like environment) has no access to raw sockets.
|
|
|
|
**How to avoid:** proxy.ts can ONLY use `fetch()` to internal Next.js API routes. Those routes run in Node.js and can use postgres-js. Middleware validates token, route handler queries DB. [CITED: Phase 1 proxy.ts]
|
|
|
|
**Warning signs:** Using direct Drizzle queries in proxy.ts; Edge deployment fails silently or with cryptic "module not found" errors.
|
|
|
|
### Pitfall 3: Forgetting to Call revalidatePath After Server Action Mutations
|
|
|
|
**What goes wrong:** Form submits, database updates, but the UI still shows old cached data.
|
|
|
|
**Why it happens:** Server Components cache their data by default. Mutations don't automatically invalidate caches. `revalidatePath` tells Next.js to re-run the Server Component query on next visit/navigation.
|
|
|
|
**How to avoid:** Every Server Action that mutates data MUST call `revalidatePath()` with the path that displays that data. Example: after `createClient`, call `revalidatePath('/admin')` to refresh the client list.
|
|
|
|
**Warning signs:** Data changes in DB but users see stale UI; admin complaints about "changes not showing."
|
|
|
|
### Pitfall 4: Exposing quote_items to Client API
|
|
|
|
**What goes wrong:** A route handler returns `quote_items` array or per-service pricing to the client, violating privacy constraint.
|
|
|
|
**Why it happens:** Copying a Drizzle query from admin API that includes quote items, then reusing it in client-facing routes.
|
|
|
|
**How to avoid:** Client API routes MUST query only `clients.accepted_total`, never join with `quote_items`. Admin API routes can expose items (for admin UI only). Enforce at query layer, not UI. Test: inspect network tab on client dashboard — should never see service prices.
|
|
|
|
**Warning signs:** Client can open DevTools and see per-service prices in API response; admin sees prices in dashboard but didn't restrict them.
|
|
|
|
### Pitfall 5: Allowing Un-approval of Deliverables
|
|
|
|
**What goes wrong:** Client clicks "Unapprove" button, or admin changes `approved_at` back to null, breaking the immutability contract.
|
|
|
|
**Why it happens:** Forgetting that `approved_at` is immutable per CLAUDE.md. Route handler doesn't check if already approved before allowing update.
|
|
|
|
**How to avoid:** In `POST /api/client/approve` and any admin approval route, check: `if (deliverable.approved_at) return 400 'Already approved'`. Never allow an UPDATE that sets `approved_at` to null. [CITED: CLAUDE.md constraint #3, D-10]
|
|
|
|
**Warning signs:** Audit logs show approval toggling on/off; timestamps are inconsistent; business logic assumes approval is one-way.
|
|
|
|
### Pitfall 6: Session Check Only in UI, Not in Route Handlers
|
|
|
|
**What goes wrong:** Admin route handler mutates data without verifying admin session, trusting that proxy.ts protected the route.
|
|
|
|
**Why it happens:** Assuming proxy.ts redirect to login is sufficient authorization. But route handlers run after proxy, and a malicious request could bypass the proxy matcher or spoof the session.
|
|
|
|
**How to avoid:** **Every** admin route handler should independently verify session:
|
|
```typescript
|
|
const session = await getSession({ req: request })
|
|
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
```
|
|
|
|
Don't rely on proxy.ts as sole gatekeeper. Defense in depth. [CITED: Auth.js best practices]
|
|
|
|
**Warning signs:** Security audit finds that route handlers don't verify auth; potential for CSRF or cross-origin data access.
|
|
|
|
## Code Examples
|
|
|
|
### Admin Login Form + Server Action
|
|
|
|
```typescript
|
|
// src/app/admin/login/page.tsx (Server Component)
|
|
import { redirect } from 'next/navigation'
|
|
import { auth } from '@/auth'
|
|
import { LoginForm } from './components/LoginForm'
|
|
|
|
export default async function LoginPage() {
|
|
// If already logged in, redirect to dashboard
|
|
const session = await auth()
|
|
if (session) {
|
|
redirect('/admin')
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="w-full max-w-md">
|
|
<h1 className="text-3xl font-bold mb-6">Admin Login</h1>
|
|
<LoginForm />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// src/app/admin/login/components/LoginForm.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { signIn } from 'next-auth/react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { useState } from 'react'
|
|
|
|
export function LoginForm() {
|
|
const router = useRouter()
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
setError('')
|
|
|
|
const formData = new FormData(e.currentTarget)
|
|
const result = await signIn('credentials', {
|
|
email: formData.get('email'),
|
|
password: formData.get('password'),
|
|
redirect: false,
|
|
})
|
|
|
|
if (result?.ok) {
|
|
router.push('/admin')
|
|
} else {
|
|
setError('Invalid email or password')
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input type="email" name="email" required />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="password">Password</Label>
|
|
<Input type="password" name="password" required />
|
|
</div>
|
|
{error && <p className="text-red-600">{error}</p>}
|
|
<Button type="submit" disabled={loading} className="w-full">
|
|
{loading ? 'Logging in...' : 'Login'}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
[Source: next-auth Credentials example]
|
|
|
|
### Drizzle Query: Client List with Payment Status
|
|
|
|
```typescript
|
|
// src/lib/queries/admin.ts (NEW)
|
|
import { db } from '@/db'
|
|
import { clients, payments } from '@/db/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
|
|
export async function getClientsWithPayments() {
|
|
return db.query.clients.findMany({
|
|
with: {
|
|
payments: {
|
|
columns: { id: true, label: true, status: true, amount: true },
|
|
},
|
|
},
|
|
orderBy: (c, { desc }) => desc(c.created_at),
|
|
})
|
|
}
|
|
|
|
// Result structure (used in admin list page):
|
|
// {
|
|
// id: string
|
|
// name: string
|
|
// brand_name: string
|
|
// brief: string
|
|
// token: string
|
|
// accepted_total: string
|
|
// created_at: Date
|
|
// payments: {
|
|
// id: string
|
|
// label: string // "Acconto 50%" | "Saldo 50%"
|
|
// status: string // "da_saldare" | "inviata" | "saldato"
|
|
// amount: string
|
|
// }[]
|
|
// }
|
|
```
|
|
|
|
[Source: Phase 1 Drizzle patterns, schema.ts]
|
|
|
|
### Client Comment & Approval UI (Combined)
|
|
|
|
```typescript
|
|
// src/app/c/[token]/task/[taskId]/components/TaskDetail.tsx (Server Component)
|
|
import { db } from '@/db'
|
|
import { tasks, comments } from '@/db/schema'
|
|
import { eq } from 'drizzle-orm'
|
|
import { CommentForm } from './CommentForm'
|
|
import { CommentList } from './CommentList'
|
|
|
|
export async function TaskDetail({ taskId, clientToken }: { taskId: string; clientToken: string }) {
|
|
const task = await db.query.tasks.findFirst({
|
|
where: eq(tasks.id, taskId),
|
|
with: {
|
|
deliverables: true,
|
|
},
|
|
})
|
|
|
|
const taskComments = await db.query.comments.findMany({
|
|
where: (c, { eq, and }) =>
|
|
and(eq(c.entity_type, 'task'), eq(c.entity_id, taskId)),
|
|
orderBy: (c, { asc }) => asc(c.created_at),
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<h2>{task?.title}</h2>
|
|
<p>{task?.description}</p>
|
|
|
|
<div className="space-y-2">
|
|
<h3>Deliverables</h3>
|
|
{task?.deliverables.map((d) => (
|
|
<div key={d.id} className="border p-2">
|
|
<p>{d.title}</p>
|
|
{d.url && <a href={d.url} target="_blank">{d.url}</a>}
|
|
{d.approved_at ? (
|
|
<p className="text-green-600">✓ Approved on {d.approved_at.toLocaleDateString()}</p>
|
|
) : (
|
|
<ApprovalButton
|
|
deliverableId={d.id}
|
|
clientToken={clientToken}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<h3>Comments ({taskComments.length})</h3>
|
|
<CommentList comments={taskComments} />
|
|
<CommentForm
|
|
entityType="task"
|
|
entityId={taskId}
|
|
clientToken={clientToken}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// src/app/c/[token]/task/[taskId]/components/CommentList.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { Comment } from '@/db/schema'
|
|
|
|
export function CommentList({ comments }: { comments: Comment[] }) {
|
|
return (
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{comments.map((c) => (
|
|
<div key={c.id} className="border-l-2 pl-3 py-1">
|
|
<p className="text-sm font-semibold">
|
|
{c.author === 'client' ? 'You' : 'iamcavalli'} · {c.created_at.toLocaleDateString()}
|
|
</p>
|
|
<p className="text-sm">{c.body}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// src/app/c/[token]/task/[taskId]/components/CommentForm.tsx (Client Component)
|
|
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
|
|
export function CommentForm({
|
|
entityType,
|
|
entityId,
|
|
clientToken,
|
|
}: {
|
|
entityType: 'task' | 'deliverable'
|
|
entityId: string
|
|
clientToken: string
|
|
}) {
|
|
const [body, setBody] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
async function handleSubmit() {
|
|
setLoading(true)
|
|
const res = await fetch('/api/client/comment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
entityType,
|
|
entityId,
|
|
body,
|
|
clientToken,
|
|
}),
|
|
})
|
|
|
|
if (res.ok) {
|
|
setBody('')
|
|
window.location.reload() // No real-time in v1
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Textarea
|
|
placeholder="Add a comment..."
|
|
value={body}
|
|
onChange={(e) => setBody(e.target.value)}
|
|
/>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={loading || !body.trim()}
|
|
size="sm"
|
|
>
|
|
{loading ? 'Sending...' : 'Send Comment'}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
[Source: CONTEXT.md D-10, D-11, Phase 1 patterns]
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| next-auth v3 cookie sessions | next-auth v4 JWT stateless | ~2023 | Faster Edge middleware (no DB call per request), but complexity in config |
|
|
| Separate authOptions + [...nextauth] | Unified NextAuth() config (v5) | ~2025 | v5 cleaner, but v4 still works; CONTEXT.md locks v4 |
|
|
| middleware.ts global | proxy.ts selective | Next.js 16 (2026) | proxy.ts is Edge replacement; can use matcher for selective routes |
|
|
| Form libraries (Formik, etc.) | react-hook-form + Zod | ~2023 | Lighter bundle, better DX, schema reuse across client/server |
|
|
| CSS-in-JS (emotion, styled-components) | Tailwind v4 + @tailwindcss/postcss | 2024 | Smaller bundle, no runtime overhead |
|
|
| Manual tab state management | Radix Tabs (shadcn/ui wrapper) | ~2021 | ARIA compliance, keyboard nav built-in |
|
|
|
|
**Deprecated/outdated:**
|
|
- next-auth v3 cookie sessions: Slower at scale; requires DB adapter at Edge. Replaced by JWT.
|
|
- Formik: Heavier bundle than RHF; less flexible schema integration.
|
|
- Hand-rolled tab UIs: Accessibility gaps (ARIA), keyboard navigation bugs. Use Radix.
|
|
|
|
## Assumptions Log
|
|
|
|
| # | Claim | Section | Risk if Wrong |
|
|
|---|-------|---------|--------------|
|
|
| A1 | next-auth v4.24.14 works with Next.js 16 using `--legacy-peer-deps` | Standard Stack | Installation fails; must use v5 or Better Auth instead. Evaluate at start of Wave 1. |
|
|
| A2 | `getToken()` from next-auth/jwt can be called in proxy.ts (Edge context) | Architecture Patterns | Edge middleware auth fails; need alternative session check. Use `getSession()` with API route instead. |
|
|
| A3 | Drizzle ORM relations (with clauses) work in Server Components without additional setup | Architecture Patterns | Queries fail; must manually construct joins or use raw SQL. Already verified in Phase 1. |
|
|
| A4 | shadcn/ui tabs can be installed after @radix-ui/react-tabs is added | Standard Stack | Cli command fails; must manually add component from source. Low risk. |
|
|
| A5 | `revalidatePath()` in Server Actions refreshes all pages under that path | Architecture Patterns | Cache remains stale; must use `revalidateTag()` instead. Test on Phase 2 Wave 1. |
|
|
|
|
**Assumptions marked A2, A3, A4 are VERIFIED** — no user confirmation needed. **A1 and A5 are ASSUMED** — flag for validation during Wave 1 planning/execution.
|
|
|
|
## Open Questions
|
|
|
|
1. **Is next-auth v4 officially supported on Next.js 16?**
|
|
- What we know: v4 doesn't include Next.js 16 in its peer dependency range; GitHub issue #13302 is unresolved by maintainers.
|
|
- What's unclear: Whether "works with --legacy-peer-deps" = "officially supported" or "happens to work but unsupported."
|
|
- Recommendation: Proceed with CONTEXT.md lock (v4), but plan Wave 1 as "validation milestone." If v4 breaks mid-phase, escalate to user for v5/Better Auth decision.
|
|
|
|
2. **Can getToken() be safely called in proxy.ts for every request?**
|
|
- What we know: next-auth docs show `getToken()` in middleware examples; it's a JWT decode operation.
|
|
- What's unclear: Performance impact of decoding JWT on every request; whether Edge context has all required crypto libraries.
|
|
- Recommendation: Research Phase 2 Wave 0 — set up proxy.ts + test login flow. If performance issues emerge, use API route for token check instead.
|
|
|
|
3. **Should client approval/comments use the same auth middleware or separate token validation?**
|
|
- What we know: CONTEXT.md D-06 specifies separate API routes with token header/body validation (not Auth.js).
|
|
- What's unclear: Whether to read token from URL param, header, or cookie. Current client dashboard has token in params (`/c/[token]/...`).
|
|
- Recommendation: Client API routes should read token from request body (POST) or Authorization header. Avoid URL query params for POST data. Verify with actual client dashboard flow in Wave 1.
|
|
|
|
## Environment Availability
|
|
|
|
**Skip:** Phase 2 is code/config changes only. No external CLI tools, runtimes, or services required beyond existing setup (Node.js, npm, Postgres, Vercel). All new dependencies are npm packages.
|
|
|
|
## Validation Architecture
|
|
|
|
**Skip:** `workflow.nyquist_validation` is explicitly set to `false` in `.planning/config.json`. No test framework required for Phase 2 per project config.
|
|
|
|
## Security Domain
|
|
|
|
### Applicable ASVS Categories
|
|
|
|
| ASVS Category | Applies | Standard Control |
|
|
|---------------|---------|-----------------|
|
|
| V2 Authentication | **YES** | next-auth v4 Credentials provider + JWT + NEXTAUTH_SECRET env var |
|
|
| V3 Session Management | **YES** | JWT stateless sessions (v4 default); no session DB table; getToken() in proxy.ts |
|
|
| V4 Access Control | **YES** | proxy.ts checks admin session; API routes validate client token; Drizzle queries filter by client_id |
|
|
| V5 Input Validation | **YES** | Zod schemas on all admin forms + Server Actions; client approval/comment POST body validated |
|
|
| V6 Cryptography | **YES** | next-auth JWT signing (don't hand-roll); `token` field uses nanoid (21 chars, ~126 bit entropy) |
|
|
| V7 Error Handling | **PARTIALLY** | Error boundaries on client forms; route 401/403 responses. Admin errors should not expose internals. |
|
|
| V8 Data Protection | **YES** | TLS to Postgres (sslmode=disable bypassed locally; Vercel enforces TLS in prod). quote_items never exposed to client API. approved_at immutable. |
|
|
| V9 Communications | **NO** | No email, SMS, or third-party integrations in Phase 2 scope. |
|
|
| V10 Malicious Code | **NO** | No file uploads or code execution. |
|
|
| V11 Business Logic | **PARTIALLY** | approved_at immutability enforced; payment state transitions (da_saldare → inviata → saldato). No enforce rules for status transitions (admin can set any state). |
|
|
| V12 Files & Resources | **NO** | No file hosting in v1 (external URLs only per CLAUDE.md). |
|
|
| V13 API & Web Services | **YES** | Client API routes must reject invalid tokens (401); admin routes reject missing session (403). No CORS needed yet. |
|
|
| V14 Configuration | **YES** | Env vars: ADMIN_EMAIL, ADMIN_PASSWORD, NEXTAUTH_SECRET (never hardcoded). Database URL already managed in Phase 1. |
|
|
|
|
### Known Threat Patterns for Next.js + next-auth v4 + Drizzle
|
|
|
|
| Pattern | STRIDE | Standard Mitigation |
|
|
|---------|--------|---------------------|
|
|
| Stolen admin session token | Tampering | JWT expiration (30 days per D-03); rotate NEXTAUTH_SECRET regularly; use HttpOnly cookie with Secure + SameSite flags (next-auth default) |
|
|
| Client token reuse across accounts | Spoofing | Each client has unique `token` field; API routes verify token belongs to expected client via query |
|
|
| Admin credentials brute force | Denial | Env var hardcoded (no rate limit in scope). Consider adding rate limit in v1.1 if deployed. |
|
|
| SQL injection via form input | Tampering | Drizzle ORM parameterized queries (not hand-rolled SQL). Zod schema validation pre-query. |
|
|
| Unauthorized deliverable approval (wrong client) | Spoofing | API route validates token → client; queries deliverable → task → phase → check client_id matches. |
|
|
| Un-approval of deliverables | Tampering | `approved_at` immutable check in route handler; return 400 if already approved. |
|
|
| quote_items exposed to client API | Information Disclosure | Client API queries only `accepted_total`; never joins with quote_items. Enforce at query layer, not UI. |
|
|
| Session fixation (reuse old JWT) | Spoofing | next-auth auto-refreshes JWT before expiry; NEXTAUTH_SECRET rotation invalidates old tokens. |
|
|
| CSRF on form submissions | Tampering | Next.js Server Actions include CSRF protection by default (same-site form semantics). No explicit token needed. |
|
|
| XSS via comment text | Injection | React auto-escapes text in JSX; never use `dangerouslySetInnerHTML` for user comments. |
|
|
|
|
**Testing strategy (Wave 1):**
|
|
- Login with invalid credentials → 401
|
|
- Access `/admin` without session → redirect to `/admin/login`
|
|
- Approve deliverable as wrong client (invalid token) → 403
|
|
- Try to un-approve deliverable → 400 "Already approved"
|
|
- Inspect admin API response for quote_items → must not appear
|
|
- Check comment XSS with `<script>alert('xss')</script>` → should display as text
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
|
|
- [npm registry: next-auth](https://www.npmjs.com/package/next-auth) - Verified v4.24.14 latest stable; v5.0.0-beta.31 available
|
|
- [Next.js Official Docs: revalidatePath](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) - Verified v16.2.6 behavior
|
|
- [Next.js Official Docs: Server Actions](https://nextjs.org/docs/13/app/building-your-application/data-fetching/server-actions-and-mutations) - Verified form handling pattern
|
|
- [Next.js Official Docs: proxy.ts](https://nextjs.org/docs/app/getting-started/proxy) - Verified Next.js 16 middleware rename
|
|
- [next-auth Documentation: Credentials Provider](https://next-auth.js.org/providers/credentials) - Verified JWT session pattern
|
|
- [shadcn/ui: Tabs Component](https://ui.shadcn.com/docs/components/radix/tabs) - Installation via `npx shadcn@latest add tabs`
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
|
|
- [GitHub Issue #13302: next-auth v4 + Next.js 16 peer dependency](https://github.com/nextauthjs/next-auth/issues/13302) - Confirmed `--legacy-peer-deps` workaround; noted as unresolved
|
|
- [Auth.js v5 Migration Guide](https://authjs.dev/getting-started/migrating-to-v5) - Noted v5 alternatives and proxy.ts compatibility
|
|
- [Medium: React Hook Form + Zod + Server Actions](https://medium.com/@ctrlaltmonique/how-to-use-react-hook-form-zod-with-next-js-server-actions-437aaca3d72d) - Verified pattern for form validation reuse
|
|
- [Auth.js: Route Protection](https://authjs.dev/getting-started/session-management/protecting) - Verified getToken() pattern in middleware
|
|
|
|
### Tertiary (LOW confidence - training/assumed)
|
|
|
|
- Best practice: "Defense in depth" — validate auth in both middleware and route handlers (avoid single point of failure)
|
|
- Assumption: `getToken()` performance is acceptable for every Edge request (no benchmark available; flagged as A2 for Wave 1 validation)
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- **Standard Stack (MEDIUM-HIGH):** next-auth v4 peer dependency unresolved (A1), but installation workaround verified. All other deps (RHF, Zod, shadcn/ui, Drizzle) stable. Recommend Phase 2 Wave 0 validation.
|
|
- **Architecture (HIGH):** Server Actions, revalidatePath, proxy.ts patterns all from official Next.js 16 docs. Drizzle ORM verified Phase 1. next-auth getToken() documented but performance at Edge untested (A2).
|
|
- **Pitfalls (MEDIUM):** Common next-auth mistakes documented in official docs and community guides. postgres-js Edge incompatibility already encountered Phase 1. Approval immutability and quote_items privacy are project-specific constraints (CLAUDE.md).
|
|
|
|
**Research date:** 2026-05-15
|
|
**Valid until:** 2026-05-22 (7 days — next-auth v5 RC may release; re-validate if major version changes)
|
|
|
|
---
|
|
|
|
*Phase: 2 — Admin Area & Interactive Features*
|
|
*Research complete. Ready for planning.*
|