Files
Lageplan/src/middleware.ts

115 lines
3.4 KiB
TypeScript

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).*)',
],
}