115 lines
3.4 KiB
TypeScript
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).*)',
|
|
],
|
|
}
|