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/IMPLEMENTATION_GUIDE.md +609 -0
- package/README.md +332 -0
- package/dist/index.cjs +1002 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +438 -0
- package/dist/index.d.ts +438 -0
- package/dist/index.js +975 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/scripts/setup.js +197 -0
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
|