shortcut-next 0.2.2 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +5 -2
  2. package/templates/base/@core/configs/clientConfig.ts +1 -1
  3. package/templates/base/@core/context/AuthContext.tsx +21 -28
  4. package/templates/base/@core/hooks/useAbility.ts +58 -0
  5. package/templates/base/app/(dashboard)/dashboard/page.tsx +104 -0
  6. package/templates/base/app/(dashboard)/layout.tsx +97 -0
  7. package/templates/base/app/home/page.tsx +112 -0
  8. package/templates/base/app/login/page.tsx +296 -0
  9. package/templates/base/app/unauthorized/page.tsx +120 -0
  10. package/templates/base/components/MSWProvider.tsx +54 -0
  11. package/templates/base/components/auth/LoginForm.tsx +279 -0
  12. package/templates/base/components/auth/SignupForm.tsx +348 -0
  13. package/templates/base/components/loaders/Spinner.tsx +5 -24
  14. package/templates/base/components/ui/ErrorMessage.tsx +17 -0
  15. package/templates/base/components/ui/FormFieldWrapper.tsx +27 -0
  16. package/templates/base/docs/AuthorizationDocumentation.md +348 -0
  17. package/templates/base/lib/abilities/checkAuthorization.ts +74 -0
  18. package/templates/base/lib/abilities/index.ts +27 -0
  19. package/templates/base/lib/abilities/roles.ts +75 -0
  20. package/templates/base/lib/abilities/routeMap.ts +35 -0
  21. package/templates/base/lib/abilities/routeMatcher.ts +117 -0
  22. package/templates/base/lib/abilities/types.ts +68 -0
  23. package/templates/base/lib/mocks/browser.ts +11 -0
  24. package/templates/base/lib/mocks/db.ts +124 -0
  25. package/templates/base/lib/mocks/handlers/auth.ts +203 -0
  26. package/templates/base/lib/mocks/handlers/index.ts +16 -0
  27. package/templates/base/lib/mocks/index.ts +34 -0
  28. package/templates/base/lib/mocks/jwt.ts +99 -0
  29. package/templates/base/middleware.ts +147 -0
  30. package/templates/base/package-lock.json +725 -2
  31. package/templates/base/package.json +13 -2
  32. package/templates/base/providers/AppProviders.tsx +8 -5
  33. package/templates/base/public/locales/ar.json +73 -0
  34. package/templates/base/public/locales/en.json +73 -0
  35. package/templates/base/public/mockServiceWorker.js +349 -0
@@ -0,0 +1,68 @@
1
+ import type { MongoAbility } from '@casl/ability'
2
+
3
+ /**
4
+ * Subject types for authorization
5
+ * These represent resource areas in the application
6
+ */
7
+ export type Subjects =
8
+ | 'Home'
9
+ | 'Dashboard'
10
+ | 'Users'
11
+ | 'Settings'
12
+ | 'Reports'
13
+ | 'Tickets'
14
+ | 'all'
15
+
16
+ /**
17
+ * Action types for CASL permissions
18
+ * - read: View resources
19
+ * - create: Create new resources
20
+ * - update: Modify existing resources
21
+ * - delete: Remove resources
22
+ * - manage: Full access (all actions)
23
+ */
24
+ export type Actions = 'read' | 'create' | 'update' | 'delete' | 'manage'
25
+
26
+ /**
27
+ * CASL Ability type for the application
28
+ */
29
+ export type AppAbility = MongoAbility<[Actions, Subjects]>
30
+
31
+ /**
32
+ * Supported user roles
33
+ * Hierarchy: admin > manager > agent > viewer
34
+ */
35
+ export type UserRole = 'admin' | 'manager' | 'agent' | 'viewer'
36
+
37
+ /**
38
+ * Minimal user type for authorization checks
39
+ */
40
+ export interface AuthUser {
41
+ id: string
42
+ role: UserRole
43
+ }
44
+
45
+ /**
46
+ * Route permission mapping entry
47
+ * Maps a route pattern to required CASL permission
48
+ */
49
+ export interface RoutePermission {
50
+ /** Route pattern (exact, [param], or wildcard /*) */
51
+ pattern: string
52
+ /** Required action to access the route */
53
+ action: Actions
54
+ /** Required subject for the route */
55
+ subject: Subjects
56
+ /** Optional description for documentation */
57
+ description?: string
58
+ }
59
+
60
+ /**
61
+ * Result of matching a route against patterns
62
+ */
63
+ export interface MatchedRoute {
64
+ /** The matched permission entry */
65
+ permission: RoutePermission
66
+ /** Extracted dynamic parameters */
67
+ params: Record<string, string>
68
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * MSW Browser Worker
3
+ *
4
+ * Sets up MSW to intercept requests in the browser.
5
+ * This is used for development to mock API responses.
6
+ */
7
+
8
+ import { setupWorker } from 'msw/browser'
9
+ import { handlers } from './handlers'
10
+
11
+ export const worker = setupWorker(...handlers)
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Mock Database for MSW
3
+ *
4
+ * Uses in-memory storage since MSW service workers don't have localStorage access.
5
+ * This simulates a database for development/testing purposes.
6
+ */
7
+
8
+ import type { UserRole } from '@/lib/abilities'
9
+
10
+ export interface MockUser {
11
+ id: string
12
+ email: string
13
+ password: string // In real app, this would be hashed
14
+ name: string
15
+ role: UserRole
16
+ phone?: string
17
+ companyName?: string
18
+ type?: 'BUSINESS' | 'CUSTOMER'
19
+ createdAt: string
20
+ }
21
+
22
+ /**
23
+ * Default test users for each role
24
+ */
25
+ const defaultUsers: MockUser[] = [
26
+ {
27
+ id: 'user_admin_001',
28
+ email: 'admin@test.com',
29
+ password: 'password123',
30
+ name: 'Admin User',
31
+ role: 'admin',
32
+ createdAt: new Date().toISOString()
33
+ },
34
+ {
35
+ id: 'user_manager_001',
36
+ email: 'manager@test.com',
37
+ password: 'password123',
38
+ name: 'Manager User',
39
+ role: 'manager',
40
+ createdAt: new Date().toISOString()
41
+ },
42
+ {
43
+ id: 'user_agent_001',
44
+ email: 'agent@test.com',
45
+ password: 'password123',
46
+ name: 'Agent User',
47
+ role: 'agent',
48
+ createdAt: new Date().toISOString()
49
+ },
50
+ {
51
+ id: 'user_viewer_001',
52
+ email: 'viewer@test.com',
53
+ password: 'password123',
54
+ name: 'Viewer User',
55
+ role: 'viewer',
56
+ createdAt: new Date().toISOString()
57
+ }
58
+ ]
59
+
60
+ // In-memory database (works in service worker context)
61
+ let usersDb: MockUser[] = [...defaultUsers]
62
+
63
+ /**
64
+ * Get all users from mock database
65
+ */
66
+ export function getUsers(): MockUser[] {
67
+ return usersDb
68
+ }
69
+
70
+ /**
71
+ * Find user by email
72
+ */
73
+ export function findUserByEmail(email: string): MockUser | undefined {
74
+ return usersDb.find((u) => u.email.toLowerCase() === email.toLowerCase())
75
+ }
76
+
77
+ /**
78
+ * Find user by ID
79
+ */
80
+ export function findUserById(id: string): MockUser | undefined {
81
+ return usersDb.find((u) => u.id === id)
82
+ }
83
+
84
+ /**
85
+ * Create a new user
86
+ */
87
+ export function createUser(userData: Omit<MockUser, 'id' | 'createdAt'>): MockUser {
88
+ const newUser: MockUser = {
89
+ ...userData,
90
+ id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
91
+ createdAt: new Date().toISOString()
92
+ }
93
+
94
+ usersDb.push(newUser)
95
+ return newUser
96
+ }
97
+
98
+ /**
99
+ * Update user
100
+ */
101
+ export function updateUser(id: string, updates: Partial<MockUser>): MockUser | null {
102
+ const index = usersDb.findIndex((u) => u.id === id)
103
+
104
+ if (index === -1) return null
105
+
106
+ usersDb[index] = { ...usersDb[index], ...updates }
107
+ return usersDb[index]
108
+ }
109
+
110
+ /**
111
+ * Delete user
112
+ */
113
+ export function deleteUser(id: string): boolean {
114
+ const initialLength = usersDb.length
115
+ usersDb = usersDb.filter((u) => u.id !== id)
116
+ return usersDb.length !== initialLength
117
+ }
118
+
119
+ /**
120
+ * Reset database to default users
121
+ */
122
+ export function resetDatabase(): void {
123
+ usersDb = [...defaultUsers]
124
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * MSW Auth Handlers
3
+ *
4
+ * Mock API handlers for authentication endpoints.
5
+ * These simulate a real backend for development and testing.
6
+ */
7
+
8
+ import { http, HttpResponse, delay } from 'msw'
9
+ import { findUserByEmail, createUser } from '../db'
10
+ import { createMockAccessToken, createMockRefreshToken, decodeMockToken } from '../jwt'
11
+ import type { UserRole } from '@/lib/abilities'
12
+
13
+ const API_BASE = '/api'
14
+
15
+ /**
16
+ * Simulate network delay
17
+ */
18
+ const SIMULATED_DELAY = 500
19
+
20
+ export const authHandlers = [
21
+ /**
22
+ * POST /api/auth/login
23
+ */
24
+ http.post(`${API_BASE}/auth/login`, async ({ request }) => {
25
+ await delay(SIMULATED_DELAY)
26
+
27
+ const body = (await request.json()) as { email: string; password: string }
28
+
29
+ if (!body.email || !body.password) {
30
+ return HttpResponse.json({ message: 'Email and password are required' }, { status: 400 })
31
+ }
32
+
33
+ const user = findUserByEmail(body.email)
34
+
35
+ if (!user) {
36
+ return HttpResponse.json({ message: 'Invalid email or password' }, { status: 401 })
37
+ }
38
+
39
+ if (user.password !== body.password) {
40
+ return HttpResponse.json({ message: 'Invalid email or password' }, { status: 401 })
41
+ }
42
+
43
+ const accessToken = createMockAccessToken({
44
+ id: user.id,
45
+ email: user.email,
46
+ name: user.name,
47
+ role: user.role
48
+ })
49
+
50
+ const refreshToken = createMockRefreshToken(user.id)
51
+
52
+ // Return user without password
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ const { password, ...userWithoutPassword } = user
55
+
56
+ return HttpResponse.json({
57
+ user: userWithoutPassword,
58
+ accessToken,
59
+ refreshToken
60
+ })
61
+ }),
62
+
63
+ /**
64
+ * POST /api/auth/signup
65
+ */
66
+ http.post(`${API_BASE}/auth/signup`, async ({ request }) => {
67
+ await delay(SIMULATED_DELAY)
68
+
69
+ const body = (await request.json()) as {
70
+ email: string
71
+ password: string
72
+ name: string
73
+ phone?: string
74
+ companyName?: string
75
+ type?: 'BUSINESS' | 'CUSTOMER'
76
+ role?: UserRole
77
+ }
78
+
79
+ if (!body.email || !body.password || !body.name) {
80
+ return HttpResponse.json({ message: 'Email, password, and name are required' }, { status: 400 })
81
+ }
82
+
83
+ // Check if user already exists
84
+ const existingUser = findUserByEmail(body.email)
85
+ if (existingUser) {
86
+ return HttpResponse.json({ message: 'User with this email already exists' }, { status: 409 })
87
+ }
88
+
89
+ // Create new user (default role is 'viewer' for new signups)
90
+ const newUser = createUser({
91
+ email: body.email,
92
+ password: body.password,
93
+ name: body.name,
94
+ role: body.role || 'viewer', // Allow role override for testing
95
+ phone: body.phone,
96
+ companyName: body.companyName,
97
+ type: body.type
98
+ })
99
+
100
+ const accessToken = createMockAccessToken({
101
+ id: newUser.id,
102
+ email: newUser.email,
103
+ name: newUser.name,
104
+ role: newUser.role
105
+ })
106
+
107
+ const refreshToken = createMockRefreshToken(newUser.id)
108
+
109
+ // Return user without password
110
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
111
+ const { password, ...userWithoutPassword } = newUser
112
+
113
+ return HttpResponse.json({
114
+ user: userWithoutPassword,
115
+ accessToken,
116
+ refreshToken
117
+ })
118
+ }),
119
+
120
+ /**
121
+ * POST /api/auth/refresh
122
+ */
123
+ http.post(`${API_BASE}/auth/refresh`, async ({ request }) => {
124
+ await delay(SIMULATED_DELAY)
125
+
126
+ const body = (await request.json()) as { refreshToken: string }
127
+
128
+ if (!body.refreshToken) {
129
+ return HttpResponse.json({ message: 'Refresh token is required' }, { status: 400 })
130
+ }
131
+
132
+ const decoded = decodeMockToken(body.refreshToken)
133
+
134
+ if (!decoded || decoded.exp * 1000 < Date.now()) {
135
+ return HttpResponse.json({ message: 'Invalid or expired refresh token' }, { status: 401 })
136
+ }
137
+
138
+ // Find user by ID from token
139
+ const { findUserById } = await import('../db')
140
+ const user = findUserById(decoded.sub)
141
+
142
+ if (!user) {
143
+ return HttpResponse.json({ message: 'User not found' }, { status: 401 })
144
+ }
145
+
146
+ const accessToken = createMockAccessToken({
147
+ id: user.id,
148
+ email: user.email,
149
+ name: user.name,
150
+ role: user.role
151
+ })
152
+
153
+ const refreshToken = createMockRefreshToken(user.id)
154
+
155
+ return HttpResponse.json({
156
+ accessToken,
157
+ refreshToken
158
+ })
159
+ }),
160
+
161
+ /**
162
+ * POST /api/auth/logout
163
+ */
164
+ http.post(`${API_BASE}/auth/logout`, async () => {
165
+ await delay(SIMULATED_DELAY / 2)
166
+
167
+ // In a real app, you might invalidate the refresh token here
168
+ return HttpResponse.json({ message: 'Logged out successfully' })
169
+ }),
170
+
171
+ /**
172
+ * GET /api/auth/me
173
+ */
174
+ http.get(`${API_BASE}/auth/me`, async ({ request }) => {
175
+ await delay(SIMULATED_DELAY / 2)
176
+
177
+ const authHeader = request.headers.get('Authorization')
178
+
179
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
180
+ return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 })
181
+ }
182
+
183
+ const token = authHeader.substring(7)
184
+ const decoded = decodeMockToken(token)
185
+
186
+ if (!decoded || decoded.exp * 1000 < Date.now()) {
187
+ return HttpResponse.json({ message: 'Token expired' }, { status: 401 })
188
+ }
189
+
190
+ const { findUserById } = await import('../db')
191
+ const user = findUserById(decoded.sub)
192
+
193
+ if (!user) {
194
+ return HttpResponse.json({ message: 'User not found' }, { status: 404 })
195
+ }
196
+
197
+ // Return user without password
198
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
199
+ const { password, ...userWithoutPassword } = user
200
+
201
+ return HttpResponse.json({ user: userWithoutPassword })
202
+ })
203
+ ]
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MSW Handlers Index
3
+ *
4
+ * Combines all mock API handlers.
5
+ * Add new handler modules here as your app grows.
6
+ */
7
+
8
+ import { authHandlers } from './auth'
9
+
10
+ export const handlers = [
11
+ ...authHandlers
12
+ // Add more handlers here as needed:
13
+ // ...userHandlers,
14
+ // ...ticketHandlers,
15
+ // etc.
16
+ ]
@@ -0,0 +1,34 @@
1
+ /**
2
+ * MSW Mock Service Worker Setup
3
+ *
4
+ * This module provides mock API functionality for development.
5
+ *
6
+ * Usage:
7
+ * 1. Call initMocks() early in your app (e.g., in a useEffect in your root layout)
8
+ * 2. The mock server will intercept API calls and return mock responses
9
+ *
10
+ * To disable mocks, set NEXT_PUBLIC_ENABLE_MOCKS=false in .env.local
11
+ */
12
+
13
+ export async function initMocks() {
14
+ // Only run in browser and development
15
+ if (typeof window === 'undefined') return
16
+ if (process.env.NODE_ENV !== 'development') return
17
+ if (process.env.NEXT_PUBLIC_ENABLE_MOCKS === 'false') return
18
+
19
+ const { worker } = await import('./browser')
20
+
21
+ // Start the worker
22
+ await worker.start({
23
+ onUnhandledRequest: 'bypass', // Don't warn about unhandled requests
24
+ serviceWorker: {
25
+ url: '/mockServiceWorker.js'
26
+ }
27
+ })
28
+
29
+ console.log('[MSW] Mock Service Worker started')
30
+ }
31
+
32
+ // Re-export utilities
33
+ export { resetDatabase, getUsers } from './db'
34
+ export type { MockUser } from './db'
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Mock JWT utilities for development
3
+ *
4
+ * Creates JWTs that can be decoded by the middleware.
5
+ * In production, JWTs should be created and signed by your backend.
6
+ */
7
+
8
+ import type { UserRole } from '@/lib/abilities'
9
+
10
+ interface TokenPayload {
11
+ sub: string
12
+ email: string
13
+ name: string
14
+ role: UserRole
15
+ iat: number
16
+ exp: number
17
+ }
18
+
19
+ /**
20
+ * Create a mock JWT token
21
+ *
22
+ * This creates a valid JWT structure that can be decoded.
23
+ * Note: This is NOT cryptographically signed - for development only.
24
+ */
25
+ export function createMockAccessToken(user: {
26
+ id: string
27
+ email: string
28
+ name: string
29
+ role: UserRole
30
+ }): string {
31
+ const header = { alg: 'HS256', typ: 'JWT' }
32
+
33
+ const payload: TokenPayload = {
34
+ sub: user.id,
35
+ email: user.email,
36
+ name: user.name,
37
+ role: user.role,
38
+ iat: Math.floor(Date.now() / 1000),
39
+ exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7 // 7 days
40
+ }
41
+
42
+ const base64Header = base64UrlEncode(JSON.stringify(header))
43
+ const base64Payload = base64UrlEncode(JSON.stringify(payload))
44
+ const mockSignature = base64UrlEncode('mock_signature_for_development')
45
+
46
+ return `${base64Header}.${base64Payload}.${mockSignature}`
47
+ }
48
+
49
+ /**
50
+ * Create a mock refresh token
51
+ */
52
+ export function createMockRefreshToken(userId: string): string {
53
+ const payload = {
54
+ sub: userId,
55
+ type: 'refresh',
56
+ iat: Math.floor(Date.now() / 1000),
57
+ exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30 // 30 days
58
+ }
59
+
60
+ const header = { alg: 'HS256', typ: 'JWT' }
61
+ const base64Header = base64UrlEncode(JSON.stringify(header))
62
+ const base64Payload = base64UrlEncode(JSON.stringify(payload))
63
+ const mockSignature = base64UrlEncode('mock_refresh_signature')
64
+
65
+ return `${base64Header}.${base64Payload}.${mockSignature}`
66
+ }
67
+
68
+ /**
69
+ * Decode a JWT token (without verification)
70
+ */
71
+ export function decodeMockToken(token: string): TokenPayload | null {
72
+ try {
73
+ const parts = token.split('.')
74
+ if (parts.length !== 3) return null
75
+
76
+ const payload = JSON.parse(base64UrlDecode(parts[1]))
77
+ return payload
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Base64 URL encode
85
+ */
86
+ function base64UrlEncode(str: string): string {
87
+ return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
88
+ }
89
+
90
+ /**
91
+ * Base64 URL decode
92
+ */
93
+ function base64UrlDecode(str: string): string {
94
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
95
+ while (base64.length % 4) {
96
+ base64 += '='
97
+ }
98
+ return atob(base64)
99
+ }
@@ -0,0 +1,147 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { NextRequest } from 'next/server'
3
+ import { decodeJwt } from 'jose'
4
+ import { checkAuthorization, isPublicRoute } from '@/lib/abilities'
5
+ import type { UserRole } from '@/lib/abilities'
6
+ import { authConfig } from './@core/configs/clientConfig'
7
+
8
+ /**
9
+ * Cookie name for access token
10
+ * Must match the value in clientConfig.ts
11
+ */
12
+ const ACCESS_TOKEN_COOKIE = 'accessToken'
13
+
14
+ /**
15
+ * Routes that middleware should skip entirely
16
+ * These are static assets and API routes handled elsewhere
17
+ */
18
+ const SKIP_ROUTES = ['/_next', '/api', '/favicon.ico', '/locales', '/images']
19
+
20
+ /**
21
+ * Valid user roles for type checking
22
+ */
23
+ const VALID_ROLES: UserRole[] = ['admin', 'manager', 'agent', 'viewer']
24
+
25
+ /**
26
+ * JWT payload structure expected from the auth system
27
+ */
28
+ interface JWTPayload {
29
+ sub: string
30
+ email?: string
31
+ role?: string
32
+ name?: string
33
+ exp?: number
34
+ iat?: number
35
+ }
36
+
37
+ /**
38
+ * Extract user role from JWT token
39
+ *
40
+ * Note: This only decodes the JWT, it does NOT verify the signature.
41
+ * Signature verification should happen in API routes.
42
+ * Middleware decoding is for early rejection of clearly unauthorized requests.
43
+ *
44
+ * @param token - JWT access token
45
+ * @returns UserRole if valid, null otherwise
46
+ */
47
+ function getUserRoleFromToken(token: string): UserRole | null {
48
+ try {
49
+ const payload = decodeJwt(token) as JWTPayload
50
+
51
+ // Check if token is expired
52
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
53
+ return null
54
+ }
55
+
56
+ // Validate and return role
57
+ const role = payload.role as UserRole
58
+ if (VALID_ROLES.includes(role)) {
59
+ return role
60
+ }
61
+
62
+ // Default to viewer if no valid role found
63
+ return 'viewer'
64
+ } catch {
65
+ // Invalid JWT format
66
+ return null
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Next.js Middleware
72
+ *
73
+ * This is the primary enforcement point for authorization.
74
+ * It runs before any page renders, on the Edge Runtime.
75
+ *
76
+ * Flow:
77
+ * 1. Skip static assets and API routes
78
+ * 2. Allow public routes without auth
79
+ * 3. Extract session from cookie
80
+ * 4. Check authorization
81
+ * 5. Redirect if unauthorized
82
+ */
83
+ export function middleware(request: NextRequest) {
84
+ const { pathname } = request.nextUrl
85
+
86
+ // Skip middleware for static assets and API routes
87
+ if (SKIP_ROUTES.some(route => pathname.startsWith(route))) {
88
+ return NextResponse.next()
89
+ }
90
+
91
+ // Get token from cookie (needed for both public and protected route checks)
92
+ const token = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value
93
+ const userRole = token ? getUserRoleFromToken(token) : null
94
+
95
+ // Redirect authenticated users away from login page
96
+ if (token && userRole && pathname === '/login') {
97
+ const homeUrl = new URL(authConfig.homePageURL, request.url)
98
+ return NextResponse.redirect(homeUrl)
99
+ }
100
+
101
+ // Allow public routes without further checks
102
+ if (isPublicRoute(pathname)) {
103
+ return NextResponse.next()
104
+ }
105
+
106
+ // Check authorization
107
+ const result = checkAuthorization(pathname, userRole)
108
+
109
+ if (result.authorized) {
110
+ return NextResponse.next()
111
+ }
112
+
113
+ // Handle unauthorized access
114
+ if (result.reason === 'unauthenticated') {
115
+ // Redirect to login with return URL
116
+ const loginUrl = new URL('/login', request.url)
117
+ loginUrl.searchParams.set('returnUrl', pathname)
118
+ return NextResponse.redirect(loginUrl)
119
+ }
120
+
121
+ if (result.reason === 'forbidden') {
122
+ // Redirect to unauthorized page with context
123
+ const unauthorizedUrl = new URL('/unauthorized', request.url)
124
+ unauthorizedUrl.searchParams.set('from', pathname)
125
+ if (result.requiredSubject) {
126
+ unauthorizedUrl.searchParams.set('resource', result.requiredSubject)
127
+ }
128
+ if (result.requiredAction) {
129
+ unauthorizedUrl.searchParams.set('action', result.requiredAction)
130
+ }
131
+ return NextResponse.redirect(unauthorizedUrl)
132
+ }
133
+
134
+ return NextResponse.next()
135
+ }
136
+
137
+ /**
138
+ * Middleware matcher configuration
139
+ *
140
+ * Excludes:
141
+ * - _next/static (static files)
142
+ * - _next/image (image optimization)
143
+ * - favicon.ico, sitemap.xml, robots.txt (metadata files)
144
+ */
145
+ export const config = {
146
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|mockServiceWorker.js).*)']
147
+ }