import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { verifyToken } from '@/lib/auth' import { prisma } from '@/lib/db' import createIntlMiddleware from 'next-intl/middleware' // Internationalization configuration const intlMiddleware = createIntlMiddleware({ locales: ['ro', 'en'], defaultLocale: 'ro' }) // Rate limiting configuration const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute const RATE_LIMITS = { general: 100, // 100 requests per minute auth: 5, // 5 auth requests per minute chat: 10, // 10 chat requests per minute search: 20, // 20 search requests per minute } async function getRateLimitKey(request: NextRequest, endpoint: string): Promise { const forwarded = request.headers.get('x-forwarded-for') const ip = forwarded ? forwarded.split(',')[0] : request.ip || 'unknown' return `ratelimit:${endpoint}:${ip}` } async function checkRateLimit(request: NextRequest, endpoint: string, limit: number): Promise<{ allowed: boolean; remaining: number }> { try { const key = await getRateLimitKey(request, endpoint) const now = Date.now() const windowStart = now - RATE_LIMIT_WINDOW // Clean up old entries and count current requests await prisma.$executeRaw` DELETE FROM verse_cache WHERE key LIKE ${key + ':%'} AND created_at < ${new Date(windowStart)} ` const currentCount = await prisma.$queryRaw<{ count: number }[]>` SELECT COUNT(*) as count FROM verse_cache WHERE key LIKE ${key + ':%'} ` const requestCount = Number(currentCount[0]?.count || 0) if (requestCount >= limit) { return { allowed: false, remaining: 0 } } // Record this request await prisma.$executeRaw` INSERT INTO verse_cache (key, value, expires_at, created_at) VALUES (${key + ':' + now}, '1', ${new Date(now + RATE_LIMIT_WINDOW)}, ${new Date(now)}) ` return { allowed: true, remaining: limit - requestCount - 1 } } catch (error) { console.error('Rate limit check error:', error) // Allow request on error return { allowed: true, remaining: limit } } } export async function middleware(request: NextRequest) { // Handle internationalization for non-API routes if (!request.nextUrl.pathname.startsWith('/api')) { return intlMiddleware(request) } // Determine endpoint type for rate limiting let endpoint = 'general' let limit = RATE_LIMITS.general if (request.nextUrl.pathname.startsWith('/api/auth')) { endpoint = 'auth' limit = RATE_LIMITS.auth } else if (request.nextUrl.pathname.startsWith('/api/chat')) { endpoint = 'chat' limit = RATE_LIMITS.chat } else if (request.nextUrl.pathname.startsWith('/api/bible/search')) { endpoint = 'search' limit = RATE_LIMITS.search } // Apply rate limiting to API routes if (request.nextUrl.pathname.startsWith('/api/')) { const rateLimit = await checkRateLimit(request, endpoint, limit) const response = rateLimit.allowed ? NextResponse.next() : new NextResponse('Too Many Requests', { status: 429 }) // Add rate limit headers response.headers.set('X-RateLimit-Limit', limit.toString()) response.headers.set('X-RateLimit-Remaining', rateLimit.remaining.toString()) response.headers.set('X-RateLimit-Reset', (Date.now() + RATE_LIMIT_WINDOW).toString()) if (!rateLimit.allowed) { return response } } // Security headers for all responses const response = NextResponse.next() // Security headers response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-Frame-Options', 'DENY') response.headers.set('X-XSS-Protection', '1; mode=block') response.headers.set('Referrer-Policy', 'origin-when-cross-origin') response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') // CSRF protection for state-changing operations if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) { const origin = request.headers.get('origin') const host = request.headers.get('host') if (origin && host && !origin.endsWith(host)) { return new NextResponse('Forbidden', { status: 403 }) } } // Authentication check for protected routes if (request.nextUrl.pathname.startsWith('/api/bookmarks') || request.nextUrl.pathname.startsWith('/api/notes') || request.nextUrl.pathname.startsWith('/dashboard')) { const token = request.headers.get('authorization')?.replace('Bearer ', '') || request.cookies.get('authToken')?.value if (!token) { if (request.nextUrl.pathname.startsWith('/api/')) { return new NextResponse('Unauthorized', { status: 401 }) } else { return NextResponse.redirect(new URL('/', request.url)) } } try { const payload = await verifyToken(token) // Add user ID to headers for API routes if (request.nextUrl.pathname.startsWith('/api/')) { const requestHeaders = new Headers(request.headers) requestHeaders.set('x-user-id', payload.userId) return NextResponse.next({ request: { headers: requestHeaders, }, }) } } catch (error) { if (request.nextUrl.pathname.startsWith('/api/')) { return new NextResponse('Invalid token', { status: 401 }) } else { return NextResponse.redirect(new URL('/', request.url)) } } } return response } export const config = { matcher: [ // Match all pathnames except for // - api routes // - _next (Next.js internals) // - static files (images, etc.) '/((?!api|_next|_vercel|.*\\..*).*)', // However, match all pathnames within `/api`, except for the Middleware to run there '/api/:path*', '/dashboard/:path*' ], }