-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.ts
More file actions
168 lines (146 loc) · 5.26 KB
/
proxy.ts
File metadata and controls
168 lines (146 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { NextResponse, type NextRequest } from 'next/server'
import { updateSession } from "@/lib/supabase/session"
import { validateOrigin, MAX_REQUEST_SIZES } from './lib/security/request-validator'
/**
* Middleware for Next.js 16+
*
* Implements:
* - Production blocking of test/debug pages
* - Edge rate limiting for API routes
* - CSRF protection via origin validation
* - Request size limits
* - Supabase session management
* - Security headers
*/
// Routes that must be BLOCKED in production (test/debug pages)
const blockedInProduction = [
"/test-error",
"/test-interface",
"/admin/seed",
]
// Rate limit store (in-memory; use Redis in production)
const rateLimitStore = new Map<string, { count: number; resetTime: number }>()
setInterval(() => {
const now = Date.now()
for (const [key, entry] of rateLimitStore.entries()) {
if (now > entry.resetTime) rateLimitStore.delete(key)
}
}, 60_000)
const apiRateLimits: Record<string, { max: number; window: number }> = {
"/api/chat": { max: 30, window: 60_000 },
"/api/nairi/chat": { max: 30, window: 60_000 },
"/api/generate": { max: 10, window: 60_000 },
"/api/generate-image": { max: 5, window: 60_000 },
"/api/generate-video": { max: 3, window: 60_000 },
"/api/generate-3d": { max: 3, window: 60_000 },
"/api/create": { max: 10, window: 60_000 },
"/api/auth": { max: 5, window: 60_000 },
}
function checkRateLimit(identifier: string, max: number, window: number): boolean {
const now = Date.now()
const key = `${identifier}:${max}:${window}`
let entry = rateLimitStore.get(key)
if (!entry || now > entry.resetTime) {
entry = { count: 1, resetTime: now + window }
rateLimitStore.set(key, entry)
return true
}
entry.count++
if (entry.count > max) return false
return true
}
function getClientIp(request: NextRequest): string {
const forwarded = request.headers.get("x-forwarded-for")
if (forwarded) return forwarded.split(",")[0].trim()
const realIp = request.headers.get("x-real-ip")
if (realIp) return realIp
return "unknown"
}
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// Skip security checks for static files and Next.js internals
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/static') ||
pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|woff|woff2|ttf|eot)$/)
) {
return await updateSession(request)
}
// Block test/debug routes in production
if (process.env.NODE_ENV === "production") {
for (const blocked of blockedInProduction) {
if (pathname === blocked || pathname.startsWith(`${blocked}/`)) {
return new NextResponse("Not Found", { status: 404 })
}
}
}
// Edge rate limiting for API routes (before heavier checks)
for (const [prefix, config] of Object.entries(apiRateLimits)) {
if (pathname.startsWith(prefix)) {
const clientId = getClientIp(request)
if (!checkRateLimit(clientId, config.max, config.window)) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429 }
)
}
break
}
}
// CSRF Protection: Validate origin for state-changing requests
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) {
const host = request.headers.get('host') || ''
const allowedOrigins = [
`https://${host}`,
`http://${host}`,
'http://localhost:3000',
'http://127.0.0.1:3000',
]
const originValidation = validateOrigin(request, allowedOrigins)
if (!originValidation.valid) {
console.warn(`[Security] CSRF attempt blocked: ${originValidation.error}`)
return new NextResponse(
JSON.stringify({ error: 'Invalid origin' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
)
}
}
// Request Size Validation for API routes
if (pathname.startsWith('/api/')) {
const contentLength = request.headers.get('content-length')
if (contentLength) {
const size = parseInt(contentLength, 10)
let maxSize = MAX_REQUEST_SIZES.default
if (pathname.includes('/chat')) {
maxSize = MAX_REQUEST_SIZES.chat
} else if (pathname.includes('/builder')) {
maxSize = MAX_REQUEST_SIZES.builder
} else if (pathname.includes('/upload')) {
maxSize = MAX_REQUEST_SIZES.upload
}
if (size > maxSize) {
console.warn(`[Security] Request too large: ${size} bytes (max: ${maxSize})`)
return new NextResponse(
JSON.stringify({
error: 'Payload too large',
maxSize: maxSize,
receivedSize: size
}),
{ status: 413, headers: { 'Content-Type': 'application/json' } }
)
}
}
}
// Update Supabase session
const response = await updateSession(request)
// Add security headers
response.headers.set('X-Frame-Options', 'SAMEORIGIN')
response.headers.set('X-Content-Type-Options', 'nosniff')
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=()')
return response
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
}