From ef3481744cbda2e7cf0500f9c7b8c29a7c9c9634 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 14 May 2026 20:15:11 +0200 Subject: [PATCH] feat(01-03): add Edge middleware + internal validate-token API route - src/middleware.ts: Edge-compatible token validation via fetch() (no Drizzle import) - src/app/api/internal/validate-token/route.ts: Node.js route queries clients.token via Drizzle - Invalid tokens rewrite to /not-found (404); matcher scoped to /c/:path* --- src/app/api/internal/validate-token/route.ts | 28 ++++++++++++++++ src/middleware.ts | 35 ++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/app/api/internal/validate-token/route.ts create mode 100644 src/middleware.ts diff --git a/src/app/api/internal/validate-token/route.ts b/src/app/api/internal/validate-token/route.ts new file mode 100644 index 0000000..11e3f8a --- /dev/null +++ b/src/app/api/internal/validate-token/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { eq } from 'drizzle-orm'; +import { db } from '@/db'; +import { clients } from '@/db/schema'; + +export async function GET(request: NextRequest) { + const token = request.nextUrl.searchParams.get('token'); + + if (!token) { + return NextResponse.json({ valid: false }, { status: 400 }); + } + + try { + const rows = await db + .select({ id: clients.id }) + .from(clients) + .where(eq(clients.token, token)) + .limit(1); + + if (rows.length === 0) { + return NextResponse.json({ valid: false }, { status: 404 }); + } + + return NextResponse.json({ valid: true }, { status: 200 }); + } catch { + return NextResponse.json({ valid: false }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..9ab66ae --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function middleware(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 { + // Call internal Node.js API route — Edge middleware cannot use postgres-js directly + // postgres-js requires Node.js net/tls which are unavailable in the Edge runtime + 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*'], +}; \ No newline at end of file