@jmruthers/pace-core 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-E7YQZD7D.js → DataTable-AOVNCPTX.js} +8 -8
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
- package/dist/{UnifiedAuthProvider-QPXO24B4.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
- package/dist/{api-6LVZTHDS.js → api-O6HTBX5Y.js} +3 -3
- package/dist/{chunk-I6DAQMWX.js → chunk-6COVEUS7.js} +130 -106
- package/dist/chunk-6COVEUS7.js.map +1 -0
- package/dist/{chunk-36LVWXB2.js → chunk-AFVQODI2.js} +37 -1
- package/dist/{chunk-36LVWXB2.js.map → chunk-AFVQODI2.js.map} +1 -1
- package/dist/{chunk-3LPHPB62.js → chunk-EFN2EIMK.js} +2 -2
- package/dist/{chunk-ATKZM7RX.js → chunk-G7QEZTYQ.js} +31 -31
- package/dist/{chunk-ATKZM7RX.js.map → chunk-G7QEZTYQ.js.map} +1 -1
- package/dist/{chunk-NN6WWZ5U.js → chunk-HU2C6SSC.js} +29 -18
- package/dist/chunk-HU2C6SSC.js.map +1 -0
- package/dist/{chunk-AVMLPIM7.js → chunk-IHB5DR3H.js} +102 -51
- package/dist/chunk-IHB5DR3H.js.map +1 -0
- package/dist/{chunk-7JPAB3T5.js → chunk-IVOFDYWT.js} +364 -208
- package/dist/chunk-IVOFDYWT.js.map +1 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
- package/dist/chunk-JGRYX5UX.js.map +1 -0
- package/dist/{chunk-OEWDTMG7.js → chunk-NTM7ZSB6.js} +4 -4
- package/dist/chunk-NTM7ZSB6.js.map +1 -0
- package/dist/{chunk-5EC5MEWX.js → chunk-RGAWHO7N.js} +4 -4
- package/dist/chunk-RGAWHO7N.js.map +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-UPPMRMYG.js} +3 -3
- package/dist/{chunk-YKRAFF5K.js.map → chunk-UPPMRMYG.js.map} +1 -1
- package/dist/components.d.ts +2 -3
- package/dist/components.js +24 -28
- package/dist/components.js.map +1 -1
- package/dist/{contextValidator-OOPCLPZW.js → contextValidator-5OGXSPKS.js} +2 -2
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +41 -139
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +27 -18
- package/dist/index.js +41 -50
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -3
- package/dist/rbac/index.d.ts +16 -9
- package/dist/rbac/index.js +6 -6
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +210 -100
- package/package.json +1 -2
- package/scripts/validate-master.js +1 -1
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.tsx +17 -19
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +248 -228
- package/src/components/Dialog/Dialog.tsx +455 -325
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
- package/src/components/FileDisplay/FileDisplay.tsx +5 -5
- package/src/components/Form/Form.test.tsx +3 -2
- package/src/components/Form/Form.tsx +4 -5
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +32 -39
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +36 -34
- package/src/components/index.ts +3 -4
- package/src/hooks/useEventTheme.ts +4 -4
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +4 -4
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/index.ts +11 -1
- package/src/rbac/README.md +20 -20
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
- package/src/rbac/hooks/useResourcePermissions.ts +57 -29
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +36 -0
- package/src/services/AuthService.ts +2 -5
- package/src/services/InactivityService.ts +139 -58
- package/src/styles/core.css +4 -0
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- /package/dist/{DataTable-E7YQZD7D.js.map → DataTable-AOVNCPTX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-QPXO24B4.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
- /package/dist/{api-6LVZTHDS.js.map → api-O6HTBX5Y.js.map} +0 -0
- /package/dist/{chunk-3LPHPB62.js.map → chunk-EFN2EIMK.js.map} +0 -0
- /package/dist/{contextValidator-OOPCLPZW.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
|
@@ -46,7 +46,7 @@ import { useOrganisations } from '../../hooks/useOrganisations';
|
|
|
46
46
|
import { useEvents } from '../../hooks/useEvents';
|
|
47
47
|
import { useResolvedScope } from './useResolvedScope';
|
|
48
48
|
import { useCan } from './usePermissions';
|
|
49
|
-
import type { Scope } from '../types';
|
|
49
|
+
import type { Scope, Permission } from '../types';
|
|
50
50
|
|
|
51
51
|
export interface UseResourcePermissionsOptions {
|
|
52
52
|
/** Whether to check read permissions (default: false) */
|
|
@@ -80,15 +80,22 @@ export interface ResourcePermissions {
|
|
|
80
80
|
* and provides a simple API for permission checking.
|
|
81
81
|
*
|
|
82
82
|
* **Page Permission Support:**
|
|
83
|
-
* When an `appId` is available in the resolved scope, the
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
83
|
+
* When an `appId` is available in the resolved scope, the hook automatically:
|
|
84
|
+
* 1. Waits for scope resolution to complete (including `appId` being set)
|
|
85
|
+
* 2. Constructs permission strings with the `page.` prefix (e.g., `create:page.planning`)
|
|
86
|
+
* 3. Passes the resource name as `pageId` to enable page-based permission checks
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* This ensures permission strings match the format returned by `rbac_permissions_get`
|
|
89
|
+
* (e.g., `create:page.planning`) rather than resource-based format (e.g., `create:planning`).
|
|
90
|
+
*
|
|
91
|
+
* **Scope Resolution Timing:**
|
|
92
|
+
* The hook waits for scope resolution to complete before constructing permission strings.
|
|
93
|
+
* This prevents timing issues where permission checks use the wrong format (e.g., `delete:planning`
|
|
94
|
+
* instead of `delete:page.planning`) when `appId` is not yet available in the scope.
|
|
95
|
+
*
|
|
96
|
+
* The RPC function `rbac_check_permission_simplified` will resolve the page name to a page ID
|
|
97
|
+
* and check page permissions if the resource matches a registered page in `rbac_app_pages`.
|
|
98
|
+
* If the resource is not a registered page, it will fall back to resource-based permission checking.
|
|
92
99
|
*
|
|
93
100
|
* @param resource - The resource name (e.g., 'contacts', 'risks', 'planning')
|
|
94
101
|
* Can be a resource name or a page name registered in rbac_app_pages
|
|
@@ -157,26 +164,42 @@ export function useResourcePermissions(
|
|
|
157
164
|
selectedEventId: selectedEvent?.event_id || null
|
|
158
165
|
});
|
|
159
166
|
|
|
160
|
-
//
|
|
167
|
+
// CRITICAL FIX: Only use resolvedScope when it's available (not during loading)
|
|
168
|
+
// This ensures we wait for appId to be resolved before constructing permission strings
|
|
169
|
+
// If resolvedScope is null (still loading), we can't determine if we should use page permissions
|
|
170
|
+
// so we must wait for scope resolution to complete
|
|
161
171
|
const scope: Scope = resolvedScope || {
|
|
162
172
|
organisationId: selectedOrganisation?.id || '',
|
|
163
173
|
eventId: selectedEvent?.event_id || undefined,
|
|
164
174
|
appId: undefined
|
|
165
175
|
};
|
|
166
176
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// This
|
|
171
|
-
const
|
|
177
|
+
// CRITICAL FIX: Only use page permissions when appId is actually available in resolvedScope
|
|
178
|
+
// If scope is still loading (resolvedScope is null), we can't know if appId will be available
|
|
179
|
+
// so we must wait for scope resolution before constructing page permission strings
|
|
180
|
+
// This prevents using wrong permission format (delete:planning instead of delete:page.planning)
|
|
181
|
+
const hasAppId = !!resolvedScope?.appId;
|
|
182
|
+
const pageId = hasAppId ? resource : undefined;
|
|
183
|
+
|
|
184
|
+
// When appId is available in resolved scope, construct permission strings with page. prefix
|
|
185
|
+
// This matches the format that rbac_permissions_get returns (e.g., 'create:page.planning')
|
|
186
|
+
// and ensures consistent permission checking for page-based resources
|
|
187
|
+
// IMPORTANT: Only use page format when appId is actually resolved, not during loading
|
|
188
|
+
const isPagePermission = hasAppId && !!pageId;
|
|
189
|
+
const createPermission = isPagePermission ? `create:page.${resource}` : `create:${resource}`;
|
|
190
|
+
const updatePermission = isPagePermission ? `update:page.${resource}` : `update:${resource}`;
|
|
191
|
+
const deletePermission = isPagePermission ? `delete:page.${resource}` : `delete:${resource}`;
|
|
192
|
+
const readPermission = isPagePermission ? `read:page.${resource}` : `read:${resource}`;
|
|
172
193
|
|
|
173
194
|
// Permission checks for create, update, delete
|
|
174
195
|
// Pass null for super admin status (not checked yet - hook will check if needed)
|
|
175
196
|
// PERFORMANCE: These hooks will each check super admin separately - could be optimized in future
|
|
197
|
+
// CRITICAL: useCan will wait for appId when pageId is provided (it checks needsAppIdForPageName)
|
|
198
|
+
// But we must ensure permission strings are correct before calling useCan
|
|
176
199
|
const { can: canCreateResult, isLoading: createLoading, error: createError } = useCan(
|
|
177
200
|
user?.id || '',
|
|
178
201
|
scope,
|
|
179
|
-
|
|
202
|
+
createPermission as Permission,
|
|
180
203
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
181
204
|
true, // useCache
|
|
182
205
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -186,7 +209,7 @@ export function useResourcePermissions(
|
|
|
186
209
|
const { can: canUpdateResult, isLoading: updateLoading, error: updateError } = useCan(
|
|
187
210
|
user?.id || '',
|
|
188
211
|
scope,
|
|
189
|
-
|
|
212
|
+
updatePermission as Permission,
|
|
190
213
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
191
214
|
true, // useCache
|
|
192
215
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -196,7 +219,7 @@ export function useResourcePermissions(
|
|
|
196
219
|
const { can: canDeleteResult, isLoading: deleteLoading, error: deleteError } = useCan(
|
|
197
220
|
user?.id || '',
|
|
198
221
|
scope,
|
|
199
|
-
|
|
222
|
+
deletePermission as Permission,
|
|
200
223
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
201
224
|
true, // useCache
|
|
202
225
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -207,7 +230,7 @@ export function useResourcePermissions(
|
|
|
207
230
|
const { can: canReadResult, isLoading: readLoading, error: readError } = useCan(
|
|
208
231
|
user?.id || '',
|
|
209
232
|
scope,
|
|
210
|
-
|
|
233
|
+
readPermission as Permission,
|
|
211
234
|
pageId, // Pass resource name as pageId when appId is available to enable page permission checks
|
|
212
235
|
true, // useCache
|
|
213
236
|
null, // precomputedSuperAdmin - not checked yet
|
|
@@ -215,9 +238,14 @@ export function useResourcePermissions(
|
|
|
215
238
|
);
|
|
216
239
|
|
|
217
240
|
// Aggregate loading states - any permission check or scope resolution loading
|
|
241
|
+
// CRITICAL: When requireScope is true, we must wait for scope resolution to complete
|
|
242
|
+
// so we can determine the correct permission format (page vs resource permissions)
|
|
243
|
+
// This prevents using wrong permission format (delete:planning instead of delete:page.planning)
|
|
218
244
|
const isLoading = useMemo(() => {
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
// If scope resolution is required, wait for it to complete
|
|
246
|
+
const waitingForScope = requireScope && scopeLoading;
|
|
247
|
+
return waitingForScope || createLoading || updateLoading || deleteLoading || (enableRead && readLoading);
|
|
248
|
+
}, [scopeLoading, requireScope, createLoading, updateLoading, deleteLoading, readLoading, enableRead]);
|
|
221
249
|
|
|
222
250
|
// Aggregate errors - prefer scope error, then any permission error
|
|
223
251
|
const error = useMemo(() => {
|
|
@@ -239,19 +267,19 @@ export function useResourcePermissions(
|
|
|
239
267
|
if (res !== resource) {
|
|
240
268
|
return false;
|
|
241
269
|
}
|
|
242
|
-
return canCreateResult;
|
|
270
|
+
return canCreateResult; // canCreateResult is already the boolean 'can' value from useCan
|
|
243
271
|
},
|
|
244
272
|
canUpdate: (res: string) => {
|
|
245
273
|
if (res !== resource) {
|
|
246
274
|
return false;
|
|
247
275
|
}
|
|
248
|
-
return canUpdateResult;
|
|
276
|
+
return canUpdateResult; // canUpdateResult is already the boolean 'can' value from useCan
|
|
249
277
|
},
|
|
250
278
|
canDelete: (res: string) => {
|
|
251
279
|
if (res !== resource) {
|
|
252
280
|
return false;
|
|
253
281
|
}
|
|
254
|
-
return canDeleteResult;
|
|
282
|
+
return canDeleteResult; // canDeleteResult is already the boolean 'can' value from useCan
|
|
255
283
|
},
|
|
256
284
|
canRead: (res: string) => {
|
|
257
285
|
if (!enableRead) {
|
|
@@ -260,17 +288,17 @@ export function useResourcePermissions(
|
|
|
260
288
|
if (res !== resource) {
|
|
261
289
|
return false;
|
|
262
290
|
}
|
|
263
|
-
return canReadResult;
|
|
291
|
+
return canReadResult; // canReadResult is already the boolean 'can' value from useCan
|
|
264
292
|
},
|
|
265
293
|
scope,
|
|
266
294
|
isLoading,
|
|
267
295
|
error
|
|
268
296
|
}), [
|
|
269
297
|
resource,
|
|
270
|
-
canCreateResult,
|
|
271
|
-
canUpdateResult,
|
|
272
|
-
canDeleteResult,
|
|
273
|
-
canReadResult,
|
|
298
|
+
canCreateResult, // This is already the boolean 'can' value
|
|
299
|
+
canUpdateResult, // This is already the boolean 'can' value
|
|
300
|
+
canDeleteResult, // This is already the boolean 'can' value
|
|
301
|
+
canReadResult, // This is already the boolean 'can' value
|
|
274
302
|
enableRead,
|
|
275
303
|
scope,
|
|
276
304
|
isLoading,
|
package/src/rbac/permissions.ts
CHANGED
|
@@ -106,35 +106,35 @@ export const EVENT_APP_PERMISSIONS = {
|
|
|
106
106
|
// ============================================================================
|
|
107
107
|
|
|
108
108
|
export const PAGE_PERMISSIONS = {
|
|
109
|
-
// General page access
|
|
109
|
+
// General page access (generic - used for wildcard checks)
|
|
110
110
|
READ_PAGE: 'read:page' as Permission,
|
|
111
111
|
CREATE_PAGE: 'create:page' as Permission,
|
|
112
112
|
UPDATE_PAGE: 'update:page' as Permission,
|
|
113
113
|
DELETE_PAGE: 'delete:page' as Permission,
|
|
114
114
|
|
|
115
115
|
// Admin pages
|
|
116
|
-
READ_ADMIN: 'read:admin' as Permission,
|
|
117
|
-
CREATE_ADMIN: 'create:admin' as Permission,
|
|
118
|
-
UPDATE_ADMIN: 'update:admin' as Permission,
|
|
119
|
-
DELETE_ADMIN: 'delete:admin' as Permission,
|
|
116
|
+
READ_ADMIN: 'read:page.admin' as Permission,
|
|
117
|
+
CREATE_ADMIN: 'create:page.admin' as Permission,
|
|
118
|
+
UPDATE_ADMIN: 'update:page.admin' as Permission,
|
|
119
|
+
DELETE_ADMIN: 'delete:page.admin' as Permission,
|
|
120
120
|
|
|
121
121
|
// Dashboard pages
|
|
122
|
-
READ_DASHBOARD: 'read:dashboard' as Permission,
|
|
123
|
-
CREATE_DASHBOARD: 'create:dashboard' as Permission,
|
|
124
|
-
UPDATE_DASHBOARD: 'update:dashboard' as Permission,
|
|
125
|
-
DELETE_DASHBOARD: 'delete:dashboard' as Permission,
|
|
122
|
+
READ_DASHBOARD: 'read:page.dashboard' as Permission,
|
|
123
|
+
CREATE_DASHBOARD: 'create:page.dashboard' as Permission,
|
|
124
|
+
UPDATE_DASHBOARD: 'update:page.dashboard' as Permission,
|
|
125
|
+
DELETE_DASHBOARD: 'delete:page.dashboard' as Permission,
|
|
126
126
|
|
|
127
127
|
// Settings pages
|
|
128
|
-
READ_SETTINGS: 'read:settings' as Permission,
|
|
129
|
-
CREATE_SETTINGS: 'create:settings' as Permission,
|
|
130
|
-
UPDATE_SETTINGS: 'update:settings' as Permission,
|
|
131
|
-
DELETE_SETTINGS: 'delete:settings' as Permission,
|
|
128
|
+
READ_SETTINGS: 'read:page.settings' as Permission,
|
|
129
|
+
CREATE_SETTINGS: 'create:page.settings' as Permission,
|
|
130
|
+
UPDATE_SETTINGS: 'update:page.settings' as Permission,
|
|
131
|
+
DELETE_SETTINGS: 'delete:page.settings' as Permission,
|
|
132
132
|
|
|
133
133
|
// Reports pages
|
|
134
|
-
READ_REPORTS: 'read:reports' as Permission,
|
|
135
|
-
CREATE_REPORTS: 'create:reports' as Permission,
|
|
136
|
-
UPDATE_REPORTS: 'update:reports' as Permission,
|
|
137
|
-
DELETE_REPORTS: 'delete:reports' as Permission,
|
|
134
|
+
READ_REPORTS: 'read:page.reports' as Permission,
|
|
135
|
+
CREATE_REPORTS: 'create:page.reports' as Permission,
|
|
136
|
+
UPDATE_REPORTS: 'update:page.reports' as Permission,
|
|
137
|
+
DELETE_REPORTS: 'delete:page.reports' as Permission,
|
|
138
138
|
} as const;
|
|
139
139
|
|
|
140
140
|
// ============================================================================
|
|
@@ -89,7 +89,19 @@ export class ContextValidator {
|
|
|
89
89
|
if (effectiveScopeType === 'both') {
|
|
90
90
|
// For 'both' pages, we need at least one context (org or event)
|
|
91
91
|
// Both will be checked during permission evaluation
|
|
92
|
+
// For PORTAL/ADMIN apps, both contexts are optional
|
|
92
93
|
if (!scope.organisationId && !scope.eventId) {
|
|
94
|
+
if (allowsOptionalContexts(appName)) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: true,
|
|
97
|
+
resolvedScope: {
|
|
98
|
+
organisationId: undefined,
|
|
99
|
+
eventId: undefined,
|
|
100
|
+
appId: scope.appId
|
|
101
|
+
},
|
|
102
|
+
error: null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
93
105
|
return {
|
|
94
106
|
isValid: false,
|
|
95
107
|
resolvedScope: null,
|
|
@@ -123,6 +135,18 @@ export class ContextValidator {
|
|
|
123
135
|
// Handle 'event' scope - requires event context
|
|
124
136
|
if (effectiveScopeType === 'event') {
|
|
125
137
|
if (!scope.eventId) {
|
|
138
|
+
// For PORTAL/ADMIN apps, event context is optional
|
|
139
|
+
if (allowsOptionalContexts(appName)) {
|
|
140
|
+
return {
|
|
141
|
+
isValid: true,
|
|
142
|
+
resolvedScope: {
|
|
143
|
+
organisationId: scope.organisationId,
|
|
144
|
+
eventId: undefined,
|
|
145
|
+
appId: scope.appId
|
|
146
|
+
},
|
|
147
|
+
error: null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
126
150
|
return {
|
|
127
151
|
isValid: false,
|
|
128
152
|
resolvedScope: null,
|
|
@@ -167,6 +191,18 @@ export class ContextValidator {
|
|
|
167
191
|
// Handle 'organisation' scope - requires organisation context
|
|
168
192
|
if (effectiveScopeType === 'organisation') {
|
|
169
193
|
if (!scope.organisationId) {
|
|
194
|
+
// For PORTAL/ADMIN apps, organisation context is optional
|
|
195
|
+
if (allowsOptionalContexts(appName)) {
|
|
196
|
+
return {
|
|
197
|
+
isValid: true,
|
|
198
|
+
resolvedScope: {
|
|
199
|
+
organisationId: undefined,
|
|
200
|
+
eventId: scope.eventId,
|
|
201
|
+
appId: scope.appId
|
|
202
|
+
},
|
|
203
|
+
error: null
|
|
204
|
+
};
|
|
205
|
+
}
|
|
170
206
|
return {
|
|
171
207
|
isValid: false,
|
|
172
208
|
resolvedScope: null,
|
|
@@ -57,11 +57,8 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
57
57
|
|
|
58
58
|
// Auth state getters
|
|
59
59
|
getUser(): User | null {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else {
|
|
63
|
-
logger.debug('AuthService', `getUser() [ID:${this.instanceId}] returning null`);
|
|
64
|
-
}
|
|
60
|
+
// Removed debug logging - getUser() is called frequently and state changes
|
|
61
|
+
// are already logged in the auth state change handler
|
|
65
62
|
return this.user;
|
|
66
63
|
}
|
|
67
64
|
|
|
@@ -291,10 +291,10 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
291
291
|
private setupEventHandlers(): void {
|
|
292
292
|
if (typeof window === 'undefined') return;
|
|
293
293
|
|
|
294
|
-
let idleTimer: NodeJS.Timeout | null = null;
|
|
295
294
|
let warningTimer: NodeJS.Timeout | null = null;
|
|
295
|
+
let logoutTimer: NodeJS.Timeout | null = null;
|
|
296
|
+
let countdownInterval: NodeJS.Timeout | null = null;
|
|
296
297
|
let lastActivity = Date.now();
|
|
297
|
-
let pollInterval: NodeJS.Timeout | null = null;
|
|
298
298
|
|
|
299
299
|
// Store previous state for comparison
|
|
300
300
|
let prevIsIdle = false;
|
|
@@ -303,19 +303,29 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
303
303
|
let prevInactivityTimeRemaining = 0;
|
|
304
304
|
let prevTimeRemaining = this.idleTimeoutMs;
|
|
305
305
|
|
|
306
|
-
//
|
|
307
|
-
const
|
|
306
|
+
// Calculate time until warning and logout
|
|
307
|
+
const getTimeUntilWarning = () => {
|
|
308
|
+
const timeSinceActivity = Date.now() - lastActivity;
|
|
309
|
+
return Math.max(0, (this.idleTimeoutMs - this.warnBeforeMs) - timeSinceActivity);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const getTimeUntilLogout = () => {
|
|
313
|
+
const timeSinceActivity = Date.now() - lastActivity;
|
|
314
|
+
return Math.max(0, this.idleTimeoutMs - timeSinceActivity);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Update state and notify if changed
|
|
318
|
+
const updateState = (forceNotify = false) => {
|
|
308
319
|
const now = Date.now();
|
|
309
320
|
const timeSinceActivity = now - lastActivity;
|
|
310
321
|
const timeUntilIdle = this.idleTimeoutMs - timeSinceActivity;
|
|
311
|
-
const timeUntilWarning = (this.idleTimeoutMs - this.warnBeforeMs) - timeSinceActivity;
|
|
312
322
|
|
|
313
|
-
// Calculate new state values
|
|
314
|
-
const newIsIdle = timeSinceActivity >=
|
|
315
|
-
const newShowWarning =
|
|
323
|
+
// Calculate new state values
|
|
324
|
+
const newIsIdle = timeSinceActivity >= this.idleTimeoutMs;
|
|
325
|
+
const newShowWarning = timeSinceActivity >= (this.idleTimeoutMs - this.warnBeforeMs) && !newIsIdle;
|
|
316
326
|
const newShowInactivityWarning = newShowWarning;
|
|
317
327
|
const newInactivityTimeRemaining = newShowWarning
|
|
318
|
-
? Math.ceil(
|
|
328
|
+
? Math.ceil(timeUntilIdle / 1000)
|
|
319
329
|
: 0;
|
|
320
330
|
const newTimeRemaining = Math.max(0, timeUntilIdle);
|
|
321
331
|
|
|
@@ -324,11 +334,11 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
324
334
|
prevIsIdle !== newIsIdle ||
|
|
325
335
|
prevShowWarning !== newShowWarning ||
|
|
326
336
|
prevShowInactivityWarning !== newShowInactivityWarning ||
|
|
327
|
-
prevInactivityTimeRemaining !== newInactivityTimeRemaining ||
|
|
337
|
+
(newShowWarning && prevInactivityTimeRemaining !== newInactivityTimeRemaining) ||
|
|
328
338
|
prevTimeRemaining !== newTimeRemaining;
|
|
329
339
|
|
|
330
|
-
// Only update and notify if state changed
|
|
331
|
-
if (stateChanged) {
|
|
340
|
+
// Only update and notify if state changed or forced
|
|
341
|
+
if (stateChanged || forceNotify) {
|
|
332
342
|
this._isIdle = newIsIdle;
|
|
333
343
|
this._showWarning = newShowWarning;
|
|
334
344
|
this._showInactivityWarning = newShowInactivityWarning;
|
|
@@ -342,76 +352,147 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
342
352
|
prevInactivityTimeRemaining = newInactivityTimeRemaining;
|
|
343
353
|
prevTimeRemaining = newTimeRemaining;
|
|
344
354
|
|
|
345
|
-
|
|
355
|
+
if (stateChanged) {
|
|
356
|
+
this.notify();
|
|
357
|
+
}
|
|
346
358
|
|
|
347
359
|
// Handle idle logout if needed
|
|
348
|
-
if (newIsIdle
|
|
360
|
+
if (newIsIdle) {
|
|
349
361
|
this.handleIdleLogout();
|
|
350
362
|
}
|
|
351
363
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Schedule next state check based on current state
|
|
367
|
+
const scheduleNextCheck = () => {
|
|
368
|
+
// Clear existing timers
|
|
359
369
|
if (warningTimer) {
|
|
360
370
|
clearTimeout(warningTimer);
|
|
361
371
|
warningTimer = null;
|
|
362
372
|
}
|
|
363
373
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
pollInactivityState();
|
|
368
|
-
}, Math.min(10000, timeUntilIdle));
|
|
374
|
+
if (logoutTimer) {
|
|
375
|
+
clearTimeout(logoutTimer);
|
|
376
|
+
logoutTimer = null;
|
|
369
377
|
}
|
|
370
378
|
|
|
371
|
-
if (
|
|
372
|
-
|
|
379
|
+
if (countdownInterval) {
|
|
380
|
+
clearInterval(countdownInterval);
|
|
381
|
+
countdownInterval = null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const timeUntilWarning = getTimeUntilWarning();
|
|
385
|
+
const timeUntilLogout = getTimeUntilLogout();
|
|
386
|
+
const timeSinceActivity = Date.now() - lastActivity;
|
|
387
|
+
|
|
388
|
+
// If already idle, nothing to schedule
|
|
389
|
+
if (timeSinceActivity >= this.idleTimeoutMs) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// If warning should be shown, start countdown interval
|
|
394
|
+
if (timeSinceActivity >= (this.idleTimeoutMs - this.warnBeforeMs)) {
|
|
395
|
+
// Update state to show warning
|
|
396
|
+
updateState();
|
|
397
|
+
|
|
398
|
+
// Start countdown interval to update remaining time every second
|
|
399
|
+
countdownInterval = setInterval(() => {
|
|
400
|
+
updateState();
|
|
401
|
+
|
|
402
|
+
// Check if we've reached logout time
|
|
403
|
+
if (Date.now() - lastActivity >= this.idleTimeoutMs) {
|
|
404
|
+
if (countdownInterval) {
|
|
405
|
+
clearInterval(countdownInterval);
|
|
406
|
+
countdownInterval = null;
|
|
407
|
+
}
|
|
408
|
+
this.handleIdleLogout();
|
|
409
|
+
}
|
|
410
|
+
}, 1000); // Update every second during warning period
|
|
411
|
+
|
|
412
|
+
// Schedule logout
|
|
413
|
+
logoutTimer = setTimeout(() => {
|
|
414
|
+
if (countdownInterval) {
|
|
415
|
+
clearInterval(countdownInterval);
|
|
416
|
+
countdownInterval = null;
|
|
417
|
+
}
|
|
373
418
|
this.handleIdleLogout();
|
|
374
|
-
},
|
|
419
|
+
}, timeUntilLogout);
|
|
420
|
+
} else {
|
|
421
|
+
// User is still active - schedule warning timer
|
|
422
|
+
// Use adaptive interval: check more frequently as we approach warning time
|
|
423
|
+
// Check every 60 seconds when far from warning, every 10 seconds when close
|
|
424
|
+
const timeUntilWarningMs = timeUntilWarning;
|
|
425
|
+
const adaptiveInterval = timeUntilWarningMs > 5 * 60 * 1000
|
|
426
|
+
? 60 * 1000 // Check every 60 seconds when > 5 minutes away
|
|
427
|
+
: timeUntilWarningMs > 60 * 1000
|
|
428
|
+
? 10 * 1000 // Check every 10 seconds when < 5 minutes but > 1 minute away
|
|
429
|
+
: 1000; // Check every second when < 1 minute away
|
|
430
|
+
|
|
431
|
+
warningTimer = setTimeout(() => {
|
|
432
|
+
updateState();
|
|
433
|
+
scheduleNextCheck(); // Reschedule for next check
|
|
434
|
+
}, Math.min(adaptiveInterval, timeUntilWarningMs));
|
|
375
435
|
}
|
|
376
436
|
};
|
|
377
437
|
|
|
378
438
|
const resetTimers = () => {
|
|
379
|
-
//
|
|
439
|
+
// Update last activity time
|
|
380
440
|
lastActivity = Date.now();
|
|
381
441
|
|
|
382
|
-
// Clear timers
|
|
383
|
-
if (idleTimer) {
|
|
384
|
-
clearTimeout(idleTimer);
|
|
385
|
-
idleTimer = null;
|
|
386
|
-
}
|
|
387
|
-
|
|
442
|
+
// Clear all timers
|
|
388
443
|
if (warningTimer) {
|
|
389
444
|
clearTimeout(warningTimer);
|
|
390
445
|
warningTimer = null;
|
|
391
446
|
}
|
|
392
447
|
|
|
393
|
-
|
|
448
|
+
if (logoutTimer) {
|
|
449
|
+
clearTimeout(logoutTimer);
|
|
450
|
+
logoutTimer = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (countdownInterval) {
|
|
454
|
+
clearInterval(countdownInterval);
|
|
455
|
+
countdownInterval = null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Reset state values
|
|
459
|
+
const hadWarning = this._showWarning || this._showInactivityWarning;
|
|
394
460
|
this._showInactivityWarning = false;
|
|
395
461
|
this._inactivityTimeRemaining = 0;
|
|
396
462
|
this._isIdle = false;
|
|
397
463
|
this._showWarning = false;
|
|
464
|
+
this._timeRemaining = this.idleTimeoutMs;
|
|
398
465
|
|
|
399
|
-
// Update previous state
|
|
466
|
+
// Update previous state
|
|
400
467
|
prevIsIdle = false;
|
|
401
468
|
prevShowWarning = false;
|
|
402
469
|
prevShowInactivityWarning = false;
|
|
403
470
|
prevInactivityTimeRemaining = 0;
|
|
404
471
|
prevTimeRemaining = this.idleTimeoutMs;
|
|
405
472
|
|
|
406
|
-
//
|
|
473
|
+
// Notify if we were showing a warning (state changed)
|
|
474
|
+
if (hadWarning) {
|
|
475
|
+
this.notify();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Reschedule next check
|
|
479
|
+
scheduleNextCheck();
|
|
407
480
|
};
|
|
408
481
|
|
|
409
|
-
// Activity detection -
|
|
482
|
+
// Activity detection - throttled to avoid excessive calls
|
|
410
483
|
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
|
|
484
|
+
let activityThrottleTimer: NodeJS.Timeout | null = null;
|
|
411
485
|
|
|
412
486
|
const handleActivity = () => {
|
|
413
|
-
|
|
414
|
-
|
|
487
|
+
// Throttle activity detection to once per second max
|
|
488
|
+
if (activityThrottleTimer) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
activityThrottleTimer = setTimeout(() => {
|
|
493
|
+
activityThrottleTimer = null;
|
|
494
|
+
resetTimers();
|
|
495
|
+
}, 1000);
|
|
415
496
|
};
|
|
416
497
|
|
|
417
498
|
// Add event listeners
|
|
@@ -419,29 +500,30 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
419
500
|
document.addEventListener(event, handleActivity, true);
|
|
420
501
|
});
|
|
421
502
|
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}, 10000); // 10 seconds
|
|
426
|
-
|
|
427
|
-
// Initial poll
|
|
428
|
-
pollInactivityState();
|
|
503
|
+
// Initial state update and schedule first check
|
|
504
|
+
updateState(true); // Force initial notification
|
|
505
|
+
scheduleNextCheck();
|
|
429
506
|
|
|
430
507
|
// Store cleanup function
|
|
431
508
|
this.cleanupHandlers = () => {
|
|
432
|
-
if (idleTimer) {
|
|
433
|
-
clearTimeout(idleTimer);
|
|
434
|
-
idleTimer = null;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
509
|
if (warningTimer) {
|
|
438
510
|
clearTimeout(warningTimer);
|
|
439
511
|
warningTimer = null;
|
|
440
512
|
}
|
|
441
513
|
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
514
|
+
if (logoutTimer) {
|
|
515
|
+
clearTimeout(logoutTimer);
|
|
516
|
+
logoutTimer = null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (countdownInterval) {
|
|
520
|
+
clearInterval(countdownInterval);
|
|
521
|
+
countdownInterval = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (activityThrottleTimer) {
|
|
525
|
+
clearTimeout(activityThrottleTimer);
|
|
526
|
+
activityThrottleTimer = null;
|
|
445
527
|
}
|
|
446
528
|
|
|
447
529
|
activityEvents.forEach(event => {
|
|
@@ -457,6 +539,5 @@ export class InactivityService extends BaseService implements IInactivityService
|
|
|
457
539
|
};
|
|
458
540
|
|
|
459
541
|
this._isTracking = true;
|
|
460
|
-
this.notify(); // Initial notification only
|
|
461
542
|
}
|
|
462
543
|
}
|
package/src/styles/core.css
CHANGED
|
@@ -202,6 +202,10 @@
|
|
|
202
202
|
font-size: 0.875em;
|
|
203
203
|
color: var(--color-main-600);
|
|
204
204
|
}
|
|
205
|
+
|
|
206
|
+
dialog::backdrop {
|
|
207
|
+
background-color: oklch(from var(--color-main-700) l c h / 0.5);
|
|
208
|
+
}
|
|
205
209
|
|
|
206
210
|
.appGradient {
|
|
207
211
|
background: linear-gradient(145deg, var(--color-main-100) 10%, var(--color-main-200) 30%, var(--color-main-200) 70%, var(--color-main-300) 90%);
|
|
@@ -161,8 +161,9 @@ describe('formatTime Utility', () => {
|
|
|
161
161
|
}
|
|
162
162
|
const end = performance.now();
|
|
163
163
|
|
|
164
|
-
// Should complete in reasonable time (less than
|
|
165
|
-
|
|
164
|
+
// Should complete in reasonable time (less than 200ms for 1000 calls)
|
|
165
|
+
// Increased threshold to account for test environment variability
|
|
166
|
+
expect(end - start).toBeLessThan(200);
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|