import { NextRequest, NextResponse } from 'next/server' import { jwtVerify } from 'jose' const JWT_SECRET = new TextEncoder().encode( process.env.NEXTAUTH_SECRET || 'dev-only-fallback-do-not-use-in-production' ) // Routes that require authentication const PROTECTED_ROUTES = ['/app', '/settings', '/admin'] // Routes that should redirect to /app if already logged in const AUTH_ROUTES = ['/login', '/register'] // API routes that are public (no auth needed) const PUBLIC_API_PREFIXES = [ '/api/auth/login', '/api/auth/register', '/api/auth/forgot-password', '/api/auth/reset-password', '/api/auth/verify-email', '/api/auth/resend-verification', '/api/auth/logout', '/api/contact', '/api/demo', '/api/donate', '/api/rapports/', '/api/tenants/by-slug/', ] export async function middleware(req: NextRequest) { const { pathname } = req.nextUrl const token = req.cookies.get('auth-token')?.value // Verify token if present let user: any = null if (token) { try { const { payload } = await jwtVerify(token, JWT_SECRET) user = payload.user } catch { // Invalid/expired token — clear it const response = NextResponse.redirect(new URL('/login', req.url)) response.cookies.delete('auth-token') // Only redirect if accessing protected routes if (PROTECTED_ROUTES.some(r => pathname.startsWith(r))) { return response } } } // Protected routes: redirect to login if not authenticated if (PROTECTED_ROUTES.some(r => pathname.startsWith(r))) { if (!user) { const loginUrl = new URL('/login', req.url) loginUrl.searchParams.set('redirect', pathname) return NextResponse.redirect(loginUrl) } // Admin routes: only SERVER_ADMIN and TENANT_ADMIN if (pathname.startsWith('/admin') && user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN') { return NextResponse.redirect(new URL('/app', req.url)) } } // Auth routes: redirect to /app if already logged in if (AUTH_ROUTES.some(r => pathname.startsWith(r))) { if (user) { return NextResponse.redirect(new URL('/app', req.url)) } } // API routes: check auth for non-public endpoints if (pathname.startsWith('/api/') && !PUBLIC_API_PREFIXES.some(p => pathname.startsWith(p))) { if (!user) { // Allow /api/auth/me to return null (used for auth check) if (pathname === '/api/auth/me') { return NextResponse.next() } // Allow /api/icons GET (public for symbol loading) if (pathname === '/api/icons' && req.method === 'GET') { return NextResponse.next() } return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 }) } } // Security: block common attack paths if ( pathname.includes('..') || pathname.includes('.env') || pathname.includes('wp-admin') || pathname.includes('wp-login') || pathname.includes('.php') || pathname.includes('xmlrpc') || pathname.match(/\.(sql|bak|config|log|ini)$/i) ) { return new NextResponse(null, { status: 404 }) } return NextResponse.next() } export const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization) * - favicon.ico, sitemap.xml, robots.txt * - public files (images, sw.js, etc.) */ '/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|icons/|sw.js|manifest.json|opengraph-image).*)', ], }