-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
252 lines (215 loc) · 7.37 KB
/
Copy pathmiddleware.ts
File metadata and controls
252 lines (215 loc) · 7.37 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import { createServerClient } from '@supabase/ssr'
import { NextRequest, NextResponse } from 'next/server'
import { adminMiddleware } from './app/backend/admin/middleware'
// Route protection configuration
interface RouteProtection {
patterns: string[]
authLevel: 'user' | 'admin' | 'super_admin'
redirect?: string
}
const PROTECTED_ROUTES: RouteProtection[] = [
// User authentication required (removed /verify from here as it needs special handling)
{
patterns: ['/dashboard'],
authLevel: 'user',
redirect: '/login'
},
// Admin authentication required
{
patterns: ['/api/system/', '/api/revalidate', '/api/debug/'],
authLevel: 'admin',
redirect: '/backend/admin/login'
},
// User-specific API routes requiring authentication
{
patterns: ['/api/v1/auth/user/', '/api/v1/billing/'],
authLevel: 'user',
redirect: '/auth/login'
}
]
// Public routes that should never be protected
const PUBLIC_ROUTES = [
'/auth/',
'/backend/admin/login',
'/api/health',
'/api/midtrans/webhook',
'/api/v1/payments/midtrans/webhook',
'/api/v1/auth/login',
'/api/v1/auth/register',
'/api/v1/auth/detect-location',
'/',
'/about',
'/contact',
'/privacy',
'/terms'
]
async function checkUserAuthentication(request: NextRequest): Promise<{ user: any; role: string } | null> {
try {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll() {
// Cannot set cookies in middleware
},
},
}
)
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
return null
}
// For admin routes, check admin role using centralized service client
if (request.nextUrl.pathname.startsWith('/api/system/') ||
request.nextUrl.pathname.startsWith('/api/debug/') ||
request.nextUrl.pathname === '/api/revalidate') {
// Use centralized service role client instead of ad-hoc creation
const { supabaseAdmin } = await import('./lib/database/supabase')
// Security validation: Only query profile for the authenticated user
if (!user?.id) {
return null
}
const { data: profile, error: profileError } = await supabaseAdmin
.from('indb_auth_user_profiles')
.select('role')
.eq('user_id', user.id)
.single()
if (profileError || !profile) {
return null
}
// Log service role operation for audit trail
console.log(`AUDIT: Service role admin check for user ${user.id} on route ${request.nextUrl.pathname}`)
// Insert audit log for service role admin check (non-blocking)
try {
await supabaseAdmin
.from('indb_security_audit_logs')
.insert({
user_id: user.id,
event_type: 'service_role_admin_check',
description: `Service role checked admin permissions for protected route: ${request.nextUrl.pathname}`,
success: true,
metadata: {
operation: 'admin_role_verification',
route: request.nextUrl.pathname,
user_role: profile?.role
}
})
} catch (auditError) {
console.error('Failed to log service role audit entry:', auditError)
}
return { user, role: profile.role }
}
return { user, role: 'user' }
} catch (error) {
return null
}
}
function isPublicRoute(pathname: string): boolean {
return PUBLIC_ROUTES.some(route => pathname.startsWith(route))
}
function getRequiredAuthLevel(pathname: string): { authLevel: string; redirect: string } | null {
for (const protection of PROTECTED_ROUTES) {
if (protection.patterns.some(pattern => pathname.startsWith(pattern))) {
return { authLevel: protection.authLevel, redirect: protection.redirect || '/auth/login' }
}
}
return null
}
function hasRequiredAccess(userRole: string, requiredLevel: string): boolean {
const roleHierarchy = {
'user': 1,
'admin': 2,
'super_admin': 3
}
const userLevel = roleHierarchy[userRole as keyof typeof roleHierarchy] || 0
const requiredLevelValue = roleHierarchy[requiredLevel as keyof typeof roleHierarchy] || 0
return userLevel >= requiredLevelValue
}
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// BLOCK Sentry monitoring HEAD requests to /api endpoint
if (request.method === 'HEAD' && pathname === '/api') {
const userAgent = request.headers.get('user-agent')
const hasSentryHeaders = request.headers.get('sentry-trace') ||
request.headers.get('baggage')?.includes('sentry-')
// Block Sentry monitoring requests to prevent 404 spam
if (userAgent === 'node' && hasSentryHeaders) {
return new NextResponse(null, { status: 204 }) // No Content - stops the noise
}
}
// Handle admin routes with dedicated middleware, except login page
if (pathname.startsWith('/backend/admin') && pathname !== '/backend/admin/login') {
return adminMiddleware(request)
}
// Allow login page to bypass admin middleware completely
if (pathname === '/backend/admin/login') {
return NextResponse.next()
}
// Allow all public routes
if (isPublicRoute(pathname)) {
return NextResponse.next()
}
// Check if route requires protection
const protection = getRequiredAuthLevel(pathname)
if (!protection) {
return NextResponse.next()
}
// Verify authentication for protected routes
const authResult = await checkUserAuthentication(request)
if (!authResult) {
// No authentication - redirect to appropriate login
const redirectUrl = new URL(protection.redirect, request.url)
return NextResponse.redirect(redirectUrl)
}
// Check if user has required access level
if (!hasRequiredAccess(authResult.role, protection.authLevel)) {
// Insufficient privileges - redirect to appropriate login
const redirectUrl = new URL(protection.redirect, request.url)
return NextResponse.redirect(redirectUrl)
}
// User is authenticated and has required access level
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
// For dashboard routes, refresh session if needed
if (pathname.startsWith('/dashboard')) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value)
response.cookies.set(name, value, options)
})
},
},
}
)
// This will refresh session if expired
await supabase.auth.getUser()
}
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)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}