Ops-dashboard/proxy.ts
Janpeter Visser 9a7191f4c1 fix(proxy): merge middleware.ts into proxy.ts for Next.js 16 compat
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) <noreply@anthropic.com>
2026-05-13 21:20:24 +02:00

67 lines
2 KiB
TypeScript

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 { method, nextUrl } = request
const { pathname } = nextUrl
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 (!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))
}
}
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: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|ico|svg)$).*)'],
}