--- phase: "02-admin-area-interactive-features" plan: 01 type: execute wave: 1 depends_on: [] files_modified: - package.json - src/proxy.ts - src/app/api/auth/[...nextauth]/route.ts - src/app/admin/login/page.tsx - src/app/admin/login/actions.ts - src/lib/auth.ts - .env.local autonomous: true requirements: - ADMIN-01 - ADMIN-02 must_haves: truths: - "Admin can POST /admin/login with ADMIN_EMAIL + ADMIN_PASSWORD and receive a session JWT cookie" - "Visiting /admin/* without a valid session redirects to /admin/login" - "Visiting /c/[token]/* still validates token at edge (proxy.ts unchanged for client routes)" - "Session is JWT-based (stateless) — no DB users table involved" - "ADMIN_EMAIL and ADMIN_PASSWORD are read from env vars, never hardcoded" artifacts: - path: "src/lib/auth.ts" provides: "NextAuth config — CredentialsProvider validating against ADMIN_EMAIL/ADMIN_PASSWORD env vars" contains: "CredentialsProvider" - path: "src/app/api/auth/[...nextauth]/route.ts" provides: "NextAuth catch-all route handler" contains: "NextAuth" - path: "src/app/admin/login/page.tsx" provides: "Admin login form UI (email + password, submit)" min_lines: 30 - path: "src/proxy.ts" provides: "Updated proxy: /c/* token validation + /admin/* session guard" contains: "getToken" key_links: - from: "src/proxy.ts" to: "src/app/api/auth/[...nextauth]/route.ts" via: "getToken({ req, secret: process.env.NEXTAUTH_SECRET })" pattern: "getToken" - from: "src/app/admin/login/page.tsx" to: "/api/auth/callback/credentials" via: "signIn('credentials', { email, password })" pattern: "signIn" - from: "ADMIN_EMAIL + ADMIN_PASSWORD" to: "CredentialsProvider authorize()" via: "process.env.ADMIN_EMAIL" pattern: "ADMIN_EMAIL" --- **Auth.js Admin Session + Proxy Guard:** Install next-auth@4, configure a CredentialsProvider that validates against ADMIN_EMAIL/ADMIN_PASSWORD env vars, wire the catch-all API route, build the login page, and extend the existing src/proxy.ts to guard /admin/* routes with a session check. Purpose: Gate the entire admin area behind Auth.js JWT session before any admin UI is built. Two independent auth paths are enforced: /c/[token]/* uses edge token validation (unchanged from Phase 1), /admin/* uses getToken() from next-auth/jwt. No DB users table — single admin, env-var credentials only (per D-01, D-02, D-03, D-04). Output: Working /admin/login page, session cookie on successful login, automatic redirect to /admin/login for unauthenticated access to any /admin/* route. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/ROADMAP.md @.planning/phases/02-admin-area-interactive-features/02-CONTEXT.md @.planning/phases/01-foundation-client-dashboard/01-05-SUMMARY.md Current src/proxy.ts content: ```typescript import { NextRequest, NextResponse } from 'next/server'; export async function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname; // Extract token from path: /c/[token]/... const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); if (!tokenMatch) { return NextResponse.rewrite(new URL('/not-found', request.url)); } 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)); } } export const config = { matcher: ['/c/:path*'], }; ``` Note: Next.js middleware MUST be exported as `middleware`, not `proxy`. The Phase 1 file uses `proxy` — this plan must rename it to `middleware` while extending it with /admin/* guard. No src/middleware.ts should ever be created. From src/db/index.ts: ```typescript import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; export const db = drizzle(client); ``` From src/db/schema.ts (types needed in this plan): ```typescript // No schema changes needed — no users table. Auth is env-var only. export type Client = typeof clients.$inferSelect; ``` Task 1: Install next-auth@4, create src/lib/auth.ts and NextAuth catch-all route package.json src/lib/auth.ts src/app/api/auth/[...nextauth]/route.ts .env.local Install next-auth v4 (stable — v5 is still beta RC as of 2026-05-15, per D-01): ``` npm install next-auth@4 ``` Add to .env.local (generate NEXTAUTH_SECRET with: `openssl rand -base64 32`): ``` NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET= ADMIN_EMAIL=simone.cavalli.gestione@gmail.com ADMIN_PASSWORD= ``` Create `src/lib/auth.ts` — NextAuth config, no DB adapter (per D-03): ```typescript import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; export const authOptions: NextAuthOptions = { providers: [ CredentialsProvider({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null; const adminEmail = process.env.ADMIN_EMAIL; const adminPassword = process.env.ADMIN_PASSWORD; if (!adminEmail || !adminPassword) { throw new Error("ADMIN_EMAIL and ADMIN_PASSWORD env vars must be set"); } if ( credentials.email === adminEmail && credentials.password === adminPassword ) { // Return minimal session user — no DB lookup needed return { id: "admin", email: adminEmail, name: "Admin" }; } return null; // null = unauthorized (NextAuth returns 401) }, }), ], session: { strategy: "jwt", // stateless JWT — no DB session table (per D-03) maxAge: 30 * 24 * 60 * 60, // 30 days }, pages: { signIn: "/admin/login", // custom login page (per D-07) }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; } return token; }, async session({ session, token }) { if (session.user) { (session.user as { id?: string }).id = token.id as string; } return session; }, }, }; ``` Create `src/app/api/auth/[...nextauth]/route.ts` — NextAuth catch-all: ```typescript import NextAuth from "next-auth"; import { authOptions } from "@/lib/auth"; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; ``` Note: next-auth@4 with App Router uses this export pattern. The handler handles GET (session fetch, CSRF) and POST (sign in, sign out). grep -q '"next-auth"' package.json && echo "next-auth installed" test -f src/lib/auth.ts && grep -q "CredentialsProvider" src/lib/auth.ts && echo "CredentialsProvider configured" grep -q "strategy.*jwt" src/lib/auth.ts && echo "JWT session strategy set" grep -q "ADMIN_EMAIL" src/lib/auth.ts && echo "ADMIN_EMAIL env var referenced" test -f src/app/api/auth/\[...nextauth\]/route.ts && grep -q "NextAuth" src/app/api/auth/\[...nextauth\]/route.ts && echo "NextAuth route created" grep -q "NEXTAUTH_SECRET" .env.local && echo "NEXTAUTH_SECRET in .env.local" npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK" - next-auth@4 is in package.json - src/lib/auth.ts exports authOptions with CredentialsProvider using env vars - src/app/api/auth/[...nextauth]/route.ts exports GET and POST handlers - NEXTAUTH_SECRET, ADMIN_EMAIL, ADMIN_PASSWORD are set in .env.local - npm run build passes without errors Task 2: Extend src/proxy.ts to guard /admin/* with session check; create /admin/login page src/proxy.ts src/app/admin/login/page.tsx src/app/admin/login/actions.ts **Replace** `src/proxy.ts` entirely. NEVER create src/middleware.ts — it would be a dead file ignored by Next.js since this project uses src/proxy.ts as the middleware entry point. The new file renames the export from `proxy` to `middleware` (required by Next.js) and adds the /admin/* guard alongside the existing /c/* token validation logic (per D-04): ```typescript import { NextRequest, NextResponse } from "next/server"; import { getToken } from "next-auth/jwt"; export async function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; // ── ADMIN GUARD ────────────────────────────────────────────────────────── if (pathname.startsWith("/admin")) { // Allow the login page and NextAuth API routes through without session check if ( pathname === "/admin/login" || pathname.startsWith("/api/auth") ) { return NextResponse.next(); } const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET, }); if (!token) { const loginUrl = new URL("/admin/login", request.url); loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } // ── CLIENT TOKEN GUARD ─────────────────────────────────────────────────── if (pathname.startsWith("/c/")) { const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); if (!tokenMatch) { return NextResponse.rewrite(new URL("/not-found", request.url)); } const clientToken = tokenMatch[1]; try { const validateUrl = new URL( `/api/internal/validate-token?token=${encodeURIComponent(clientToken)}`, 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.next(); } export const config = { matcher: ["/admin/:path*", "/c/:path*"], }; ``` Note: The export is renamed from `proxy` to `middleware`. This is the only correct Next.js middleware export name. The /c/* logic is preserved verbatim from Phase 1. Create `src/app/admin/login/page.tsx` — login form as Client Component: ```typescript "use client"; import { useState } from "react"; import { signIn } from "next-auth/react"; import { useRouter, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export default function AdminLoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl") ?? "/admin"; const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); const result = await signIn("credentials", { email, password, redirect: false, // handle redirect manually to show errors }); if (result?.error) { setError("Email o password non corretti."); setLoading(false); return; } router.replace(callbackUrl); } return (
Admin — ClientHub
setEmail(e.target.value)} required autoComplete="email" />
setPassword(e.target.value)} required autoComplete="current-password" />
{error && (

{error}

)}
); } ``` Do NOT create src/app/admin/login/actions.ts — the login is handled client-side via signIn(). No Server Action file is needed.
test -f src/proxy.ts && grep -q "getToken" src/proxy.ts && echo "getToken imported in proxy.ts" grep -q "export async function middleware" src/proxy.ts && echo "export named middleware (not proxy)" grep -q '"/admin/:path\*"' src/proxy.ts && echo "admin matcher configured" grep -q '"/c/:path\*"' src/proxy.ts && echo "client matcher still present" grep -q "pathname === \"/admin/login\"" src/proxy.ts && echo "login page exempted from auth guard" test -f src/app/admin/login/page.tsx && grep -q "signIn" src/app/admin/login/page.tsx && echo "login page uses signIn" grep -q '"use client"' src/app/admin/login/page.tsx && echo "login page is Client Component" test ! -f src/middleware.ts && echo "src/middleware.ts does NOT exist (correct)" npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK" - src/proxy.ts guards /admin/* routes: unauthenticated requests redirect to /admin/login?callbackUrl=... - Export renamed from proxy to middleware (required by Next.js) - /admin/login and /api/auth/* are exempt from the session guard - /c/:path* token validation is unchanged - /admin/login page renders email+password form, calls signIn('credentials'), shows error on failure, redirects on success - src/middleware.ts does NOT exist - npm run build passes - Manual verification: visiting http://localhost:3000/admin redirects to /admin/login; successful login redirects back to /admin
## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser → /admin/* | All admin routes gated by JWT session cookie; proxy.ts rejects unauthenticated requests before any page code runs | | Login form → CredentialsProvider | Email + password transmitted over HTTPS; validated in server-side authorize() only | | NEXTAUTH_SECRET → JWT signing | All session tokens are HMAC-signed; tampering is detectable | | ADMIN_EMAIL/ADMIN_PASSWORD → env vars | Credentials never in source code; must be in .env.local and Vercel environment | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-02-01 | Spoofing | Admin login | mitigate | CredentialsProvider validates against env vars server-side; password never logged; JWT signed with NEXTAUTH_SECRET | | T-02-02 | Tampering | JWT session cookie | mitigate | next-auth signs JWT with NEXTAUTH_SECRET (HMAC-SHA256); proxy.ts verifies signature on every /admin request via getToken() | | T-02-03 | Information Disclosure | ADMIN_PASSWORD in env | mitigate | Stored only in .env.local (gitignored) and Vercel environment secrets; never returned in API responses | | T-02-04 | Elevation of Privilege | /api/auth/* exemption | accept | NextAuth API routes are exempt from session guard by design; they perform their own CSRF and credential validation internally | | T-02-05 | Denial of Service | Brute-force login | accept | Single admin, not a public product; no rate limiting in v1. If needed in v2, add next-auth rate limit middleware. | After plan execution: 1. `npm run build` — no TypeScript errors 2. `npm run dev`, visit http://localhost:3000/admin → redirects to /admin/login 3. Submit wrong credentials → error message "Email o password non corretti." appears 4. Submit correct ADMIN_EMAIL + ADMIN_PASSWORD → redirects to /admin (200, even if page is blank) 5. Visit http://localhost:3000/c/any-token → still validates token (client path unchanged) 6. Visit http://localhost:3000/api/auth/session after login → returns `{ user: { email, id: "admin" } }` 7. Confirm src/middleware.ts does not exist in the repo - Admin can log in at /admin/login with env-var credentials and receive a JWT session cookie - All /admin/* routes (except /admin/login and /api/auth/*) redirect unauthenticated visitors to /admin/login - Client token route /c/:path* is unaffected - No DB users table exists or is needed - src/proxy.ts is the middleware file — src/middleware.ts never created - npm run build passes cleanly After completion, create `.planning/phases/02-admin-area-interactive-features/02-01-SUMMARY.md`