Files
biblical-guide.com/middleware.ts
andupetcu 3b375c869b Add complete Biblical Guide web application with Material UI
Implemented comprehensive Romanian Biblical Guide web app:
- Next.js 15 with App Router and TypeScript
- Material UI 7.3.2 for modern, responsive design
- PostgreSQL database with Prisma ORM
- Complete Bible reader with book/chapter navigation
- AI-powered biblical chat with Romanian responses
- Prayer wall for community prayer requests
- Advanced Bible search with filters and highlighting
- Sample Bible data imported from API.Bible
- All API endpoints created and working
- Professional Material UI components throughout
- Responsive layout with navigation and theme

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 14:10:28 +03:00

161 lines
5.1 KiB
TypeScript

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from '@/lib/auth'
import { prisma } from '@/lib/db'
// 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<string> {
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) {
// 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: [
'/api/:path*',
'/dashboard/:path*'
],
}