From 9a7191f4c1b3351f3f4c238623016dbe95512096 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 21:20:24 +0200 Subject: [PATCH] fix(proxy): merge middleware.ts into proxy.ts for Next.js 16 compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js 16 staat alleen proxy.ts toe als de twee co-existeren; build faalt met "Both middleware file and proxy file are detected". De CSP- en CSRF-logica uit middleware.ts is samengevoegd in proxy.ts en de auth-redirect blijft. CSRF-validatie geldt nu alleen voor POST /api/*, auth-redirect alleen buiten /api — matcher uitgebreid om beide te dekken. Co-Authored-By: Claude Opus 4.7 (1M context) --- middleware.ts | 55 --------------------------------------------- proxy.ts | 62 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 64 deletions(-) delete mode 100644 middleware.ts diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 99f0bae..0000000 --- a/middleware.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -const CSP = [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline'", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "img-src 'self' data:", - "connect-src 'self'", - "frame-ancestors 'none'", - "base-uri 'self'", - "form-action 'self'", -].join('; ') - -const CSRF_COOKIE = 'csrf_token' -const CSRF_HEADER = 'x-csrf-token' - -export function middleware(request: NextRequest) { - const { method, nextUrl } = request - - // Validate CSRF token on all POST requests to API routes - if (method === 'POST' && nextUrl.pathname.startsWith('/api/')) { - const cookieToken = request.cookies.get(CSRF_COOKIE)?.value - const headerToken = request.headers.get(CSRF_HEADER) - if (!cookieToken || cookieToken !== headerToken) { - return new NextResponse( - JSON.stringify({ error: 'CSRF validation failed' }), - { status: 403, headers: { 'Content-Type': 'application/json' } }, - ) - } - } - - const response = NextResponse.next() - - response.headers.set('Content-Security-Policy', CSP) - response.headers.set('X-Frame-Options', 'DENY') - response.headers.set('X-Content-Type-Options', 'nosniff') - response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') - - // Issue a CSRF token cookie on GET requests when not yet present - if (method === 'GET' && !request.cookies.get(CSRF_COOKIE)) { - response.cookies.set(CSRF_COOKIE, crypto.randomUUID(), { - httpOnly: false, // must be readable by client JS for the double-submit pattern - sameSite: 'strict', - secure: process.env.NODE_ENV === 'production', - path: '/', - }) - } - - return response -} - -export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], -} diff --git a/proxy.ts b/proxy.ts index e2b24d3..2f902de 100644 --- a/proxy.ts +++ b/proxy.ts @@ -2,22 +2,66 @@ import { NextRequest, NextResponse } from 'next/server' const PUBLIC_PATHS = ['/login'] +const CSP = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "img-src 'self' data:", + "connect-src 'self'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", +].join('; ') + +const CSRF_COOKIE = 'csrf_token' +const CSRF_HEADER = 'x-csrf-token' + export default function proxy(request: NextRequest) { - const { pathname } = request.nextUrl - const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) - const hasSession = request.cookies.has('ops_session') + const { method, nextUrl } = request + const { pathname } = nextUrl - if (!isPublic && !hasSession) { - return NextResponse.redirect(new URL('/login', request.url)) + if (method === 'POST' && pathname.startsWith('/api/')) { + const cookieToken = request.cookies.get(CSRF_COOKIE)?.value + const headerToken = request.headers.get(CSRF_HEADER) + if (!cookieToken || cookieToken !== headerToken) { + return new NextResponse( + JSON.stringify({ error: 'CSRF validation failed' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ) + } } - if (isPublic && hasSession) { - return NextResponse.redirect(new URL('/', request.url)) + if (!pathname.startsWith('/api/')) { + const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) + const hasSession = request.cookies.has('ops_session') + if (!isPublic && !hasSession) { + return NextResponse.redirect(new URL('/login', request.url)) + } + if (isPublic && hasSession) { + return NextResponse.redirect(new URL('/', request.url)) + } } - return NextResponse.next() + const response = NextResponse.next() + + response.headers.set('Content-Security-Policy', CSP) + response.headers.set('X-Frame-Options', 'DENY') + response.headers.set('X-Content-Type-Options', 'nosniff') + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') + + if (method === 'GET' && !request.cookies.get(CSRF_COOKIE)) { + response.cookies.set(CSRF_COOKIE, crypto.randomUUID(), { + httpOnly: false, + sameSite: 'strict', + secure: process.env.NODE_ENV === 'production', + path: '/', + }) + } + + return response } export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.(?:png|ico|svg)$).*)'], + matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|ico|svg)$).*)'], }