-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
107 lines (94 loc) · 4.29 KB
/
middleware.ts
File metadata and controls
107 lines (94 loc) · 4.29 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
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { addSecurityHeaders } from './lib/security-headers'
import { validateCsrf } from './lib/csrf-edge'
import { logger } from './lib/logger'
// Note: Middleware automatically runs on Edge runtime in Next.js 15+
// No need to declare export const runtime = 'edge'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/student-intake', '/preceptor-intake'])
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/', '/api/webhook(.*)', '/help', '/terms', '/privacy'])
const isStudentRoute = createRouteMatcher(['/dashboard/student(.*)'])
const isPreceptorRoute = createRouteMatcher(['/dashboard/preceptor(.*)'])
const isAdminRoute = createRouteMatcher(['/dashboard/admin(.*)'])
const isStudentIntakeRoute = createRouteMatcher(['/student-intake(.*)'])
const isDashboardRoute = createRouteMatcher(['/dashboard(.*)'])
export default clerkMiddleware(async (auth, req) => {
try {
// CSRF Protection - validate before other middleware
const csrfError = await validateCsrf(req)
if (csrfError) {
return csrfError
}
// Create response object first
let response = NextResponse.next();
// Add security headers to all responses
response = addSecurityHeaders(response);
// Handle authentication for protected routes
if (isProtectedRoute(req)) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.redirect(new URL('/sign-in', req.url));
}
// E2E test authentication bypass with strict production safeguards
// Only allow bypass when ALL of the following are true:
// 1. Explicitly in development mode (not production)
// 2. E2E_TEST flag is set
// 3. NOT running on Netlify (production deployment platform)
// 4. NOT in CI environment (could be staging)
const isE2ETesting =
process.env.NODE_ENV === 'development' &&
process.env.E2E_TEST === 'true' &&
!process.env.NETLIFY &&
process.env.CI !== 'true'
if (isE2ETesting) {
logger.info('E2E test bypass active', { path: req.url, env: process.env.NODE_ENV })
return response
}
await auth.protect()
// Note: User metadata checks are handled by RoleGuard component
// Middleware only handles basic authentication
} catch (error) {
logger.error('Authentication protection failed', error as Error, { action: 'auth_protect', path: req.url })
// Redirect to sign-in on auth error
return NextResponse.redirect(new URL('/sign-in', req.url))
}
}
// Role-based redirect when landing on generic /dashboard
if (isDashboardRoute(req)) {
try {
const url = new URL(req.url)
const pathname = url.pathname
// Only handle the base dashboard path, not subpaths
if (pathname === '/dashboard') {
const { sessionClaims } = await auth()
const publicMetadata: any = (sessionClaims as any)?.publicMetadata || {}
const userType = publicMetadata?.userType as 'student' | 'preceptor' | 'admin' | 'enterprise' | undefined
if (userType === 'student' || userType === 'preceptor' || userType === 'admin' || userType === 'enterprise') {
const target = '/dashboard/' + userType
return NextResponse.redirect(new URL(target, req.url))
}
}
} catch (_e) {
// If anything fails, fall through to app-side redirect logic
}
}
return response
} catch (error) {
logger.error('Middleware processing failed', error as Error, { action: 'middleware_error', path: req.url })
// On any middleware error, redirect to sign-in for protected routes
if (isProtectedRoute(req)) {
return NextResponse.redirect(new URL('/sign-in', req.url))
}
// For public routes, continue with the request
return NextResponse.next()
}
})
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}