@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
|
@@ -1,974 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Simplified Auth Provider
|
|
3
|
-
* @package @jmruthers/pace-core
|
|
4
|
-
* @module Providers
|
|
5
|
-
* @since 2.0.0
|
|
6
|
-
*
|
|
7
|
-
* BREAKING CHANGES FROM v1:
|
|
8
|
-
* - Single provider instead of nested contexts
|
|
9
|
-
* - No service classes (direct React state)
|
|
10
|
-
* - No observer pattern (React's built-in re-rendering)
|
|
11
|
-
* - No UnifiedAuthProvider nesting
|
|
12
|
-
*
|
|
13
|
-
* This provides:
|
|
14
|
-
* - Authentication (user, session)
|
|
15
|
-
* - Organisation management
|
|
16
|
-
* - Event management
|
|
17
|
-
* - Inactivity tracking
|
|
18
|
-
*
|
|
19
|
-
* All in one provider with React state management.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import React, {
|
|
23
|
-
createContext,
|
|
24
|
-
useContext,
|
|
25
|
-
useState,
|
|
26
|
-
useEffect,
|
|
27
|
-
useCallback,
|
|
28
|
-
useMemo,
|
|
29
|
-
useRef
|
|
30
|
-
} from 'react';
|
|
31
|
-
import {
|
|
32
|
-
type SupabaseClient,
|
|
33
|
-
type User,
|
|
34
|
-
type Session,
|
|
35
|
-
type AuthError
|
|
36
|
-
} from '@supabase/supabase-js';
|
|
37
|
-
import type { UUID } from '../rbac/types';
|
|
38
|
-
|
|
39
|
-
// ============================================================================
|
|
40
|
-
// TYPES
|
|
41
|
-
// ============================================================================
|
|
42
|
-
|
|
43
|
-
export interface Organisation {
|
|
44
|
-
id: UUID;
|
|
45
|
-
name: string;
|
|
46
|
-
display_name?: string;
|
|
47
|
-
subscription_tier?: string;
|
|
48
|
-
settings?: Record<string, any>;
|
|
49
|
-
is_active: boolean;
|
|
50
|
-
created_at: string;
|
|
51
|
-
updated_at?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface Event {
|
|
55
|
-
id: string;
|
|
56
|
-
event_id: string;
|
|
57
|
-
name: string;
|
|
58
|
-
event_name?: string;
|
|
59
|
-
organisation_id: UUID;
|
|
60
|
-
event_date?: string; // Date from database for sorting
|
|
61
|
-
start_date?: string; // Alias for event_date
|
|
62
|
-
end_date?: string;
|
|
63
|
-
status?: string;
|
|
64
|
-
settings?: Record<string, any>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface AuthContextType {
|
|
68
|
-
// ========== Auth State ==========
|
|
69
|
-
user: User | null;
|
|
70
|
-
session: Session | null;
|
|
71
|
-
isAuthenticated: boolean;
|
|
72
|
-
authLoading: boolean;
|
|
73
|
-
authError: AuthError | null;
|
|
74
|
-
supabase: SupabaseClient;
|
|
75
|
-
|
|
76
|
-
// ========== Auth Methods ==========
|
|
77
|
-
signIn: (email: string, password?: string) => Promise<{ error: AuthError | null }>;
|
|
78
|
-
signUp: (email: string, password: string) => Promise<{ error: AuthError | null }>;
|
|
79
|
-
signOut: () => Promise<{ error: AuthError | null }>;
|
|
80
|
-
resetPassword: (email: string) => Promise<{ error: AuthError | null }>;
|
|
81
|
-
updatePassword: (password: string) => Promise<{ error: AuthError | null }>;
|
|
82
|
-
refreshSession: () => Promise<{ error: AuthError | null }>;
|
|
83
|
-
|
|
84
|
-
// ========== Organisation State ==========
|
|
85
|
-
selectedOrganisation: Organisation | null;
|
|
86
|
-
organisations: Organisation[];
|
|
87
|
-
organisationLoading: boolean;
|
|
88
|
-
organisationError: Error | null;
|
|
89
|
-
hasValidOrganisationContext: boolean;
|
|
90
|
-
isContextReady: boolean;
|
|
91
|
-
|
|
92
|
-
// ========== Organisation Methods ==========
|
|
93
|
-
switchOrganisation: (orgId: UUID) => Promise<void>;
|
|
94
|
-
refreshOrganisations: () => Promise<void>;
|
|
95
|
-
getUserRole: (orgId?: UUID) => string | null;
|
|
96
|
-
getPrimaryOrganisation: () => Organisation | null;
|
|
97
|
-
|
|
98
|
-
// ========== Event State ==========
|
|
99
|
-
events: Event[];
|
|
100
|
-
selectedEvent: Event | null;
|
|
101
|
-
eventLoading: boolean;
|
|
102
|
-
eventError: Error | null;
|
|
103
|
-
|
|
104
|
-
// ========== Event Methods ==========
|
|
105
|
-
setSelectedEvent: (event: Event | null) => void;
|
|
106
|
-
refreshEvents: () => Promise<void>;
|
|
107
|
-
|
|
108
|
-
// ========== Inactivity State ==========
|
|
109
|
-
showInactivityWarning: boolean;
|
|
110
|
-
inactivityTimeRemaining: number;
|
|
111
|
-
isIdle: boolean;
|
|
112
|
-
isTracking: boolean;
|
|
113
|
-
|
|
114
|
-
// ========== Inactivity Methods ==========
|
|
115
|
-
resetActivity: () => void;
|
|
116
|
-
startTracking: () => void;
|
|
117
|
-
stopTracking: () => void;
|
|
118
|
-
|
|
119
|
-
// ========== Session Management ==========
|
|
120
|
-
sessionId: string | null;
|
|
121
|
-
deviceFingerprint: string | null;
|
|
122
|
-
hasConcurrentSessions: boolean;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface AuthProviderProps {
|
|
126
|
-
children: React.ReactNode;
|
|
127
|
-
supabaseClient: SupabaseClient;
|
|
128
|
-
appName: string;
|
|
129
|
-
persistState?: boolean;
|
|
130
|
-
requireOrganisationContext?: boolean;
|
|
131
|
-
|
|
132
|
-
// Inactivity configuration (MANDATORY for security)
|
|
133
|
-
idleTimeoutMs: number;
|
|
134
|
-
warnBeforeMs: number;
|
|
135
|
-
onIdleLogout: (reason: 'inactivity') => void;
|
|
136
|
-
renderInactivityWarning?: (args: {
|
|
137
|
-
timeRemaining: number;
|
|
138
|
-
onStaySignedIn: () => void;
|
|
139
|
-
onSignOutNow: () => void;
|
|
140
|
-
}) => React.ReactNode;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ============================================================================
|
|
144
|
-
// CONTEXT
|
|
145
|
-
// ============================================================================
|
|
146
|
-
|
|
147
|
-
const AuthContext = createContext<AuthContextType | null>(null);
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
// PROVIDER
|
|
151
|
-
// ============================================================================
|
|
152
|
-
|
|
153
|
-
export function AuthProvider({
|
|
154
|
-
children,
|
|
155
|
-
supabaseClient,
|
|
156
|
-
appName,
|
|
157
|
-
persistState = true,
|
|
158
|
-
requireOrganisationContext = true,
|
|
159
|
-
idleTimeoutMs = 30 * 60 * 1000, // 30 minutes - MANDATORY for security
|
|
160
|
-
warnBeforeMs = 60 * 1000, // 1 minute
|
|
161
|
-
onIdleLogout, // MANDATORY - must be provided
|
|
162
|
-
renderInactivityWarning,
|
|
163
|
-
}: AuthProviderProps) {
|
|
164
|
-
// MANDATORY: Inactivity timeout cannot be disabled
|
|
165
|
-
if (!idleTimeoutMs || idleTimeoutMs < 60000) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
'AuthProvider: idleTimeoutMs is MANDATORY and must be at least 60 seconds (60000ms) for security. ' +
|
|
168
|
-
'The dangerouslyDisableInactivity flag has been removed.'
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!onIdleLogout) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
'AuthProvider: onIdleLogout callback is MANDATORY and must be provided for security.'
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ==========================================================================
|
|
179
|
-
// AUTH STATE (replaces AuthService)
|
|
180
|
-
// ==========================================================================
|
|
181
|
-
|
|
182
|
-
const [user, setUser] = useState<User | null>(null);
|
|
183
|
-
const [session, setSession] = useState<Session | null>(null);
|
|
184
|
-
const [authLoading, setAuthLoading] = useState(true);
|
|
185
|
-
const [authError, setAuthError] = useState<AuthError | null>(null);
|
|
186
|
-
|
|
187
|
-
// ==========================================================================
|
|
188
|
-
// ORGANISATION STATE (replaces OrganisationService)
|
|
189
|
-
// ==========================================================================
|
|
190
|
-
|
|
191
|
-
const [selectedOrganisation, setSelectedOrganisation] = useState<Organisation | null>(null);
|
|
192
|
-
const [organisations, setOrganisations] = useState<Organisation[]>([]);
|
|
193
|
-
const [organisationLoading, setOrganisationLoading] = useState(false);
|
|
194
|
-
const [organisationError, setOrganisationError] = useState<Error | null>(null);
|
|
195
|
-
|
|
196
|
-
// ==========================================================================
|
|
197
|
-
// EVENT STATE (replaces EventService)
|
|
198
|
-
// ==========================================================================
|
|
199
|
-
|
|
200
|
-
const [events, setEvents] = useState<Event[]>([]);
|
|
201
|
-
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
|
202
|
-
const [eventLoading, setEventLoading] = useState(false);
|
|
203
|
-
const [eventError, setEventError] = useState<Error | null>(null);
|
|
204
|
-
|
|
205
|
-
// ==========================================================================
|
|
206
|
-
// INACTIVITY STATE (replaces InactivityService)
|
|
207
|
-
// ==========================================================================
|
|
208
|
-
|
|
209
|
-
const [showInactivityWarning, setShowInactivityWarning] = useState(false);
|
|
210
|
-
const [inactivityTimeRemaining, setInactivityTimeRemaining] = useState(idleTimeoutMs);
|
|
211
|
-
const [isIdle, setIsIdle] = useState(false);
|
|
212
|
-
const [isTracking, setIsTracking] = useState(false);
|
|
213
|
-
|
|
214
|
-
const lastActivityRef = useRef<number>(Date.now());
|
|
215
|
-
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
216
|
-
const warningTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
217
|
-
const deviceFingerprintRef = useRef<string | null>(null);
|
|
218
|
-
const sessionIdRef = useRef<string | null>(null);
|
|
219
|
-
|
|
220
|
-
// ==========================================================================
|
|
221
|
-
// AUTH EFFECTS (replaces AuthService initialization)
|
|
222
|
-
// ==========================================================================
|
|
223
|
-
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
// Setup auth state listener
|
|
226
|
-
const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
|
|
227
|
-
(event, newSession) => {
|
|
228
|
-
console.log('[AuthProvider] Auth state change:', event);
|
|
229
|
-
|
|
230
|
-
if (event === 'SIGNED_OUT') {
|
|
231
|
-
setSession(null);
|
|
232
|
-
setUser(null);
|
|
233
|
-
setAuthError(null);
|
|
234
|
-
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
235
|
-
setSession(newSession);
|
|
236
|
-
setUser(newSession?.user ?? null);
|
|
237
|
-
if (newSession) {
|
|
238
|
-
setAuthError(null);
|
|
239
|
-
}
|
|
240
|
-
} else if (event === 'INITIAL_SESSION') {
|
|
241
|
-
if (newSession) {
|
|
242
|
-
setSession(newSession);
|
|
243
|
-
setUser(newSession.user ?? null);
|
|
244
|
-
setAuthError(null);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
setAuthLoading(false);
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
// Initial session check
|
|
253
|
-
supabaseClient.auth.getSession().then(({ data: { session: initialSession } }) => {
|
|
254
|
-
setSession(initialSession);
|
|
255
|
-
setUser(initialSession?.user ?? null);
|
|
256
|
-
setAuthLoading(false);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
return () => {
|
|
260
|
-
subscription.unsubscribe();
|
|
261
|
-
};
|
|
262
|
-
}, [supabaseClient]);
|
|
263
|
-
|
|
264
|
-
// ==========================================================================
|
|
265
|
-
// ORGANISATION EFFECTS (replaces OrganisationService initialization)
|
|
266
|
-
// ==========================================================================
|
|
267
|
-
|
|
268
|
-
useEffect(() => {
|
|
269
|
-
if (!user) {
|
|
270
|
-
setOrganisations([]);
|
|
271
|
-
setSelectedOrganisation(null);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Load user's organisations
|
|
276
|
-
const loadOrganisations = async () => {
|
|
277
|
-
setOrganisationLoading(true);
|
|
278
|
-
setOrganisationError(null);
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
// Use RPC to get organisations with roles
|
|
282
|
-
const { data, error } = await supabaseClient
|
|
283
|
-
.rpc('rbac_user_organisation_roles_get', {
|
|
284
|
-
p_user_id: user.id
|
|
285
|
-
}) as { data: Array<{ organisation_id: UUID }> | null; error: any };
|
|
286
|
-
|
|
287
|
-
if (error) throw error;
|
|
288
|
-
|
|
289
|
-
if (data && data.length > 0) {
|
|
290
|
-
// Fetch full organisation details
|
|
291
|
-
const orgIds = data.map(r => r.organisation_id);
|
|
292
|
-
const { data: orgs, error: orgsError } = await supabaseClient
|
|
293
|
-
.from('organisations')
|
|
294
|
-
.select('*')
|
|
295
|
-
.in('id', orgIds)
|
|
296
|
-
.eq('is_active', true) as { data: Organisation[] | null; error: any };
|
|
297
|
-
|
|
298
|
-
if (orgsError) throw orgsError;
|
|
299
|
-
|
|
300
|
-
setOrganisations(orgs || []);
|
|
301
|
-
|
|
302
|
-
// Auto-select first organisation if none selected
|
|
303
|
-
if (!selectedOrganisation && orgs && orgs.length > 0) {
|
|
304
|
-
const savedOrgId = persistState ? localStorage.getItem(`pace_${appName}_org`) : null;
|
|
305
|
-
const orgToSelect = savedOrgId
|
|
306
|
-
? orgs.find(o => o.id === savedOrgId) || orgs[0]
|
|
307
|
-
: orgs[0];
|
|
308
|
-
setSelectedOrganisation(orgToSelect);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.error('[AuthProvider] Failed to load organisations:', error);
|
|
313
|
-
setOrganisationError(error as Error);
|
|
314
|
-
} finally {
|
|
315
|
-
setOrganisationLoading(false);
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
loadOrganisations();
|
|
320
|
-
}, [user, supabaseClient, appName, persistState, selectedOrganisation]);
|
|
321
|
-
|
|
322
|
-
// ==========================================================================
|
|
323
|
-
// EVENT EFFECTS (replaces EventService initialization)
|
|
324
|
-
// ==========================================================================
|
|
325
|
-
|
|
326
|
-
useEffect(() => {
|
|
327
|
-
if (!user || !selectedOrganisation) {
|
|
328
|
-
setEvents([]);
|
|
329
|
-
setSelectedEvent(null);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Load user's events for the selected organisation
|
|
334
|
-
const loadEvents = async () => {
|
|
335
|
-
setEventLoading(true);
|
|
336
|
-
setEventError(null);
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
// Use RPC to get user's events
|
|
340
|
-
const { data, error } = await supabaseClient
|
|
341
|
-
.rpc('data_user_events_get', {
|
|
342
|
-
p_user_id: user.id,
|
|
343
|
-
p_app_name: appName
|
|
344
|
-
}) as { data: Event[] | null; error: any };
|
|
345
|
-
|
|
346
|
-
if (error) throw error;
|
|
347
|
-
|
|
348
|
-
// Filter events for current organisation
|
|
349
|
-
const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
|
|
350
|
-
setEvents(orgEvents);
|
|
351
|
-
|
|
352
|
-
// Auto-select next event in the future by date if none selected
|
|
353
|
-
if (!selectedEvent && orgEvents.length > 0) {
|
|
354
|
-
const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
|
|
355
|
-
let eventToSelect: Event | null = null;
|
|
356
|
-
|
|
357
|
-
if (savedEventId) {
|
|
358
|
-
// Try to restore persisted event
|
|
359
|
-
eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// If no persisted event, select the next event in the future by date
|
|
363
|
-
if (!eventToSelect) {
|
|
364
|
-
const now = new Date();
|
|
365
|
-
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
366
|
-
const futureEvents = orgEvents
|
|
367
|
-
.filter((e): e is Event & { event_date: string } => {
|
|
368
|
-
if (!e.event_date) return false;
|
|
369
|
-
const eventDate = new Date(e.event_date);
|
|
370
|
-
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
371
|
-
return startOfEventDate >= startOfToday;
|
|
372
|
-
})
|
|
373
|
-
.sort((a, b) => {
|
|
374
|
-
const dateA = new Date(a.event_date);
|
|
375
|
-
const dateB = new Date(b.event_date);
|
|
376
|
-
return dateA.getTime() - dateB.getTime();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Fallback to most recent past event if no future events found
|
|
383
|
-
if (!eventToSelect && orgEvents.length > 0) {
|
|
384
|
-
const now = new Date();
|
|
385
|
-
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
386
|
-
const pastEvents = orgEvents
|
|
387
|
-
.filter((e): e is Event & { event_date: string } => {
|
|
388
|
-
if (!e.event_date) return false;
|
|
389
|
-
const eventDate = new Date(e.event_date);
|
|
390
|
-
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
391
|
-
return startOfEventDate < startOfToday;
|
|
392
|
-
})
|
|
393
|
-
.sort((a, b) => {
|
|
394
|
-
const dateA = new Date(a.event_date);
|
|
395
|
-
const dateB = new Date(b.event_date);
|
|
396
|
-
return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (eventToSelect) {
|
|
403
|
-
setSelectedEvent(eventToSelect);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
console.error('[AuthProvider] Failed to load events:', error);
|
|
408
|
-
setEventError(error as Error);
|
|
409
|
-
} finally {
|
|
410
|
-
setEventLoading(false);
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
loadEvents();
|
|
415
|
-
}, [user, selectedOrganisation, supabaseClient, appName, persistState, selectedEvent]);
|
|
416
|
-
|
|
417
|
-
// ==========================================================================
|
|
418
|
-
// INACTIVITY EFFECTS (replaces InactivityService)
|
|
419
|
-
// ==========================================================================
|
|
420
|
-
|
|
421
|
-
const resetActivity = useCallback(() => {
|
|
422
|
-
lastActivityRef.current = Date.now();
|
|
423
|
-
setInactivityTimeRemaining(idleTimeoutMs);
|
|
424
|
-
setShowInactivityWarning(false);
|
|
425
|
-
setIsIdle(false);
|
|
426
|
-
}, [idleTimeoutMs]);
|
|
427
|
-
|
|
428
|
-
const startTracking = useCallback(() => {
|
|
429
|
-
if (isTracking) return;
|
|
430
|
-
|
|
431
|
-
setIsTracking(true);
|
|
432
|
-
resetActivity();
|
|
433
|
-
|
|
434
|
-
// Activity event listeners
|
|
435
|
-
const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
|
436
|
-
const handleActivity = () => resetActivity();
|
|
437
|
-
|
|
438
|
-
activityEvents.forEach(event => {
|
|
439
|
-
window.addEventListener(event, handleActivity, { passive: true });
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Idle check interval
|
|
443
|
-
idleTimerRef.current = setInterval(() => {
|
|
444
|
-
const timeSinceActivity = Date.now() - lastActivityRef.current;
|
|
445
|
-
const remaining = idleTimeoutMs - timeSinceActivity;
|
|
446
|
-
|
|
447
|
-
setInactivityTimeRemaining(Math.max(0, remaining));
|
|
448
|
-
|
|
449
|
-
// Show warning
|
|
450
|
-
if (remaining <= warnBeforeMs && remaining > 0 && !showInactivityWarning) {
|
|
451
|
-
setShowInactivityWarning(true);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Auto logout
|
|
455
|
-
if (remaining <= 0) {
|
|
456
|
-
setIsIdle(true);
|
|
457
|
-
onIdleLogout('inactivity');
|
|
458
|
-
}
|
|
459
|
-
}, 1000);
|
|
460
|
-
|
|
461
|
-
// Cleanup
|
|
462
|
-
return () => {
|
|
463
|
-
activityEvents.forEach(event => {
|
|
464
|
-
window.removeEventListener(event, handleActivity);
|
|
465
|
-
});
|
|
466
|
-
if (idleTimerRef.current) {
|
|
467
|
-
clearInterval(idleTimerRef.current);
|
|
468
|
-
}
|
|
469
|
-
if (warningTimerRef.current) {
|
|
470
|
-
clearTimeout(warningTimerRef.current);
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
}, [isTracking, idleTimeoutMs, warnBeforeMs, showInactivityWarning, onIdleLogout, resetActivity]);
|
|
474
|
-
|
|
475
|
-
const stopTracking = useCallback(() => {
|
|
476
|
-
setIsTracking(false);
|
|
477
|
-
if (idleTimerRef.current) {
|
|
478
|
-
clearInterval(idleTimerRef.current);
|
|
479
|
-
idleTimerRef.current = null;
|
|
480
|
-
}
|
|
481
|
-
if (warningTimerRef.current) {
|
|
482
|
-
clearTimeout(warningTimerRef.current);
|
|
483
|
-
warningTimerRef.current = null;
|
|
484
|
-
}
|
|
485
|
-
}, []);
|
|
486
|
-
|
|
487
|
-
// Generate and track device fingerprint
|
|
488
|
-
useEffect(() => {
|
|
489
|
-
if (user) {
|
|
490
|
-
// Generate device fingerprint (simple version - in production, use more sophisticated approach)
|
|
491
|
-
if (!deviceFingerprintRef.current) {
|
|
492
|
-
const fingerprint = btoa(JSON.stringify({
|
|
493
|
-
userAgent: navigator.userAgent,
|
|
494
|
-
language: navigator.language,
|
|
495
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
496
|
-
screen: `${window.screen.width}x${window.screen.height}`,
|
|
497
|
-
timestamp: Date.now()
|
|
498
|
-
})).substring(0, 64);
|
|
499
|
-
deviceFingerprintRef.current = fingerprint;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Track session with device fingerprint
|
|
503
|
-
const trackSession = async () => {
|
|
504
|
-
if (!deviceFingerprintRef.current || !user) return;
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
const { data, error } = await supabaseClient.rpc('rbac_session_track', {
|
|
508
|
-
p_user_id: user.id,
|
|
509
|
-
p_session_type: 'login',
|
|
510
|
-
p_app_id: null, // Will be resolved by the RPC function
|
|
511
|
-
p_ip_address: null, // Will be handled by backend
|
|
512
|
-
p_user_agent: navigator.userAgent,
|
|
513
|
-
p_device_fingerprint: deviceFingerprintRef.current
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
if (!error && data) {
|
|
517
|
-
sessionIdRef.current = data;
|
|
518
|
-
|
|
519
|
-
// Check for concurrent sessions
|
|
520
|
-
const { data: concurrentData } = await supabaseClient.rpc('rbac_detect_concurrent_sessions', {
|
|
521
|
-
p_user_id: user.id,
|
|
522
|
-
p_device_fingerprint: deviceFingerprintRef.current,
|
|
523
|
-
p_timeout_seconds: 300
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
if (concurrentData && concurrentData.has_concurrent) {
|
|
527
|
-
console.warn(
|
|
528
|
-
'[AuthProvider] Concurrent session detected:',
|
|
529
|
-
concurrentData.concurrent_sessions_count,
|
|
530
|
-
'other session(s) active'
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
} catch (err) {
|
|
535
|
-
console.error('[AuthProvider] Session tracking failed:', err);
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
trackSession();
|
|
540
|
-
}
|
|
541
|
-
}, [user, supabaseClient]);
|
|
542
|
-
|
|
543
|
-
// Auto-start tracking when user is authenticated
|
|
544
|
-
useEffect(() => {
|
|
545
|
-
if (user && !authLoading) {
|
|
546
|
-
startTracking();
|
|
547
|
-
} else {
|
|
548
|
-
stopTracking();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return () => {
|
|
552
|
-
stopTracking();
|
|
553
|
-
};
|
|
554
|
-
}, [user, authLoading, startTracking, stopTracking]);
|
|
555
|
-
|
|
556
|
-
// ==========================================================================
|
|
557
|
-
// AUTH METHODS
|
|
558
|
-
// ==========================================================================
|
|
559
|
-
|
|
560
|
-
const signIn = useCallback(async (email: string, password?: string) => {
|
|
561
|
-
setAuthLoading(true);
|
|
562
|
-
setAuthError(null);
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
let result;
|
|
566
|
-
if (password) {
|
|
567
|
-
// Email + password sign in
|
|
568
|
-
result = await supabaseClient.auth.signInWithPassword({ email, password });
|
|
569
|
-
} else {
|
|
570
|
-
// Magic link sign in
|
|
571
|
-
result = await supabaseClient.auth.signInWithOtp({ email });
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (result.error) {
|
|
575
|
-
setAuthError(result.error);
|
|
576
|
-
return { error: result.error };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return { error: null };
|
|
580
|
-
} catch (error) {
|
|
581
|
-
const authError = error as AuthError;
|
|
582
|
-
setAuthError(authError);
|
|
583
|
-
return { error: authError };
|
|
584
|
-
} finally {
|
|
585
|
-
setAuthLoading(false);
|
|
586
|
-
}
|
|
587
|
-
}, [supabaseClient]);
|
|
588
|
-
|
|
589
|
-
const signUp = useCallback(async (email: string, password: string) => {
|
|
590
|
-
setAuthLoading(true);
|
|
591
|
-
setAuthError(null);
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
const { error } = await supabaseClient.auth.signUp({ email, password });
|
|
595
|
-
|
|
596
|
-
if (error) {
|
|
597
|
-
setAuthError(error);
|
|
598
|
-
return { error };
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return { error: null };
|
|
602
|
-
} catch (error) {
|
|
603
|
-
const authError = error as AuthError;
|
|
604
|
-
setAuthError(authError);
|
|
605
|
-
return { error: authError };
|
|
606
|
-
} finally {
|
|
607
|
-
setAuthLoading(false);
|
|
608
|
-
}
|
|
609
|
-
}, [supabaseClient]);
|
|
610
|
-
|
|
611
|
-
const signOut = useCallback(async () => {
|
|
612
|
-
setAuthLoading(true);
|
|
613
|
-
setAuthError(null);
|
|
614
|
-
|
|
615
|
-
try {
|
|
616
|
-
const { error } = await supabaseClient.auth.signOut();
|
|
617
|
-
|
|
618
|
-
if (error) {
|
|
619
|
-
setAuthError(error);
|
|
620
|
-
return { error };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Clear local state
|
|
624
|
-
setUser(null);
|
|
625
|
-
setSession(null);
|
|
626
|
-
setSelectedOrganisation(null);
|
|
627
|
-
setOrganisations([]);
|
|
628
|
-
setSelectedEvent(null);
|
|
629
|
-
setEvents([]);
|
|
630
|
-
|
|
631
|
-
// Clear persisted state
|
|
632
|
-
if (persistState) {
|
|
633
|
-
localStorage.removeItem(`pace_${appName}_org`);
|
|
634
|
-
localStorage.removeItem(`pace_${appName}_event`);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return { error: null };
|
|
638
|
-
} catch (error) {
|
|
639
|
-
const authError = error as AuthError;
|
|
640
|
-
setAuthError(authError);
|
|
641
|
-
return { error: authError };
|
|
642
|
-
} finally {
|
|
643
|
-
setAuthLoading(false);
|
|
644
|
-
}
|
|
645
|
-
}, [supabaseClient, appName, persistState]);
|
|
646
|
-
|
|
647
|
-
const resetPassword = useCallback(async (email: string) => {
|
|
648
|
-
setAuthError(null);
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
const { error } = await supabaseClient.auth.resetPasswordForEmail(email);
|
|
652
|
-
|
|
653
|
-
if (error) {
|
|
654
|
-
setAuthError(error);
|
|
655
|
-
return { error };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return { error: null };
|
|
659
|
-
} catch (error) {
|
|
660
|
-
const authError = error as AuthError;
|
|
661
|
-
setAuthError(authError);
|
|
662
|
-
return { error: authError };
|
|
663
|
-
}
|
|
664
|
-
}, [supabaseClient]);
|
|
665
|
-
|
|
666
|
-
const updatePassword = useCallback(async (password: string) => {
|
|
667
|
-
setAuthError(null);
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
const { error } = await supabaseClient.auth.updateUser({ password });
|
|
671
|
-
|
|
672
|
-
if (error) {
|
|
673
|
-
setAuthError(error);
|
|
674
|
-
return { error };
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return { error: null };
|
|
678
|
-
} catch (error) {
|
|
679
|
-
const authError = error as AuthError;
|
|
680
|
-
setAuthError(authError);
|
|
681
|
-
return { error: authError };
|
|
682
|
-
}
|
|
683
|
-
}, [supabaseClient]);
|
|
684
|
-
|
|
685
|
-
const refreshSession = useCallback(async () => {
|
|
686
|
-
setAuthError(null);
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const { data, error } = await supabaseClient.auth.refreshSession();
|
|
690
|
-
|
|
691
|
-
if (error) {
|
|
692
|
-
setAuthError(error);
|
|
693
|
-
return { error };
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (data.session) {
|
|
697
|
-
setSession(data.session);
|
|
698
|
-
setUser(data.session.user);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return { error: null };
|
|
702
|
-
} catch (error) {
|
|
703
|
-
const authError = error as AuthError;
|
|
704
|
-
setAuthError(authError);
|
|
705
|
-
return { error: authError };
|
|
706
|
-
}
|
|
707
|
-
}, [supabaseClient]);
|
|
708
|
-
|
|
709
|
-
// ==========================================================================
|
|
710
|
-
// ORGANISATION METHODS
|
|
711
|
-
// ==========================================================================
|
|
712
|
-
|
|
713
|
-
const switchOrganisation = useCallback(async (orgId: UUID) => {
|
|
714
|
-
const org = organisations.find(o => o.id === orgId);
|
|
715
|
-
if (!org) {
|
|
716
|
-
throw new Error(`Organisation ${orgId} not found`);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
setSelectedOrganisation(org);
|
|
720
|
-
|
|
721
|
-
if (persistState) {
|
|
722
|
-
localStorage.setItem(`pace_${appName}_org`, orgId);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Clear event selection when switching orgs
|
|
726
|
-
setSelectedEvent(null);
|
|
727
|
-
if (persistState) {
|
|
728
|
-
localStorage.removeItem(`pace_${appName}_event`);
|
|
729
|
-
}
|
|
730
|
-
}, [organisations, appName, persistState]);
|
|
731
|
-
|
|
732
|
-
const refreshOrganisations = useCallback(async () => {
|
|
733
|
-
if (!user) return;
|
|
734
|
-
|
|
735
|
-
setOrganisationLoading(true);
|
|
736
|
-
setOrganisationError(null);
|
|
737
|
-
|
|
738
|
-
try {
|
|
739
|
-
const { data, error } = await supabaseClient
|
|
740
|
-
.rpc('rbac_user_organisation_roles_get', {
|
|
741
|
-
p_user_id: user.id
|
|
742
|
-
}) as { data: Array<{ organisation_id: UUID }> | null; error: any };
|
|
743
|
-
|
|
744
|
-
if (error) throw error;
|
|
745
|
-
|
|
746
|
-
if (data && data.length > 0) {
|
|
747
|
-
const orgIds = data.map(r => r.organisation_id);
|
|
748
|
-
const { data: orgs, error: orgsError } = await supabaseClient
|
|
749
|
-
.from('organisations')
|
|
750
|
-
.select('*')
|
|
751
|
-
.in('id', orgIds)
|
|
752
|
-
.eq('is_active', true) as { data: Organisation[] | null; error: any };
|
|
753
|
-
|
|
754
|
-
if (orgsError) throw orgsError;
|
|
755
|
-
|
|
756
|
-
setOrganisations(orgs || []);
|
|
757
|
-
}
|
|
758
|
-
} catch (error) {
|
|
759
|
-
console.error('[AuthProvider] Failed to refresh organisations:', error);
|
|
760
|
-
setOrganisationError(error as Error);
|
|
761
|
-
} finally {
|
|
762
|
-
setOrganisationLoading(false);
|
|
763
|
-
}
|
|
764
|
-
}, [user, supabaseClient]);
|
|
765
|
-
|
|
766
|
-
const getUserRole = useCallback((orgId?: UUID): string | null => {
|
|
767
|
-
// This would require fetching role data - placeholder for now
|
|
768
|
-
// In real implementation, we'd cache role data when loading organisations
|
|
769
|
-
return 'member';
|
|
770
|
-
}, []);
|
|
771
|
-
|
|
772
|
-
const getPrimaryOrganisation = useCallback((): Organisation | null => {
|
|
773
|
-
return organisations[0] || null;
|
|
774
|
-
}, [organisations]);
|
|
775
|
-
|
|
776
|
-
// ==========================================================================
|
|
777
|
-
// EVENT METHODS
|
|
778
|
-
// ==========================================================================
|
|
779
|
-
|
|
780
|
-
const handleSetSelectedEvent = useCallback((event: Event | null) => {
|
|
781
|
-
setSelectedEvent(event);
|
|
782
|
-
|
|
783
|
-
if (persistState) {
|
|
784
|
-
if (event) {
|
|
785
|
-
localStorage.setItem(`pace_${appName}_event`, event.event_id);
|
|
786
|
-
} else {
|
|
787
|
-
localStorage.removeItem(`pace_${appName}_event`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}, [appName, persistState]);
|
|
791
|
-
|
|
792
|
-
const refreshEvents = useCallback(async () => {
|
|
793
|
-
if (!user || !selectedOrganisation) return;
|
|
794
|
-
|
|
795
|
-
setEventLoading(true);
|
|
796
|
-
setEventError(null);
|
|
797
|
-
|
|
798
|
-
try {
|
|
799
|
-
const { data, error } = await supabaseClient
|
|
800
|
-
.rpc('data_user_events_get', {
|
|
801
|
-
p_user_id: user.id,
|
|
802
|
-
p_app_name: appName
|
|
803
|
-
}) as { data: Event[] | null; error: any };
|
|
804
|
-
|
|
805
|
-
if (error) throw error;
|
|
806
|
-
|
|
807
|
-
const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
|
|
808
|
-
setEvents(orgEvents);
|
|
809
|
-
|
|
810
|
-
// Auto-select next event in the future by date if none selected
|
|
811
|
-
if (!selectedEvent && orgEvents.length > 0) {
|
|
812
|
-
const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
|
|
813
|
-
let eventToSelect: Event | null = null;
|
|
814
|
-
|
|
815
|
-
if (savedEventId) {
|
|
816
|
-
// Try to restore persisted event
|
|
817
|
-
eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// If no persisted event, select the next event in the future by date
|
|
821
|
-
if (!eventToSelect) {
|
|
822
|
-
const now = new Date();
|
|
823
|
-
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
824
|
-
const futureEvents = orgEvents
|
|
825
|
-
.filter((e): e is Event & { event_date: string } => {
|
|
826
|
-
if (!e.event_date) return false;
|
|
827
|
-
const eventDate = new Date(e.event_date);
|
|
828
|
-
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
829
|
-
return startOfEventDate >= startOfToday;
|
|
830
|
-
})
|
|
831
|
-
.sort((a, b) => {
|
|
832
|
-
const dateA = new Date(a.event_date);
|
|
833
|
-
const dateB = new Date(b.event_date);
|
|
834
|
-
return dateA.getTime() - dateB.getTime();
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Fallback to most recent past event if no future events found
|
|
841
|
-
if (!eventToSelect && orgEvents.length > 0) {
|
|
842
|
-
const now = new Date();
|
|
843
|
-
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
844
|
-
const pastEvents = orgEvents
|
|
845
|
-
.filter((e): e is Event & { event_date: string } => {
|
|
846
|
-
if (!e.event_date) return false;
|
|
847
|
-
const eventDate = new Date(e.event_date);
|
|
848
|
-
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
849
|
-
return startOfEventDate < startOfToday;
|
|
850
|
-
})
|
|
851
|
-
.sort((a, b) => {
|
|
852
|
-
const dateA = new Date(a.event_date);
|
|
853
|
-
const dateB = new Date(b.event_date);
|
|
854
|
-
return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (eventToSelect) {
|
|
861
|
-
setSelectedEvent(eventToSelect);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
} catch (error) {
|
|
865
|
-
console.error('[AuthProvider] Failed to refresh events:', error);
|
|
866
|
-
setEventError(error as Error);
|
|
867
|
-
} finally {
|
|
868
|
-
setEventLoading(false);
|
|
869
|
-
}
|
|
870
|
-
}, [user, selectedOrganisation, supabaseClient, appName, selectedEvent, persistState]);
|
|
871
|
-
|
|
872
|
-
// ==========================================================================
|
|
873
|
-
// COMPUTED VALUES
|
|
874
|
-
// ==========================================================================
|
|
875
|
-
|
|
876
|
-
const isAuthenticated = !!(user && session);
|
|
877
|
-
const hasValidOrganisationContext = !!(selectedOrganisation && !organisationLoading);
|
|
878
|
-
const isContextReady = isAuthenticated && (!requireOrganisationContext || hasValidOrganisationContext);
|
|
879
|
-
|
|
880
|
-
// ==========================================================================
|
|
881
|
-
// CONTEXT VALUE
|
|
882
|
-
// ==========================================================================
|
|
883
|
-
|
|
884
|
-
const contextValue = useMemo<AuthContextType>(() => ({
|
|
885
|
-
// Auth
|
|
886
|
-
user,
|
|
887
|
-
session,
|
|
888
|
-
isAuthenticated,
|
|
889
|
-
authLoading,
|
|
890
|
-
authError,
|
|
891
|
-
supabase: supabaseClient,
|
|
892
|
-
signIn,
|
|
893
|
-
signUp,
|
|
894
|
-
signOut,
|
|
895
|
-
resetPassword,
|
|
896
|
-
updatePassword,
|
|
897
|
-
refreshSession,
|
|
898
|
-
|
|
899
|
-
// Organisation
|
|
900
|
-
selectedOrganisation,
|
|
901
|
-
organisations,
|
|
902
|
-
organisationLoading,
|
|
903
|
-
organisationError,
|
|
904
|
-
hasValidOrganisationContext,
|
|
905
|
-
isContextReady: hasValidOrganisationContext,
|
|
906
|
-
switchOrganisation,
|
|
907
|
-
refreshOrganisations,
|
|
908
|
-
getUserRole,
|
|
909
|
-
getPrimaryOrganisation,
|
|
910
|
-
|
|
911
|
-
// Event
|
|
912
|
-
events,
|
|
913
|
-
selectedEvent,
|
|
914
|
-
eventLoading,
|
|
915
|
-
eventError,
|
|
916
|
-
setSelectedEvent: handleSetSelectedEvent,
|
|
917
|
-
refreshEvents,
|
|
918
|
-
|
|
919
|
-
// Inactivity
|
|
920
|
-
showInactivityWarning,
|
|
921
|
-
inactivityTimeRemaining,
|
|
922
|
-
isIdle,
|
|
923
|
-
isTracking,
|
|
924
|
-
resetActivity,
|
|
925
|
-
startTracking,
|
|
926
|
-
stopTracking,
|
|
927
|
-
|
|
928
|
-
// Session Management
|
|
929
|
-
sessionId: sessionIdRef.current,
|
|
930
|
-
deviceFingerprint: deviceFingerprintRef.current,
|
|
931
|
-
hasConcurrentSessions: false, // TODO: implement state tracking
|
|
932
|
-
}), [
|
|
933
|
-
user, session, isAuthenticated, authLoading, authError,
|
|
934
|
-
selectedOrganisation, organisations, organisationLoading, organisationError,
|
|
935
|
-
hasValidOrganisationContext, isContextReady,
|
|
936
|
-
events, selectedEvent, eventLoading, eventError,
|
|
937
|
-
showInactivityWarning, inactivityTimeRemaining, isIdle, isTracking,
|
|
938
|
-
signIn, signUp, signOut, resetPassword, updatePassword, refreshSession,
|
|
939
|
-
switchOrganisation, refreshOrganisations, getUserRole, getPrimaryOrganisation,
|
|
940
|
-
handleSetSelectedEvent, refreshEvents,
|
|
941
|
-
resetActivity, startTracking, stopTracking,
|
|
942
|
-
supabaseClient
|
|
943
|
-
]);
|
|
944
|
-
|
|
945
|
-
// ==========================================================================
|
|
946
|
-
// RENDER
|
|
947
|
-
// ==========================================================================
|
|
948
|
-
|
|
949
|
-
return (
|
|
950
|
-
<AuthContext.Provider value={contextValue}>
|
|
951
|
-
{children}
|
|
952
|
-
{showInactivityWarning && renderInactivityWarning && renderInactivityWarning({
|
|
953
|
-
timeRemaining: inactivityTimeRemaining,
|
|
954
|
-
onStaySignedIn: resetActivity,
|
|
955
|
-
onSignOutNow: signOut,
|
|
956
|
-
})}
|
|
957
|
-
</AuthContext.Provider>
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// ============================================================================
|
|
962
|
-
// HOOK
|
|
963
|
-
// ============================================================================
|
|
964
|
-
|
|
965
|
-
export function useAuth(): AuthContextType {
|
|
966
|
-
const context = useContext(AuthContext);
|
|
967
|
-
|
|
968
|
-
if (!context) {
|
|
969
|
-
throw new Error('useAuth must be used within AuthProvider');
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return context;
|
|
973
|
-
}
|
|
974
|
-
|