exguard-client 1.0.0

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/index.cjs ADDED
@@ -0,0 +1,1002 @@
1
+ 'use strict';
2
+
3
+ var axios = require('axios');
4
+ var reactRouter = require('react-router');
5
+ var React = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var lucideReact = require('lucide-react');
8
+ var socket_ioClient = require('socket.io-client');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var axios__default = /*#__PURE__*/_interopDefault(axios);
13
+ var React__default = /*#__PURE__*/_interopDefault(React);
14
+
15
+ var __defProp = Object.defineProperty;
16
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
17
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
18
+
19
+ // src/config/exguard-config.ts
20
+ var DEFAULT_EXGUARD_CONFIG = {
21
+ apiUrl: "",
22
+ withCredentials: true
23
+ };
24
+ var configuredApiUrl = "";
25
+ var setExGuardConfig = (config) => {
26
+ if (config.apiUrl) {
27
+ configuredApiUrl = config.apiUrl;
28
+ }
29
+ };
30
+ var getExGuardApiUrl = () => {
31
+ if (configuredApiUrl) {
32
+ return configuredApiUrl;
33
+ }
34
+ if (typeof window !== "undefined") {
35
+ if (window.__EXGUARD_API_URL__) {
36
+ return window.__EXGUARD_API_URL__;
37
+ }
38
+ }
39
+ return "http://localhost:3000";
40
+ };
41
+ var EXGUARD_STORAGE_KEYS = {
42
+ ACCESS_TOKEN: "access_token",
43
+ ID_TOKEN: "id_token",
44
+ REFRESH_TOKEN: "refresh_token"
45
+ };
46
+
47
+ // src/config/realtime-rbac-config.ts
48
+ var EXGUARD_RBAC_CHANNELS = {
49
+ ROLES: "roles",
50
+ PERMISSIONS: "permissions",
51
+ RBAC: "rbac"
52
+ };
53
+ var EXGUARD_RBAC_EVENTS = {
54
+ // Role events
55
+ ROLE_CREATED: "role:created",
56
+ ROLE_UPDATED: "role:updated",
57
+ ROLE_DELETED: "role:deleted",
58
+ // Permission events
59
+ PERMISSION_CREATED: "permission:created",
60
+ PERMISSION_UPDATED: "permission:updated",
61
+ PERMISSION_DELETED: "permission:deleted",
62
+ // Role-Permission events
63
+ ROLE_PERMISSION_ASSIGNED: "role-permission:assigned",
64
+ ROLE_PERMISSION_REMOVED: "role-permission:removed",
65
+ // User RBAC events
66
+ USER_ROLES_UPDATED: "user:roles-updated",
67
+ USER_PERMISSIONS_CHANGED: "user:permissions-changed",
68
+ USER_ACCESS_CHANGED: "user:access-changed",
69
+ RBAC_USER_ASSIGNED_TO_GROUP: "rbac:user-assigned-to-group",
70
+ RBAC_USER_REMOVED_FROM_GROUP: "rbac:user-removed-from-group",
71
+ RBAC_PERMISSIONS_UPDATED: "rbac:permissions-updated",
72
+ // User status events
73
+ USER_ONLINE: "user:online",
74
+ USER_OFFLINE: "user:offline",
75
+ // Group events
76
+ GROUP_UPDATED: "group:updated"
77
+ };
78
+ var RBAC_RECONNECT_DELAY = 300;
79
+ [
80
+ EXGUARD_RBAC_EVENTS.ROLE_UPDATED,
81
+ EXGUARD_RBAC_EVENTS.PERMISSION_UPDATED,
82
+ EXGUARD_RBAC_EVENTS.USER_ACCESS_CHANGED
83
+ ];
84
+ [
85
+ EXGUARD_RBAC_EVENTS.USER_ROLES_UPDATED,
86
+ EXGUARD_RBAC_EVENTS.USER_PERMISSIONS_CHANGED,
87
+ EXGUARD_RBAC_EVENTS.USER_ACCESS_CHANGED,
88
+ EXGUARD_RBAC_EVENTS.RBAC_USER_ASSIGNED_TO_GROUP,
89
+ EXGUARD_RBAC_EVENTS.RBAC_USER_REMOVED_FROM_GROUP,
90
+ EXGUARD_RBAC_EVENTS.RBAC_PERMISSIONS_UPDATED
91
+ ];
92
+ [
93
+ EXGUARD_RBAC_EVENTS.ROLE_CREATED,
94
+ EXGUARD_RBAC_EVENTS.ROLE_UPDATED,
95
+ EXGUARD_RBAC_EVENTS.ROLE_DELETED,
96
+ EXGUARD_RBAC_EVENTS.ROLE_PERMISSION_ASSIGNED
97
+ ];
98
+ [
99
+ EXGUARD_RBAC_EVENTS.PERMISSION_CREATED,
100
+ EXGUARD_RBAC_EVENTS.PERMISSION_UPDATED,
101
+ EXGUARD_RBAC_EVENTS.PERMISSION_DELETED
102
+ ];
103
+ var guardApiClient = axios__default.default.create({
104
+ baseURL: getExGuardApiUrl(),
105
+ withCredentials: true,
106
+ headers: {
107
+ "Content-Type": "application/json"
108
+ }
109
+ });
110
+ guardApiClient.interceptors.request.use(
111
+ (config) => {
112
+ const token = window.localStorage.getItem(EXGUARD_STORAGE_KEYS.ACCESS_TOKEN);
113
+ if (token) {
114
+ config.headers.Authorization = `Bearer ${token}`;
115
+ }
116
+ return config;
117
+ },
118
+ (error) => Promise.reject(error instanceof Error ? error : new Error(String(error)))
119
+ );
120
+ async function verifyToken() {
121
+ try {
122
+ const idToken = window.localStorage.getItem(EXGUARD_STORAGE_KEYS.ID_TOKEN);
123
+ const response = await guardApiClient.post("/guard/verify-token", {
124
+ id_token: idToken
125
+ });
126
+ if (response.data.success && response.data.data) {
127
+ console.log("Token verification response:", response.data.data);
128
+ const backendData = response.data.data;
129
+ const mappedResponse = {
130
+ valid: true,
131
+ user: backendData.user ? {
132
+ userId: backendData.user.id,
133
+ username: backendData.user.username,
134
+ email: backendData.user.email,
135
+ givenName: backendData.user.givenName,
136
+ familyName: backendData.user.familyName
137
+ } : void 0
138
+ };
139
+ console.log("Mapped user data:", mappedResponse.user);
140
+ return mappedResponse;
141
+ }
142
+ throw new Error(response.data.message ?? "Token verification failed");
143
+ } catch (error) {
144
+ const axiosError = error;
145
+ if (axiosError.response?.data) {
146
+ const errorData = axiosError.response.data;
147
+ const errorMessage = errorData.message ?? "Token verification failed";
148
+ throw new Error(errorMessage);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+ async function getUserAccess() {
154
+ try {
155
+ const response = await guardApiClient.get("/guard/me");
156
+ if (response.data.success && response.data.data) {
157
+ console.log("User Access Data:", response.data.data);
158
+ console.log("User Details:", response.data.data.user);
159
+ console.log("User Roles:", response.data.data.roles);
160
+ console.log("User Groups:", response.data.data.groups);
161
+ console.log("User Modules:", response.data.data.modules);
162
+ console.log("User Field Offices:", response.data.data.fieldOffices);
163
+ return response.data.data;
164
+ }
165
+ throw new Error(response.data.message ?? "Failed to get user access data");
166
+ } catch (error) {
167
+ const axiosError = error;
168
+ if (axiosError.response?.data) {
169
+ const errorData = axiosError.response.data;
170
+ const errorMessage = errorData.message ?? "Failed to get user access data";
171
+ throw new Error(errorMessage);
172
+ }
173
+ throw error;
174
+ }
175
+ }
176
+ var ExGuardRealtimeContext = React.createContext(void 0);
177
+
178
+ // src/hooks/use-user-access.ts
179
+ var useUserAccess = (options = {}) => {
180
+ const { enabled = true, refetchInterval = 0 } = options;
181
+ const [userAccess, setUserAccess] = React.useState(null);
182
+ const [isLoading, setIsLoading] = React.useState(true);
183
+ const [error, setError] = React.useState(null);
184
+ const realtimeContext = React.use(ExGuardRealtimeContext);
185
+ const fetchUserAccess = React.useCallback(async () => {
186
+ if (!enabled) return;
187
+ try {
188
+ console.log("[useUserAccess] \u{1F504} Fetching user access data...");
189
+ setIsLoading(true);
190
+ setError(null);
191
+ const data = await getUserAccess();
192
+ setUserAccess(data);
193
+ console.log("[useUserAccess] \u2705 User access refreshed successfully", {
194
+ userId: data.user.id,
195
+ username: data.user.username,
196
+ modulesCount: data.modules.length,
197
+ modules: data.modules.map((m) => m.key)
198
+ });
199
+ } catch (err) {
200
+ const errorObj = err instanceof Error ? err : new Error("Failed to fetch user access");
201
+ setError(errorObj);
202
+ console.error("[useUserAccess] \u274C Failed to fetch user access:", errorObj);
203
+ } finally {
204
+ setIsLoading(false);
205
+ }
206
+ }, [enabled]);
207
+ React.useEffect(() => {
208
+ if (!realtimeContext) {
209
+ console.warn("[useUserAccess] \u26A0\uFE0F RealtimeContext not available");
210
+ return;
211
+ }
212
+ console.log("[useUserAccess] \u{1F4E1} Registering RBAC refresh callback");
213
+ const unsubscribe = realtimeContext.registerRbacRefreshCallback(fetchUserAccess);
214
+ console.log("[useUserAccess] \u2705 RBAC refresh callback registered");
215
+ return () => {
216
+ console.log("[useUserAccess] \u{1F507} Unregistering RBAC refresh callback");
217
+ unsubscribe();
218
+ };
219
+ }, [realtimeContext, fetchUserAccess]);
220
+ React.useEffect(() => {
221
+ void fetchUserAccess();
222
+ if (refetchInterval > 0) {
223
+ const interval = setInterval(() => {
224
+ void fetchUserAccess();
225
+ }, refetchInterval);
226
+ return () => {
227
+ clearInterval(interval);
228
+ };
229
+ }
230
+ }, [fetchUserAccess, refetchInterval]);
231
+ const hasModuleAccess = React.useCallback(
232
+ (moduleKey) => {
233
+ if (!userAccess) return false;
234
+ const module = userAccess.modules.find((m) => m.key.toLowerCase() === moduleKey.toLowerCase());
235
+ return !!module && module.permissions.length > 0;
236
+ },
237
+ [userAccess]
238
+ );
239
+ const getModulePermissions = React.useCallback(
240
+ (moduleKey) => {
241
+ if (!userAccess) return [];
242
+ const module = userAccess.modules.find((m) => m.key.toLowerCase() === moduleKey.toLowerCase());
243
+ return module?.permissions ?? [];
244
+ },
245
+ [userAccess]
246
+ );
247
+ const hasPermission = React.useCallback(
248
+ (moduleKey, permission) => {
249
+ const permissions = getModulePermissions(moduleKey);
250
+ return permissions.includes(permission);
251
+ },
252
+ [getModulePermissions]
253
+ );
254
+ return {
255
+ userAccess,
256
+ isLoading,
257
+ error,
258
+ hasModuleAccess,
259
+ getModulePermissions,
260
+ hasPermission,
261
+ refetch: fetchUserAccess
262
+ };
263
+ };
264
+ function Spinner({ className = "" }) {
265
+ return /* @__PURE__ */ jsxRuntime.jsx(
266
+ "div",
267
+ {
268
+ className: `animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite] ${className}`,
269
+ role: "status",
270
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]", children: "Loading..." })
271
+ }
272
+ );
273
+ }
274
+ function Button({
275
+ className = "",
276
+ variant = "default",
277
+ size = "default",
278
+ children,
279
+ ...props
280
+ }) {
281
+ const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
282
+ const variantStyles = {
283
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
284
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
285
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
286
+ ghost: "hover:bg-accent hover:text-accent-foreground",
287
+ link: "text-primary underline-offset-4 hover:underline"
288
+ };
289
+ const sizeStyles = {
290
+ default: "h-10 px-4 py-2",
291
+ sm: "h-9 rounded-md px-3",
292
+ lg: "h-11 rounded-md px-8",
293
+ icon: "h-10 w-10"
294
+ };
295
+ return /* @__PURE__ */ jsxRuntime.jsx(
296
+ "button",
297
+ {
298
+ className: `${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`,
299
+ ...props,
300
+ children
301
+ }
302
+ );
303
+ }
304
+ function Card({ className = "", children, ...props }) {
305
+ return /* @__PURE__ */ jsxRuntime.jsx(
306
+ "div",
307
+ {
308
+ className: `rounded-lg border bg-card text-card-foreground shadow-sm ${className}`,
309
+ ...props,
310
+ children
311
+ }
312
+ );
313
+ }
314
+ function CardHeader({ className = "", children, ...props }) {
315
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex flex-col space-y-1.5 p-6 ${className}`, ...props, children });
316
+ }
317
+ function CardTitle({ className = "", children, ...props }) {
318
+ return /* @__PURE__ */ jsxRuntime.jsx("h3", { className: `text-2xl font-semibold leading-none tracking-tight ${className}`, ...props, children });
319
+ }
320
+ function CardDescription({ className = "", children, ...props }) {
321
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm text-muted-foreground ${className}`, ...props, children });
322
+ }
323
+ function CardContent({ className = "", children, ...props }) {
324
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `p-6 pt-0 ${className}`, ...props, children });
325
+ }
326
+ function PermissionGuard({
327
+ module,
328
+ permission,
329
+ requireModule = true,
330
+ requirePermission = true,
331
+ fallbackPath
332
+ }) {
333
+ const { isLoading, hasModuleAccess, hasPermission } = useUserAccess();
334
+ if (isLoading) {
335
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { className: "size-8" }) });
336
+ }
337
+ const hasModule = hasModuleAccess(module);
338
+ const moduleCheckFailed = requireModule && !hasModule;
339
+ const hasSpecificPermission = permission ? hasPermission(module, permission) : true;
340
+ const permissionCheckFailed = permission && requirePermission && !hasSpecificPermission;
341
+ const accessDenied = moduleCheckFailed || permissionCheckFailed;
342
+ if (accessDenied && fallbackPath) {
343
+ return /* @__PURE__ */ jsxRuntime.jsx(reactRouter.Navigate, { to: fallbackPath, replace: true });
344
+ }
345
+ if (accessDenied) {
346
+ const denialReason = moduleCheckFailed ? {
347
+ title: "Module Access Required",
348
+ description: `Access to the ${module} module is required to view this page`,
349
+ detail: `Your current account permissions do not include access to the ${module} module. This module may be restricted to specific roles or user groups.`
350
+ } : {
351
+ title: "Permission Required",
352
+ description: `The permission "${permission ?? "unknown"}" is needed to access this resource`,
353
+ detail: `Your account has access to the ${module} module but lacks the specific permission required for this action. This permission may need to be assigned to your role or user group.`
354
+ };
355
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center min-h-[calc(100vh-4rem)] p-6", children: /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "w-full max-w-lg", children: [
356
+ /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { className: "text-center pb-4", children: [
357
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-destructive/10", children: moduleCheckFailed ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { className: "size-8 text-destructive" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ShieldAlert, { className: "size-8 text-destructive" }) }),
358
+ /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "text-2xl", children: denialReason.title }),
359
+ /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { className: "text-base", children: denialReason.description })
360
+ ] }),
361
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent, { className: "space-y-4", children: [
362
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg bg-muted p-4", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: denialReason.detail }) }),
363
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:justify-center", children: [
364
+ /* @__PURE__ */ jsxRuntime.jsxs(
365
+ Button,
366
+ {
367
+ variant: "outline",
368
+ onClick: () => {
369
+ window.history.back();
370
+ },
371
+ className: "w-full sm:w-auto",
372
+ children: [
373
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "mr-2 size-4" }),
374
+ "Go Back"
375
+ ]
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsxRuntime.jsx(
379
+ Button,
380
+ {
381
+ variant: "default",
382
+ onClick: () => {
383
+ window.location.href = "/";
384
+ },
385
+ className: "w-full sm:w-auto",
386
+ children: "Return to Dashboard"
387
+ }
388
+ )
389
+ ] })
390
+ ] })
391
+ ] }) });
392
+ }
393
+ return /* @__PURE__ */ jsxRuntime.jsx(reactRouter.Outlet, {});
394
+ }
395
+ var RealtimeClient = class {
396
+ constructor(apiUrl) {
397
+ __publicField(this, "socket", null);
398
+ __publicField(this, "apiUrl");
399
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
400
+ __publicField(this, "isConnected", false);
401
+ __publicField(this, "debug", true);
402
+ // Enable/disable debug logging
403
+ __publicField(this, "connectionPromise", null);
404
+ this.apiUrl = apiUrl ?? getExGuardApiUrl();
405
+ this.log("\u{1F7E2} RealtimeClient initialized", { apiUrl: this.apiUrl });
406
+ }
407
+ log(message, data) {
408
+ if (this.debug) {
409
+ console.log(`[RealtimeClient] ${message}`, data);
410
+ }
411
+ }
412
+ logError(message, error) {
413
+ console.error(`[RealtimeClient] ${message}`, error);
414
+ }
415
+ logWarn(message, data) {
416
+ console.warn(`[RealtimeClient] ${message}`, data);
417
+ }
418
+ /**
419
+ * Connect to the realtime server
420
+ */
421
+ connect(token, userId) {
422
+ if (!token) {
423
+ this.logError("\u274C Cannot connect - token is missing or empty");
424
+ return Promise.reject(new Error("Missing authentication token"));
425
+ }
426
+ if (!userId) {
427
+ this.logError("\u274C Cannot connect - userId is missing or empty");
428
+ return Promise.reject(new Error("Missing userId"));
429
+ }
430
+ if (this.connectionPromise) {
431
+ this.log("\u23F3 Connection already in progress, returning existing promise");
432
+ return this.connectionPromise;
433
+ }
434
+ this.connectionPromise = new Promise((resolve, reject) => {
435
+ try {
436
+ this.log("\u{1F50C} Attempting to connect...", { apiUrl: this.apiUrl, userId, hasToken: !!token });
437
+ if (this.socket?.connected) {
438
+ this.log("\u2705 Already connected");
439
+ this.connectionPromise = null;
440
+ resolve();
441
+ return;
442
+ }
443
+ const socketUrl = `${this.apiUrl}/realtime`;
444
+ this.log("\u{1F680} Creating Socket.IO connection", {
445
+ url: socketUrl,
446
+ transports: ["websocket", "polling"],
447
+ userId
448
+ });
449
+ this.socket = socket_ioClient.io(socketUrl, {
450
+ auth: {
451
+ token,
452
+ userId
453
+ },
454
+ transports: ["websocket", "polling"],
455
+ reconnection: true,
456
+ reconnectionDelay: 1e3,
457
+ reconnectionDelayMax: 5e3,
458
+ reconnectionAttempts: 5
459
+ });
460
+ this.socket.on("connect", () => {
461
+ this.isConnected = true;
462
+ this.log("\u2705 Connected to realtime server", {
463
+ socketId: this.socket?.id,
464
+ userId
465
+ });
466
+ this.connectionPromise = null;
467
+ resolve();
468
+ });
469
+ this.socket.on("disconnect", (reason) => {
470
+ this.isConnected = false;
471
+ this.log("\u274C Disconnected from realtime server", { reason });
472
+ });
473
+ this.socket.on("connect_error", (error) => {
474
+ this.logError("\u274C Connection error (check token and backend)", error);
475
+ this.connectionPromise = null;
476
+ const errorMessage = error instanceof Error ? error.message : String(error);
477
+ reject(new Error(`Connection error: ${errorMessage}`));
478
+ });
479
+ this.socket.on("error", (error) => {
480
+ this.logError("\u274C Realtime error", error);
481
+ this.connectionPromise = null;
482
+ const errorMessage = error instanceof Error ? error.message : String(error);
483
+ reject(new Error(`Socket.IO error: ${errorMessage}`));
484
+ });
485
+ this.socket.on("reconnect_attempt", () => {
486
+ this.log("\u{1F504} Attempting to reconnect...");
487
+ });
488
+ this.socket.on("reconnect", () => {
489
+ this.isConnected = true;
490
+ this.log("\u{1F501} Reconnected to realtime server", {
491
+ socketId: this.socket?.id
492
+ });
493
+ });
494
+ this.socket.on("reconnect_failed", () => {
495
+ this.logError("\u274C Reconnection failed after maximum attempts");
496
+ this.connectionPromise = null;
497
+ reject(new Error("Reconnection failed"));
498
+ });
499
+ this.socket.onAny((eventName, payload) => {
500
+ this.log(`\u{1F4E8} Raw event received`, {
501
+ eventName,
502
+ payloadType: typeof payload,
503
+ payload
504
+ });
505
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
506
+ const eventType = eventName;
507
+ const normalizedPayload = {
508
+ type: eventType,
509
+ timestamp: "timestamp" in payloadObj ? typeof payloadObj.timestamp === "number" ? payloadObj.timestamp : new Date(payloadObj.timestamp).getTime() : Date.now(),
510
+ data: "data" in payloadObj ? payloadObj.data : payload
511
+ };
512
+ this.log(`\u{1F4E8} Event processed: ${eventType}`, {
513
+ type: normalizedPayload.type,
514
+ timestamp: normalizedPayload.timestamp,
515
+ dataKeys: typeof normalizedPayload.data === "object" && normalizedPayload.data !== null ? Object.keys(normalizedPayload.data) : "N/A"
516
+ });
517
+ this.handleEvent(eventType, normalizedPayload);
518
+ });
519
+ } catch (error) {
520
+ this.logError("Failed to initialize connection", error);
521
+ const errorMessage = error instanceof Error ? error.message : String(error);
522
+ reject(new Error(`Connection initialization failed: ${errorMessage}`));
523
+ }
524
+ });
525
+ return this.connectionPromise;
526
+ }
527
+ /**
528
+ * Disconnect from the realtime server
529
+ */
530
+ disconnect() {
531
+ this.log("\u{1F50C} Disconnecting from realtime server...");
532
+ if (this.socket) {
533
+ this.socket.disconnect();
534
+ this.socket = null;
535
+ this.isConnected = false;
536
+ this.log("\u2705 Disconnected");
537
+ }
538
+ }
539
+ /**
540
+ * Check if connected
541
+ */
542
+ getIsConnected() {
543
+ return this.isConnected && this.socket?.connected === true;
544
+ }
545
+ /**
546
+ * Wait for connection to be ready (with timeout)
547
+ */
548
+ async waitForConnection(timeoutMs = 3e4) {
549
+ const startTime = Date.now();
550
+ while (Date.now() - startTime < timeoutMs) {
551
+ if (this.getIsConnected()) {
552
+ this.log("\u2705 Connection ready");
553
+ return;
554
+ }
555
+ await new Promise((resolve) => setTimeout(resolve, 100));
556
+ }
557
+ this.logError("\u274C Timeout waiting for connection", { timeoutMs, elapsed: Date.now() - startTime });
558
+ throw new Error("Connection timeout - failed to establish WebSocket connection");
559
+ }
560
+ /**
561
+ * Subscribe to a specific event type
562
+ */
563
+ subscribe(eventType, handler) {
564
+ if (!this.listeners.has(eventType)) {
565
+ this.listeners.set(eventType, /* @__PURE__ */ new Set());
566
+ }
567
+ this.listeners.get(eventType)?.add(handler);
568
+ const listenerCount = this.listeners.get(eventType)?.size ?? 0;
569
+ this.log(`\u{1F4CC} Subscribed to event: ${eventType}`, { totalListeners: listenerCount });
570
+ return () => {
571
+ const handlers = this.listeners.get(eventType);
572
+ if (handlers) {
573
+ handlers.delete(handler);
574
+ this.log(`\u{1F4CD} Unsubscribed from event: ${eventType}`, {
575
+ remainingListeners: handlers.size
576
+ });
577
+ }
578
+ };
579
+ }
580
+ /**
581
+ * Unsubscribe from an event
582
+ */
583
+ unsubscribe(eventType, handler) {
584
+ const handlers = this.listeners.get(eventType);
585
+ if (handlers) {
586
+ handlers.delete(handler);
587
+ this.log(`\u{1F4CD} Unsubscribed from event: ${eventType}`, {
588
+ remainingListeners: handlers.size
589
+ });
590
+ }
591
+ }
592
+ /**
593
+ * Send a subscription message to the server with connection wait
594
+ */
595
+ async subscribeToChannel(channel) {
596
+ try {
597
+ this.log(`\u{1F4E1} Subscribing to channel: ${channel}`);
598
+ await this.waitForConnection(15e3);
599
+ this.log(`\u{1F4E1} Emitting subscribe event for channel: ${channel}`);
600
+ this.socket?.emit(`subscribe:${channel}`);
601
+ this.log(`\u2705 Subscribed to channel: ${channel}`);
602
+ } catch (error) {
603
+ this.logError(`\u274C Failed to subscribe to channel "${channel}"`, error);
604
+ throw error;
605
+ }
606
+ }
607
+ /**
608
+ * Send an unsubscription message to the server
609
+ */
610
+ unsubscribeFromChannel(channel) {
611
+ if (!this.socket?.connected) {
612
+ this.logWarn(`Cannot unsubscribe from channel "${channel}" - socket not connected`);
613
+ return;
614
+ }
615
+ this.log(`\u{1F4E1} Unsubscribing from channel: ${channel}`);
616
+ this.socket.emit(`unsubscribe:${channel}`);
617
+ }
618
+ /**
619
+ * Handle incoming events
620
+ */
621
+ handleEvent(eventType, payload) {
622
+ const handlers = this.listeners.get(eventType);
623
+ if (handlers && handlers.size > 0) {
624
+ this.log(`\u{1F514} Triggering ${String(handlers.size)} handler(s) for event: ${eventType}`);
625
+ handlers.forEach((handler) => {
626
+ try {
627
+ handler(payload);
628
+ } catch (error) {
629
+ this.logError(`Error in event handler for ${eventType}`, error);
630
+ }
631
+ });
632
+ }
633
+ const wildcardHandlers = this.listeners.get("*");
634
+ if (wildcardHandlers && wildcardHandlers.size > 0) {
635
+ this.log(`\u{1F514} Triggering ${String(wildcardHandlers.size)} wildcard handler(s)`);
636
+ wildcardHandlers.forEach((handler) => {
637
+ try {
638
+ handler(payload);
639
+ } catch (error) {
640
+ this.logError("Error in wildcard event handler", error);
641
+ }
642
+ });
643
+ }
644
+ }
645
+ /**
646
+ * Get all connected listeners count
647
+ */
648
+ getListenerCount(eventType) {
649
+ if (eventType) {
650
+ return this.listeners.get(eventType)?.size ?? 0;
651
+ }
652
+ let total = 0;
653
+ this.listeners.forEach((handlers) => {
654
+ total += handlers.size;
655
+ });
656
+ return total;
657
+ }
658
+ /**
659
+ * Enable or disable debug logging
660
+ */
661
+ setDebug(enabled) {
662
+ this.debug = enabled;
663
+ this.log(`Debug logging ${enabled ? "enabled" : "disabled"}`);
664
+ }
665
+ /**
666
+ * Get connection status summary
667
+ */
668
+ getStatus() {
669
+ const status = {
670
+ isConnected: this.getIsConnected(),
671
+ socketId: this.socket?.id ?? "N/A",
672
+ apiUrl: this.apiUrl,
673
+ totalListeners: this.getListenerCount(),
674
+ listeners: Array.from(this.listeners.entries()).map(([event, handlers]) => ({
675
+ event,
676
+ count: handlers.size
677
+ }))
678
+ };
679
+ this.log("\u{1F4CA} Connection Status", status);
680
+ return status;
681
+ }
682
+ };
683
+ var realtimeClient = new RealtimeClient();
684
+ if (typeof window !== "undefined") {
685
+ const w = window;
686
+ w.__realtimeClient = {
687
+ client: realtimeClient,
688
+ connect: (token, userId) => realtimeClient.connect(token, userId),
689
+ disconnect: () => {
690
+ realtimeClient.disconnect();
691
+ },
692
+ isConnected: () => realtimeClient.getIsConnected(),
693
+ setDebug: (enabled) => {
694
+ realtimeClient.setDebug(enabled);
695
+ },
696
+ status: () => realtimeClient.getStatus(),
697
+ getListenerCount: (eventType) => realtimeClient.getListenerCount(eventType)
698
+ };
699
+ console.log("\u{1F3AF} RealtimeClient debug tools available at: window.__realtimeClient");
700
+ }
701
+
702
+ // src/hooks/use-realtime.ts
703
+ function useRealtimeRbac(eventType, handler) {
704
+ const handlerRef = React.useRef(handler);
705
+ React.useEffect(() => {
706
+ handlerRef.current = handler;
707
+ }, [handler]);
708
+ React.useEffect(() => {
709
+ const unsubscribe = realtimeClient.subscribe(eventType, (payload) => {
710
+ handlerRef.current(payload);
711
+ });
712
+ return () => {
713
+ unsubscribe();
714
+ };
715
+ }, [eventType]);
716
+ }
717
+ function useRealtimeSubscription(channel) {
718
+ React.useEffect(() => {
719
+ let isMounted = true;
720
+ realtimeClient.subscribeToChannel(channel).catch((error) => {
721
+ if (isMounted) {
722
+ console.error(`Failed to subscribe to channel "${channel}":`, error);
723
+ }
724
+ });
725
+ return () => {
726
+ isMounted = false;
727
+ realtimeClient.unsubscribeFromChannel(channel);
728
+ };
729
+ }, [channel]);
730
+ }
731
+ function useRealtimeAll(handler) {
732
+ const handlerRef = React.useRef(handler);
733
+ React.useEffect(() => {
734
+ handlerRef.current = handler;
735
+ }, [handler]);
736
+ React.useEffect(() => {
737
+ const unsubscribe = realtimeClient.subscribe("*", (payload) => {
738
+ handlerRef.current(payload);
739
+ });
740
+ return () => {
741
+ unsubscribe();
742
+ };
743
+ }, []);
744
+ }
745
+ function useExGuardRealtime() {
746
+ const context = React.use(ExGuardRealtimeContext);
747
+ if (!context) {
748
+ throw new Error("useExGuardRealtime must be used within a ExGuardRealtimeProvider");
749
+ }
750
+ return context;
751
+ }
752
+
753
+ // src/hooks/use-exguard-rbac.ts
754
+ function useExGuardRbacChannels() {
755
+ useRealtimeSubscription(EXGUARD_RBAC_CHANNELS.RBAC);
756
+ useRealtimeSubscription(EXGUARD_RBAC_CHANNELS.ROLES);
757
+ useRealtimeSubscription(EXGUARD_RBAC_CHANNELS.PERMISSIONS);
758
+ }
759
+
760
+ // src/utils/exguard-realtime-utils.ts
761
+ async function getCurrentUserId() {
762
+ try {
763
+ const userAccess = await getUserAccess();
764
+ console.log("[ExGuardRealtimeUtils] \u{1F4E1} Fetched user access:", {
765
+ id: userAccess.user.id,
766
+ cognitoSubId: userAccess.user.cognitoSubId,
767
+ email: userAccess.user.email
768
+ });
769
+ return userAccess.user.cognitoSubId;
770
+ } catch (error) {
771
+ console.error("[ExGuardRealtimeUtils] Failed to fetch user:", error);
772
+ return null;
773
+ }
774
+ }
775
+ function ExGuardRealtimeProvider({ children }) {
776
+ const [isConnected, setIsConnected] = React__default.default.useState(false);
777
+ const connectionAttempted = React.useRef(false);
778
+ const rbacRefreshCallbacks = React.useRef(/* @__PURE__ */ new Set());
779
+ const currentUserRef = React.useRef(null);
780
+ const currentUserIdRef = React.useRef(null);
781
+ const registerRbacRefreshCallback = (callback) => {
782
+ rbacRefreshCallbacks.current.add(callback);
783
+ return () => {
784
+ rbacRefreshCallbacks.current.delete(callback);
785
+ };
786
+ };
787
+ const triggerRbacRefresh = async () => {
788
+ console.log("[ExGuardRealtimeProvider] \u{1F504} Triggering RBAC refresh for all listeners", {
789
+ callbackCount: rbacRefreshCallbacks.current.size
790
+ });
791
+ if (rbacRefreshCallbacks.current.size === 0) {
792
+ console.warn("[ExGuardRealtimeProvider] \u26A0\uFE0F No RBAC refresh callbacks registered!");
793
+ return;
794
+ }
795
+ const refreshPromises = Array.from(rbacRefreshCallbacks.current).map((cb, index) => {
796
+ console.log(
797
+ `[ExGuardRealtimeProvider] \u{1F504} Executing callback ${String(index + 1)}/${String(rbacRefreshCallbacks.current.size)}`
798
+ );
799
+ return cb().catch((err) => {
800
+ console.error(`[ExGuardRealtimeProvider] \u274C Error during RBAC refresh callback ${String(index + 1)}:`, err);
801
+ });
802
+ });
803
+ await Promise.all(refreshPromises);
804
+ console.log("[ExGuardRealtimeProvider] \u2705 All RBAC refresh callbacks completed");
805
+ };
806
+ const handleReconnect = async () => {
807
+ if (!currentUserRef.current) {
808
+ console.warn("[ExGuardRealtimeProvider] \u26A0\uFE0F No user credentials available for reconnect");
809
+ return;
810
+ }
811
+ const { token, userId } = currentUserRef.current;
812
+ console.log("[ExGuardRealtimeProvider] \u{1F504} Manual reconnect requested for", { userId });
813
+ await handleConnect(token, userId);
814
+ };
815
+ const handleRefreshRbac = async () => {
816
+ console.log("[ExGuardRealtimeProvider] \u{1F504} Manual RBAC refresh requested");
817
+ await triggerRbacRefresh();
818
+ };
819
+ React.useEffect(() => {
820
+ const accessToken = localStorage.getItem("access_token");
821
+ console.log("[ExGuardRealtimeProvider] \u{1F50D} Checking credentials on mount:", {
822
+ hasAccessToken: Boolean(accessToken),
823
+ tokenLength: accessToken?.length
824
+ });
825
+ if (!accessToken) {
826
+ console.warn("[ExGuardRealtimeProvider] \u26A0\uFE0F No access token found in localStorage");
827
+ return;
828
+ }
829
+ if (connectionAttempted.current) {
830
+ return;
831
+ }
832
+ connectionAttempted.current = true;
833
+ const initializeConnection = async () => {
834
+ try {
835
+ const userId = await getCurrentUserId();
836
+ if (!userId) {
837
+ console.error("[ExGuardRealtimeProvider] \u274C Failed to get userId from backend");
838
+ return;
839
+ }
840
+ console.log("[ExGuardRealtimeProvider] \u{1F680} Initiating realtime connection", {
841
+ userId
842
+ });
843
+ currentUserRef.current = { token: accessToken, userId };
844
+ currentUserIdRef.current = userId;
845
+ await realtimeClient.connect(accessToken, userId);
846
+ console.log("[ExGuardRealtimeProvider] \u2705 Auto-connection successful");
847
+ setIsConnected(true);
848
+ console.log("[ExGuardRealtimeProvider] \u{1F3A7} Setting up INDIVIDUAL user notification listener");
849
+ realtimeClient.subscribe("user:access-changed", (payload) => {
850
+ const { userId: eventUserId, roleId, action } = payload.data;
851
+ console.log("[ExGuardRealtimeProvider] \u{1F3AF} INDIVIDUAL: Your access has been changed!", {
852
+ eventUserId,
853
+ roleId,
854
+ action,
855
+ currentUserId: currentUserIdRef.current
856
+ });
857
+ console.log("[ExGuardRealtimeProvider] \u2705 Event received in our personal room! Refreshing access...");
858
+ console.log(
859
+ `[ExGuardRealtimeProvider] \u{1F4E2} Triggering RBAC refresh for ${String(rbacRefreshCallbacks.current.size)} registered callbacks`
860
+ );
861
+ void triggerRbacRefresh();
862
+ });
863
+ console.log("[ExGuardRealtimeProvider] \u2705 Individual user event listener registered");
864
+ } catch (error) {
865
+ console.error("[ExGuardRealtimeProvider] \u274C Failed to establish realtime connection:");
866
+ const errorMessage = error instanceof Error ? error.message : String(error);
867
+ const tokenStr = accessToken ? accessToken.substring(0, 20) + "..." : "";
868
+ console.error("[ExGuardRealtimeProvider] Error details:", {
869
+ message: errorMessage,
870
+ token: tokenStr
871
+ });
872
+ setIsConnected(false);
873
+ }
874
+ };
875
+ void initializeConnection();
876
+ let lastConnectedState = realtimeClient.getIsConnected();
877
+ const interval = setInterval(() => {
878
+ const currentConnectedState = realtimeClient.getIsConnected();
879
+ if (currentConnectedState !== lastConnectedState) {
880
+ console.log("[ExGuardRealtimeProvider] \u{1F504} Connection status changed:", {
881
+ from: lastConnectedState,
882
+ to: currentConnectedState
883
+ });
884
+ lastConnectedState = currentConnectedState;
885
+ setIsConnected(currentConnectedState);
886
+ }
887
+ }, 1e3);
888
+ return () => {
889
+ clearInterval(interval);
890
+ };
891
+ }, []);
892
+ const handleConnect = async (token, userId) => {
893
+ try {
894
+ console.log("[ExGuardRealtimeProvider] \u{1F50C} Manual connection requested", { userId });
895
+ currentUserRef.current = { token, userId };
896
+ await realtimeClient.connect(token, userId);
897
+ setIsConnected(true);
898
+ console.log("[ExGuardRealtimeProvider] \u2705 Manual connection successful");
899
+ } catch (error) {
900
+ console.error("[ExGuardRealtimeProvider] \u274C Failed to connect to realtime:", error);
901
+ setIsConnected(false);
902
+ throw error;
903
+ }
904
+ };
905
+ const handleDisconnect = () => {
906
+ console.log("[ExGuardRealtimeProvider] \u{1F50C} Disconnection requested");
907
+ realtimeClient.disconnect();
908
+ setIsConnected(false);
909
+ };
910
+ const value = {
911
+ isConnected,
912
+ connect: handleConnect,
913
+ disconnect: handleDisconnect,
914
+ reconnect: handleReconnect,
915
+ refreshRbac: handleRefreshRbac,
916
+ registerRbacRefreshCallback
917
+ };
918
+ return /* @__PURE__ */ jsxRuntime.jsx(ExGuardRealtimeContext, { value, children });
919
+ }
920
+ function useExGuardRealtimeSubscription(channel = "RBAC", options = {}) {
921
+ const { registerRbacRefreshCallback } = useExGuardRealtime();
922
+ const { autoSubscribe = true, events } = options;
923
+ const channelName = EXGUARD_RBAC_CHANNELS[channel];
924
+ React.useEffect(() => {
925
+ if (!autoSubscribe) {
926
+ return;
927
+ }
928
+ console.log(`[useExGuardRealtimeSubscription] \u{1F4E1} Subscribing to channel: ${channelName}`);
929
+ void realtimeClient.subscribeToChannel(channelName);
930
+ if (events && events.length > 0) {
931
+ const unsubscribers2 = [];
932
+ events.forEach((eventType) => {
933
+ const unsubscribe = realtimeClient.subscribe(eventType, () => {
934
+ console.log(`[useExGuardRealtimeSubscription] \u{1F514} Received event: ${eventType}`);
935
+ });
936
+ unsubscribers2.push(unsubscribe);
937
+ });
938
+ return () => {
939
+ console.log(`[useExGuardRealtimeSubscription] \u{1F9F9} Cleaning up subscriptions for channel: ${channelName}`);
940
+ unsubscribers2.forEach((unsubscribe) => {
941
+ unsubscribe();
942
+ });
943
+ realtimeClient.unsubscribeFromChannel(channelName);
944
+ };
945
+ }
946
+ const defaultRbacEvents = [
947
+ EXGUARD_RBAC_EVENTS.ROLE_CREATED,
948
+ EXGUARD_RBAC_EVENTS.ROLE_UPDATED,
949
+ EXGUARD_RBAC_EVENTS.ROLE_DELETED,
950
+ EXGUARD_RBAC_EVENTS.PERMISSION_CREATED,
951
+ EXGUARD_RBAC_EVENTS.PERMISSION_UPDATED,
952
+ EXGUARD_RBAC_EVENTS.PERMISSION_DELETED,
953
+ EXGUARD_RBAC_EVENTS.ROLE_PERMISSION_ASSIGNED,
954
+ EXGUARD_RBAC_EVENTS.ROLE_PERMISSION_REMOVED,
955
+ EXGUARD_RBAC_EVENTS.USER_ONLINE,
956
+ EXGUARD_RBAC_EVENTS.GROUP_UPDATED
957
+ ];
958
+ const unsubscribers = [];
959
+ defaultRbacEvents.forEach((eventType) => {
960
+ const unsubscribe = realtimeClient.subscribe(eventType, () => {
961
+ console.log(`[useExGuardRealtimeSubscription] \u{1F514} Received event: ${eventType}`);
962
+ });
963
+ unsubscribers.push(unsubscribe);
964
+ });
965
+ const unregisterCallback = registerRbacRefreshCallback(() => {
966
+ console.log(`[useExGuardRealtimeSubscription] \u{1F504} RBAC refresh triggered for channel: ${channelName}`);
967
+ return Promise.resolve();
968
+ });
969
+ return () => {
970
+ console.log(`[useExGuardRealtimeSubscription] \u{1F9F9} Cleaning up subscriptions for channel: ${channelName}`);
971
+ unsubscribers.forEach((unsubscribe) => {
972
+ unsubscribe();
973
+ });
974
+ unregisterCallback();
975
+ realtimeClient.unsubscribeFromChannel(channelName);
976
+ };
977
+ }, [channelName, autoSubscribe, events, registerRbacRefreshCallback]);
978
+ }
979
+
980
+ exports.DEFAULT_EXGUARD_CONFIG = DEFAULT_EXGUARD_CONFIG;
981
+ exports.EXGUARD_RBAC_CHANNELS = EXGUARD_RBAC_CHANNELS;
982
+ exports.EXGUARD_RBAC_EVENTS = EXGUARD_RBAC_EVENTS;
983
+ exports.EXGUARD_STORAGE_KEYS = EXGUARD_STORAGE_KEYS;
984
+ exports.ExGuardRealtimeContext = ExGuardRealtimeContext;
985
+ exports.ExGuardRealtimeProvider = ExGuardRealtimeProvider;
986
+ exports.PermissionGuard = PermissionGuard;
987
+ exports.RBAC_RECONNECT_DELAY = RBAC_RECONNECT_DELAY;
988
+ exports.getCurrentUserId = getCurrentUserId;
989
+ exports.getExGuardApiUrl = getExGuardApiUrl;
990
+ exports.getUserAccess = getUserAccess;
991
+ exports.realtimeClient = realtimeClient;
992
+ exports.setExGuardConfig = setExGuardConfig;
993
+ exports.useExGuardRbacChannels = useExGuardRbacChannels;
994
+ exports.useExGuardRealtime = useExGuardRealtime;
995
+ exports.useExGuardRealtimeSubscription = useExGuardRealtimeSubscription;
996
+ exports.useRealtimeAll = useRealtimeAll;
997
+ exports.useRealtimeRbac = useRealtimeRbac;
998
+ exports.useRealtimeSubscription = useRealtimeSubscription;
999
+ exports.useUserAccess = useUserAccess;
1000
+ exports.verifyToken = verifyToken;
1001
+ //# sourceMappingURL=index.cjs.map
1002
+ //# sourceMappingURL=index.cjs.map