-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
143 lines (123 loc) · 4.65 KB
/
middleware.ts
File metadata and controls
143 lines (123 loc) · 4.65 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
import { NextResponse } from 'next/server'
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { withRateLimit, apiRateLimit, adminApiRateLimit, contactFormRateLimit, uploadRateLimit } from './lib/rate-limit'
import { SECURITY_HEADERS } from './lib/security'
// Define protected routes that require authentication
const isProtectedRoute = createRouteMatcher([
'/admin/dashboard(.*)',
'/api/upload(.*)',
])
// Define admin API routes that need authentication (for write operations)
const isAdminApiRoute = createRouteMatcher([
'/api/admin(.*)',
'/api/webhooks(.*)',
])
// Define public Clerk routes that should not be protected
const isPublicClerkRoute = createRouteMatcher([
'/admin/login(.*)',
'/admin/sign-up(.*)',
])
export default clerkMiddleware(async (auth, request) => {
const { userId } = await auth()
// Skip protection for public Clerk routes
if (isPublicClerkRoute(request)) {
// Allow Clerk authentication pages to work normally
const response = NextResponse.next()
// Add security headers
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
// Handle protected routes
if (isProtectedRoute(request) || isAdminApiRoute(request)) {
if (!userId) {
// User not authenticated, redirect to login
const loginUrl = new URL('/admin/login', request.url)
loginUrl.searchParams.set('redirect', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
// For admin routes, we'll let the page components handle role verification
// This allows for better error handling and user experience
// The actual role check happens in the page components using requireAdminAuth()
}
// Handle projects and experiences API routes - only protect write operations
if (request.nextUrl.pathname.startsWith('/api/projects') ||
request.nextUrl.pathname.startsWith('/api/experiences')) {
const method = request.method
// Only protect POST, PUT, DELETE operations - GET is public
if (method !== 'GET' && !userId) {
// Return JSON error for API endpoints instead of redirect
return new NextResponse(
JSON.stringify({
success: false,
error: 'Authentication required',
message: 'You must be authenticated to perform this action.'
}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
...SECURITY_HEADERS
}
}
)
}
}
const response = NextResponse.next()
// Add security headers
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
response.headers.set(key, value)
})
// Apply rate limiting to API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
let rateLimiter = apiRateLimit
// Choose appropriate rate limiter based on endpoint
if (request.nextUrl.pathname.startsWith('/api/contact')) {
rateLimiter = contactFormRateLimit
} else if (request.nextUrl.pathname.startsWith('/api/upload')) {
rateLimiter = uploadRateLimit
} else if (request.nextUrl.pathname.includes('/admin/') ||
(request.nextUrl.pathname.startsWith('/api/projects') && request.method !== 'GET') ||
(request.nextUrl.pathname.startsWith('/api/experiences') && request.method !== 'GET')) {
// Admin operations get higher rate limits
rateLimiter = adminApiRateLimit
}
const rateLimitResult = withRateLimit(rateLimiter, request)
// Add rate limit headers
response.headers.set('X-RateLimit-Limit', rateLimitResult.limit.toString())
response.headers.set('X-RateLimit-Remaining', rateLimitResult.remaining.toString())
response.headers.set('X-RateLimit-Reset', rateLimitResult.reset.toString())
if (!rateLimitResult.success) {
return new NextResponse(
JSON.stringify({
success: false,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.'
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': rateLimitResult.reset.toString(),
...SECURITY_HEADERS
}
}
)
}
}
return response
})
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}