@jmruthers/pace-core 0.5.91 → 0.5.93
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.
- package/dist/{DataTable-VIP44OB6.js → DataTable-HC5S4RKB.js} +6 -6
- package/dist/{PublicLoadingSpinner-Dx5c2g3S.d.ts → PublicLoadingSpinner-n74JgA9h.d.ts} +50 -1
- package/dist/{UnifiedAuthProvider-6JRTOFPS.js → UnifiedAuthProvider-ZM7VUC45.js} +3 -3
- package/dist/{chunk-G2SCPUKC.js → chunk-6WFM22A4.js} +2 -2
- package/dist/{chunk-AIV3VYBQ.js → chunk-AAM57AEU.js} +4 -2
- package/dist/chunk-AAM57AEU.js.map +1 -0
- package/dist/{chunk-7XBW2P7B.js → chunk-AYC2P377.js} +2 -2
- package/dist/{chunk-GD3ENUKD.js → chunk-AZ2QJYKU.js} +3 -3
- package/dist/{chunk-G2YT64FA.js → chunk-GP3HU6WS.js} +3 -3
- package/dist/{chunk-7NIERLC6.js → chunk-HW5BGOWB.js} +4 -4
- package/dist/{chunk-JDPFQV3V.js → chunk-M52CQP5W.js} +4 -4
- package/dist/{chunk-JQWSAYZC.js → chunk-OXFOS62D.js} +2 -2
- package/dist/{chunk-VJJNZKHO.js → chunk-SVMPR5IV.js} +365 -293
- package/dist/chunk-SVMPR5IV.js.map +1 -0
- package/dist/{chunk-4DYK5KCK.js → chunk-TZXYSZT3.js} +4 -4
- package/dist/{chunk-XZHZYSAK.js → chunk-XIBSVWJW.js} +5 -5
- package/dist/components.d.ts +1 -1
- package/dist/components.js +10 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +97 -0
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +41 -5
- package/docs/api-reference/components.md +146 -1
- package/docs/best-practices/common-patterns.md +26 -8
- package/docs/getting-started/examples/README.md +10 -26
- package/docs/getting-started/quick-reference.md +23 -0
- package/docs/implementation-guides/authentication.md +39 -16
- package/package.json +1 -1
- package/src/components/EventSelector/EventSelector.tsx +19 -1
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +224 -0
- package/src/components/ProtectedRoute/README.md +164 -0
- package/src/components/ProtectedRoute/index.ts +3 -0
- package/src/components/PublicLayout/EventLogo.tsx +8 -2
- package/src/components/PublicLayout/PublicPageHeader.tsx +7 -12
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +6 -3
- package/src/components/index.ts +4 -1
- package/src/index.ts +3 -0
- package/src/providers/AuthProvider.simplified.tsx +108 -14
- package/src/services/EventService.ts +6 -1
- package/dist/chunk-AIV3VYBQ.js.map +0 -1
- package/dist/chunk-VJJNZKHO.js.map +0 -1
- /package/dist/{DataTable-VIP44OB6.js.map → DataTable-HC5S4RKB.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-6JRTOFPS.js.map → UnifiedAuthProvider-ZM7VUC45.js.map} +0 -0
- /package/dist/{chunk-G2SCPUKC.js.map → chunk-6WFM22A4.js.map} +0 -0
- /package/dist/{chunk-7XBW2P7B.js.map → chunk-AYC2P377.js.map} +0 -0
- /package/dist/{chunk-GD3ENUKD.js.map → chunk-AZ2QJYKU.js.map} +0 -0
- /package/dist/{chunk-G2YT64FA.js.map → chunk-GP3HU6WS.js.map} +0 -0
- /package/dist/{chunk-7NIERLC6.js.map → chunk-HW5BGOWB.js.map} +0 -0
- /package/dist/{chunk-JDPFQV3V.js.map → chunk-M52CQP5W.js.map} +0 -0
- /package/dist/{chunk-JQWSAYZC.js.map → chunk-OXFOS62D.js.map} +0 -0
- /package/dist/{chunk-4DYK5KCK.js.map → chunk-TZXYSZT3.js.map} +0 -0
- /package/dist/{chunk-XZHZYSAK.js.map → chunk-XIBSVWJW.js.map} +0 -0
|
@@ -1212,7 +1212,152 @@ import { Label } from '@jmruthers/pace-core';
|
|
|
1212
1212
|
|
|
1213
1213
|
## Security Components
|
|
1214
1214
|
|
|
1215
|
-
|
|
1215
|
+
### ProtectedRoute
|
|
1216
|
+
|
|
1217
|
+
A route protection component that handles authentication and optional event selection without creating blocking issues. This component solves the common chicken-and-egg problem where apps check for `selectedEvent` before rendering, which blocks the event selector from being visible.
|
|
1218
|
+
|
|
1219
|
+
#### Props
|
|
1220
|
+
|
|
1221
|
+
```typescript
|
|
1222
|
+
interface ProtectedRouteProps {
|
|
1223
|
+
/**
|
|
1224
|
+
* Whether an event is required for routes inside this component.
|
|
1225
|
+
* When true, routes will only render if an event is selected or can be selected.
|
|
1226
|
+
* When false, routes render regardless of event state.
|
|
1227
|
+
* @default true
|
|
1228
|
+
*/
|
|
1229
|
+
requireEvent?: boolean;
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Whether super admins can bypass event requirement.
|
|
1233
|
+
* Note: This feature requires additional RBAC setup. For simple bypass, set requireEvent={false} instead.
|
|
1234
|
+
* @default false
|
|
1235
|
+
* @deprecated Use requireEvent={false} for routes that don't need events
|
|
1236
|
+
*/
|
|
1237
|
+
allowSuperAdminBypass?: boolean;
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Custom component to render when no events are available.
|
|
1241
|
+
* If not provided, a default message is shown.
|
|
1242
|
+
*/
|
|
1243
|
+
noEventsFallback?: React.ReactNode;
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Custom component to render while events are loading.
|
|
1247
|
+
* If not provided, a default loading spinner is shown.
|
|
1248
|
+
*/
|
|
1249
|
+
loadingFallback?: React.ReactNode;
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Login redirect path when user is not authenticated.
|
|
1253
|
+
* @default '/login'
|
|
1254
|
+
*/
|
|
1255
|
+
loginPath?: string;
|
|
1256
|
+
}
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
#### Usage
|
|
1260
|
+
|
|
1261
|
+
**Basic Usage (with event requirement):**
|
|
1262
|
+
```tsx
|
|
1263
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
1264
|
+
import { Routes, Route } from 'react-router-dom';
|
|
1265
|
+
|
|
1266
|
+
function App() {
|
|
1267
|
+
return (
|
|
1268
|
+
<Routes>
|
|
1269
|
+
<Route path="/login" element={<LoginPage />} />
|
|
1270
|
+
<Route element={<ProtectedRoute />}>
|
|
1271
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
1272
|
+
<Route path="/events" element={<EventsPage />} />
|
|
1273
|
+
</Route>
|
|
1274
|
+
</Routes>
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
**Without Event Requirement:**
|
|
1280
|
+
```tsx
|
|
1281
|
+
<Route element={<ProtectedRoute requireEvent={false} />}>
|
|
1282
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
1283
|
+
</Route>
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
**Custom Fallbacks:**
|
|
1287
|
+
```tsx
|
|
1288
|
+
<Route element={
|
|
1289
|
+
<ProtectedRoute
|
|
1290
|
+
requireEvent={true}
|
|
1291
|
+
loginPath="/login"
|
|
1292
|
+
loadingFallback={<CustomLoader />}
|
|
1293
|
+
noEventsFallback={
|
|
1294
|
+
<div>
|
|
1295
|
+
<h2>No Events Available</h2>
|
|
1296
|
+
<p>Contact your administrator for access.</p>
|
|
1297
|
+
</div>
|
|
1298
|
+
}
|
|
1299
|
+
/>
|
|
1300
|
+
}>
|
|
1301
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
1302
|
+
</Route>
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
#### Features
|
|
1306
|
+
|
|
1307
|
+
- ✅ **Authentication checking** - Redirects to login if not authenticated
|
|
1308
|
+
- ✅ **Session restoration** - Handles session restoration states automatically
|
|
1309
|
+
- ✅ **Event loading management** - Allows rendering during event loading (prevents UI blocking)
|
|
1310
|
+
- ✅ **Event selector visibility** - Allows rendering when events exist but none selected (so event selector in header is visible)
|
|
1311
|
+
- ✅ **Clear error states** - Shows helpful messages when no events are available
|
|
1312
|
+
- ✅ **Flexible configuration** - Optional event requirement, customizable fallbacks
|
|
1313
|
+
|
|
1314
|
+
#### Important Notes
|
|
1315
|
+
|
|
1316
|
+
The `ProtectedRoute` component solves a critical UX issue: **When apps check for `selectedEvent` before rendering, the event selector dropdown (typically in `PaceAppLayout` header) is never visible, creating a deadlock where users cannot select an event.**
|
|
1317
|
+
|
|
1318
|
+
This component allows rendering when:
|
|
1319
|
+
- Events are loading (prevents blocking UI)
|
|
1320
|
+
- Events exist but none selected (allows event selector to be visible)
|
|
1321
|
+
- Individual pages should handle "no selected event" state gracefully
|
|
1322
|
+
|
|
1323
|
+
#### Example: Complete App Setup
|
|
1324
|
+
|
|
1325
|
+
```tsx
|
|
1326
|
+
import {
|
|
1327
|
+
UnifiedAuthProvider,
|
|
1328
|
+
ProtectedRoute,
|
|
1329
|
+
PaceAppLayout
|
|
1330
|
+
} from '@jmruthers/pace-core';
|
|
1331
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
1332
|
+
|
|
1333
|
+
function App() {
|
|
1334
|
+
return (
|
|
1335
|
+
<UnifiedAuthProvider
|
|
1336
|
+
supabaseClient={supabase}
|
|
1337
|
+
appName="my-app"
|
|
1338
|
+
idleTimeoutMs={30 * 60 * 1000}
|
|
1339
|
+
warnBeforeMs={60 * 1000}
|
|
1340
|
+
onIdleLogout={() => window.location.href = '/login'}
|
|
1341
|
+
>
|
|
1342
|
+
<BrowserRouter>
|
|
1343
|
+
<Routes>
|
|
1344
|
+
<Route path="/login" element={<PaceLoginPage appName="My App" />} />
|
|
1345
|
+
<Route element={<ProtectedRoute />}>
|
|
1346
|
+
<Route element={<PaceAppLayout appName="My App" />}>
|
|
1347
|
+
<Route path="/" element={<DashboardPage />} />
|
|
1348
|
+
<Route path="/events" element={<EventsPage />} />
|
|
1349
|
+
</Route>
|
|
1350
|
+
</Route>
|
|
1351
|
+
</Routes>
|
|
1352
|
+
</BrowserRouter>
|
|
1353
|
+
</UnifiedAuthProvider>
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
For more details, see the [Authentication Implementation Guide](../implementation-guides/authentication.md#protected-routes).
|
|
1359
|
+
|
|
1360
|
+
---
|
|
1216
1361
|
|
|
1217
1362
|
### PasswordChangeForm
|
|
1218
1363
|
|
|
@@ -44,19 +44,37 @@ function App() {
|
|
|
44
44
|
|
|
45
45
|
### Protected Routes
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
import { useUnifiedAuth, Navigate } from '@jmruthers/pace-core';
|
|
49
|
-
|
|
50
|
-
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
51
|
-
const { user, loading } = useUnifiedAuth();
|
|
47
|
+
**✅ Use the standard ProtectedRoute component from pace-core:**
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
```tsx
|
|
50
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
51
|
+
import { Routes, Route } from 'react-router-dom';
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
function App() {
|
|
54
|
+
return (
|
|
55
|
+
<Routes>
|
|
56
|
+
<Route path="/login" element={<LoginPage />} />
|
|
57
|
+
{/* Routes requiring authentication and event selection */}
|
|
58
|
+
<Route element={<ProtectedRoute />}>
|
|
59
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
60
|
+
</Route>
|
|
61
|
+
{/* Routes requiring authentication only (no event needed) */}
|
|
62
|
+
<Route element={<ProtectedRoute requireEvent={false} />}>
|
|
63
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
64
|
+
</Route>
|
|
65
|
+
</Routes>
|
|
66
|
+
);
|
|
57
67
|
}
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
**Why use the standard component?**
|
|
71
|
+
- Prevents the chicken-and-egg problem where checking `selectedEvent` blocks the event selector from being visible
|
|
72
|
+
- Handles session restoration automatically
|
|
73
|
+
- Manages loading states properly
|
|
74
|
+
- Provides consistent behavior across all pace apps
|
|
75
|
+
|
|
76
|
+
**❌ Avoid custom ProtectedRoute implementations that block rendering when no event is selected**, as this prevents users from seeing the event selector dropdown in the header.
|
|
77
|
+
|
|
60
78
|
### Session Management
|
|
61
79
|
|
|
62
80
|
```tsx
|
|
@@ -516,7 +516,7 @@ src/
|
|
|
516
516
|
├── components/
|
|
517
517
|
│ ├── auth/
|
|
518
518
|
│ │ ├── LoginPage.tsx # Login form
|
|
519
|
-
│ │ └── ProtectedRoute
|
|
519
|
+
│ │ └── (ProtectedRoute now provided by @jmruthers/pace-core)
|
|
520
520
|
│ ├── layout/
|
|
521
521
|
│ │ ├── AppLayout.tsx # Main layout
|
|
522
522
|
│ │ └── Navigation.tsx # Navigation menu
|
|
@@ -587,9 +587,7 @@ export default App;
|
|
|
587
587
|
|
|
588
588
|
```tsx
|
|
589
589
|
import { Routes, Route } from 'react-router-dom';
|
|
590
|
-
import {
|
|
591
|
-
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
|
592
|
-
import { AppLayout } from './components/layout/AppLayout';
|
|
590
|
+
import { ProtectedRoute, PaceLoginPage, PaceAppLayout } from '@jmruthers/pace-core';
|
|
593
591
|
import { DashboardPage } from './components/dashboard/DashboardPage';
|
|
594
592
|
import { MealsPage } from './components/meals/MealsPage';
|
|
595
593
|
import { UsersPage } from './components/users/UsersPage';
|
|
@@ -597,28 +595,14 @@ import { UsersPage } from './components/users/UsersPage';
|
|
|
597
595
|
export function AppRoutes() {
|
|
598
596
|
return (
|
|
599
597
|
<Routes>
|
|
600
|
-
<Route path="/login" element={<
|
|
601
|
-
<Route
|
|
602
|
-
<
|
|
603
|
-
<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
</
|
|
607
|
-
|
|
608
|
-
<Route path="/meals" element={
|
|
609
|
-
<ProtectedRoute>
|
|
610
|
-
<AppLayout>
|
|
611
|
-
<MealsPage />
|
|
612
|
-
</AppLayout>
|
|
613
|
-
</ProtectedRoute>
|
|
614
|
-
} />
|
|
615
|
-
<Route path="/users" element={
|
|
616
|
-
<ProtectedRoute>
|
|
617
|
-
<AppLayout>
|
|
618
|
-
<UsersPage />
|
|
619
|
-
</AppLayout>
|
|
620
|
-
</ProtectedRoute>
|
|
621
|
-
} />
|
|
598
|
+
<Route path="/login" element={<PaceLoginPage appName="Meal Manager" />} />
|
|
599
|
+
<Route element={<ProtectedRoute />}>
|
|
600
|
+
<Route element={<PaceAppLayout appName="Meal Manager" />}>
|
|
601
|
+
<Route path="/" element={<DashboardPage />} />
|
|
602
|
+
<Route path="/meals" element={<MealsPage />} />
|
|
603
|
+
<Route path="/users" element={<UsersPage />} />
|
|
604
|
+
</Route>
|
|
605
|
+
</Route>
|
|
622
606
|
</Routes>
|
|
623
607
|
);
|
|
624
608
|
}
|
|
@@ -140,6 +140,29 @@ function MyComponent() {
|
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
### Protected Routes
|
|
143
|
+
|
|
144
|
+
**✅ Use the standard ProtectedRoute component for authentication and event selection:**
|
|
145
|
+
```tsx
|
|
146
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
147
|
+
import { Routes, Route } from 'react-router-dom';
|
|
148
|
+
|
|
149
|
+
function App() {
|
|
150
|
+
return (
|
|
151
|
+
<Routes>
|
|
152
|
+
<Route path="/login" element={<LoginPage />} />
|
|
153
|
+
<Route element={<ProtectedRoute />}>
|
|
154
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
155
|
+
</Route>
|
|
156
|
+
{/* Routes without event requirement */}
|
|
157
|
+
<Route element={<ProtectedRoute requireEvent={false} />}>
|
|
158
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
159
|
+
</Route>
|
|
160
|
+
</Routes>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**For permission-based protection, use PermissionGuard:**
|
|
143
166
|
```tsx
|
|
144
167
|
import { PermissionGuard } from '@jmruthers/pace-core';
|
|
145
168
|
|
|
@@ -134,34 +134,57 @@ export function LoginPage() {
|
|
|
134
134
|
|
|
135
135
|
### 6. Protected Routes
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
// src/components/ProtectedRoute.tsx
|
|
139
|
-
import { useUnifiedAuth, Navigate } from '@jmruthers/pace-core';
|
|
140
|
-
|
|
141
|
-
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
142
|
-
const { user, loading } = useUnifiedAuth();
|
|
137
|
+
PACE Core provides a standard `ProtectedRoute` component that handles authentication, session restoration, and event selection without creating blocking issues.
|
|
143
138
|
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
**Key Features:**
|
|
140
|
+
- ✅ Handles authentication checks and redirects
|
|
141
|
+
- ✅ Manages session restoration states
|
|
142
|
+
- ✅ Allows rendering during event loading (prevents UI blocking)
|
|
143
|
+
- ✅ Allows rendering when events exist but none selected (so event selector is visible)
|
|
144
|
+
- ✅ Shows clear error states when no events are available
|
|
145
|
+
- ✅ Optional event requirement
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
```tsx
|
|
148
|
+
// ✅ Recommended: Use the standard ProtectedRoute from pace-core
|
|
149
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
150
|
+
import { Routes, Route } from 'react-router-dom';
|
|
149
151
|
|
|
150
|
-
// Usage in App.tsx
|
|
151
152
|
function App() {
|
|
152
153
|
return (
|
|
153
154
|
<Routes>
|
|
154
155
|
<Route path="/login" element={<LoginPage />} />
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
{/* Protected routes with event requirement (default) */}
|
|
157
|
+
<Route element={<ProtectedRoute />}>
|
|
158
|
+
<Route path="/" element={<DashboardPage />} />
|
|
159
|
+
<Route path="/events" element={<EventsPage />} />
|
|
160
|
+
</Route>
|
|
161
|
+
{/* Protected routes without event requirement */}
|
|
162
|
+
<Route element={<ProtectedRoute requireEvent={false} />}>
|
|
163
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
164
|
+
</Route>
|
|
160
165
|
</Routes>
|
|
161
166
|
);
|
|
162
167
|
}
|
|
163
168
|
```
|
|
164
169
|
|
|
170
|
+
**Advanced Usage:**
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// Custom fallbacks for loading and error states
|
|
174
|
+
<Route element={
|
|
175
|
+
<ProtectedRoute
|
|
176
|
+
requireEvent={true}
|
|
177
|
+
loginPath="/login"
|
|
178
|
+
loadingFallback={<CustomLoader />}
|
|
179
|
+
noEventsFallback={<CustomNoEventsMessage />}
|
|
180
|
+
/>
|
|
181
|
+
}>
|
|
182
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
183
|
+
</Route>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Important:** The `ProtectedRoute` component solves the common chicken-and-egg problem where apps check for `selectedEvent` before rendering, which blocks the event selector (typically in the `PaceAppLayout` header) from being visible. The component allows rendering when events exist but none is selected, enabling users to see and use the event selector dropdown.
|
|
187
|
+
|
|
165
188
|
## Configuration Options
|
|
166
189
|
|
|
167
190
|
### UnifiedAuthProvider Props
|
package/package.json
CHANGED
|
@@ -214,17 +214,35 @@ export function EventSelector({
|
|
|
214
214
|
return [...events].sort((a, b) => getTime(b) - getTime(a));
|
|
215
215
|
}, [events]);
|
|
216
216
|
|
|
217
|
-
// Default to the next upcoming event if none selected
|
|
217
|
+
// Default to the next upcoming event if none selected, fallback to most recent past event
|
|
218
218
|
useEffect(() => {
|
|
219
219
|
if (!selectedEvent && events.length > 0) {
|
|
220
220
|
const today = new Date();
|
|
221
221
|
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
|
|
222
|
+
|
|
223
|
+
// Try to find next future event
|
|
222
224
|
const next = [...events]
|
|
223
225
|
.filter(e => e.event_date && new Date(e.event_date).getTime() >= startOfToday)
|
|
224
226
|
.sort((a, b) => new Date(a.event_date as string).getTime() - new Date(b.event_date as string).getTime())[0];
|
|
227
|
+
|
|
225
228
|
if (next) {
|
|
226
229
|
setSelectedEvent(next);
|
|
227
230
|
if (onEventChange) onEventChange(next);
|
|
231
|
+
} else {
|
|
232
|
+
// Fallback to most recent past event if no future events found
|
|
233
|
+
const mostRecentPast = [...events]
|
|
234
|
+
.filter(e => {
|
|
235
|
+
if (!e.event_date) return false;
|
|
236
|
+
const eventDate = new Date(e.event_date);
|
|
237
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
238
|
+
return startOfEventDate < startOfToday;
|
|
239
|
+
})
|
|
240
|
+
.sort((a, b) => new Date(b.event_date as string).getTime() - new Date(a.event_date as string).getTime())[0];
|
|
241
|
+
|
|
242
|
+
if (mostRecentPast) {
|
|
243
|
+
setSelectedEvent(mostRecentPast);
|
|
244
|
+
if (onEventChange) onEventChange(mostRecentPast);
|
|
245
|
+
}
|
|
228
246
|
}
|
|
229
247
|
}
|
|
230
248
|
}, [events, selectedEvent, setSelectedEvent, onEventChange]);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Protected Route Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/ProtectedRoute
|
|
5
|
+
* @since 0.6.0
|
|
6
|
+
*
|
|
7
|
+
* A route protection component that handles authentication and optional event selection
|
|
8
|
+
* without creating a chicken-and-egg problem where users cannot see the event selector.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Authentication checking with redirect to login
|
|
12
|
+
* - Session restoration handling
|
|
13
|
+
* - Event loading state management
|
|
14
|
+
* - Smart event selection logic (allows rendering when events exist but none selected)
|
|
15
|
+
* - Optional event requirement (can be disabled for apps that don't need events)
|
|
16
|
+
* - Super admin bypass support
|
|
17
|
+
* - Clear error states for no events available
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* Basic protected route:
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
23
|
+
* import { Routes, Route } from 'react-router-dom';
|
|
24
|
+
*
|
|
25
|
+
* function App() {
|
|
26
|
+
* return (
|
|
27
|
+
* <Routes>
|
|
28
|
+
* <Route path="/login" element={<LoginPage />} />
|
|
29
|
+
* <Route element={<ProtectedRoute />}>
|
|
30
|
+
* <Route path="/dashboard" element={<DashboardPage />} />
|
|
31
|
+
* </Route>
|
|
32
|
+
* </Routes>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* Protected route without event requirement:
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Route element={<ProtectedRoute requireEvent={false} />}>
|
|
41
|
+
* <Route path="/settings" element={<SettingsPage />} />
|
|
42
|
+
* </Route>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* Protected route with custom no events message:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <Route element={
|
|
49
|
+
* <ProtectedRoute
|
|
50
|
+
* requireEvent={true}
|
|
51
|
+
* noEventsFallback={<CustomNoEventsMessage />}
|
|
52
|
+
* />
|
|
53
|
+
* }>
|
|
54
|
+
* <Route path="/dashboard" element={<DashboardPage />} />
|
|
55
|
+
* </Route>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @accessibility
|
|
59
|
+
* - Proper loading states with screen reader support
|
|
60
|
+
* - Clear error messages
|
|
61
|
+
* - Keyboard navigation support
|
|
62
|
+
*
|
|
63
|
+
* @dependencies
|
|
64
|
+
* - React Router v6 - Routing functionality
|
|
65
|
+
* - useUnifiedAuth - Authentication context
|
|
66
|
+
* - useEvents - Event context
|
|
67
|
+
* - SessionRestorationLoader - Session restoration UI
|
|
68
|
+
* - LoadingSpinner - Loading state UI
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
import React, { useMemo } from 'react';
|
|
72
|
+
import { Navigate, Outlet } from 'react-router-dom';
|
|
73
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
74
|
+
import { useSessionRestoration } from '../../hooks/useSessionRestoration';
|
|
75
|
+
import { useEvents } from '../../hooks/useEvents';
|
|
76
|
+
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
|
|
77
|
+
import { SessionRestorationLoader } from '../SessionRestorationLoader';
|
|
78
|
+
import { Alert, AlertDescription, AlertTitle } from '../Alert/Alert';
|
|
79
|
+
|
|
80
|
+
export interface ProtectedRouteProps {
|
|
81
|
+
/**
|
|
82
|
+
* Whether an event is required for routes inside this component.
|
|
83
|
+
* When true, routes will only render if an event is selected or can be selected.
|
|
84
|
+
* When false, routes render regardless of event state.
|
|
85
|
+
* @default true
|
|
86
|
+
*/
|
|
87
|
+
requireEvent?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether super admins can bypass event requirement.
|
|
91
|
+
* Note: This feature requires additional RBAC setup. For simple bypass, set requireEvent={false} instead.
|
|
92
|
+
* @default false
|
|
93
|
+
* @deprecated Use requireEvent={false} for routes that don't need events
|
|
94
|
+
*/
|
|
95
|
+
allowSuperAdminBypass?: boolean;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Custom component to render when no events are available.
|
|
99
|
+
* If not provided, a default message is shown.
|
|
100
|
+
*/
|
|
101
|
+
noEventsFallback?: React.ReactNode;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Custom component to render while events are loading.
|
|
105
|
+
* If not provided, a default loading spinner is shown.
|
|
106
|
+
*/
|
|
107
|
+
loadingFallback?: React.ReactNode;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Login redirect path when user is not authenticated.
|
|
111
|
+
* @default '/login'
|
|
112
|
+
*/
|
|
113
|
+
loginPath?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ProtectedRoute component that handles authentication and optional event selection.
|
|
118
|
+
*
|
|
119
|
+
* This component solves the chicken-and-egg problem where apps check for `selectedEvent`
|
|
120
|
+
* before rendering, which blocks the event selector (typically in the header) from being visible.
|
|
121
|
+
*
|
|
122
|
+
* Strategy:
|
|
123
|
+
* 1. Check authentication first - redirect to login if not authenticated
|
|
124
|
+
* 2. Allow rendering during event loading - prevents blocking UI
|
|
125
|
+
* 3. If events exist but none selected - allow rendering so selector is visible
|
|
126
|
+
* 4. If no events available - show error message
|
|
127
|
+
* 5. Individual pages should handle "no selected event" state gracefully
|
|
128
|
+
*
|
|
129
|
+
* @param props - Configuration for route protection
|
|
130
|
+
* @returns React element with route protection logic
|
|
131
|
+
*/
|
|
132
|
+
export function ProtectedRoute({
|
|
133
|
+
requireEvent = true,
|
|
134
|
+
allowSuperAdminBypass = false,
|
|
135
|
+
noEventsFallback,
|
|
136
|
+
loadingFallback,
|
|
137
|
+
loginPath = '/login'
|
|
138
|
+
}: ProtectedRouteProps) {
|
|
139
|
+
const { isAuthenticated, isLoading } = useUnifiedAuth();
|
|
140
|
+
const { selectedEvent, events, isLoading: eventLoading } = useEvents();
|
|
141
|
+
const sessionRestoration = useSessionRestoration();
|
|
142
|
+
|
|
143
|
+
const isRestoringSession = useMemo(() => {
|
|
144
|
+
return sessionRestoration.isRestoring &&
|
|
145
|
+
!sessionRestoration.restorationComplete &&
|
|
146
|
+
!sessionRestoration.restorationError &&
|
|
147
|
+
!sessionRestoration.hasTimedOut;
|
|
148
|
+
}, [
|
|
149
|
+
sessionRestoration.isRestoring,
|
|
150
|
+
sessionRestoration.restorationComplete,
|
|
151
|
+
sessionRestoration.restorationError,
|
|
152
|
+
sessionRestoration.hasTimedOut
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Show session restoration loader during restoration
|
|
156
|
+
if (isRestoringSession) {
|
|
157
|
+
return <SessionRestorationLoader />;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Show loading state while auth is being determined
|
|
161
|
+
if (isLoading && !sessionRestoration.hasTimedOut) {
|
|
162
|
+
return loadingFallback || (
|
|
163
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
164
|
+
<LoadingSpinner />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Redirect to login if not authenticated
|
|
170
|
+
if (!isAuthenticated) {
|
|
171
|
+
if (sessionRestoration.hasTimedOut || sessionRestoration.restorationError) {
|
|
172
|
+
console.warn('[ProtectedRoute] Session restoration failed, redirecting to login', {
|
|
173
|
+
timedOut: sessionRestoration.hasTimedOut,
|
|
174
|
+
error: sessionRestoration.restorationError?.message
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return <Navigate to={loginPath} replace />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If event is not required, allow rendering
|
|
181
|
+
if (!requireEvent) {
|
|
182
|
+
return <Outlet />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Note: Super admin bypass would require useRBAC hook which adds complexity
|
|
186
|
+
// Apps that need super admin access without events should set requireEvent={false}
|
|
187
|
+
// For now, we keep it simple and always require events when requireEvent=true
|
|
188
|
+
|
|
189
|
+
// Allow rendering during event loading - prevents blocking UI while events load
|
|
190
|
+
if (eventLoading) {
|
|
191
|
+
return <Outlet />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If no events are available, show error message
|
|
195
|
+
if (!events || events.length === 0) {
|
|
196
|
+
return noEventsFallback || (
|
|
197
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '2rem' }}>
|
|
198
|
+
<Alert variant="destructive" className="max-w-md">
|
|
199
|
+
<AlertTitle>No Events Available</AlertTitle>
|
|
200
|
+
<AlertDescription>
|
|
201
|
+
You don't have access to any events. Please contact your administrator if you believe this is an error.
|
|
202
|
+
</AlertDescription>
|
|
203
|
+
</Alert>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// KEY FIX: Allow rendering when events exist but none selected
|
|
209
|
+
// This allows the event selector (typically in PaceAppLayout header) to be visible
|
|
210
|
+
// Individual pages should handle "no selected event" state gracefully
|
|
211
|
+
// Auto-selection will handle selecting the next event, or user can manually select
|
|
212
|
+
|
|
213
|
+
// If no event selected but events exist, allow rendering
|
|
214
|
+
// The event selector will be visible and user can select, or auto-selection will kick in
|
|
215
|
+
if (!selectedEvent) {
|
|
216
|
+
// Log for debugging - this is expected behavior, not an error
|
|
217
|
+
console.debug('[ProtectedRoute] Events available but none selected - allowing render so selector is visible');
|
|
218
|
+
return <Outlet />;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Event is selected - allow rendering
|
|
222
|
+
return <Outlet />;
|
|
223
|
+
}
|
|
224
|
+
|