@jmruthers/pace-core 0.5.107 → 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-H2WIR2DN.js → DataTable-5HITILXS.js} +7 -7
- package/dist/{PublicLoadingSpinner-48ewSMKK.d.ts → PublicLoadingSpinner-DgDWTFqn.d.ts} +4 -2
- 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-EWKCROSF.js → chunk-P72NKAT5.js} +84 -28
- package/dist/chunk-P72NKAT5.js.map +1 -0
- package/dist/{chunk-VJ7MPS2K.js → chunk-S4D3Z723.js} +6 -6
- package/dist/{chunk-5JJCXTVE.js → chunk-UW2DE6JX.js} +108 -86
- package/dist/{chunk-5JJCXTVE.js.map → chunk-UW2DE6JX.js.map} +1 -1
- package/dist/{chunk-QDDUU625.js → chunk-WWNOVFDC.js} +4 -4
- package/dist/{chunk-GVRSXXAA.js → chunk-YFMENCR4.js} +3 -3
- package/dist/components.d.ts +1 -1
- 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 +7 -7
- 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 +42 -6
- 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/DataTable/components/ColumnFilter.tsx +2 -1
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/FilterRow.tsx +22 -11
- package/src/components/DataTable/components/PaginationControls.tsx +1 -1
- package/src/components/DataTable/components/UnifiedTableBody.tsx +39 -10
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +126 -25
- 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-EWKCROSF.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-H2WIR2DN.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-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
|
@@ -62,13 +62,14 @@ export function ColumnFilter({
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
case 'number':
|
|
65
|
+
// Always hide spinner arrows for number filter inputs (cleaner UX)
|
|
65
66
|
return (
|
|
66
67
|
<Input
|
|
67
68
|
type="number"
|
|
68
69
|
value={columnFilterValue as string || ''}
|
|
69
70
|
onChange={(e) => handleFilterChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
70
71
|
placeholder={placeholder || `Filter ${column.id}...`}
|
|
71
|
-
className="h-8"
|
|
72
|
+
className="h-8 datatable-number-no-spinners"
|
|
72
73
|
/>
|
|
73
74
|
);
|
|
74
75
|
|
|
@@ -44,7 +44,10 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
44
44
|
onChange: (value: CellValue) => void;
|
|
45
45
|
className?: string;
|
|
46
46
|
}) {
|
|
47
|
-
|
|
47
|
+
// Determine if searchable - explicitly check for true to ensure visible search input appears
|
|
48
|
+
// When selectSearchable is true or undefined, show the visible search input box
|
|
49
|
+
// When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
|
|
50
|
+
const isSearchable = columnDef.selectSearchable !== false;
|
|
48
51
|
const isCreatable = columnDef.creatable === true;
|
|
49
52
|
const selectRef = React.useRef<HTMLFormElement>(null);
|
|
50
53
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
@@ -137,7 +140,7 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
137
140
|
<SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
|
|
138
141
|
</SelectTrigger>
|
|
139
142
|
<SelectContent
|
|
140
|
-
searchable={isSearchable}
|
|
143
|
+
searchable={Boolean(isSearchable)}
|
|
141
144
|
searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
|
|
142
145
|
maxHeight={columnDef.selectMaxHeight}
|
|
143
146
|
className={columnDef.selectContentClassName}
|
|
@@ -232,6 +235,8 @@ const renderEditField = <TData extends DataRecord>(
|
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
if (columnDef.fieldType === 'number') {
|
|
238
|
+
// Hide spinner arrows by default for number, currency, and percentage fields
|
|
239
|
+
// Only show spinners if explicitly set to false
|
|
235
240
|
const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
|
|
236
241
|
return (
|
|
237
242
|
<Input
|
|
@@ -12,7 +12,7 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
12
12
|
const { columnFilters } = getState();
|
|
13
13
|
|
|
14
14
|
// Get unique values for select filters
|
|
15
|
-
const getColumnOptions = (columnId: string) => {
|
|
15
|
+
const getColumnOptions = React.useCallback((columnId: string) => {
|
|
16
16
|
const column = table.getColumn(columnId);
|
|
17
17
|
if (!column) return [];
|
|
18
18
|
|
|
@@ -40,44 +40,55 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
40
40
|
return Array.from(uniqueValues)
|
|
41
41
|
.sort()
|
|
42
42
|
.map((value) => ({ value, label: value }));
|
|
43
|
-
};
|
|
43
|
+
}, [table]);
|
|
44
44
|
|
|
45
45
|
// Determine filter type based on column data
|
|
46
|
-
|
|
46
|
+
// IMPORTANT: Explicit filterType always takes priority - auto-detection only runs if filterType is not set
|
|
47
|
+
const getFilterType = React.useCallback((columnId: string) => {
|
|
47
48
|
const column = table.getColumn(columnId);
|
|
48
49
|
if (!column) return 'text';
|
|
49
50
|
|
|
50
51
|
const columnDef = column.columnDef as any;
|
|
51
52
|
|
|
52
|
-
// Check if column has explicit filter type configuration
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// PRIORITY 1: Check if column has explicit filter type configuration
|
|
54
|
+
// This MUST be checked first and must respect any explicit value, including 'text'
|
|
55
|
+
// Use explicit !== undefined && !== null check to ensure 'text' is not treated as falsy
|
|
56
|
+
// This prevents auto-detection from overriding explicit filterType settings
|
|
57
|
+
const explicitFilterType = columnDef.filterType;
|
|
58
|
+
if (explicitFilterType !== undefined && explicitFilterType !== null && explicitFilterType !== '') {
|
|
59
|
+
// Explicit filterType set - return it immediately (no auto-detection)
|
|
60
|
+
// This ensures filterType: 'text' is always respected, even for columns with ≤10 unique values
|
|
61
|
+
return explicitFilterType as 'text' | 'select' | 'number' | 'date';
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
//
|
|
64
|
+
// Only proceed with auto-detection if filterType was NOT explicitly set
|
|
65
|
+
|
|
66
|
+
// PRIORITY 2: Auto-detect select filter if filterSelectOptions is explicitly provided
|
|
58
67
|
if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
|
|
59
68
|
return 'select';
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
// Check if it's a date column
|
|
71
|
+
// PRIORITY 3: Check if it's a date column (by column ID pattern)
|
|
63
72
|
if (columnId.toLowerCase().includes('date') || columnId.toLowerCase().includes('time')) {
|
|
64
73
|
return 'date';
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
// Check if it's a number column
|
|
76
|
+
// PRIORITY 4: Check if it's a number column (by data type)
|
|
68
77
|
const firstValue = table.getRowModel().rows[0]?.getValue(columnId);
|
|
69
78
|
if (typeof firstValue === 'number') {
|
|
70
79
|
return 'number';
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
//
|
|
82
|
+
// PRIORITY 5: Auto-detect select filter if limited unique values (≤10)
|
|
83
|
+
// Only runs if filterType was NOT explicitly set (checked above)
|
|
74
84
|
const uniqueValues = getColumnOptions(columnId);
|
|
75
85
|
if (uniqueValues.length <= 10 && uniqueValues.length > 1) {
|
|
76
86
|
return 'select';
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
// Default to text filter
|
|
79
90
|
return 'text';
|
|
80
|
-
};
|
|
91
|
+
}, [table, getColumnOptions]);
|
|
81
92
|
|
|
82
93
|
return (
|
|
83
94
|
<tr className="border-b bg-sec-50/50">
|
|
@@ -259,7 +259,7 @@ export function EnhancedPaginationControls<TData extends DataRecord>({
|
|
|
259
259
|
max={pageCount}
|
|
260
260
|
value={jumpToPage}
|
|
261
261
|
onChange={(e) => setJumpToPage(e.target.value)}
|
|
262
|
-
className="w-16 h-6 px-2 border rounded text-xs"
|
|
262
|
+
className="w-16 h-6 px-2 border rounded text-xs datatable-number-no-spinners"
|
|
263
263
|
placeholder="1"
|
|
264
264
|
/>
|
|
265
265
|
<Button type="submit" size="sm" variant="outline" className="h-6 px-2 text-xs">
|
|
@@ -134,7 +134,10 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
134
134
|
placeholder?: string;
|
|
135
135
|
onChange: (value: CellValue) => void;
|
|
136
136
|
}) {
|
|
137
|
-
|
|
137
|
+
// Determine if searchable - explicitly check for true to ensure visible search input appears
|
|
138
|
+
// When selectSearchable is true or undefined, show the visible search input box
|
|
139
|
+
// When selectSearchable is false, hide the search input (type-to-search still works via SelectContent internals)
|
|
140
|
+
const isSearchable = columnDef.selectSearchable !== false;
|
|
138
141
|
const isCreatable = columnDef.creatable === true;
|
|
139
142
|
const selectRef = React.useRef<HTMLFormElement>(null);
|
|
140
143
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
@@ -227,7 +230,7 @@ function SelectEditField<TData extends DataRecord>({
|
|
|
227
230
|
<SelectValue placeholder={placeholder || `Select ${columnDef.header || 'option'}...`} />
|
|
228
231
|
</SelectTrigger>
|
|
229
232
|
<SelectContent
|
|
230
|
-
searchable={isSearchable}
|
|
233
|
+
searchable={Boolean(isSearchable)}
|
|
231
234
|
searchPlaceholder={`Search ${columnDef.header || 'options'}...`}
|
|
232
235
|
maxHeight={columnDef.selectMaxHeight}
|
|
233
236
|
className={columnDef.selectContentClassName}
|
|
@@ -312,8 +315,11 @@ const renderEditField = <TData extends DataRecord>(
|
|
|
312
315
|
);
|
|
313
316
|
}
|
|
314
317
|
|
|
315
|
-
// Check for number type
|
|
318
|
+
// Check for number type (applies to number, currency, and percentage fields)
|
|
316
319
|
if (columnDef.fieldType === 'number') {
|
|
320
|
+
// Hide spinner arrows by default for all number-related fields
|
|
321
|
+
// Currency and percentage columns use fieldType: 'number' with formatting in cell renderer
|
|
322
|
+
// Only show spinners if explicitly set to false
|
|
317
323
|
const hideSpinners = columnDef.hideNumberSpinners !== false; // Default to true
|
|
318
324
|
return (
|
|
319
325
|
<Input
|
|
@@ -879,6 +885,26 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
879
885
|
}
|
|
880
886
|
|
|
881
887
|
// Render edit fields for data columns
|
|
888
|
+
// Determine the correct key to use for creationData
|
|
889
|
+
// Priority: editAccessorKey > accessorKey > column.id
|
|
890
|
+
const columnDef = header.column.columnDef as EditableColumnDef<TData>;
|
|
891
|
+
const dataKey = columnDef.editAccessorKey || columnDef.accessorKey || header.column.id;
|
|
892
|
+
|
|
893
|
+
// Always render a cell to maintain alignment - renderEditField always returns something
|
|
894
|
+
const editField = renderEditField(
|
|
895
|
+
header.column,
|
|
896
|
+
creationData[dataKey] ?? creationData[header.column.id] ?? '',
|
|
897
|
+
(value) => {
|
|
898
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
899
|
+
onCreationDataChange({ ...creationData, ...(value as Record<string, CellValue>) });
|
|
900
|
+
} else {
|
|
901
|
+
// Use the determined dataKey for consistent data access
|
|
902
|
+
onCreationDataChange({ ...creationData, [dataKey]: value as CellValue });
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
creationData
|
|
906
|
+
);
|
|
907
|
+
|
|
882
908
|
return (
|
|
883
909
|
<td
|
|
884
910
|
key={header.column.id}
|
|
@@ -887,13 +913,16 @@ export function UnifiedTableBody<TData extends Record<string, any>>({
|
|
|
887
913
|
className: "px-3 py-2"
|
|
888
914
|
})}
|
|
889
915
|
>
|
|
890
|
-
{
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
916
|
+
{editField || (
|
|
917
|
+
// Fallback: render a text input if renderEditField somehow returns nothing
|
|
918
|
+
<Input
|
|
919
|
+
type="text"
|
|
920
|
+
value={String(creationData[dataKey] ?? creationData[header.column.id] ?? '')}
|
|
921
|
+
onChange={(e) => onCreationDataChange({ ...creationData, [dataKey]: e.target.value as CellValue })}
|
|
922
|
+
placeholder={`Enter ${columnDef.header || header.column.id}...`}
|
|
923
|
+
className="h-8"
|
|
924
|
+
/>
|
|
925
|
+
)}
|
|
897
926
|
</td>
|
|
898
927
|
);
|
|
899
928
|
})}
|
|
@@ -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 });
|