@pol-studios/db 1.0.38 → 1.0.40

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
- useDbQuery
3
- } from "./chunk-UBHORKBS.js";
2
+ useDbQuery,
3
+ useDbQueryById
4
+ } from "./chunk-5SJ5O2NQ.js";
4
5
  import {
5
6
  buildNormalizedQuery,
6
7
  encode,
@@ -71,9 +72,292 @@ function useDbQuery2(query, config) {
71
72
  import { createContext } from "react";
72
73
  var setupAuthContext = createContext({});
73
74
 
74
- // src/auth/context/PermissionContext.tsx
75
- import { createContext as createContext2, useCallback, useContext, useEffect, useMemo as useMemo2, useRef as useRef2, useState } from "react";
75
+ // src/auth/context/AuthProvider.tsx
76
+ import { useCallback, useEffect, useMemo as useMemo2, useRef as useRef2, useState } from "react";
77
+ import { isUsable, newUuid } from "@pol-studios/utils";
76
78
  import { jsx } from "react/jsx-runtime";
79
+ var SESSION_FETCH_TIMEOUT_MS = 3e3;
80
+ function getPermissionLevel(action) {
81
+ switch (action.toLowerCase()) {
82
+ case "view":
83
+ case "read":
84
+ return 1;
85
+ case "edit":
86
+ case "write":
87
+ return 2;
88
+ case "admin":
89
+ case "delete":
90
+ case "share":
91
+ return 3;
92
+ default:
93
+ return 0;
94
+ }
95
+ }
96
+ function isNotExpired(expiresAt) {
97
+ return !expiresAt || new Date(expiresAt) > /* @__PURE__ */ new Date();
98
+ }
99
+ function matchesAccessPattern(pattern, requestedKey) {
100
+ if (pattern === requestedKey) return true;
101
+ if (pattern === "*:*:*") return true;
102
+ const patternParts = pattern.split(":");
103
+ const keyParts = requestedKey.split(":");
104
+ if (patternParts.length !== keyParts.length) return false;
105
+ for (let i = 0; i < patternParts.length; i++) {
106
+ if (patternParts[i] === "*") continue;
107
+ if (patternParts[i] !== keyParts[i]) return false;
108
+ }
109
+ return true;
110
+ }
111
+ function AuthProvider({
112
+ children
113
+ }) {
114
+ const supabase = useSupabase();
115
+ const [currentUser, setCurrentUser] = useState(void 0);
116
+ const [userNeedsChange, setUserNeedsChange] = useState(true);
117
+ const [onSignOutCallbacks, setOnSignOutCallbacks] = useState(/* @__PURE__ */ new Map());
118
+ async function registerAsync(register) {
119
+ const response = await supabase.auth.signUp(register);
120
+ if (response.data.user) {
121
+ setCurrentUser((prev) => prev?.id === response.data.user?.id ? prev : response.data.user);
122
+ }
123
+ return response;
124
+ }
125
+ async function signInAsync(username, password) {
126
+ const response_0 = await supabase.auth.signInWithPassword({
127
+ email: username,
128
+ password
129
+ });
130
+ if (response_0.data.user) {
131
+ setCurrentUser((prev_0) => prev_0?.id === response_0.data.user?.id ? prev_0 : response_0.data.user);
132
+ }
133
+ return response_0;
134
+ }
135
+ const signOutAsync = useCallback(async () => {
136
+ const response_1 = await supabase.auth.signOut();
137
+ if (!response_1.error) {
138
+ Array.from(onSignOutCallbacks.values()).forEach((cb) => cb());
139
+ }
140
+ return response_1;
141
+ }, [supabase.auth, onSignOutCallbacks]);
142
+ function onSignOut(action) {
143
+ const id = newUuid();
144
+ setOnSignOutCallbacks((x) => new Map(x).set(id, action));
145
+ return id;
146
+ }
147
+ function removeOnSignOut(id_0) {
148
+ setOnSignOutCallbacks((x_0) => {
149
+ const map = new Map(x_0);
150
+ map.delete(id_0);
151
+ return map;
152
+ });
153
+ }
154
+ async function refreshAsync() {
155
+ }
156
+ useEffect(() => {
157
+ const {
158
+ data: {
159
+ subscription
160
+ }
161
+ } = supabase.auth.onAuthStateChange((event) => {
162
+ if (event === "SIGNED_IN" || event === "SIGNED_OUT") {
163
+ setUserNeedsChange(true);
164
+ }
165
+ });
166
+ return () => subscription.unsubscribe();
167
+ }, [supabase.auth]);
168
+ useEffect(() => {
169
+ if (!userNeedsChange) return;
170
+ let cancelled = false;
171
+ async function fetchSessionWithTimeout() {
172
+ try {
173
+ const timeoutPromise = new Promise((_, reject) => {
174
+ setTimeout(() => {
175
+ reject(new Error(`Session fetch timed out after ${SESSION_FETCH_TIMEOUT_MS}ms`));
176
+ }, SESSION_FETCH_TIMEOUT_MS);
177
+ });
178
+ const result = await Promise.race([supabase.auth.getSession(), timeoutPromise]);
179
+ if (cancelled) return;
180
+ const newUser = result?.data?.session?.user ?? null;
181
+ setCurrentUser((prev_2) => {
182
+ if (newUser === null) return null;
183
+ if (prev_2?.id === newUser?.id) return prev_2;
184
+ return newUser;
185
+ });
186
+ setUserNeedsChange(false);
187
+ } catch (error) {
188
+ if (cancelled) return;
189
+ console.error("Failed to get session (timeout or error):", error);
190
+ setCurrentUser((prev_1) => prev_1 === null ? prev_1 : null);
191
+ setUserNeedsChange(false);
192
+ }
193
+ }
194
+ fetchSessionWithTimeout();
195
+ return () => {
196
+ cancelled = true;
197
+ };
198
+ }, [userNeedsChange, supabase.auth]);
199
+ const isUserReady = isUsable(currentUser);
200
+ const {
201
+ data: profile,
202
+ isLoading: profileLoading
203
+ } = useDbQueryById("core.Profile", currentUser?.id ?? "", {
204
+ enabled: isUserReady
205
+ });
206
+ const {
207
+ data: directAccess,
208
+ isLoading: directAccessLoading
209
+ } = useDbQuery("core.UserAccess", {
210
+ where: {
211
+ userId: currentUser?.id
212
+ },
213
+ enabled: isUserReady,
214
+ realtime: true
215
+ });
216
+ const {
217
+ data: userGroups,
218
+ isLoading: userGroupsLoading
219
+ } = useDbQuery("core.UserGroup", {
220
+ where: {
221
+ userId: currentUser?.id
222
+ },
223
+ enabled: isUserReady,
224
+ realtime: true
225
+ });
226
+ const {
227
+ data: groups,
228
+ isLoading: groupsLoading
229
+ } = useDbQuery("core.Group", {
230
+ where: {
231
+ isActive: 1
232
+ },
233
+ enabled: isUserReady,
234
+ realtime: true
235
+ });
236
+ const groupIds = useMemo2(() => userGroups?.map((ug) => ug.groupId) ?? [], [userGroups]);
237
+ const groupsMap = useMemo2(() => new Map(groups?.map((g) => [g.id, g]) ?? []), [groups]);
238
+ const {
239
+ data: groupAccess,
240
+ isLoading: groupAccessLoading
241
+ } = useDbQuery("core.GroupAccessKey", {
242
+ where: groupIds.length > 0 ? {
243
+ groupId: {
244
+ in: groupIds
245
+ }
246
+ } : void 0,
247
+ enabled: groupIds.length > 0,
248
+ realtime: true
249
+ });
250
+ const prevProfileStatusRef = useRef2(void 0);
251
+ useEffect(() => {
252
+ const currentStatus = profile?.status;
253
+ const prevStatus = prevProfileStatusRef.current;
254
+ if (prevStatus === "active" && (currentStatus === "archived" || currentStatus === "suspended")) {
255
+ signOutAsync();
256
+ }
257
+ prevProfileStatusRef.current = currentStatus;
258
+ }, [profile?.status, signOutAsync]);
259
+ const allAccessKeys = useMemo2(() => {
260
+ const keys = [];
261
+ directAccess?.forEach((a) => {
262
+ if (isNotExpired(a.expiresAt)) {
263
+ keys.push({
264
+ accessKey: a.accessKey,
265
+ effect: a.effect ?? "allow",
266
+ source: "direct",
267
+ expiresAt: a.expiresAt
268
+ });
269
+ }
270
+ });
271
+ const activeGroupIds = new Set(userGroups?.filter((ug_0) => {
272
+ const group = groupsMap.get(ug_0.groupId);
273
+ return group?.isActive === 1 && isNotExpired(ug_0.expiresAt);
274
+ }).map((ug_1) => ug_1.groupId) ?? []);
275
+ groupAccess?.forEach((ga) => {
276
+ if (activeGroupIds.has(ga.groupId) && isNotExpired(ga.expiresAt)) {
277
+ keys.push({
278
+ accessKey: ga.accessKey,
279
+ effect: ga.effect ?? "allow",
280
+ source: "group",
281
+ expiresAt: ga.expiresAt
282
+ });
283
+ }
284
+ });
285
+ return keys;
286
+ }, [directAccess, userGroups, groupsMap, groupAccess]);
287
+ const combinedAccess = useMemo2(() => {
288
+ const uniqueKeys = /* @__PURE__ */ new Set();
289
+ for (const item of allAccessKeys) {
290
+ if (item.accessKey && item.effect === "allow") {
291
+ uniqueKeys.add(item.accessKey);
292
+ }
293
+ }
294
+ return Array.from(uniqueKeys);
295
+ }, [allAccessKeys]);
296
+ const effectivePermissions = useMemo2(() => {
297
+ const permissions = [];
298
+ for (const item_0 of allAccessKeys) {
299
+ if (item_0.effect !== "allow") continue;
300
+ const parts = item_0.accessKey.split(":");
301
+ if (parts.length === 3) {
302
+ const [resourceType, resourceId, permission] = parts;
303
+ permissions.push({
304
+ resourceType,
305
+ resourceId,
306
+ permission,
307
+ permissionLevel: getPermissionLevel(permission),
308
+ source: item_0.source,
309
+ inheritedFrom: null,
310
+ expiresAt: item_0.expiresAt ?? null
311
+ });
312
+ }
313
+ }
314
+ return permissions;
315
+ }, [allAccessKeys]);
316
+ const profileStatus = profile?.status;
317
+ const isArchived = profileStatus === "archived";
318
+ const isSuspended = profileStatus === "suspended";
319
+ const hasAccess = useCallback((key) => {
320
+ if (isArchived || isSuspended) return false;
321
+ if (!isUsable(combinedAccess)) return false;
322
+ if (combinedAccess.includes("*:*:*")) return true;
323
+ if (combinedAccess.includes(key)) return true;
324
+ if (!isUsable(key)) return true;
325
+ for (const pattern of combinedAccess) {
326
+ if (matchesAccessPattern(pattern, key)) return true;
327
+ }
328
+ const parts_0 = key.split(":");
329
+ if (parts_0.length === 3) {
330
+ const [type, id_1, action_0] = parts_0;
331
+ const requiredLevel = getPermissionLevel(action_0);
332
+ const hasPermission = effectivePermissions.some((p) => p.resourceType === type && p.resourceId === id_1 && p.permissionLevel >= requiredLevel);
333
+ if (hasPermission) return true;
334
+ }
335
+ return false;
336
+ }, [combinedAccess, effectivePermissions, isArchived, isSuspended]);
337
+ const isAccessLoading = directAccessLoading || userGroupsLoading || groupsLoading || groupAccessLoading;
338
+ const authState = useMemo2(() => ({
339
+ hasAccess,
340
+ user: currentUser,
341
+ profile,
342
+ access: combinedAccess,
343
+ effectivePermissions,
344
+ profileStatus,
345
+ isArchived,
346
+ isSuspended,
347
+ isLoading: currentUser === null ? false : profileLoading || isAccessLoading || currentUser === void 0,
348
+ signInAsync,
349
+ signOutAsync,
350
+ onSignOut,
351
+ removeOnSignOut,
352
+ registerAsync,
353
+ refreshAsync
354
+ }), [hasAccess, currentUser, profile, combinedAccess, effectivePermissions, profileStatus, isArchived, isSuspended, profileLoading, isAccessLoading]);
355
+ return /* @__PURE__ */ jsx(setupAuthContext.Provider, { value: authState, children });
356
+ }
357
+
358
+ // src/auth/context/PermissionContext.tsx
359
+ import { createContext as createContext2, useCallback as useCallback2, useContext, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
360
+ import { jsx as jsx2 } from "react/jsx-runtime";
77
361
  function getCacheKey(userId, entityType, entityId) {
78
362
  return `${userId || "anon"}:${entityType}:${entityId}`;
79
363
  }
@@ -188,13 +472,13 @@ function PermissionProvider({
188
472
  const supabase = useSupabase();
189
473
  const setupAuth = useContext(setupAuthContext);
190
474
  const user = setupAuth?.user;
191
- const cacheRef = useRef2(/* @__PURE__ */ new Map());
192
- const pendingLookupsRef = useRef2(/* @__PURE__ */ new Set());
193
- const inFlightRef = useRef2(/* @__PURE__ */ new Set());
194
- const batchTimerRef = useRef2(null);
195
- const [isLoading, setIsLoading] = useState(false);
196
- const [, forceUpdate] = useState(0);
197
- const cleanupExpiredEntries = useCallback(() => {
475
+ const cacheRef = useRef3(/* @__PURE__ */ new Map());
476
+ const pendingLookupsRef = useRef3(/* @__PURE__ */ new Set());
477
+ const inFlightRef = useRef3(/* @__PURE__ */ new Set());
478
+ const batchTimerRef = useRef3(null);
479
+ const [isLoading, setIsLoading] = useState2(false);
480
+ const [, forceUpdate] = useState2(0);
481
+ const cleanupExpiredEntries = useCallback2(() => {
198
482
  const now = Date.now();
199
483
  const cache = cacheRef.current;
200
484
  let hasExpired = false;
@@ -208,11 +492,11 @@ function PermissionProvider({
208
492
  forceUpdate((prev) => prev + 1);
209
493
  }
210
494
  }, []);
211
- useEffect(() => {
495
+ useEffect2(() => {
212
496
  const cleanupInterval = setInterval(cleanupExpiredEntries, 60 * 1e3);
213
497
  return () => clearInterval(cleanupInterval);
214
498
  }, [cleanupExpiredEntries]);
215
- const executeBatchLookup = useCallback(async () => {
499
+ const executeBatchLookup = useCallback2(async () => {
216
500
  const pending = Array.from(pendingLookupsRef.current);
217
501
  pendingLookupsRef.current.clear();
218
502
  if (pending.length === 0 || !user?.id) {
@@ -272,7 +556,7 @@ function PermissionProvider({
272
556
  setIsLoading(false);
273
557
  }
274
558
  }, [supabase, user?.id]);
275
- const scheduleBatchLookup = useCallback(() => {
559
+ const scheduleBatchLookup = useCallback2(() => {
276
560
  if (batchTimerRef.current) {
277
561
  clearTimeout(batchTimerRef.current);
278
562
  }
@@ -281,7 +565,7 @@ function PermissionProvider({
281
565
  executeBatchLookup();
282
566
  }, BATCH_DELAY_MS);
283
567
  }, [executeBatchLookup]);
284
- const getPermission = useCallback((entityType_0, entityId) => {
568
+ const getPermission = useCallback2((entityType_0, entityId) => {
285
569
  const key_4 = getCacheKey(user?.id, entityType_0, entityId);
286
570
  const cache_2 = cacheRef.current;
287
571
  const cached = cache_2.get(key_4);
@@ -295,7 +579,7 @@ function PermissionProvider({
295
579
  }
296
580
  return loadingPermission;
297
581
  }, [scheduleBatchLookup, user?.id]);
298
- const checkPermission = useCallback((entityType_1, entityId_0, action) => {
582
+ const checkPermission = useCallback2((entityType_1, entityId_0, action) => {
299
583
  const permission = getPermission(entityType_1, entityId_0);
300
584
  if (permission.isLoading) {
301
585
  return false;
@@ -317,7 +601,7 @@ function PermissionProvider({
317
601
  return false;
318
602
  }
319
603
  }, [getPermission]);
320
- const prefetchPermissions = useCallback(async (entities_0) => {
604
+ const prefetchPermissions = useCallback2(async (entities_0) => {
321
605
  if (!user?.id || entities_0.length === 0) {
322
606
  return;
323
607
  }
@@ -347,416 +631,126 @@ function PermissionProvider({
347
631
  p_entities: entitiesParam
348
632
  });
349
633
  if (error_0) {
350
- console.error("Failed to prefetch entity permissions:", error_0);
351
- return;
352
- }
353
- if (data_0) {
354
- const cacheTimestamp = Date.now();
355
- const resultsMap_0 = /* @__PURE__ */ new Map();
356
- const results_0 = data_0;
357
- for (const result_0 of results_0) {
358
- const key_6 = getCacheKey(user?.id, result_0.entity_type, result_0.entity_id);
359
- resultsMap_0.set(key_6, result_0.permission);
360
- }
361
- for (const entity_0 of toFetch) {
362
- const key_7 = getCacheKey(user?.id, entity_0.entityType, entity_0.entityId);
363
- const permissionLevel_0 = resultsMap_0.get(key_7) || null;
364
- cache_3.set(key_7, {
365
- permission: mapPermissionLevel(permissionLevel_0),
366
- expiresAt: cacheTimestamp + CACHE_TTL_MS
367
- });
368
- }
369
- forceUpdate((prev_1) => prev_1 + 1);
370
- }
371
- } catch (err_0) {
372
- console.error("Unexpected error prefetching entity permissions:", err_0);
373
- } finally {
374
- setIsLoading(false);
375
- }
376
- }, [supabase, user?.id]);
377
- const invalidatePermission = useCallback((entityType_2, entityId_1) => {
378
- const key_8 = getCacheKey(user?.id, entityType_2, entityId_1);
379
- cacheRef.current.delete(key_8);
380
- forceUpdate((prev_2) => prev_2 + 1);
381
- }, [user?.id]);
382
- const parseScopedAccessKey = useCallback((key_9) => {
383
- if (!key_9 || typeof key_9 !== "string") {
384
- return null;
385
- }
386
- const parts_0 = key_9.split(":");
387
- if (parts_0.length < 2) {
388
- return null;
389
- }
390
- const entityType_3 = parts_0[0];
391
- const entityId_2 = parseInt(parts_0[1], 10);
392
- if (isNaN(entityId_2)) {
393
- return null;
394
- }
395
- const entityTypeMap = {
396
- client: "Client",
397
- project: "Project",
398
- database: "ProjectDatabase",
399
- projectdatabase: "ProjectDatabase"
400
- };
401
- const normalizedEntityType = entityTypeMap[entityType_3.toLowerCase()];
402
- if (!normalizedEntityType) {
403
- return null;
404
- }
405
- return {
406
- entityType: normalizedEntityType,
407
- entityId: entityId_2
408
- };
409
- }, []);
410
- useEffect(() => {
411
- if (!user?.id) {
412
- return;
413
- }
414
- const channel = supabase.channel(`entity-permissions-${user.id}`).on("postgres_changes", {
415
- event: "*",
416
- schema: "core",
417
- table: "UserAccess",
418
- filter: `userId=eq.${user.id}`
419
- }, (payload) => {
420
- if (payload.new && typeof payload.new === "object" && "scopedAccessKey" in payload.new && typeof payload.new.scopedAccessKey === "string") {
421
- const parsed = parseScopedAccessKey(payload.new.scopedAccessKey);
422
- if (parsed) {
423
- invalidatePermission(parsed.entityType, parsed.entityId);
424
- }
425
- }
426
- if (payload.old && typeof payload.old === "object" && "scopedAccessKey" in payload.old && typeof payload.old.scopedAccessKey === "string") {
427
- const parsed_0 = parseScopedAccessKey(payload.old.scopedAccessKey);
428
- if (parsed_0) {
429
- invalidatePermission(parsed_0.entityType, parsed_0.entityId);
430
- }
431
- }
432
- }).subscribe();
433
- return () => {
434
- channel.unsubscribe();
435
- supabase.removeChannel(channel);
436
- };
437
- }, [supabase, user?.id, invalidatePermission, parseScopedAccessKey]);
438
- useEffect(() => {
439
- cacheRef.current.clear();
440
- pendingLookupsRef.current.clear();
441
- inFlightRef.current.clear();
442
- if (batchTimerRef.current) {
443
- clearTimeout(batchTimerRef.current);
444
- batchTimerRef.current = null;
445
- }
446
- forceUpdate((prev_3) => prev_3 + 1);
447
- }, [user?.id]);
448
- useEffect(() => {
449
- return () => {
450
- if (batchTimerRef.current) {
451
- clearTimeout(batchTimerRef.current);
452
- }
453
- };
454
- }, []);
455
- const value = useMemo2(() => ({
456
- getPermission,
457
- checkPermission,
458
- prefetchPermissions,
459
- invalidatePermission,
460
- isLoading
461
- }), [getPermission, checkPermission, prefetchPermissions, invalidatePermission, isLoading]);
462
- return /* @__PURE__ */ jsx(permissionContext.Provider, { value, children });
463
- }
464
- function usePermissions() {
465
- const context = useContext(permissionContext);
466
- if (!context || Object.keys(context).length === 0) {
467
- throw new Error("usePermissions must be used within a PermissionProvider");
468
- }
469
- return context;
470
- }
471
-
472
- // src/auth/context/AuthProvider.tsx
473
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
474
- import { isUsable, newUuid } from "@pol-studios/utils";
475
- import { jsx as jsx2 } from "react/jsx-runtime";
476
- var SESSION_FETCH_TIMEOUT_MS = 3e3;
477
- function getPermissionLevel(action) {
478
- switch (action.toLowerCase()) {
479
- case "view":
480
- case "read":
481
- return 1;
482
- case "edit":
483
- case "write":
484
- return 2;
485
- case "admin":
486
- case "delete":
487
- case "share":
488
- return 3;
489
- default:
490
- return 0;
491
- }
492
- }
493
- function isNotExpired(expiresAt) {
494
- return !expiresAt || new Date(expiresAt) > /* @__PURE__ */ new Date();
495
- }
496
- function matchesAccessPattern(pattern, requestedKey) {
497
- if (pattern === requestedKey) return true;
498
- if (pattern === "*:*:*") return true;
499
- const patternParts = pattern.split(":");
500
- const keyParts = requestedKey.split(":");
501
- if (patternParts.length !== keyParts.length) return false;
502
- for (let i = 0; i < patternParts.length; i++) {
503
- if (patternParts[i] === "*") continue;
504
- if (patternParts[i] !== keyParts[i]) return false;
505
- }
506
- return true;
507
- }
508
- function AuthProvider({
509
- children,
510
- enableEntityPermissions = false
511
- }) {
512
- const supabase = useSupabase();
513
- const [currentUser, setCurrentUser] = useState2(void 0);
514
- const [userNeedsChange, setUserNeedsChange] = useState2(true);
515
- const [onSignOutCallbacks, setOnSignOutCallbacks] = useState2(/* @__PURE__ */ new Map());
516
- async function registerAsync(register) {
517
- const response = await supabase.auth.signUp(register);
518
- if (response.data.user) {
519
- setCurrentUser((prev) => prev?.id === response.data.user?.id ? prev : response.data.user);
520
- }
521
- return response;
522
- }
523
- async function signInAsync(username, password) {
524
- const response_0 = await supabase.auth.signInWithPassword({
525
- email: username,
526
- password
527
- });
528
- if (response_0.data.user) {
529
- setCurrentUser((prev_0) => prev_0?.id === response_0.data.user?.id ? prev_0 : response_0.data.user);
530
- }
531
- return response_0;
532
- }
533
- const signOutAsync = useCallback2(async () => {
534
- const response_1 = await supabase.auth.signOut();
535
- if (!response_1.error) {
536
- Array.from(onSignOutCallbacks.values()).forEach((cb) => cb());
537
- }
538
- return response_1;
539
- }, [supabase.auth, onSignOutCallbacks]);
540
- function onSignOut(action) {
541
- const id = newUuid();
542
- setOnSignOutCallbacks((x) => new Map(x).set(id, action));
543
- return id;
544
- }
545
- function removeOnSignOut(id_0) {
546
- setOnSignOutCallbacks((x_0) => {
547
- const map = new Map(x_0);
548
- map.delete(id_0);
549
- return map;
550
- });
551
- }
552
- async function refreshAsync() {
553
- }
554
- useEffect2(() => {
555
- const {
556
- data: {
557
- subscription
558
- }
559
- } = supabase.auth.onAuthStateChange((event) => {
560
- if (event === "SIGNED_IN" || event === "SIGNED_OUT") {
561
- setUserNeedsChange(true);
562
- }
563
- });
564
- return () => subscription.unsubscribe();
565
- }, [supabase.auth]);
566
- useEffect2(() => {
567
- if (!userNeedsChange) return;
568
- let cancelled = false;
569
- async function fetchSessionWithTimeout() {
570
- try {
571
- const timeoutPromise = new Promise((_, reject) => {
572
- setTimeout(() => {
573
- reject(new Error(`Session fetch timed out after ${SESSION_FETCH_TIMEOUT_MS}ms`));
574
- }, SESSION_FETCH_TIMEOUT_MS);
575
- });
576
- const result = await Promise.race([supabase.auth.getSession(), timeoutPromise]);
577
- if (cancelled) return;
578
- const newUser = result?.data?.session?.user ?? null;
579
- setCurrentUser((prev_2) => {
580
- if (newUser === null) return null;
581
- if (prev_2?.id === newUser?.id) return prev_2;
582
- return newUser;
583
- });
584
- setUserNeedsChange(false);
585
- } catch (error) {
586
- if (cancelled) return;
587
- console.error("Failed to get session (timeout or error):", error);
588
- setCurrentUser((prev_1) => prev_1 === null ? prev_1 : null);
589
- setUserNeedsChange(false);
634
+ console.error("Failed to prefetch entity permissions:", error_0);
635
+ return;
636
+ }
637
+ if (data_0) {
638
+ const cacheTimestamp = Date.now();
639
+ const resultsMap_0 = /* @__PURE__ */ new Map();
640
+ const results_0 = data_0;
641
+ for (const result_0 of results_0) {
642
+ const key_6 = getCacheKey(user?.id, result_0.entity_type, result_0.entity_id);
643
+ resultsMap_0.set(key_6, result_0.permission);
644
+ }
645
+ for (const entity_0 of toFetch) {
646
+ const key_7 = getCacheKey(user?.id, entity_0.entityType, entity_0.entityId);
647
+ const permissionLevel_0 = resultsMap_0.get(key_7) || null;
648
+ cache_3.set(key_7, {
649
+ permission: mapPermissionLevel(permissionLevel_0),
650
+ expiresAt: cacheTimestamp + CACHE_TTL_MS
651
+ });
652
+ }
653
+ forceUpdate((prev_1) => prev_1 + 1);
590
654
  }
655
+ } catch (err_0) {
656
+ console.error("Unexpected error prefetching entity permissions:", err_0);
657
+ } finally {
658
+ setIsLoading(false);
591
659
  }
592
- fetchSessionWithTimeout();
593
- return () => {
594
- cancelled = true;
660
+ }, [supabase, user?.id]);
661
+ const invalidatePermission = useCallback2((entityType_2, entityId_1) => {
662
+ const key_8 = getCacheKey(user?.id, entityType_2, entityId_1);
663
+ cacheRef.current.delete(key_8);
664
+ forceUpdate((prev_2) => prev_2 + 1);
665
+ }, [user?.id]);
666
+ const parseScopedAccessKey = useCallback2((key_9) => {
667
+ if (!key_9 || typeof key_9 !== "string") {
668
+ return null;
669
+ }
670
+ const parts_0 = key_9.split(":");
671
+ if (parts_0.length < 2) {
672
+ return null;
673
+ }
674
+ const entityType_3 = parts_0[0];
675
+ const entityId_2 = parseInt(parts_0[1], 10);
676
+ if (isNaN(entityId_2)) {
677
+ return null;
678
+ }
679
+ const entityTypeMap = {
680
+ client: "Client",
681
+ project: "Project",
682
+ database: "ProjectDatabase",
683
+ projectdatabase: "ProjectDatabase"
595
684
  };
596
- }, [userNeedsChange, supabase.auth]);
597
- const isUserReady = isUsable(currentUser);
598
- const {
599
- data: profileData,
600
- isLoading: profileLoading
601
- } = useDbQuery("core.Profile", {
602
- where: {
603
- id: currentUser?.id
604
- },
605
- enabled: isUserReady,
606
- realtime: true
607
- });
608
- const {
609
- data: directAccess,
610
- isLoading: directAccessLoading
611
- } = useDbQuery("core.UserAccess", {
612
- where: {
613
- userId: currentUser?.id
614
- },
615
- enabled: isUserReady,
616
- realtime: true
617
- });
618
- const {
619
- data: userGroups,
620
- isLoading: userGroupsLoading
621
- } = useDbQuery("core.UserGroup", {
622
- where: {
623
- userId: currentUser?.id
624
- },
625
- enabled: isUserReady,
626
- realtime: true
627
- });
628
- const {
629
- data: groups,
630
- isLoading: groupsLoading
631
- } = useDbQuery("core.Group", {
632
- where: {
633
- isActive: 1
634
- },
635
- enabled: isUserReady,
636
- realtime: true
637
- });
638
- const groupIds = useMemo3(() => userGroups?.map((ug) => ug.groupId) ?? [], [userGroups]);
639
- const groupsMap = useMemo3(() => new Map(groups?.map((g) => [g.id, g]) ?? []), [groups]);
640
- const {
641
- data: groupAccess,
642
- isLoading: groupAccessLoading
643
- } = useDbQuery("core.GroupAccessKey", {
644
- where: groupIds.length > 0 ? {
645
- groupId: {
646
- in: groupIds
647
- }
648
- } : void 0,
649
- enabled: groupIds.length > 0,
650
- realtime: true
651
- });
652
- const profile = profileData?.[0] ?? null;
653
- const prevProfileStatusRef = useRef3(void 0);
685
+ const normalizedEntityType = entityTypeMap[entityType_3.toLowerCase()];
686
+ if (!normalizedEntityType) {
687
+ return null;
688
+ }
689
+ return {
690
+ entityType: normalizedEntityType,
691
+ entityId: entityId_2
692
+ };
693
+ }, []);
654
694
  useEffect2(() => {
655
- const currentStatus = profile?.status;
656
- const prevStatus = prevProfileStatusRef.current;
657
- if (prevStatus === "active" && (currentStatus === "archived" || currentStatus === "suspended")) {
658
- signOutAsync();
695
+ if (!user?.id) {
696
+ return;
659
697
  }
660
- prevProfileStatusRef.current = currentStatus;
661
- }, [profile?.status, signOutAsync]);
662
- const allAccessKeys = useMemo3(() => {
663
- const keys = [];
664
- directAccess?.forEach((a) => {
665
- if (isNotExpired(a.expiresAt)) {
666
- keys.push({
667
- accessKey: a.accessKey,
668
- effect: a.effect ?? "allow",
669
- source: "direct",
670
- expiresAt: a.expiresAt
671
- });
672
- }
673
- });
674
- const activeGroupIds = new Set(userGroups?.filter((ug_0) => {
675
- const group = groupsMap.get(ug_0.groupId);
676
- return group?.isActive === 1 && isNotExpired(ug_0.expiresAt);
677
- }).map((ug_1) => ug_1.groupId) ?? []);
678
- groupAccess?.forEach((ga) => {
679
- if (activeGroupIds.has(ga.groupId) && isNotExpired(ga.expiresAt)) {
680
- keys.push({
681
- accessKey: ga.accessKey,
682
- effect: ga.effect ?? "allow",
683
- source: "group",
684
- expiresAt: ga.expiresAt
685
- });
698
+ const channel = supabase.channel(`entity-permissions-${user.id}`).on("postgres_changes", {
699
+ event: "*",
700
+ schema: "core",
701
+ table: "UserAccess",
702
+ filter: `userId=eq.${user.id}`
703
+ }, (payload) => {
704
+ if (payload.new && typeof payload.new === "object" && "scopedAccessKey" in payload.new && typeof payload.new.scopedAccessKey === "string") {
705
+ const parsed = parseScopedAccessKey(payload.new.scopedAccessKey);
706
+ if (parsed) {
707
+ invalidatePermission(parsed.entityType, parsed.entityId);
708
+ }
686
709
  }
687
- });
688
- return keys;
689
- }, [directAccess, userGroups, groupsMap, groupAccess]);
690
- const combinedAccess = useMemo3(() => {
691
- const uniqueKeys = /* @__PURE__ */ new Set();
692
- for (const item of allAccessKeys) {
693
- if (item.accessKey && item.effect === "allow") {
694
- uniqueKeys.add(item.accessKey);
710
+ if (payload.old && typeof payload.old === "object" && "scopedAccessKey" in payload.old && typeof payload.old.scopedAccessKey === "string") {
711
+ const parsed_0 = parseScopedAccessKey(payload.old.scopedAccessKey);
712
+ if (parsed_0) {
713
+ invalidatePermission(parsed_0.entityType, parsed_0.entityId);
714
+ }
695
715
  }
716
+ }).subscribe();
717
+ return () => {
718
+ channel.unsubscribe();
719
+ supabase.removeChannel(channel);
720
+ };
721
+ }, [supabase, user?.id, invalidatePermission, parseScopedAccessKey]);
722
+ useEffect2(() => {
723
+ cacheRef.current.clear();
724
+ pendingLookupsRef.current.clear();
725
+ inFlightRef.current.clear();
726
+ if (batchTimerRef.current) {
727
+ clearTimeout(batchTimerRef.current);
728
+ batchTimerRef.current = null;
696
729
  }
697
- return Array.from(uniqueKeys);
698
- }, [allAccessKeys]);
699
- const effectivePermissions = useMemo3(() => {
700
- const permissions = [];
701
- for (const item_0 of allAccessKeys) {
702
- if (item_0.effect !== "allow") continue;
703
- const parts = item_0.accessKey.split(":");
704
- if (parts.length === 3) {
705
- const [resourceType, resourceId, permission] = parts;
706
- permissions.push({
707
- resourceType,
708
- resourceId,
709
- permission,
710
- permissionLevel: getPermissionLevel(permission),
711
- source: item_0.source,
712
- inheritedFrom: null,
713
- expiresAt: item_0.expiresAt ?? null
714
- });
730
+ forceUpdate((prev_3) => prev_3 + 1);
731
+ }, [user?.id]);
732
+ useEffect2(() => {
733
+ return () => {
734
+ if (batchTimerRef.current) {
735
+ clearTimeout(batchTimerRef.current);
715
736
  }
716
- }
717
- return permissions;
718
- }, [allAccessKeys]);
719
- const profileStatus = profile?.status;
720
- const isArchived = profileStatus === "archived";
721
- const isSuspended = profileStatus === "suspended";
722
- const hasAccess = useCallback2((key) => {
723
- if (isArchived || isSuspended) return false;
724
- if (!isUsable(combinedAccess)) return false;
725
- if (combinedAccess.includes("*:*:*")) return true;
726
- if (combinedAccess.includes(key)) return true;
727
- if (!isUsable(key)) return true;
728
- for (const pattern of combinedAccess) {
729
- if (matchesAccessPattern(pattern, key)) return true;
730
- }
731
- const parts_0 = key.split(":");
732
- if (parts_0.length === 3) {
733
- const [type, id_1, action_0] = parts_0;
734
- const requiredLevel = getPermissionLevel(action_0);
735
- const hasPermission = effectivePermissions.some((p) => p.resourceType === type && p.resourceId === id_1 && p.permissionLevel >= requiredLevel);
736
- if (hasPermission) return true;
737
- }
738
- return false;
739
- }, [combinedAccess, effectivePermissions, isArchived, isSuspended]);
740
- const isAccessLoading = directAccessLoading || userGroupsLoading || groupsLoading || groupAccessLoading;
741
- const authState = useMemo3(() => ({
742
- hasAccess,
743
- user: currentUser,
744
- profile,
745
- access: combinedAccess,
746
- effectivePermissions,
747
- profileStatus,
748
- isArchived,
749
- isSuspended,
750
- isLoading: currentUser === null ? false : profileLoading || isAccessLoading || currentUser === void 0,
751
- signInAsync,
752
- signOutAsync,
753
- onSignOut,
754
- removeOnSignOut,
755
- registerAsync,
756
- refreshAsync
757
- }), [hasAccess, currentUser, profile, combinedAccess, effectivePermissions, profileStatus, isArchived, isSuspended, profileLoading, isAccessLoading]);
758
- const content = enableEntityPermissions ? /* @__PURE__ */ jsx2(PermissionProvider, { children }) : children;
759
- return /* @__PURE__ */ jsx2(setupAuthContext.Provider, { value: authState, children: content });
737
+ };
738
+ }, []);
739
+ const value = useMemo3(() => ({
740
+ getPermission,
741
+ checkPermission,
742
+ prefetchPermissions,
743
+ invalidatePermission,
744
+ isLoading
745
+ }), [getPermission, checkPermission, prefetchPermissions, invalidatePermission, isLoading]);
746
+ return /* @__PURE__ */ jsx2(permissionContext.Provider, { value, children });
747
+ }
748
+ function usePermissions() {
749
+ const context = useContext(permissionContext);
750
+ if (!context || Object.keys(context).length === 0) {
751
+ throw new Error("usePermissions must be used within a PermissionProvider");
752
+ }
753
+ return context;
760
754
  }
761
755
 
762
756
  // src/auth/context/UserMetadataContext.tsx
@@ -1100,11 +1094,11 @@ export {
1100
1094
  isTimeoutError,
1101
1095
  useDbQuery2 as useDbQuery,
1102
1096
  setupAuthContext,
1097
+ AuthProvider,
1103
1098
  permissionContext,
1104
1099
  entityPermissionContext,
1105
1100
  PermissionProvider,
1106
1101
  usePermissions,
1107
- AuthProvider,
1108
1102
  userMetadataContext,
1109
1103
  UserMetadataProvider,
1110
1104
  useUserMetadata,
@@ -1112,4 +1106,4 @@ export {
1112
1106
  useSetUserMetadata,
1113
1107
  useUserMetadataState
1114
1108
  };
1115
- //# sourceMappingURL=chunk-6OSNGHRE.js.map
1109
+ //# sourceMappingURL=chunk-MZLEDWJF.js.map