mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
- CSRF: Require CSRF_SECRET in production, throw error on missing secret - IP Allowlist: TRUST_PROXY must be explicitly set to 'true' for proxy headers - Rate Limiter: Add proper proxy trust handling for client IP detection - Login: Add browser form redirect support with safe URL validation - Add custom admin login page with styled form - Update CLAUDE.md with TRUST_PROXY documentation - Update tests for new security behavior BREAKING: Server will not start in production without CSRF_SECRET or PAYLOAD_SECRET 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
3 KiB
TypeScript
110 lines
3 KiB
TypeScript
// src/middleware.ts
|
|
// Next.js Middleware for locale detection and routing
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { defaultLocale, isValidLocale, type Locale } from '@/lib/i18n'
|
|
|
|
// Paths that should not be affected by locale routing
|
|
const PUBLIC_FILE = /\.(.*)$/
|
|
const EXCLUDED_PATHS = ['/admin', '/api', '/_next', '/favicon.ico', '/robots.txt', '/sitemap.xml']
|
|
|
|
/**
|
|
* Detect user's preferred locale from Accept-Language header
|
|
*/
|
|
function getPreferredLocale(request: NextRequest): Locale {
|
|
const acceptLanguage = request.headers.get('accept-language')
|
|
|
|
if (!acceptLanguage) {
|
|
return defaultLocale
|
|
}
|
|
|
|
// Parse Accept-Language header
|
|
const languages = acceptLanguage
|
|
.split(',')
|
|
.map((lang) => {
|
|
const [code, priority] = lang.trim().split(';q=')
|
|
return {
|
|
code: code.split('-')[0].toLowerCase(), // Get primary language code
|
|
priority: priority ? parseFloat(priority) : 1,
|
|
}
|
|
})
|
|
.sort((a, b) => b.priority - a.priority)
|
|
|
|
// Find first matching locale
|
|
for (const { code } of languages) {
|
|
if (isValidLocale(code)) {
|
|
return code
|
|
}
|
|
}
|
|
|
|
return defaultLocale
|
|
}
|
|
|
|
/**
|
|
* Get locale from cookie
|
|
*/
|
|
function getLocaleFromCookie(request: NextRequest): Locale | null {
|
|
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value
|
|
|
|
if (cookieLocale && isValidLocale(cookieLocale)) {
|
|
return cookieLocale
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const { pathname, searchParams } = request.nextUrl
|
|
|
|
// Admin-Pfade komplett von Middleware ausschließen - Payload handled diese selbst
|
|
if (pathname.startsWith('/admin')) {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
// Skip locale routing for excluded paths and public files
|
|
if (
|
|
EXCLUDED_PATHS.some((path) => pathname.startsWith(path)) ||
|
|
PUBLIC_FILE.test(pathname)
|
|
) {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
// Check if pathname already has a valid locale prefix
|
|
const pathnameLocale = pathname.split('/')[1]
|
|
|
|
if (isValidLocale(pathnameLocale)) {
|
|
// Valid locale in URL, set cookie and continue
|
|
const response = NextResponse.next()
|
|
response.cookies.set('NEXT_LOCALE', pathnameLocale, {
|
|
maxAge: 60 * 60 * 24 * 365, // 1 year
|
|
path: '/',
|
|
})
|
|
return response
|
|
}
|
|
|
|
// No locale in URL, redirect to preferred locale
|
|
const cookieLocale = getLocaleFromCookie(request)
|
|
const preferredLocale = cookieLocale || getPreferredLocale(request)
|
|
|
|
// Build new URL with locale prefix
|
|
const newUrl = new URL(request.url)
|
|
newUrl.pathname = `/${preferredLocale}${pathname === '/' ? '' : pathname}`
|
|
|
|
// Redirect to localized URL
|
|
const response = NextResponse.redirect(newUrl)
|
|
response.cookies.set('NEXT_LOCALE', preferredLocale, {
|
|
maxAge: 60 * 60 * 24 * 365, // 1 year
|
|
path: '/',
|
|
})
|
|
|
|
return response
|
|
}
|
|
|
|
export const config = {
|
|
// Match all paths except static files and API routes
|
|
// Explicitly include /admin/login for redirect loop prevention
|
|
matcher: [
|
|
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
'/admin/login',
|
|
],
|
|
}
|