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,27 @@
1
+ 'use client'
2
+
3
+ import { Box, Typography } from '@mui/material'
4
+ import type { ReactNode } from 'react'
5
+
6
+ interface FormFieldWrapperProps {
7
+ title: string
8
+ children: ReactNode
9
+ required?: boolean
10
+ }
11
+
12
+ export default function FormFieldWrapper({ title, children, required }: FormFieldWrapperProps) {
13
+ return (
14
+ <Box>
15
+ <Typography
16
+ variant="body2"
17
+ fontWeight={500}
18
+ color="text.secondary"
19
+ sx={{ mb: 0.5 }}
20
+ >
21
+ {title}
22
+ {required && <span style={{ color: 'red' }}> *</span>}
23
+ </Typography>
24
+ {children}
25
+ </Box>
26
+ )
27
+ }
@@ -0,0 +1,348 @@
1
+ # Adding Protected Pages
2
+
3
+ This guide explains how to add new pages with authentication and authorization.
4
+
5
+ ## Quick Start
6
+
7
+ ### Step 1: Create Your Page
8
+
9
+ Create a new page file **anywhere** in the `app/` folder:
10
+
11
+ ```tsx
12
+ // app/invoices/page.tsx (or anywhere you want!)
13
+ 'use client'
14
+
15
+ import { Container, Typography } from '@mui/material'
16
+
17
+ export default function InvoicesPage() {
18
+ return (
19
+ <Container maxWidth="lg" sx={{ py: 4 }}>
20
+ <Typography variant="h4">Invoices</Typography>
21
+ {/* Your page content */}
22
+ </Container>
23
+ )
24
+ }
25
+ ```
26
+
27
+ > **Important:** Always add `'use client'` at the top when using MUI components.
28
+
29
+ **Note:** The file location doesn't matter for protection. You can place pages at:
30
+
31
+ - `app/invoices/page.tsx` → `/invoices`
32
+ - `app/(dashboard)/invoices/page.tsx` → `/invoices` (same URL, just organized)
33
+ - `app/admin/invoices/page.tsx` → `/admin/invoices`
34
+
35
+ Protection is determined by `routeMap.ts`, not folder structure.
36
+
37
+ ### Step 2: Add Permission to Route Map
38
+
39
+ Open `lib/abilities/routeMap.ts` and add your route:
40
+
41
+ ```ts
42
+ export const routePermissions: RoutePermission[] = [
43
+ // ... existing routes ...
44
+
45
+ // Add your new route
46
+ {
47
+ pattern: '/dashboard/invoices',
48
+ action: 'read',
49
+ subject: 'Invoices', // New subject
50
+ description: 'View invoices list',
51
+ },
52
+ ]
53
+ ```
54
+
55
+ ### Step 3: Add Subject to Types (if new)
56
+
57
+ If you created a new subject (like `'Invoices'`), add it to `lib/abilities/types.ts`:
58
+
59
+ ```ts
60
+ export type Subjects =
61
+ | 'Dashboard'
62
+ | 'Users'
63
+ | 'Settings'
64
+ | 'Reports'
65
+ | 'Tickets'
66
+ | 'Invoices' // Add your new subject
67
+ | 'all'
68
+ ```
69
+
70
+ ### Step 4: Define Role Permissions
71
+
72
+ Open `lib/abilities/roles.ts` and add permissions for each role:
73
+
74
+ ```ts
75
+ case 'manager':
76
+ can('read', 'all')
77
+ can('manage', 'Users')
78
+ can('manage', 'Tickets')
79
+ can('manage', 'Reports')
80
+ can('manage', 'Invoices') // Add permission
81
+ cannot('manage', 'Settings')
82
+ break
83
+
84
+ case 'agent':
85
+ can('read', 'Dashboard')
86
+ can('read', 'Tickets')
87
+ can('read', 'Reports')
88
+ can('read', 'Invoices') // Add permission
89
+ // ...
90
+ break
91
+ ```
92
+
93
+ **Done!** Your page is now protected.
94
+
95
+ ---
96
+
97
+ ## Route Pattern Examples
98
+
99
+ | Pattern | Matches | Use Case |
100
+ |---------|---------|----------|
101
+ | `/dashboard/invoices` | Exact path only | List page |
102
+ | `/dashboard/invoices/[id]` | `/dashboard/invoices/123` | Detail page |
103
+ | `/dashboard/invoices/*` | Any nested path | Section with sub-pages |
104
+
105
+ ### Dynamic Route Example
106
+
107
+ ```ts
108
+ // For /dashboard/invoices/[id]
109
+ {
110
+ pattern: '/dashboard/invoices/[id]',
111
+ action: 'read',
112
+ subject: 'Invoices',
113
+ },
114
+
115
+ // For edit page /dashboard/invoices/[id]/edit
116
+ {
117
+ pattern: '/dashboard/invoices/[id]/edit',
118
+ action: 'update',
119
+ subject: 'Invoices',
120
+ },
121
+
122
+ // For all nested pages under /invoices, including the page itself
123
+ {
124
+ pattern: '/dashboard/invoices/*',
125
+ action: 'update',
126
+ subject: 'Invoices',
127
+ },
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Actions Reference
133
+
134
+ | Action | Meaning | Typical Use |
135
+ |--------|---------|-------------|
136
+ | `read` | View/list resources | List pages, detail pages |
137
+ | `create` | Create new resources | "New" or "Add" pages |
138
+ | `update` | Modify resources | Edit pages |
139
+ | `delete` | Remove resources | Delete functionality |
140
+ | `manage` | All of the above | Full access |
141
+
142
+ ---
143
+
144
+ ## Hiding UI Based on Permissions
145
+
146
+ To show/hide navigation or buttons based on user's role:
147
+
148
+ ```tsx
149
+ 'use client'
150
+
151
+ import { useAbility } from '@/@core/hooks/useAbility'
152
+
153
+ function Navigation() {
154
+ const ability = useAbility()
155
+
156
+ return (
157
+ <nav>
158
+ {ability.can('read', 'Invoices') && (
159
+ <Link href="/dashboard/invoices">Invoices</Link>
160
+ )}
161
+
162
+ {ability.can('create', 'Invoices') && (
163
+ <Button>New Invoice</Button>
164
+ )}
165
+ </nav>
166
+ )
167
+ }
168
+ ```
169
+
170
+ Or use the simpler `useCan` hook:
171
+
172
+ ```tsx
173
+ import { useCan } from '@/@core/hooks/useAbility'
174
+
175
+ function InvoiceActions() {
176
+ const canCreate = useCan('create', 'Invoices')
177
+ const canDelete = useCan('delete', 'Invoices')
178
+
179
+ return (
180
+ <>
181
+ {canCreate && <Button>New Invoice</Button>}
182
+ {canDelete && <Button color="error">Delete</Button>}
183
+ </>
184
+ )
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Complete Example: Adding an "Orders" Section
191
+
192
+ ### 1. Create the pages
193
+
194
+ ```
195
+ app/(dashboard)/dashboard/orders/
196
+ ├── page.tsx # /dashboard/orders (list)
197
+ ├── [id]/
198
+ │ └── page.tsx # /dashboard/orders/123 (detail)
199
+ └── new/
200
+ └── page.tsx # /dashboard/orders/new (create)
201
+ ```
202
+
203
+ ### 2. Update types.ts
204
+
205
+ ```ts
206
+ export type Subjects =
207
+ | 'Dashboard'
208
+ | 'Users'
209
+ | 'Settings'
210
+ | 'Reports'
211
+ | 'Tickets'
212
+ | 'Orders' // Added
213
+ | 'all'
214
+ ```
215
+
216
+ ### 3. Update routeMap.ts
217
+
218
+ ```ts
219
+ // Orders - manager and admin
220
+ { pattern: '/dashboard/orders', action: 'read', subject: 'Orders' },
221
+ { pattern: '/dashboard/orders/[id]', action: 'read', subject: 'Orders' },
222
+ { pattern: '/dashboard/orders/new', action: 'create', subject: 'Orders' },
223
+ { pattern: '/dashboard/orders/[id]/edit', action: 'update', subject: 'Orders' },
224
+ ```
225
+
226
+ ### 4. Update roles.ts
227
+
228
+ ```ts
229
+ case 'admin':
230
+ can('manage', 'all') // Already has access
231
+ break
232
+
233
+ case 'manager':
234
+ can('read', 'all')
235
+ can('manage', 'Orders') // Full access to orders
236
+ break
237
+
238
+ case 'agent':
239
+ can('read', 'Orders') // Can only view
240
+ break
241
+
242
+ case 'viewer':
243
+ // No access to orders
244
+ break
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Understanding Roles
250
+
251
+ ### Default Roles
252
+
253
+ The authorization system includes four roles with hierarchical permissions. Find them in `lib/abilities/roles.ts`:
254
+
255
+ | Role | Description | Access Level |
256
+ |------|-------------|--------------|
257
+ | `admin` | Full system access | `can('manage', 'all')` - everything |
258
+ | `manager` | Team management | Read all, manage Users/Tickets/Reports |
259
+ | `agent` | Operational user | Read Dashboard/Tickets/Reports, manage Tickets |
260
+ | `viewer` | Read-only access | Read Dashboard/Reports only |
261
+
262
+ ### Where Roles are Defined
263
+
264
+ **Role types:** `lib/abilities/types.ts`
265
+ ```ts
266
+ export type UserRole = 'admin' | 'manager' | 'agent' | 'viewer'
267
+ ```
268
+
269
+ **Role permissions:** `lib/abilities/roles.ts`
270
+ ```ts
271
+ export function defineAbilitiesFor(role: UserRole): AppAbility {
272
+ const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)
273
+
274
+ switch (role) {
275
+ case 'admin':
276
+ can('manage', 'all')
277
+ break
278
+ case 'manager':
279
+ can('read', 'all')
280
+ can('manage', 'Users')
281
+ can('manage', 'Tickets')
282
+ can('manage', 'Reports')
283
+ cannot('manage', 'Settings')
284
+ break
285
+ // ... other roles
286
+ }
287
+
288
+ return build()
289
+ }
290
+ ```
291
+
292
+ ### Adding a New Role
293
+
294
+ 1. **Add the role type** in `lib/abilities/types.ts`:
295
+ ```ts
296
+ export type UserRole = 'admin' | 'manager' | 'agent' | 'viewer' | 'support'
297
+ ```
298
+
299
+ 2. **Define permissions** in `lib/abilities/roles.ts`:
300
+ ```ts
301
+ case 'support':
302
+ can('read', 'Dashboard')
303
+ can('read', 'Tickets')
304
+ can('create', 'Tickets')
305
+ break
306
+ ```
307
+
308
+ 3. **Update your JWT** - ensure the backend includes the new role in the token payload.
309
+
310
+ ### Modifying Existing Role Permissions
311
+
312
+ Open `lib/abilities/roles.ts` and modify the `switch` cases:
313
+
314
+ ```ts
315
+ // Example: Give agents access to create invoices
316
+ case 'agent':
317
+ can('read', 'Dashboard')
318
+ can('read', 'Tickets')
319
+ can('manage', 'Tickets')
320
+ can('read', 'Reports')
321
+ can('create', 'Invoices') // Added new permission
322
+ break
323
+ ```
324
+
325
+ ### Role Permission Matrix
326
+
327
+ Current default permissions:
328
+
329
+ | Subject | admin | manager | agent | viewer |
330
+ |---------|-------|---------|-------|--------|
331
+ | Dashboard | manage | read | read | read |
332
+ | Users | manage | manage | - | - |
333
+ | Tickets | manage | manage | manage | - |
334
+ | Reports | manage | manage | read | read |
335
+ | Settings | manage | - | - | - |
336
+
337
+ > **Tip:** Use `can('manage', 'Subject')` to grant all actions (read, create, update, delete) at once.
338
+
339
+ ---
340
+
341
+ ## Summary
342
+
343
+ 1. **Create page** in `app/(dashboard)/dashboard/`
344
+ 2. **Add route** to `lib/abilities/routeMap.ts`
345
+ 3. **Add subject** to `lib/abilities/types.ts` (if new)
346
+ 4. **Define permissions** in `lib/abilities/roles.ts`
347
+
348
+ The middleware automatically enforces these rules - no changes needed to individual pages!
@@ -0,0 +1,74 @@
1
+ import type { UserRole, Actions, Subjects } from './types'
2
+ import { matchRoute, isPublicRoute, isAuthenticatedOnlyRoute } from './routeMatcher'
3
+ import { canAccess } from './roles'
4
+
5
+ /**
6
+ * Result of an authorization check
7
+ */
8
+ export interface AuthorizationResult {
9
+ /** Whether access is authorized */
10
+ authorized: boolean
11
+ /** Reason for the result */
12
+ reason?: 'unauthenticated' | 'forbidden' | 'public' | 'authenticated'
13
+ /** The action that was required (for forbidden results) */
14
+ requiredAction?: Actions
15
+ /** The subject that was required (for forbidden results) */
16
+ requiredSubject?: Subjects
17
+ }
18
+
19
+ /**
20
+ * Check if a user can access a given pathname
21
+ *
22
+ * This is the main authorization orchestration function.
23
+ * It checks:
24
+ * 1. If the route is public (always allowed)
25
+ * 2. If the user is authenticated
26
+ * 3. If the route has specific permissions and user satisfies them
27
+ *
28
+ * @param pathname - The URL pathname to check
29
+ * @param userRole - The user's role, or null if not authenticated
30
+ * @returns AuthorizationResult with authorized status and reason
31
+ */
32
+ export function checkAuthorization(
33
+ pathname: string,
34
+ userRole: UserRole | null
35
+ ): AuthorizationResult {
36
+ // 1. Public routes - always allowed
37
+ if (isPublicRoute(pathname)) {
38
+ return { authorized: true, reason: 'public' }
39
+ }
40
+
41
+ // 2. No user = unauthenticated
42
+ if (!userRole) {
43
+ return { authorized: false, reason: 'unauthenticated' }
44
+ }
45
+
46
+ // 3. Routes that only need authentication (no specific permission)
47
+ if (isAuthenticatedOnlyRoute(pathname)) {
48
+ return { authorized: true, reason: 'authenticated' }
49
+ }
50
+
51
+ // 4. Check route-specific permissions
52
+ const matched = matchRoute(pathname)
53
+
54
+ // No matching route permission = allow by default
55
+ // This means the route exists but has no specific permission requirement
56
+ if (!matched) {
57
+ return { authorized: true, reason: 'authenticated' }
58
+ }
59
+
60
+ const { permission } = matched
61
+ const hasPermission = canAccess(userRole, permission.action, permission.subject)
62
+
63
+ if (hasPermission) {
64
+ return { authorized: true }
65
+ }
66
+
67
+ // User doesn't have required permission
68
+ return {
69
+ authorized: false,
70
+ reason: 'forbidden',
71
+ requiredAction: permission.action,
72
+ requiredSubject: permission.subject,
73
+ }
74
+ }
@@ -0,0 +1,27 @@
1
+ // Types
2
+ export type {
3
+ Subjects,
4
+ Actions,
5
+ AppAbility,
6
+ AuthUser,
7
+ UserRole,
8
+ RoutePermission,
9
+ MatchedRoute,
10
+ } from './types'
11
+
12
+ // Role definitions
13
+ export { defineAbilitiesFor, canAccess } from './roles'
14
+
15
+ // Route configuration
16
+ export { routePermissions, publicRoutes, authenticatedOnlyRoutes } from './routeMap'
17
+
18
+ // Route matching
19
+ export {
20
+ matchRoute,
21
+ isPublicRoute,
22
+ isAuthenticatedOnlyRoute,
23
+ isProtectedRoute,
24
+ } from './routeMatcher'
25
+
26
+ // Authorization check
27
+ export { checkAuthorization, type AuthorizationResult } from './checkAuthorization'
@@ -0,0 +1,75 @@
1
+ import { AbilityBuilder, createMongoAbility } from '@casl/ability'
2
+ import type { AppAbility, UserRole, Actions, Subjects } from './types'
3
+
4
+ /**
5
+ * Define CASL abilities for each role
6
+ *
7
+ * Role hierarchy: admin > manager > agent > viewer
8
+ *
9
+ * @param role - The user's role
10
+ * @returns CASL Ability instance with permissions for that role
11
+ */
12
+ export function defineAbilitiesFor(role: UserRole): AppAbility {
13
+ const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)
14
+
15
+ switch (role) {
16
+ case 'admin':
17
+ // Admin has full access to everything
18
+ can('manage', 'all')
19
+ break
20
+
21
+ case 'manager':
22
+ // Manager can read everything
23
+ can('read', 'all')
24
+ // Manager can manage Users, Tickets, and Reports
25
+ can('manage', 'Users')
26
+ can('manage', 'Tickets')
27
+ can('manage', 'Reports')
28
+ // Manager cannot access Settings
29
+ cannot('manage', 'Settings')
30
+ break
31
+
32
+ case 'agent':
33
+ // Agent can read Home, Dashboard, Tickets, and Reports
34
+ can('read', 'Home')
35
+ can('read', 'Dashboard')
36
+ can('read', 'Tickets')
37
+ can('read', 'Reports')
38
+ // Agent can manage (CRUD) Tickets
39
+ can('manage', 'Tickets')
40
+ // Agent cannot access Users or Settings
41
+ cannot('read', 'Users')
42
+ cannot('read', 'Settings')
43
+ break
44
+
45
+ case 'viewer':
46
+ // Viewer can read Home, Dashboard and Reports
47
+ can('read', 'Home')
48
+ can('read', 'Dashboard')
49
+ can('read', 'Reports')
50
+ // Viewer cannot access anything else
51
+ cannot('read', 'Users')
52
+ cannot('read', 'Tickets')
53
+ cannot('read', 'Settings')
54
+ break
55
+
56
+ default:
57
+ // Unknown roles get no permissions
58
+ break
59
+ }
60
+
61
+ return build()
62
+ }
63
+
64
+ /**
65
+ * Quick check if a role can access a subject with given action
66
+ *
67
+ * @param role - User's role
68
+ * @param action - The action to check
69
+ * @param subject - The subject to check against
70
+ * @returns true if the role has permission
71
+ */
72
+ export function canAccess(role: UserRole, action: Actions, subject: Subjects): boolean {
73
+ const ability = defineAbilitiesFor(role)
74
+ return ability.can(action, subject)
75
+ }
@@ -0,0 +1,35 @@
1
+ import type { RoutePermission } from './types'
2
+
3
+ /**
4
+ * Centralized route to permission mapping
5
+ *
6
+ * Pattern types supported:
7
+ * - Exact: '/dashboard' matches only '/dashboard'
8
+ * - Dynamic: '/users/[id]' matches '/users/123', '/users/abc'
9
+ * - Wildcard: '/settings/*' matches '/settings/profile', '/settings/a/b/c'
10
+ *
11
+ * To add a new protected route:
12
+ * 1. Add an entry here with the pattern, action, and subject
13
+ * 2. That's it - middleware handles the rest
14
+ */
15
+ export const routePermissions: RoutePermission[] = [
16
+ // Home - accessible to all authenticated users
17
+ {
18
+ pattern: '/home',
19
+ action: 'read',
20
+ subject: 'Home',
21
+ description: 'Home page for authenticated users'
22
+ }
23
+ ]
24
+
25
+ /**
26
+ * Public routes that don't require authentication
27
+ * These bypass all auth checks
28
+ */
29
+ export const publicRoutes: string[] = ['/', '/login', '/signup', '/forgot-password', '/reset-password', '/unauthorized']
30
+
31
+ /**
32
+ * Routes that only require authentication (any role can access)
33
+ * No specific permission check needed
34
+ */
35
+ export const authenticatedOnlyRoutes: string[] = ['/home']
@@ -0,0 +1,117 @@
1
+ import type { MatchedRoute } from './types'
2
+ import { routePermissions, publicRoutes, authenticatedOnlyRoutes } from './routeMap'
3
+
4
+ /**
5
+ * Convert route pattern to regex for matching
6
+ *
7
+ * Handles:
8
+ * - [param] → named capture group (?<param>[^/]+)
9
+ * - /* → wildcard match (?:/.*)?
10
+ * - Escapes other regex special characters
11
+ */
12
+ function patternToRegex(pattern: string): RegExp {
13
+ const regexStr = pattern
14
+ // Escape special regex characters (except [ ] and *)
15
+ .replace(/[.+?^${}()|\\]/g, '\\$&')
16
+ // Convert [param] to named capture group
17
+ .replace(/\[(\w+)\]/g, '(?<$1>[^/]+)')
18
+ // Convert /* wildcard to match anything after
19
+ .replace(/\/\*$/, '(?:/.*)?')
20
+
21
+ return new RegExp(`^${regexStr}$`)
22
+ }
23
+
24
+ /**
25
+ * Calculate match priority score
26
+ * Higher score = more specific match
27
+ *
28
+ * - Exact matches get highest priority
29
+ * - Dynamic segments get medium priority
30
+ * - Wildcards get lowest priority
31
+ */
32
+ function getMatchScore(pattern: string): number {
33
+ let score = 100
34
+
35
+ // Count dynamic segments (lower score)
36
+ const dynamicCount = (pattern.match(/\[\w+\]/g) || []).length
37
+ score -= dynamicCount * 10
38
+
39
+ // Wildcard patterns get lowest score
40
+ if (pattern.endsWith('/*')) {
41
+ score -= 50
42
+ }
43
+
44
+ return score
45
+ }
46
+
47
+ /**
48
+ * Match a pathname against all route patterns
49
+ *
50
+ * @param pathname - The URL pathname to match
51
+ * @returns MatchedRoute with permission and params, or null if no match
52
+ */
53
+ export function matchRoute(pathname: string): MatchedRoute | null {
54
+ const matches: Array<{ route: MatchedRoute; score: number }> = []
55
+
56
+ for (const permission of routePermissions) {
57
+ const regex = patternToRegex(permission.pattern)
58
+ const match = pathname.match(regex)
59
+
60
+ if (match) {
61
+ matches.push({
62
+ route: {
63
+ permission,
64
+ params: match.groups || {},
65
+ },
66
+ score: getMatchScore(permission.pattern),
67
+ })
68
+ }
69
+ }
70
+
71
+ // Return highest scoring match (most specific)
72
+ if (matches.length > 0) {
73
+ matches.sort((a, b) => b.score - a.score)
74
+ return matches[0].route
75
+ }
76
+
77
+ return null
78
+ }
79
+
80
+ /**
81
+ * Check if a route is public (no auth required)
82
+ *
83
+ * @param pathname - The URL pathname to check
84
+ * @returns true if the route is public
85
+ */
86
+ export function isPublicRoute(pathname: string): boolean {
87
+ return publicRoutes.some((route) => {
88
+ // Exact match
89
+ if (pathname === route) return true
90
+
91
+ // Check if pathname starts with public route (for sub-paths)
92
+ // e.g., /about matches /about/team
93
+ if (pathname.startsWith(route + '/')) return true
94
+
95
+ return false
96
+ })
97
+ }
98
+
99
+ /**
100
+ * Check if route only requires authentication (no specific permission)
101
+ *
102
+ * @param pathname - The URL pathname to check
103
+ * @returns true if route just needs auth, no specific permission
104
+ */
105
+ export function isAuthenticatedOnlyRoute(pathname: string): boolean {
106
+ return authenticatedOnlyRoutes.includes(pathname)
107
+ }
108
+
109
+ /**
110
+ * Check if route is protected (has specific permission requirements)
111
+ *
112
+ * @param pathname - The URL pathname to check
113
+ * @returns true if route has permission requirements
114
+ */
115
+ export function isProtectedRoute(pathname: string): boolean {
116
+ return matchRoute(pathname) !== null
117
+ }