@jmruthers/pace-core 0.5.108 → 0.5.109
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/CHANGELOG.md +75 -177
- package/dist/{AuthService-1D2ifNfa.d.ts → AuthService-DrHrvXNZ.d.ts} +8 -1
- package/dist/{DataTable-WFCHVWTY.js → DataTable-5HITILXS.js} +7 -7
- package/dist/{UnifiedAuthProvider-XU4BHFXZ.js → UnifiedAuthProvider-A7I23UCN.js} +3 -3
- package/dist/{api-KG4A2X7P.js → api-5I3E47G2.js} +2 -2
- package/dist/{chunk-DMNMZKWS.js → chunk-2W4WKJVF.js} +4 -4
- package/dist/{chunk-MOMYOQMC.js → chunk-3TKTL5AZ.js} +13 -13
- package/dist/{chunk-X4FRXJV6.js → chunk-AUXS7XSO.js} +57 -6
- package/dist/{chunk-X4FRXJV6.js.map → chunk-AUXS7XSO.js.map} +1 -1
- package/dist/{chunk-LT6RKRA7.js → chunk-D6MEKC27.js} +2 -2
- package/dist/{chunk-KBG34SVL.js → chunk-EYSXQ756.js} +2 -2
- package/dist/{chunk-ZXY5NTJB.js → chunk-EZ64QG2I.js} +2 -2
- package/dist/{chunk-S63MFSY6.js → chunk-F6TSYCKP.js} +4 -2
- package/dist/{chunk-S63MFSY6.js.map → chunk-F6TSYCKP.js.map} +1 -1
- package/dist/chunk-GZRXOUBE.js +176 -0
- package/dist/chunk-GZRXOUBE.js.map +1 -0
- package/dist/{chunk-B3QX32P5.js → chunk-P72NKAT5.js} +41 -24
- package/dist/chunk-P72NKAT5.js.map +1 -0
- package/dist/{chunk-VJ7MPS2K.js → chunk-S4D3Z723.js} +6 -6
- package/dist/{chunk-IMZGJ2X7.js → chunk-UW2DE6JX.js} +4 -4
- package/dist/{chunk-QDDUU625.js → chunk-WWNOVFDC.js} +4 -4
- package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
- package/dist/components.js +9 -9
- package/dist/{database-BXAfr2Y_.d.ts → database-C6jy7EOu.d.ts} +21 -9
- package/dist/{formatting-BiEv5oEk.d.ts → formatting-B1jSqgl-.d.ts} +16 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +6 -6
- package/dist/index.js +16 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +4 -3
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-CnM-IK2I.d.ts → usePublicRouteParams-BdF8bZgs.d.ts} +1 -1
- package/dist/utils.d.ts +2 -15
- package/dist/utils.js +4 -145
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +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 +3 -3
- 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/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 +1 -1
- 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/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.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 +37 -3
- package/docs/api-reference/hooks.md +53 -0
- package/docs/api-reference/providers.md +60 -0
- package/docs/core-concepts/authentication.md +2 -0
- package/docs/implementation-guides/authentication.md +1 -0
- package/docs/security/README.md +59 -0
- package/package.json +1 -1
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +48 -16
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +2 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +9 -9
- package/src/index.ts +3 -0
- package/src/providers/services/AuthServiceProvider.tsx +4 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +1 -1
- package/src/rbac/engine.ts +2 -0
- package/src/services/AuthService.ts +79 -1
- package/src/services/__tests__/AuthService.test.ts +184 -0
- package/src/types/database.ts +21 -9
- package/src/types/rbac-functions.ts +2 -1
- package/src/utils/__tests__/sessionTracking.unit.test.ts +6 -171
- package/src/utils/sessionTracking.ts +7 -81
- package/dist/chunk-B3QX32P5.js.map +0 -1
- package/dist/chunk-NFPV7MRN.js +0 -94
- package/dist/chunk-NFPV7MRN.js.map +0 -1
- package/src/providers/AuthProvider.simplified.tsx +0 -974
- package/dist/{DataTable-WFCHVWTY.js.map → DataTable-5HITILXS.js.map} +0 -0
- package/dist/{UnifiedAuthProvider-XU4BHFXZ.js.map → UnifiedAuthProvider-A7I23UCN.js.map} +0 -0
- package/dist/{api-KG4A2X7P.js.map → api-5I3E47G2.js.map} +0 -0
- package/dist/{chunk-DMNMZKWS.js.map → chunk-2W4WKJVF.js.map} +0 -0
- package/dist/{chunk-MOMYOQMC.js.map → chunk-3TKTL5AZ.js.map} +0 -0
- package/dist/{chunk-LT6RKRA7.js.map → chunk-D6MEKC27.js.map} +0 -0
- package/dist/{chunk-KBG34SVL.js.map → chunk-EYSXQ756.js.map} +0 -0
- package/dist/{chunk-ZXY5NTJB.js.map → chunk-EZ64QG2I.js.map} +0 -0
- package/dist/{chunk-VJ7MPS2K.js.map → chunk-S4D3Z723.js.map} +0 -0
- package/dist/{chunk-IMZGJ2X7.js.map → chunk-UW2DE6JX.js.map} +0 -0
- package/dist/{chunk-QDDUU625.js.map → chunk-WWNOVFDC.js.map} +0 -0
- package/dist/{chunk-GVRSXXAA.js.map → chunk-YFMENCR4.js.map} +0 -0
- package/dist/{validation-D8VcbTzC.d.ts → validation-DnhrNMju.d.ts} +2 -2
|
@@ -76,6 +76,59 @@ function App() {
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
> **Note**: Login and logout tracking is automatically handled by `UnifiedAuthProvider`. No manual intervention is required.
|
|
80
|
+
|
|
81
|
+
### useSessionTracking
|
|
82
|
+
|
|
83
|
+
Utility hook for manual session tracking of event switches and session expiration. **Note**: Login and logout are automatically tracked by `UnifiedAuthProvider`, so those methods are not available here.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
function useSessionTracking(
|
|
87
|
+
supabaseClient: SupabaseClient,
|
|
88
|
+
appName?: string
|
|
89
|
+
): {
|
|
90
|
+
trackEventSwitch: (eventId: string) => Promise<void>;
|
|
91
|
+
trackSessionExpired: () => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Usage
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useSessionTracking } from '@jmruthers/pace-core';
|
|
99
|
+
import { supabase } from './lib/supabase';
|
|
100
|
+
|
|
101
|
+
function MyComponent() {
|
|
102
|
+
const { trackEventSwitch, trackSessionExpired } = useSessionTracking(
|
|
103
|
+
supabase,
|
|
104
|
+
'MY_APP'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const handleEventSwitch = async (eventId: string) => {
|
|
108
|
+
await trackEventSwitch(eventId);
|
|
109
|
+
// Event switch logic...
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleSessionExpiration = async () => {
|
|
113
|
+
await trackSessionExpired();
|
|
114
|
+
// Session expiration logic...
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
// Component JSX
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Methods
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `trackEventSwitch(eventId)` | Track when a user switches to a different event. |
|
|
128
|
+
| `trackSessionExpired()` | Track when a session expires. |
|
|
129
|
+
|
|
130
|
+
> **Automatic Tracking**: When using `UnifiedAuthProvider`, login and logout events are **automatically tracked**. You only need to use this hook for event switches or session expirations, which are not automatically tracked.
|
|
131
|
+
|
|
79
132
|
|
|
80
133
|
## Event Management Hooks
|
|
81
134
|
|
|
@@ -206,6 +206,66 @@ The inactivity tracker monitors the following user interactions:
|
|
|
206
206
|
- **Automatic cleanup**: All timers and listeners are properly cleaned up
|
|
207
207
|
- **Error handling**: Graceful fallback if localStorage or BroadcastChannel fail
|
|
208
208
|
|
|
209
|
+
#### Automatic Login History Tracking
|
|
210
|
+
|
|
211
|
+
The `UnifiedAuthProvider` automatically tracks all user logins for security auditing and compliance:
|
|
212
|
+
|
|
213
|
+
- **Automatic Tracking** - No manual intervention required, tracking happens automatically on login/logout
|
|
214
|
+
- **Complete Audit Trail** - Records user ID, email, timestamp, IP address, user agent, and application context
|
|
215
|
+
- **Database Storage** - All login events are stored in `rbac_user_login_history` table
|
|
216
|
+
- **Application Context** - Tracks which application the user logged into (when `appName` is provided)
|
|
217
|
+
- **Non-Blocking** - Tracking failures don't prevent authentication from succeeding
|
|
218
|
+
- **Privacy Compliant** - Users can only view their own login history (RLS enforced)
|
|
219
|
+
|
|
220
|
+
Login history is tracked automatically when you use `UnifiedAuthProvider`. No additional configuration is required:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<UnifiedAuthProvider
|
|
224
|
+
supabaseClient={supabase}
|
|
225
|
+
appName="MY_APP" // Enables app-specific tracking in login history
|
|
226
|
+
// ... other props
|
|
227
|
+
>
|
|
228
|
+
<AppContent />
|
|
229
|
+
</UnifiedAuthProvider>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**What Gets Tracked:**
|
|
233
|
+
|
|
234
|
+
- User ID and email
|
|
235
|
+
- Login timestamp
|
|
236
|
+
- Session ID
|
|
237
|
+
- IP address (if available)
|
|
238
|
+
- User agent string
|
|
239
|
+
- Application ID (if `appName` is provided)
|
|
240
|
+
- Organisation ID
|
|
241
|
+
- Event ID (if applicable)
|
|
242
|
+
|
|
243
|
+
**Querying Login History:**
|
|
244
|
+
|
|
245
|
+
Login history can be queried directly from the database using RLS-protected queries:
|
|
246
|
+
|
|
247
|
+
```sql
|
|
248
|
+
-- Get user's login history
|
|
249
|
+
SELECT
|
|
250
|
+
login_timestamp,
|
|
251
|
+
email,
|
|
252
|
+
ip_address,
|
|
253
|
+
user_agent,
|
|
254
|
+
app_id,
|
|
255
|
+
event_id
|
|
256
|
+
FROM rbac_user_login_history
|
|
257
|
+
WHERE user_id = auth.uid()
|
|
258
|
+
ORDER BY login_timestamp DESC
|
|
259
|
+
LIMIT 100;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Security Notes:**
|
|
263
|
+
|
|
264
|
+
- Login history insertion uses `SECURITY DEFINER` functions (bypasses RLS)
|
|
265
|
+
- RLS policies ensure users can only view their own login history
|
|
266
|
+
- Failed tracking attempts are logged as warnings but don't break authentication
|
|
267
|
+
- All tracking is asynchronous and non-blocking
|
|
268
|
+
|
|
209
269
|
## OrganisationProvider
|
|
210
270
|
|
|
211
271
|
Manages multi-tenant organisation context and user organisation memberships. **Automatically sets database organisation context** to ensure RLS policies work correctly.
|
|
@@ -77,6 +77,7 @@ sequenceDiagram
|
|
|
77
77
|
- **Persistent State** - Authentication state persists across page reloads
|
|
78
78
|
- **Multi-Tab Support** - Authentication state synchronized across tabs
|
|
79
79
|
- **Graceful Degradation** - Handles network issues and token expiry
|
|
80
|
+
- **Automatic Login History** - User login events are automatically tracked in `rbac_user_login_history` table
|
|
80
81
|
|
|
81
82
|
### Security Features
|
|
82
83
|
|
|
@@ -84,6 +85,7 @@ sequenceDiagram
|
|
|
84
85
|
- **JWT Tokens** - Secure, stateless authentication
|
|
85
86
|
- **CSRF Protection** - Cross-site request forgery prevention
|
|
86
87
|
- **Audit Logging** - Complete action tracking for compliance
|
|
88
|
+
- **Login History Tracking** - Automatic tracking of all user logins with timestamps, IP addresses, user agents, and application context
|
|
87
89
|
|
|
88
90
|
## Multi-Tenancy
|
|
89
91
|
|
|
@@ -23,6 +23,7 @@ PACE Core provides a comprehensive authentication system built on Supabase that
|
|
|
23
23
|
- **🔒 Session Persistence** - Secure session management with auto-refresh
|
|
24
24
|
- **🎯 Permission Integration** - Built-in RBAC integration
|
|
25
25
|
- **📊 Debug Support** - Comprehensive debugging and monitoring
|
|
26
|
+
- **📝 Automatic Login History** - All user logins automatically tracked for audit trails
|
|
26
27
|
|
|
27
28
|
## Quick Start
|
|
28
29
|
|
package/docs/security/README.md
CHANGED
|
@@ -16,6 +16,7 @@ PACE Core is designed with security as a first-class concern, providing:
|
|
|
16
16
|
- **Comprehensive RBAC system** with fine-grained permissions
|
|
17
17
|
- **Secure data access** with row-level security
|
|
18
18
|
- **Audit logging** for compliance and monitoring
|
|
19
|
+
- **Automatic login history tracking** for security audits and compliance
|
|
19
20
|
- **Input validation** and sanitization
|
|
20
21
|
- **XSS protection** and secure coding practices
|
|
21
22
|
- **Auto-logout on inactivity** for enhanced security
|
|
@@ -59,6 +60,64 @@ Sessions are automatically managed by PACE Core:
|
|
|
59
60
|
- **Session validation** on every request
|
|
60
61
|
- **Automatic logout** on token expiration
|
|
61
62
|
- **Inactivity auto-logout** after 30 minutes of inactivity (configurable)
|
|
63
|
+
- **Automatic login history tracking** - All user logins are automatically recorded
|
|
64
|
+
|
|
65
|
+
### 2.1 Login History Tracking
|
|
66
|
+
|
|
67
|
+
PACE Core **automatically tracks all user logins** for security auditing and compliance - no manual intervention required.
|
|
68
|
+
|
|
69
|
+
- **Fully Automatic** - Simply use `UnifiedAuthProvider` with `appName` prop - tracking happens automatically
|
|
70
|
+
- **No Configuration Needed** - No calls to tracking functions, no setup code, no manual intervention
|
|
71
|
+
- **Complete Audit Trail** - Records user ID, email, timestamp, IP address, user agent, and application context
|
|
72
|
+
- **Database Storage** - All login events are stored in `rbac_user_login_history` table automatically
|
|
73
|
+
- **Application Context** - Tracks which application the user logged into (when `appName` is provided)
|
|
74
|
+
- **Non-Blocking** - Tracking failures don't prevent authentication from succeeding
|
|
75
|
+
- **Privacy Compliant** - Users can only view their own login history (RLS enforced)
|
|
76
|
+
|
|
77
|
+
**How It Works:**
|
|
78
|
+
|
|
79
|
+
Login history tracking is **completely automatic** when you use `UnifiedAuthProvider`. Simply provide the `appName` prop (which is already required) and tracking happens automatically:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { UnifiedAuthProvider } from '@jmruthers/pace-core';
|
|
83
|
+
|
|
84
|
+
function App() {
|
|
85
|
+
return (
|
|
86
|
+
<UnifiedAuthProvider
|
|
87
|
+
supabaseClient={supabaseClient}
|
|
88
|
+
appName="MY_APP" // Optional: Enables app-specific tracking
|
|
89
|
+
// ... other props
|
|
90
|
+
>
|
|
91
|
+
<YourApp />
|
|
92
|
+
</UnifiedAuthProvider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Querying Login History:**
|
|
98
|
+
|
|
99
|
+
Login history can be queried directly from the database:
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
-- Get user's login history
|
|
103
|
+
SELECT
|
|
104
|
+
login_timestamp,
|
|
105
|
+
email,
|
|
106
|
+
ip_address,
|
|
107
|
+
user_agent,
|
|
108
|
+
app_id
|
|
109
|
+
FROM rbac_user_login_history
|
|
110
|
+
WHERE user_id = auth.uid()
|
|
111
|
+
ORDER BY login_timestamp DESC
|
|
112
|
+
LIMIT 100;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Security Notes:**
|
|
116
|
+
|
|
117
|
+
- Login history insertion uses `SECURITY DEFINER` functions (bypasses RLS)
|
|
118
|
+
- RLS policies ensure users can only view their own login history
|
|
119
|
+
- Failed tracking attempts are logged but don't break authentication
|
|
120
|
+
- All tracking is asynchronous and non-blocking
|
|
62
121
|
|
|
63
122
|
```tsx
|
|
64
123
|
import { useUnifiedAuth } from '@jmruthers/pace-core';
|
package/package.json
CHANGED
|
@@ -540,7 +540,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
540
540
|
eventId: 'event-123',
|
|
541
541
|
appId: 'app-123',
|
|
542
542
|
},
|
|
543
|
-
permission: 'update',
|
|
543
|
+
permission: 'update:page.dashboard-page',
|
|
544
544
|
pageId: 'dashboard-page',
|
|
545
545
|
});
|
|
546
546
|
}, { timeout: 3000 });
|
|
@@ -571,7 +571,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
571
571
|
eventId: 'event-123',
|
|
572
572
|
appId: 'app-123',
|
|
573
573
|
},
|
|
574
|
-
permission: 'create',
|
|
574
|
+
permission: 'create:page.dashboard',
|
|
575
575
|
pageId: 'dashboard',
|
|
576
576
|
});
|
|
577
577
|
}, { timeout: 3000 });
|
|
@@ -395,10 +395,16 @@ export function PaceAppLayout({
|
|
|
395
395
|
return false;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// Construct the full permission string in format "operation:page.pageId"
|
|
399
|
+
// If permission already includes ':', use it as-is, otherwise format as "operation:page.pageId"
|
|
400
|
+
const fullPermission: Permission = permission.includes(':')
|
|
401
|
+
? (permission as Permission)
|
|
402
|
+
: (pageId ? `${permission}:page.${pageId}` : permission) as Permission;
|
|
403
|
+
|
|
398
404
|
return await isPermitted({
|
|
399
405
|
userId: user.id,
|
|
400
406
|
scope,
|
|
401
|
-
permission:
|
|
407
|
+
permission: fullPermission,
|
|
402
408
|
pageId
|
|
403
409
|
});
|
|
404
410
|
} catch (error) {
|
|
@@ -577,30 +583,56 @@ export function PaceAppLayout({
|
|
|
577
583
|
}
|
|
578
584
|
|
|
579
585
|
// Organisation context is ready - now filter items based on permissions
|
|
580
|
-
|
|
581
|
-
|
|
586
|
+
// OPTIMIZATION: Use batch permission map instead of individual checks to avoid rate limits
|
|
587
|
+
// This makes 1 call instead of N calls (where N = number of navigation items)
|
|
588
|
+
try {
|
|
589
|
+
const { getPermissionMap } = await import('../../rbac/api');
|
|
590
|
+
const permissionMap = await getPermissionMap({
|
|
591
|
+
userId: user.id,
|
|
592
|
+
scope,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Filter items using the permission map (synchronous, no rate limit issues)
|
|
596
|
+
const filtered = baseMenuItems.map((item) => {
|
|
582
597
|
if (!item.href) return { item, hasAccess: true };
|
|
583
598
|
|
|
584
599
|
const pageId = pageIdMapping[item.href] || item.href.slice(1) || 'home';
|
|
585
600
|
const permission = routePermissions[item.href] || defaultPermission;
|
|
601
|
+
const fullPermission: Permission = permission.includes(':')
|
|
602
|
+
? (permission as Permission)
|
|
603
|
+
: (pageId ? `${permission}:page.${pageId}` : permission) as Permission;
|
|
586
604
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
605
|
+
// Check permission map (super admin check already handled in getPermissionMap)
|
|
606
|
+
const hasAccess = permissionMap['*'] === true || permissionMap[fullPermission] === true;
|
|
607
|
+
|
|
608
|
+
if (auditLog) {
|
|
609
|
+
console.log(`[PaceAppLayout] Navigation filtering:`, {
|
|
610
|
+
item: item.label,
|
|
611
|
+
href: item.href,
|
|
612
|
+
pageId,
|
|
613
|
+
permission: fullPermission,
|
|
614
|
+
hasAccess,
|
|
615
|
+
});
|
|
593
616
|
}
|
|
594
|
-
|
|
595
|
-
|
|
617
|
+
|
|
618
|
+
return { item, hasAccess };
|
|
619
|
+
});
|
|
596
620
|
|
|
597
|
-
|
|
621
|
+
if (!isMounted) return;
|
|
598
622
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
623
|
+
const accessibleItems = filtered
|
|
624
|
+
.filter(({ hasAccess }) => hasAccess)
|
|
625
|
+
.map(({ item }) => item);
|
|
602
626
|
|
|
603
|
-
|
|
627
|
+
setFilteredMenuItems(accessibleItems);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
// On error, fall back to showing all items (graceful degradation)
|
|
630
|
+
// This prevents navigation from being empty if permission checks fail
|
|
631
|
+
console.error('[PaceAppLayout] Failed to load permission map for navigation filtering:', error);
|
|
632
|
+
if (isMounted) {
|
|
633
|
+
setFilteredMenuItems(baseMenuItems);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
604
636
|
};
|
|
605
637
|
|
|
606
638
|
filterItems();
|
|
@@ -715,7 +715,8 @@ describe('PaceAppLayout Security', () => {
|
|
|
715
715
|
const { isPermitted } = await import('../../../rbac/api');
|
|
716
716
|
vi.mocked(isPermitted).mockImplementation(({ userId, scope, permission, pageId }) => {
|
|
717
717
|
// Simulate user trying to access admin with read permission
|
|
718
|
-
|
|
718
|
+
// Permission is now formatted as "operation:page.pageId"
|
|
719
|
+
if (pageId === 'admin' && permission === 'read:page.admin') {
|
|
719
720
|
return Promise.resolve(false);
|
|
720
721
|
}
|
|
721
722
|
return Promise.resolve(true);
|
|
@@ -572,7 +572,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
572
572
|
expect(mockIsPermitted).toHaveBeenCalledWith({
|
|
573
573
|
userId: 'test-user-id',
|
|
574
574
|
scope: expect.objectContaining({ organisationId: 'test-org-123' }),
|
|
575
|
-
permission: 'delete',
|
|
575
|
+
permission: 'delete:page.test-path',
|
|
576
576
|
pageId: 'test-path'
|
|
577
577
|
});
|
|
578
578
|
});
|
|
@@ -597,7 +597,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
597
597
|
expect(mockIsPermitted).toHaveBeenCalledWith({
|
|
598
598
|
userId: 'test-user-id',
|
|
599
599
|
scope: expect.objectContaining({ organisationId: 'test-org-123' }),
|
|
600
|
-
permission: 'read',
|
|
600
|
+
permission: 'read:page.custom-page-id',
|
|
601
601
|
pageId: 'custom-page-id'
|
|
602
602
|
});
|
|
603
603
|
});
|
|
@@ -816,11 +816,8 @@ describe('PaceAppLayout Component', () => {
|
|
|
816
816
|
});
|
|
817
817
|
|
|
818
818
|
it('handles location changes correctly', async () => {
|
|
819
|
-
// Change location
|
|
820
|
-
|
|
821
|
-
value: { pathname: '/new-path' },
|
|
822
|
-
writable: true
|
|
823
|
-
});
|
|
819
|
+
// Change location - update the mockLocation object since component uses useLocation()
|
|
820
|
+
mockLocation.pathname = '/new-path';
|
|
824
821
|
|
|
825
822
|
render(
|
|
826
823
|
<TestWrapper>
|
|
@@ -832,10 +829,13 @@ describe('PaceAppLayout Component', () => {
|
|
|
832
829
|
expect(mockIsPermitted).toHaveBeenCalledWith({
|
|
833
830
|
userId: 'test-user-id',
|
|
834
831
|
scope: expect.objectContaining({ organisationId: 'test-org-123' }),
|
|
835
|
-
permission: 'read',
|
|
836
|
-
pageId: '
|
|
832
|
+
permission: 'read:page.new-path',
|
|
833
|
+
pageId: 'new-path'
|
|
837
834
|
});
|
|
838
835
|
});
|
|
836
|
+
|
|
837
|
+
// Reset for other tests
|
|
838
|
+
mockLocation.pathname = '/test-path';
|
|
839
839
|
});
|
|
840
840
|
});
|
|
841
841
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
export { UnifiedAuthProvider, useUnifiedAuth } from './providers/UnifiedAuthProvider';
|
|
21
21
|
export type { UnifiedAuthProviderProps, UnifiedAuthContextType, UserEventAccess } from './providers/UnifiedAuthProvider';
|
|
22
22
|
|
|
23
|
+
// Session tracking utility (for manual use if needed)
|
|
24
|
+
export { useSessionTracking } from './utils/sessionTracking';
|
|
25
|
+
|
|
23
26
|
// Provider components (using service architecture)
|
|
24
27
|
export { EventProvider } from './providers/EventProvider';
|
|
25
28
|
export { OrganisationProvider } from './providers/OrganisationProvider';
|
|
@@ -24,13 +24,14 @@ export const AuthServiceContext = createContext<AuthServiceContextType | null>(n
|
|
|
24
24
|
export interface AuthServiceProviderProps {
|
|
25
25
|
children: React.ReactNode;
|
|
26
26
|
supabaseClient: SupabaseClient;
|
|
27
|
+
appName?: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export function AuthServiceProvider({ children, supabaseClient }: AuthServiceProviderProps) {
|
|
30
|
+
export function AuthServiceProvider({ children, supabaseClient, appName }: AuthServiceProviderProps) {
|
|
30
31
|
// Create service instance with useMemo to prevent recreation on every render
|
|
31
32
|
const authService = useMemo(
|
|
32
|
-
() => new AuthService(supabaseClient),
|
|
33
|
-
[supabaseClient]
|
|
33
|
+
() => new AuthService(supabaseClient, appName),
|
|
34
|
+
[supabaseClient, appName]
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
const [sessionRestoration, setSessionRestoration] = useState<SessionRestorationState>(
|
|
@@ -526,7 +526,7 @@ export function UnifiedAuthProvider({
|
|
|
526
526
|
dangerouslyDisableInactivity = false
|
|
527
527
|
}: UnifiedAuthProviderProps) {
|
|
528
528
|
return (
|
|
529
|
-
<AuthServiceProvider supabaseClient={supabaseClient}>
|
|
529
|
+
<AuthServiceProvider supabaseClient={supabaseClient} appName={appName}>
|
|
530
530
|
<ServiceAwareProviders
|
|
531
531
|
supabaseClient={supabaseClient}
|
|
532
532
|
appName={appName}
|
package/src/rbac/engine.ts
CHANGED
|
@@ -474,11 +474,13 @@ export class RBACEngine {
|
|
|
474
474
|
|
|
475
475
|
try {
|
|
476
476
|
const { userId, scope } = input;
|
|
477
|
+
// Call unified function (tech debt removed: consolidated from 2 overloaded versions)
|
|
477
478
|
const { data, error } = await (this.supabase as any).rpc('rbac_permissions_get', {
|
|
478
479
|
p_user_id: userId,
|
|
479
480
|
p_organisation_id: scope.organisationId || null,
|
|
480
481
|
p_event_id: scope.eventId || null,
|
|
481
482
|
p_app_id: scope.appId || null,
|
|
483
|
+
p_page_id: null, // Optional: can filter to specific page if needed
|
|
482
484
|
});
|
|
483
485
|
|
|
484
486
|
if (error) {
|
|
@@ -28,10 +28,12 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
28
28
|
private restorationTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
29
29
|
private readonly restorationTimeoutMs = 5000;
|
|
30
30
|
private restorationStartTime: number | null = null;
|
|
31
|
+
private appName: string | undefined = undefined;
|
|
31
32
|
|
|
32
|
-
constructor(supabaseClient: SupabaseClient) {
|
|
33
|
+
constructor(supabaseClient: SupabaseClient, appName?: string) {
|
|
33
34
|
super();
|
|
34
35
|
this.supabaseClient = supabaseClient;
|
|
36
|
+
this.appName = appName;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// Auth state getters
|
|
@@ -386,6 +388,13 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
386
388
|
this.session = null;
|
|
387
389
|
this.user = null;
|
|
388
390
|
this.authError = null;
|
|
391
|
+
|
|
392
|
+
// Automatic session tracking (non-blocking)
|
|
393
|
+
if (session?.user) {
|
|
394
|
+
this.trackSession('logout', session).catch(err => {
|
|
395
|
+
console.warn('[AuthService] Failed to track logout session:', err);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
389
398
|
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
390
399
|
this.session = session;
|
|
391
400
|
this.user = session?.user ?? null;
|
|
@@ -394,6 +403,14 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
394
403
|
if (session) {
|
|
395
404
|
this.authError = null;
|
|
396
405
|
}
|
|
406
|
+
|
|
407
|
+
// Automatic session tracking for login (non-blocking)
|
|
408
|
+
// Only track on SIGNED_IN, not TOKEN_REFRESHED (to avoid duplicate login records)
|
|
409
|
+
if (event === 'SIGNED_IN' && session?.user) {
|
|
410
|
+
this.trackSession('login', session).catch(err => {
|
|
411
|
+
console.warn('[AuthService] Failed to track login session:', err);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
397
414
|
} else if (event === 'INITIAL_SESSION') {
|
|
398
415
|
if (session) {
|
|
399
416
|
this.session = session;
|
|
@@ -502,6 +519,67 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
502
519
|
}
|
|
503
520
|
}
|
|
504
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Automatically track user session using rbac_session_track
|
|
524
|
+
* This method is called automatically on SIGNED_IN and SIGNED_OUT events.
|
|
525
|
+
* It's non-blocking and failures are logged as warnings.
|
|
526
|
+
*/
|
|
527
|
+
private async trackSession(
|
|
528
|
+
sessionType: 'login' | 'logout',
|
|
529
|
+
session: Session | null
|
|
530
|
+
): Promise<void> {
|
|
531
|
+
if (!this.supabaseClient || !session?.user) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
// Resolve app_id from appName if available
|
|
537
|
+
let appId: string | undefined = undefined;
|
|
538
|
+
if (this.appName) {
|
|
539
|
+
const { data, error } = await this.supabaseClient
|
|
540
|
+
.from('rbac_apps')
|
|
541
|
+
.select('id')
|
|
542
|
+
.eq('name', this.appName)
|
|
543
|
+
.eq('is_active', true)
|
|
544
|
+
.single();
|
|
545
|
+
|
|
546
|
+
if (!error && data) {
|
|
547
|
+
appId = data.id;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Get IP address and user agent from browser (if available)
|
|
552
|
+
const ipAddress = undefined; // Browser doesn't expose IP directly, could use API
|
|
553
|
+
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : undefined;
|
|
554
|
+
|
|
555
|
+
// Get device fingerprint from localStorage if available
|
|
556
|
+
// Note: Device fingerprinting should be done by consuming app and passed via custom header
|
|
557
|
+
// For now, we'll skip it to avoid dependencies
|
|
558
|
+
const deviceFingerprint = undefined;
|
|
559
|
+
|
|
560
|
+
// Call rbac_session_track RPC function
|
|
561
|
+
// This automatically inserts into rbac_user_sessions AND rbac_user_login_history (for login)
|
|
562
|
+
const { error } = await (this.supabaseClient as any).rpc('rbac_session_track', {
|
|
563
|
+
p_user_id: session.user.id,
|
|
564
|
+
p_session_type: sessionType,
|
|
565
|
+
p_event_id: null, // Event ID should come from context, not auth service
|
|
566
|
+
p_app_id: appId,
|
|
567
|
+
p_ip_address: ipAddress,
|
|
568
|
+
p_user_agent: userAgent,
|
|
569
|
+
p_device_fingerprint: deviceFingerprint,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (error) {
|
|
573
|
+
console.warn(`[AuthService] Failed to track ${sessionType} session:`, error);
|
|
574
|
+
} else {
|
|
575
|
+
console.debug(`[AuthService] Successfully tracked ${sessionType} session`);
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
// Log error but don't throw (non-blocking)
|
|
579
|
+
console.warn(`[AuthService] Error tracking ${sessionType} session:`, error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
505
583
|
private setupErrorHandlers(): void {
|
|
506
584
|
if (typeof window === 'undefined') return;
|
|
507
585
|
|