antaeus.keycloak.react 1.0.7 → 2.1.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/README.md +135 -706
- package/dist/index.d.mts +307 -11
- package/dist/index.d.ts +307 -11
- package/dist/index.js +765 -132
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +763 -135
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -17,14 +17,20 @@ var defaultConfig = {
|
|
|
17
17
|
scope: "openid profile email offline_access",
|
|
18
18
|
features: {
|
|
19
19
|
deviceFlow: false,
|
|
20
|
-
sessionMonitor:
|
|
20
|
+
sessionMonitor: true,
|
|
21
21
|
autoRefresh: true
|
|
22
22
|
},
|
|
23
23
|
silentRenew: {
|
|
24
24
|
enabled: true,
|
|
25
25
|
silentRedirectUri: typeof window !== "undefined" ? window.location.origin + "/silent-renew.html" : "/silent-renew.html",
|
|
26
|
-
thresholdSeconds: 300
|
|
26
|
+
thresholdSeconds: 300,
|
|
27
27
|
// 5 minutes
|
|
28
|
+
refreshStrategy: "both",
|
|
29
|
+
// Check on startup AND retry on 401
|
|
30
|
+
maxRefreshTokenLifetime: 2592e3,
|
|
31
|
+
// 30 days in seconds
|
|
32
|
+
showLoadingIndicator: false,
|
|
33
|
+
loadingMessage: "Refreshing session..."
|
|
28
34
|
},
|
|
29
35
|
advanced: {
|
|
30
36
|
responseType: "code",
|
|
@@ -56,7 +62,11 @@ var KeycloakConfigBuilder = class {
|
|
|
56
62
|
scope: config.scope,
|
|
57
63
|
automaticSilentRenew: config.silentRenew.enabled,
|
|
58
64
|
loadUserInfo: config.advanced.loadUserInfo,
|
|
59
|
-
silent_redirect_uri: config.silentRenew.silentRedirectUri
|
|
65
|
+
silent_redirect_uri: config.silentRenew.silentRedirectUri,
|
|
66
|
+
// Add PKCE support (required for public clients)
|
|
67
|
+
...config.advanced.codeChallengeMethod && {
|
|
68
|
+
response_mode: "query"
|
|
69
|
+
}
|
|
60
70
|
};
|
|
61
71
|
return oidcSettings;
|
|
62
72
|
}
|
|
@@ -128,16 +138,711 @@ function getConfigFromEnv() {
|
|
|
128
138
|
}
|
|
129
139
|
};
|
|
130
140
|
}
|
|
131
|
-
var
|
|
141
|
+
var useAuth = reactOidcContext.useAuth;
|
|
142
|
+
|
|
143
|
+
// src/hooks/useTokenLifecycle.ts
|
|
144
|
+
var STORAGE_KEY_PREFIX = "keycloak_first_login_";
|
|
145
|
+
function useTokenLifecycle(options) {
|
|
146
|
+
const {
|
|
147
|
+
config,
|
|
148
|
+
debug = false,
|
|
149
|
+
onRefreshStart,
|
|
150
|
+
onRefreshSuccess,
|
|
151
|
+
onRefreshError,
|
|
152
|
+
onRefreshTokenExpired
|
|
153
|
+
} = options;
|
|
154
|
+
const auth = useAuth();
|
|
155
|
+
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
156
|
+
const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
|
|
157
|
+
const hasCheckedOnStartup = react.useRef(false);
|
|
158
|
+
const log = react.useCallback(
|
|
159
|
+
(message, ...args) => {
|
|
160
|
+
if (debug) {
|
|
161
|
+
console.log("[TokenLifecycle]", message, ...args);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[debug]
|
|
165
|
+
);
|
|
166
|
+
const getStorageKey = react.useCallback(() => {
|
|
167
|
+
const authority = config.authority;
|
|
168
|
+
const clientId = config.clientId;
|
|
169
|
+
return `${STORAGE_KEY_PREFIX}${authority}_${clientId}`;
|
|
170
|
+
}, [config.authority, config.clientId]);
|
|
171
|
+
const storeFirstLoginTime = react.useCallback(() => {
|
|
172
|
+
const key = getStorageKey();
|
|
173
|
+
const existingTime = localStorage.getItem(key);
|
|
174
|
+
if (!existingTime) {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
localStorage.setItem(key, now.toString());
|
|
177
|
+
log("Stored first login time:", new Date(now).toISOString());
|
|
178
|
+
}
|
|
179
|
+
}, [getStorageKey, log]);
|
|
180
|
+
const checkRefreshTokenLifetime = react.useCallback(() => {
|
|
181
|
+
const maxLifetime = config.silentRenew?.maxRefreshTokenLifetime;
|
|
182
|
+
if (!maxLifetime || maxLifetime <= 0) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const key = getStorageKey();
|
|
186
|
+
const firstLoginStr = localStorage.getItem(key);
|
|
187
|
+
if (!firstLoginStr) {
|
|
188
|
+
storeFirstLoginTime();
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const firstLoginTime = parseInt(firstLoginStr, 10);
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const elapsedSeconds = (now - firstLoginTime) / 1e3;
|
|
194
|
+
log("Refresh token lifetime check:", {
|
|
195
|
+
firstLogin: new Date(firstLoginTime).toISOString(),
|
|
196
|
+
elapsedSeconds,
|
|
197
|
+
maxLifetime,
|
|
198
|
+
isExpired: elapsedSeconds >= maxLifetime
|
|
199
|
+
});
|
|
200
|
+
return elapsedSeconds >= maxLifetime;
|
|
201
|
+
}, [config.silentRenew?.maxRefreshTokenLifetime, getStorageKey, log, storeFirstLoginTime]);
|
|
202
|
+
const refreshToken = react.useCallback(async () => {
|
|
203
|
+
if (isRefreshing) {
|
|
204
|
+
log("Refresh already in progress, skipping");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (checkRefreshTokenLifetime()) {
|
|
208
|
+
log("Refresh token exceeded max lifetime, cannot refresh");
|
|
209
|
+
setIsRefreshTokenExpired(true);
|
|
210
|
+
onRefreshTokenExpired?.();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
setIsRefreshing(true);
|
|
215
|
+
onRefreshStart?.();
|
|
216
|
+
log("Starting token refresh");
|
|
217
|
+
await auth.signinSilent();
|
|
218
|
+
log("Token refresh successful");
|
|
219
|
+
onRefreshSuccess?.();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
222
|
+
log("Token refresh failed:", err.message);
|
|
223
|
+
onRefreshError?.(err);
|
|
224
|
+
throw err;
|
|
225
|
+
} finally {
|
|
226
|
+
setIsRefreshing(false);
|
|
227
|
+
}
|
|
228
|
+
}, [
|
|
229
|
+
isRefreshing,
|
|
230
|
+
auth,
|
|
231
|
+
checkRefreshTokenLifetime,
|
|
232
|
+
onRefreshStart,
|
|
233
|
+
onRefreshSuccess,
|
|
234
|
+
onRefreshError,
|
|
235
|
+
onRefreshTokenExpired,
|
|
236
|
+
log
|
|
237
|
+
]);
|
|
238
|
+
const checkAndRefreshTokens = react.useCallback(async () => {
|
|
239
|
+
if (!auth.user) {
|
|
240
|
+
log("No user session, skipping token check");
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (checkRefreshTokenLifetime()) {
|
|
244
|
+
log("Refresh token exceeded max lifetime");
|
|
245
|
+
setIsRefreshTokenExpired(true);
|
|
246
|
+
onRefreshTokenExpired?.();
|
|
247
|
+
const key = getStorageKey();
|
|
248
|
+
localStorage.removeItem(key);
|
|
249
|
+
try {
|
|
250
|
+
await auth.removeUser();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
log("Error removing user after max lifetime:", error);
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const expiresAt = auth.user.expires_at;
|
|
257
|
+
if (!expiresAt) {
|
|
258
|
+
log("No expiration time found, skipping token check");
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
262
|
+
const threshold = config.silentRenew?.thresholdSeconds || 300;
|
|
263
|
+
const timeUntilExpiry = expiresAt - now;
|
|
264
|
+
log("Token expiration check:", {
|
|
265
|
+
expiresAt: new Date(expiresAt * 1e3).toISOString(),
|
|
266
|
+
now: new Date(now * 1e3).toISOString(),
|
|
267
|
+
timeUntilExpiry,
|
|
268
|
+
threshold,
|
|
269
|
+
needsRefresh: timeUntilExpiry <= threshold
|
|
270
|
+
});
|
|
271
|
+
if (timeUntilExpiry <= threshold) {
|
|
272
|
+
try {
|
|
273
|
+
await refreshToken();
|
|
274
|
+
return true;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
log("Failed to refresh tokens:", error);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
log("Tokens are still valid, no refresh needed");
|
|
281
|
+
return false;
|
|
282
|
+
}, [
|
|
283
|
+
auth,
|
|
284
|
+
config.silentRenew?.thresholdSeconds,
|
|
285
|
+
checkRefreshTokenLifetime,
|
|
286
|
+
refreshToken,
|
|
287
|
+
onRefreshTokenExpired,
|
|
288
|
+
getStorageKey,
|
|
289
|
+
log
|
|
290
|
+
]);
|
|
291
|
+
react.useEffect(() => {
|
|
292
|
+
const strategy = config.silentRenew?.refreshStrategy || "both";
|
|
293
|
+
const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
|
|
294
|
+
if (!shouldCheckOnStartup || hasCheckedOnStartup.current) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!auth.isAuthenticated || auth.isLoading) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
hasCheckedOnStartup.current = true;
|
|
301
|
+
log("Running startup token check with strategy:", strategy);
|
|
302
|
+
checkAndRefreshTokens().catch((error) => {
|
|
303
|
+
log("Startup token check failed:", error);
|
|
304
|
+
});
|
|
305
|
+
}, [
|
|
306
|
+
auth.isAuthenticated,
|
|
307
|
+
auth.isLoading,
|
|
308
|
+
config.silentRenew?.refreshStrategy,
|
|
309
|
+
checkAndRefreshTokens,
|
|
310
|
+
log
|
|
311
|
+
]);
|
|
312
|
+
react.useEffect(() => {
|
|
313
|
+
if (auth.user && !auth.isLoading) {
|
|
314
|
+
storeFirstLoginTime();
|
|
315
|
+
}
|
|
316
|
+
}, [auth.user, auth.isLoading, storeFirstLoginTime]);
|
|
317
|
+
react.useEffect(() => {
|
|
318
|
+
if (!auth.isAuthenticated || auth.isLoading) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const maxLifetime = config.silentRenew?.maxRefreshTokenLifetime;
|
|
322
|
+
if (!maxLifetime || maxLifetime <= 0) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const intervalId = setInterval(() => {
|
|
326
|
+
if (checkRefreshTokenLifetime()) {
|
|
327
|
+
log("Refresh token expired during periodic check");
|
|
328
|
+
setIsRefreshTokenExpired(true);
|
|
329
|
+
onRefreshTokenExpired?.();
|
|
330
|
+
const key = getStorageKey();
|
|
331
|
+
localStorage.removeItem(key);
|
|
332
|
+
auth.removeUser().catch((error) => {
|
|
333
|
+
log("Error removing user during periodic check:", error);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}, 5 * 60 * 1e3);
|
|
337
|
+
return () => clearInterval(intervalId);
|
|
338
|
+
}, [
|
|
339
|
+
auth,
|
|
340
|
+
config.silentRenew?.maxRefreshTokenLifetime,
|
|
341
|
+
checkRefreshTokenLifetime,
|
|
342
|
+
onRefreshTokenExpired,
|
|
343
|
+
getStorageKey,
|
|
344
|
+
log
|
|
345
|
+
]);
|
|
346
|
+
return {
|
|
347
|
+
isRefreshing,
|
|
348
|
+
isRefreshTokenExpired,
|
|
349
|
+
refreshToken,
|
|
350
|
+
checkAndRefreshTokens
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
var TokenLifecycleManager = ({
|
|
132
354
|
config,
|
|
133
355
|
children,
|
|
134
356
|
debug = false
|
|
135
357
|
}) => {
|
|
358
|
+
const [showLoading, setShowLoading] = react.useState(false);
|
|
359
|
+
const handleRefreshStart = react.useCallback(() => {
|
|
360
|
+
if (config.silentRenew?.showLoadingIndicator) {
|
|
361
|
+
setShowLoading(true);
|
|
362
|
+
}
|
|
363
|
+
}, [config.silentRenew?.showLoadingIndicator]);
|
|
364
|
+
const handleRefreshSuccess = react.useCallback(() => {
|
|
365
|
+
setShowLoading(false);
|
|
366
|
+
}, []);
|
|
367
|
+
const handleRefreshError = react.useCallback((error) => {
|
|
368
|
+
setShowLoading(false);
|
|
369
|
+
if (debug) {
|
|
370
|
+
console.error("[TokenLifecycleManager] Token refresh failed:", error);
|
|
371
|
+
}
|
|
372
|
+
config.events?.onSilentRenewError?.(error);
|
|
373
|
+
}, [config.events, debug]);
|
|
374
|
+
const handleRefreshTokenExpired = react.useCallback(() => {
|
|
375
|
+
setShowLoading(false);
|
|
376
|
+
if (debug) {
|
|
377
|
+
console.warn("[TokenLifecycleManager] Refresh token exceeded max lifetime");
|
|
378
|
+
}
|
|
379
|
+
config.events?.onTokenExpired?.();
|
|
380
|
+
}, [config.events, debug]);
|
|
381
|
+
const { isRefreshing, isRefreshTokenExpired } = useTokenLifecycle({
|
|
382
|
+
config,
|
|
383
|
+
debug,
|
|
384
|
+
onRefreshStart: handleRefreshStart,
|
|
385
|
+
onRefreshSuccess: handleRefreshSuccess,
|
|
386
|
+
onRefreshError: handleRefreshError,
|
|
387
|
+
onRefreshTokenExpired: handleRefreshTokenExpired
|
|
388
|
+
});
|
|
389
|
+
if (isRefreshTokenExpired) {
|
|
390
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
391
|
+
"div",
|
|
392
|
+
{
|
|
393
|
+
style: {
|
|
394
|
+
display: "flex",
|
|
395
|
+
flexDirection: "column",
|
|
396
|
+
alignItems: "center",
|
|
397
|
+
justifyContent: "center",
|
|
398
|
+
height: "100vh",
|
|
399
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
400
|
+
backgroundColor: "#f5f5f5",
|
|
401
|
+
padding: "20px",
|
|
402
|
+
textAlign: "center"
|
|
403
|
+
},
|
|
404
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
405
|
+
"div",
|
|
406
|
+
{
|
|
407
|
+
style: {
|
|
408
|
+
backgroundColor: "white",
|
|
409
|
+
padding: "40px",
|
|
410
|
+
borderRadius: "8px",
|
|
411
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
|
412
|
+
maxWidth: "400px"
|
|
413
|
+
},
|
|
414
|
+
children: [
|
|
415
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 16px 0", color: "#333" }, children: "Session Expired" }),
|
|
416
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 24px 0", color: "#666" }, children: "Your session has been inactive for too long. Please sign in again to continue." }),
|
|
417
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
418
|
+
"button",
|
|
419
|
+
{
|
|
420
|
+
onClick: () => window.location.reload(),
|
|
421
|
+
style: {
|
|
422
|
+
padding: "12px 24px",
|
|
423
|
+
backgroundColor: "#007bff",
|
|
424
|
+
color: "white",
|
|
425
|
+
border: "none",
|
|
426
|
+
borderRadius: "4px",
|
|
427
|
+
fontSize: "16px",
|
|
428
|
+
cursor: "pointer",
|
|
429
|
+
fontWeight: "500"
|
|
430
|
+
},
|
|
431
|
+
children: "Sign In Again"
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (showLoading && isRefreshing) {
|
|
441
|
+
const message = config.silentRenew?.loadingMessage || "Refreshing session...";
|
|
442
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
443
|
+
"div",
|
|
444
|
+
{
|
|
445
|
+
style: {
|
|
446
|
+
display: "flex",
|
|
447
|
+
flexDirection: "column",
|
|
448
|
+
alignItems: "center",
|
|
449
|
+
justifyContent: "center",
|
|
450
|
+
height: "100vh",
|
|
451
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
452
|
+
backgroundColor: "#f5f5f5"
|
|
453
|
+
},
|
|
454
|
+
children: [
|
|
455
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
456
|
+
"div",
|
|
457
|
+
{
|
|
458
|
+
style: {
|
|
459
|
+
display: "flex",
|
|
460
|
+
flexDirection: "column",
|
|
461
|
+
alignItems: "center",
|
|
462
|
+
gap: "16px"
|
|
463
|
+
},
|
|
464
|
+
children: [
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
466
|
+
"div",
|
|
467
|
+
{
|
|
468
|
+
style: {
|
|
469
|
+
width: "40px",
|
|
470
|
+
height: "40px",
|
|
471
|
+
border: "4px solid #e0e0e0",
|
|
472
|
+
borderTop: "4px solid #007bff",
|
|
473
|
+
borderRadius: "50%",
|
|
474
|
+
animation: "spin 1s linear infinite"
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
),
|
|
478
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, color: "#666", fontSize: "16px" }, children: message })
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
),
|
|
482
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `
|
|
483
|
+
@keyframes spin {
|
|
484
|
+
0% { transform: rotate(0deg); }
|
|
485
|
+
100% { transform: rotate(360deg); }
|
|
486
|
+
}
|
|
487
|
+
` })
|
|
488
|
+
]
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
493
|
+
};
|
|
494
|
+
var SessionMonitor = ({
|
|
495
|
+
position = "top-right",
|
|
496
|
+
warningThreshold = 300,
|
|
497
|
+
// 5 minutes
|
|
498
|
+
criticalThreshold = 60,
|
|
499
|
+
// 1 minute
|
|
500
|
+
showProgressBar = true,
|
|
501
|
+
minimizable = true,
|
|
502
|
+
startMinimized = false,
|
|
503
|
+
className = "",
|
|
504
|
+
onWarning,
|
|
505
|
+
onCritical,
|
|
506
|
+
onExpired,
|
|
507
|
+
style
|
|
508
|
+
}) => {
|
|
509
|
+
const auth = useAuth();
|
|
510
|
+
const [timeRemaining, setTimeRemaining] = react.useState(null);
|
|
511
|
+
const [initialTimeRemaining, setInitialTimeRemaining] = react.useState(null);
|
|
512
|
+
const [minimized, setMinimized] = react.useState(startMinimized);
|
|
513
|
+
const [warningTriggered, setWarningTriggered] = react.useState(false);
|
|
514
|
+
const [criticalTriggered, setCriticalTriggered] = react.useState(false);
|
|
515
|
+
react.useEffect(() => {
|
|
516
|
+
if (!auth.isAuthenticated || !auth.user?.expires_at) {
|
|
517
|
+
setTimeRemaining(null);
|
|
518
|
+
setInitialTimeRemaining(null);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let logoutTimeoutId = null;
|
|
522
|
+
const updateTimeRemaining = () => {
|
|
523
|
+
const expiresAt = auth.user.expires_at;
|
|
524
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
525
|
+
const remaining = expiresAt - now;
|
|
526
|
+
if (initialTimeRemaining === null && remaining > 0) {
|
|
527
|
+
setInitialTimeRemaining(remaining);
|
|
528
|
+
}
|
|
529
|
+
if (remaining <= 0) {
|
|
530
|
+
setTimeRemaining(0);
|
|
531
|
+
onExpired?.();
|
|
532
|
+
logoutTimeoutId = setTimeout(() => {
|
|
533
|
+
auth.signoutRedirect();
|
|
534
|
+
}, 2e3);
|
|
535
|
+
} else {
|
|
536
|
+
setTimeRemaining(remaining);
|
|
537
|
+
if (remaining <= criticalThreshold && !criticalTriggered) {
|
|
538
|
+
setCriticalTriggered(true);
|
|
539
|
+
onCritical?.(remaining);
|
|
540
|
+
} else if (remaining <= warningThreshold && !warningTriggered) {
|
|
541
|
+
setWarningTriggered(true);
|
|
542
|
+
onWarning?.(remaining);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
updateTimeRemaining();
|
|
547
|
+
const interval = setInterval(updateTimeRemaining, 1e3);
|
|
548
|
+
return () => {
|
|
549
|
+
clearInterval(interval);
|
|
550
|
+
if (logoutTimeoutId) {
|
|
551
|
+
clearTimeout(logoutTimeoutId);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}, [
|
|
555
|
+
auth,
|
|
556
|
+
warningThreshold,
|
|
557
|
+
criticalThreshold,
|
|
558
|
+
warningTriggered,
|
|
559
|
+
criticalTriggered,
|
|
560
|
+
initialTimeRemaining,
|
|
561
|
+
onWarning,
|
|
562
|
+
onCritical,
|
|
563
|
+
onExpired
|
|
564
|
+
]);
|
|
565
|
+
react.useEffect(() => {
|
|
566
|
+
if (timeRemaining && timeRemaining > warningThreshold) {
|
|
567
|
+
setWarningTriggered(false);
|
|
568
|
+
setCriticalTriggered(false);
|
|
569
|
+
}
|
|
570
|
+
}, [timeRemaining, warningThreshold]);
|
|
571
|
+
const formatTime = react.useCallback((seconds) => {
|
|
572
|
+
const mins = Math.floor(seconds / 60);
|
|
573
|
+
const secs = seconds % 60;
|
|
574
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
575
|
+
}, []);
|
|
576
|
+
const getStatusClass = react.useCallback(() => {
|
|
577
|
+
if (!timeRemaining) return "normal";
|
|
578
|
+
if (timeRemaining <= criticalThreshold) return "critical";
|
|
579
|
+
if (timeRemaining <= warningThreshold) return "warning";
|
|
580
|
+
return "normal";
|
|
581
|
+
}, [timeRemaining, warningThreshold, criticalThreshold]);
|
|
582
|
+
const getProgress = react.useCallback(() => {
|
|
583
|
+
if (!timeRemaining || !initialTimeRemaining) return 100;
|
|
584
|
+
return Math.max(0, Math.min(100, timeRemaining / initialTimeRemaining * 100));
|
|
585
|
+
}, [timeRemaining, initialTimeRemaining]);
|
|
586
|
+
if (!auth.isAuthenticated || timeRemaining === null) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const baseClass = "kc-session-monitor";
|
|
590
|
+
const positionClass = `kc-session-monitor--${position}`;
|
|
591
|
+
const statusClass = `kc-session-monitor--${getStatusClass()}`;
|
|
592
|
+
const minimizedClass = minimized ? "kc-session-monitor--minimized" : "";
|
|
593
|
+
const classes = `${baseClass} ${positionClass} ${statusClass} ${minimizedClass} ${className}`.trim();
|
|
594
|
+
const progress = getProgress();
|
|
595
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes, style, children: [
|
|
596
|
+
minimizable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
597
|
+
"button",
|
|
598
|
+
{
|
|
599
|
+
className: "kc-session-monitor__toggle",
|
|
600
|
+
onClick: () => setMinimized(!minimized),
|
|
601
|
+
"aria-label": minimized ? "Expand session monitor" : "Minimize session monitor",
|
|
602
|
+
title: minimized ? "Expand" : "Minimize",
|
|
603
|
+
children: minimized ? "\u25B2" : "\u25BC"
|
|
604
|
+
}
|
|
605
|
+
),
|
|
606
|
+
!minimized && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
607
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-session-monitor__content", children: [
|
|
608
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__label", children: timeRemaining === 0 ? "Session expired" : "Session expires in:" }),
|
|
609
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__time", children: formatTime(timeRemaining) })
|
|
610
|
+
] }),
|
|
611
|
+
showProgressBar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-session-monitor__progress-bar", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
612
|
+
"div",
|
|
613
|
+
{
|
|
614
|
+
className: "kc-session-monitor__progress-fill",
|
|
615
|
+
style: { width: `${progress}%` },
|
|
616
|
+
role: "progressbar",
|
|
617
|
+
"aria-valuenow": progress,
|
|
618
|
+
"aria-valuemin": 0,
|
|
619
|
+
"aria-valuemax": 100
|
|
620
|
+
}
|
|
621
|
+
) })
|
|
622
|
+
] })
|
|
623
|
+
] });
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/config/presets.ts
|
|
627
|
+
var CONFIG_PRESETS = {
|
|
628
|
+
/**
|
|
629
|
+
* Default preset - Balanced configuration for most applications
|
|
630
|
+
*
|
|
631
|
+
* Features:
|
|
632
|
+
* - 30-day refresh token lifetime
|
|
633
|
+
* - Proactive token refresh on startup + reactive on 401
|
|
634
|
+
* - Silent background token refresh
|
|
635
|
+
* - Session monitoring enabled
|
|
636
|
+
* - Auto token refresh enabled
|
|
637
|
+
*
|
|
638
|
+
* Best for: Most web applications, dashboards, internal tools
|
|
639
|
+
*/
|
|
640
|
+
default: {
|
|
641
|
+
scope: "openid profile email offline_access",
|
|
642
|
+
features: {
|
|
643
|
+
autoRefresh: true,
|
|
644
|
+
sessionMonitor: true,
|
|
645
|
+
deviceFlow: false
|
|
646
|
+
},
|
|
647
|
+
silentRenew: {
|
|
648
|
+
enabled: true,
|
|
649
|
+
thresholdSeconds: 300,
|
|
650
|
+
// 5 minutes
|
|
651
|
+
refreshStrategy: "both",
|
|
652
|
+
maxRefreshTokenLifetime: 2592e3,
|
|
653
|
+
// 30 days
|
|
654
|
+
showLoadingIndicator: false,
|
|
655
|
+
loadingMessage: "Refreshing session..."
|
|
656
|
+
},
|
|
657
|
+
advanced: {
|
|
658
|
+
responseType: "code",
|
|
659
|
+
codeChallengeMethod: "S256",
|
|
660
|
+
loadUserInfo: true,
|
|
661
|
+
automaticSilentSignIn: false
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
/**
|
|
665
|
+
* Mobile/PWA preset - Optimized for mobile and progressive web apps
|
|
666
|
+
*
|
|
667
|
+
* Features:
|
|
668
|
+
* - 30-day refresh token lifetime
|
|
669
|
+
* - Proactive startup refresh (apps close frequently)
|
|
670
|
+
* - Loading indicator during refresh (better UX for app reopening)
|
|
671
|
+
* - Session monitoring enabled
|
|
672
|
+
*
|
|
673
|
+
* Best for: Mobile apps, PWAs, apps that frequently close/reopen
|
|
674
|
+
*/
|
|
675
|
+
mobile: {
|
|
676
|
+
scope: "openid profile email offline_access",
|
|
677
|
+
features: {
|
|
678
|
+
autoRefresh: true,
|
|
679
|
+
sessionMonitor: true,
|
|
680
|
+
deviceFlow: false
|
|
681
|
+
},
|
|
682
|
+
silentRenew: {
|
|
683
|
+
enabled: true,
|
|
684
|
+
thresholdSeconds: 300,
|
|
685
|
+
refreshStrategy: "startup",
|
|
686
|
+
// Focus on startup refresh
|
|
687
|
+
maxRefreshTokenLifetime: 2592e3,
|
|
688
|
+
// 30 days
|
|
689
|
+
showLoadingIndicator: true,
|
|
690
|
+
// Show loading for better UX
|
|
691
|
+
loadingMessage: "Restoring your session..."
|
|
692
|
+
},
|
|
693
|
+
advanced: {
|
|
694
|
+
responseType: "code",
|
|
695
|
+
codeChallengeMethod: "S256",
|
|
696
|
+
loadUserInfo: true,
|
|
697
|
+
automaticSilentSignIn: false
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
/**
|
|
701
|
+
* High Security preset - Strict security for sensitive applications
|
|
702
|
+
*
|
|
703
|
+
* Features:
|
|
704
|
+
* - 24-hour forced re-authentication
|
|
705
|
+
* - Proactive + reactive token refresh
|
|
706
|
+
* - 10-minute refresh threshold (earlier refresh)
|
|
707
|
+
* - Silent refresh (no UI hints about security measures)
|
|
708
|
+
* - Session monitoring enabled
|
|
709
|
+
*
|
|
710
|
+
* Best for: Banking, healthcare, admin panels, sensitive data applications
|
|
711
|
+
*/
|
|
712
|
+
"high-security": {
|
|
713
|
+
scope: "openid profile email offline_access",
|
|
714
|
+
features: {
|
|
715
|
+
autoRefresh: true,
|
|
716
|
+
sessionMonitor: true,
|
|
717
|
+
deviceFlow: false
|
|
718
|
+
},
|
|
719
|
+
silentRenew: {
|
|
720
|
+
enabled: true,
|
|
721
|
+
thresholdSeconds: 600,
|
|
722
|
+
// 10 minutes (more aggressive)
|
|
723
|
+
refreshStrategy: "both",
|
|
724
|
+
maxRefreshTokenLifetime: 86400,
|
|
725
|
+
// 24 hours only
|
|
726
|
+
showLoadingIndicator: false,
|
|
727
|
+
// Silent for security
|
|
728
|
+
loadingMessage: "Validating session..."
|
|
729
|
+
},
|
|
730
|
+
advanced: {
|
|
731
|
+
responseType: "code",
|
|
732
|
+
codeChallengeMethod: "S256",
|
|
733
|
+
loadUserInfo: true,
|
|
734
|
+
automaticSilentSignIn: false
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
/**
|
|
738
|
+
* Long-Running preset - Optimized for desktop apps and long sessions
|
|
739
|
+
*
|
|
740
|
+
* Features:
|
|
741
|
+
* - 7-day refresh token lifetime
|
|
742
|
+
* - Proactive + reactive token refresh
|
|
743
|
+
* - Silent background refresh
|
|
744
|
+
* - Session monitoring enabled
|
|
745
|
+
*
|
|
746
|
+
* Best for: Desktop applications, monitoring dashboards, always-on tools
|
|
747
|
+
*/
|
|
748
|
+
"long-running": {
|
|
749
|
+
scope: "openid profile email offline_access",
|
|
750
|
+
features: {
|
|
751
|
+
autoRefresh: true,
|
|
752
|
+
sessionMonitor: true,
|
|
753
|
+
deviceFlow: false
|
|
754
|
+
},
|
|
755
|
+
silentRenew: {
|
|
756
|
+
enabled: true,
|
|
757
|
+
thresholdSeconds: 300,
|
|
758
|
+
refreshStrategy: "both",
|
|
759
|
+
maxRefreshTokenLifetime: 604800,
|
|
760
|
+
// 7 days
|
|
761
|
+
showLoadingIndicator: false,
|
|
762
|
+
loadingMessage: "Refreshing session..."
|
|
763
|
+
},
|
|
764
|
+
advanced: {
|
|
765
|
+
responseType: "code",
|
|
766
|
+
codeChallengeMethod: "S256",
|
|
767
|
+
loadUserInfo: true,
|
|
768
|
+
automaticSilentSignIn: false
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
/**
|
|
772
|
+
* Development preset - Convenient settings for development
|
|
773
|
+
*
|
|
774
|
+
* Features:
|
|
775
|
+
* - No refresh token lifetime limit (never expires)
|
|
776
|
+
* - Proactive + reactive token refresh
|
|
777
|
+
* - Loading indicators visible (easier debugging)
|
|
778
|
+
* - Session monitoring enabled
|
|
779
|
+
* - More verbose feedback
|
|
780
|
+
*
|
|
781
|
+
* Best for: Local development, testing, debugging
|
|
782
|
+
* WARNING: Do not use in production!
|
|
783
|
+
*/
|
|
784
|
+
development: {
|
|
785
|
+
scope: "openid profile email offline_access",
|
|
786
|
+
features: {
|
|
787
|
+
autoRefresh: true,
|
|
788
|
+
sessionMonitor: true,
|
|
789
|
+
deviceFlow: false
|
|
790
|
+
},
|
|
791
|
+
silentRenew: {
|
|
792
|
+
enabled: true,
|
|
793
|
+
thresholdSeconds: 300,
|
|
794
|
+
refreshStrategy: "both",
|
|
795
|
+
maxRefreshTokenLifetime: 0,
|
|
796
|
+
// No limit (for convenience)
|
|
797
|
+
showLoadingIndicator: true,
|
|
798
|
+
// Show for debugging
|
|
799
|
+
loadingMessage: "[DEV] Refreshing tokens..."
|
|
800
|
+
},
|
|
801
|
+
advanced: {
|
|
802
|
+
responseType: "code",
|
|
803
|
+
codeChallengeMethod: "S256",
|
|
804
|
+
loadUserInfo: true,
|
|
805
|
+
automaticSilentSignIn: false
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
function getConfigPreset(preset) {
|
|
810
|
+
return CONFIG_PRESETS[preset];
|
|
811
|
+
}
|
|
812
|
+
function mergeWithPreset(preset, customConfig) {
|
|
813
|
+
const presetConfig = getConfigPreset(preset);
|
|
814
|
+
return {
|
|
815
|
+
...presetConfig,
|
|
816
|
+
...customConfig,
|
|
817
|
+
features: {
|
|
818
|
+
...presetConfig.features,
|
|
819
|
+
...customConfig.features
|
|
820
|
+
},
|
|
821
|
+
silentRenew: {
|
|
822
|
+
...presetConfig.silentRenew,
|
|
823
|
+
...customConfig.silentRenew
|
|
824
|
+
},
|
|
825
|
+
advanced: {
|
|
826
|
+
...presetConfig.advanced,
|
|
827
|
+
...customConfig.advanced
|
|
828
|
+
},
|
|
829
|
+
events: {
|
|
830
|
+
...presetConfig.events,
|
|
831
|
+
...customConfig.events
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function isZeroConfig(props) {
|
|
836
|
+
return "authority" in props && "clientId" in props;
|
|
837
|
+
}
|
|
838
|
+
var KeycloakProvider = (props) => {
|
|
839
|
+
const { children, debug = false } = props;
|
|
840
|
+
const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
|
|
136
841
|
const oidcConfig = KeycloakConfigBuilder.build(config);
|
|
137
842
|
const log = react.useCallback(
|
|
138
843
|
(level, message, ...args) => {
|
|
139
844
|
if (debug) {
|
|
140
|
-
const prefix = "[
|
|
845
|
+
const prefix = "[KeycloakProvider]";
|
|
141
846
|
switch (level) {
|
|
142
847
|
case "info":
|
|
143
848
|
console.log(prefix, message, ...args);
|
|
@@ -165,11 +870,13 @@ var KeycloakProvider = ({
|
|
|
165
870
|
);
|
|
166
871
|
const onSignoutCallback = react.useCallback(() => {
|
|
167
872
|
log("info", "Sign out successful");
|
|
873
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
168
874
|
config.events?.onLogout?.();
|
|
169
875
|
}, [config.events, log]);
|
|
170
876
|
const onRemoveUser = react.useCallback(() => {
|
|
171
|
-
log("
|
|
877
|
+
log("warn", "User removed from storage - token validation likely failed");
|
|
172
878
|
}, [log]);
|
|
879
|
+
const showSessionMonitor = config.features?.sessionMonitor !== false;
|
|
173
880
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
174
881
|
reactOidcContext.AuthProvider,
|
|
175
882
|
{
|
|
@@ -177,11 +884,41 @@ var KeycloakProvider = ({
|
|
|
177
884
|
onSigninCallback,
|
|
178
885
|
onSignoutCallback,
|
|
179
886
|
onRemoveUser,
|
|
180
|
-
children
|
|
887
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
|
|
888
|
+
showSessionMonitor && /* @__PURE__ */ jsxRuntime.jsx(SessionMonitor, {}),
|
|
889
|
+
children
|
|
890
|
+
] })
|
|
181
891
|
}
|
|
182
892
|
);
|
|
183
893
|
};
|
|
184
|
-
|
|
894
|
+
function buildZeroConfig(props) {
|
|
895
|
+
const { authority, clientId, preset = "default", config: overrides = {} } = props;
|
|
896
|
+
const presetConfig = mergeWithPreset(preset, overrides);
|
|
897
|
+
const fullConfig = {
|
|
898
|
+
authority,
|
|
899
|
+
clientId,
|
|
900
|
+
redirectUri: presetConfig.redirectUri || defaultConfig.redirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
|
|
901
|
+
postLogoutRedirectUri: presetConfig.postLogoutRedirectUri || defaultConfig.postLogoutRedirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
|
|
902
|
+
scope: presetConfig.scope || defaultConfig.scope,
|
|
903
|
+
features: {
|
|
904
|
+
...defaultConfig.features,
|
|
905
|
+
...presetConfig.features
|
|
906
|
+
},
|
|
907
|
+
silentRenew: {
|
|
908
|
+
...defaultConfig.silentRenew,
|
|
909
|
+
...presetConfig.silentRenew
|
|
910
|
+
},
|
|
911
|
+
advanced: {
|
|
912
|
+
...defaultConfig.advanced,
|
|
913
|
+
...presetConfig.advanced
|
|
914
|
+
},
|
|
915
|
+
events: {
|
|
916
|
+
...defaultConfig.events,
|
|
917
|
+
...presetConfig.events
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
return fullConfig;
|
|
921
|
+
}
|
|
185
922
|
function useRoles() {
|
|
186
923
|
const auth = useAuth();
|
|
187
924
|
const roles = react.useMemo(() => {
|
|
@@ -273,6 +1010,7 @@ var ProtectedRoute = ({
|
|
|
273
1010
|
const { hasAnyRole, hasAllRoles } = useRoles();
|
|
274
1011
|
const { hasAnyScope, hasAllScopes } = useScopes();
|
|
275
1012
|
const location = reactRouterDom.useLocation();
|
|
1013
|
+
const hasRedirected = react.useRef(false);
|
|
276
1014
|
const isAuthenticated = !auth.isLoading && auth.isAuthenticated;
|
|
277
1015
|
const policyAccess = policy && auth.user ? policy(auth.user, { location }) : null;
|
|
278
1016
|
const roleAccess = requiredRoles.length > 0 ? requireAllRoles ? hasAllRoles(requiredRoles) : hasAnyRole(requiredRoles) : null;
|
|
@@ -284,12 +1022,21 @@ var ProtectedRoute = ({
|
|
|
284
1022
|
}
|
|
285
1023
|
}, [unauthorizedReason, onUnauthorized]);
|
|
286
1024
|
react.useEffect(() => {
|
|
287
|
-
if (!auth.isLoading && !auth.isAuthenticated) {
|
|
1025
|
+
if (!auth.isLoading && !auth.isAuthenticated && !hasRedirected.current) {
|
|
1026
|
+
hasRedirected.current = true;
|
|
288
1027
|
auth.signinRedirect({
|
|
289
1028
|
redirect_uri: window.location.origin + location.pathname + location.search
|
|
1029
|
+
}).catch((error) => {
|
|
1030
|
+
hasRedirected.current = false;
|
|
1031
|
+
console.error("[ProtectedRoute] Signin redirect failed:", error);
|
|
290
1032
|
});
|
|
291
1033
|
}
|
|
292
|
-
}, [auth.isLoading, auth.isAuthenticated,
|
|
1034
|
+
}, [auth.isLoading, auth.isAuthenticated, location.pathname, location.search]);
|
|
1035
|
+
react.useEffect(() => {
|
|
1036
|
+
if (auth.isAuthenticated) {
|
|
1037
|
+
hasRedirected.current = false;
|
|
1038
|
+
}
|
|
1039
|
+
}, [auth.isAuthenticated]);
|
|
293
1040
|
if (auth.isLoading) {
|
|
294
1041
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent || /* @__PURE__ */ jsxRuntime.jsx(DefaultLoadingComponent, {}) });
|
|
295
1042
|
}
|
|
@@ -509,125 +1256,6 @@ function getInitials(name) {
|
|
|
509
1256
|
}
|
|
510
1257
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
511
1258
|
}
|
|
512
|
-
var SessionMonitor = ({
|
|
513
|
-
position = "top-right",
|
|
514
|
-
warningThreshold = 300,
|
|
515
|
-
// 5 minutes
|
|
516
|
-
criticalThreshold = 60,
|
|
517
|
-
// 1 minute
|
|
518
|
-
showProgressBar = true,
|
|
519
|
-
minimizable = true,
|
|
520
|
-
startMinimized = false,
|
|
521
|
-
className = "",
|
|
522
|
-
onWarning,
|
|
523
|
-
onCritical,
|
|
524
|
-
onExpired,
|
|
525
|
-
style
|
|
526
|
-
}) => {
|
|
527
|
-
const auth = useAuth();
|
|
528
|
-
const [timeRemaining, setTimeRemaining] = react.useState(null);
|
|
529
|
-
const [minimized, setMinimized] = react.useState(startMinimized);
|
|
530
|
-
const [warningTriggered, setWarningTriggered] = react.useState(false);
|
|
531
|
-
const [criticalTriggered, setCriticalTriggered] = react.useState(false);
|
|
532
|
-
react.useEffect(() => {
|
|
533
|
-
if (!auth.isAuthenticated || !auth.user?.expires_at) {
|
|
534
|
-
setTimeRemaining(null);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const updateTimeRemaining = () => {
|
|
538
|
-
const expiresAt = auth.user.expires_at;
|
|
539
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
540
|
-
const remaining = expiresAt - now;
|
|
541
|
-
if (remaining <= 0) {
|
|
542
|
-
setTimeRemaining(0);
|
|
543
|
-
onExpired?.();
|
|
544
|
-
setTimeout(() => {
|
|
545
|
-
auth.signoutRedirect();
|
|
546
|
-
}, 2e3);
|
|
547
|
-
} else {
|
|
548
|
-
setTimeRemaining(remaining);
|
|
549
|
-
if (remaining <= criticalThreshold && !criticalTriggered) {
|
|
550
|
-
setCriticalTriggered(true);
|
|
551
|
-
onCritical?.(remaining);
|
|
552
|
-
} else if (remaining <= warningThreshold && !warningTriggered) {
|
|
553
|
-
setWarningTriggered(true);
|
|
554
|
-
onWarning?.(remaining);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
updateTimeRemaining();
|
|
559
|
-
const interval = setInterval(updateTimeRemaining, 1e3);
|
|
560
|
-
return () => clearInterval(interval);
|
|
561
|
-
}, [
|
|
562
|
-
auth,
|
|
563
|
-
warningThreshold,
|
|
564
|
-
criticalThreshold,
|
|
565
|
-
warningTriggered,
|
|
566
|
-
criticalTriggered,
|
|
567
|
-
onWarning,
|
|
568
|
-
onCritical,
|
|
569
|
-
onExpired
|
|
570
|
-
]);
|
|
571
|
-
react.useEffect(() => {
|
|
572
|
-
if (timeRemaining && timeRemaining > warningThreshold) {
|
|
573
|
-
setWarningTriggered(false);
|
|
574
|
-
setCriticalTriggered(false);
|
|
575
|
-
}
|
|
576
|
-
}, [timeRemaining, warningThreshold]);
|
|
577
|
-
const formatTime = react.useCallback((seconds) => {
|
|
578
|
-
const mins = Math.floor(seconds / 60);
|
|
579
|
-
const secs = seconds % 60;
|
|
580
|
-
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
581
|
-
}, []);
|
|
582
|
-
const getStatusClass = react.useCallback(() => {
|
|
583
|
-
if (!timeRemaining) return "normal";
|
|
584
|
-
if (timeRemaining <= criticalThreshold) return "critical";
|
|
585
|
-
if (timeRemaining <= warningThreshold) return "warning";
|
|
586
|
-
return "normal";
|
|
587
|
-
}, [timeRemaining, warningThreshold, criticalThreshold]);
|
|
588
|
-
const getProgress = react.useCallback(() => {
|
|
589
|
-
if (!timeRemaining || !auth.user?.expires_in) return 100;
|
|
590
|
-
return timeRemaining / auth.user.expires_in * 100;
|
|
591
|
-
}, [timeRemaining, auth.user]);
|
|
592
|
-
if (!auth.isAuthenticated || timeRemaining === null) {
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
const baseClass = "kc-session-monitor";
|
|
596
|
-
const positionClass = `kc-session-monitor--${position}`;
|
|
597
|
-
const statusClass = `kc-session-monitor--${getStatusClass()}`;
|
|
598
|
-
const minimizedClass = minimized ? "kc-session-monitor--minimized" : "";
|
|
599
|
-
const classes = `${baseClass} ${positionClass} ${statusClass} ${minimizedClass} ${className}`.trim();
|
|
600
|
-
const progress = getProgress();
|
|
601
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes, style, children: [
|
|
602
|
-
minimizable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
603
|
-
"button",
|
|
604
|
-
{
|
|
605
|
-
className: "kc-session-monitor__toggle",
|
|
606
|
-
onClick: () => setMinimized(!minimized),
|
|
607
|
-
"aria-label": minimized ? "Expand session monitor" : "Minimize session monitor",
|
|
608
|
-
title: minimized ? "Expand" : "Minimize",
|
|
609
|
-
children: minimized ? "\u25B2" : "\u25BC"
|
|
610
|
-
}
|
|
611
|
-
),
|
|
612
|
-
!minimized && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
613
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-session-monitor__content", children: [
|
|
614
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__label", children: timeRemaining === 0 ? "Session expired" : "Session expires in:" }),
|
|
615
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__time", children: formatTime(timeRemaining) })
|
|
616
|
-
] }),
|
|
617
|
-
showProgressBar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-session-monitor__progress-bar", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
618
|
-
"div",
|
|
619
|
-
{
|
|
620
|
-
className: "kc-session-monitor__progress-fill",
|
|
621
|
-
style: { width: `${progress}%` },
|
|
622
|
-
role: "progressbar",
|
|
623
|
-
"aria-valuenow": progress,
|
|
624
|
-
"aria-valuemin": 0,
|
|
625
|
-
"aria-valuemax": 100
|
|
626
|
-
}
|
|
627
|
-
) })
|
|
628
|
-
] })
|
|
629
|
-
] });
|
|
630
|
-
};
|
|
631
1259
|
|
|
632
1260
|
// src/utils/deviceFlow.ts
|
|
633
1261
|
async function requestDeviceCode(apiBaseUrl) {
|
|
@@ -864,14 +1492,14 @@ function useProtectedFetch(options = {}) {
|
|
|
864
1492
|
},
|
|
865
1493
|
[baseUrl]
|
|
866
1494
|
);
|
|
867
|
-
const getAuthHeader =
|
|
1495
|
+
const getAuthHeader = () => {
|
|
868
1496
|
if (!auth.user?.access_token) {
|
|
869
1497
|
return {};
|
|
870
1498
|
}
|
|
871
1499
|
return {
|
|
872
1500
|
Authorization: `Bearer ${auth.user.access_token}`
|
|
873
1501
|
};
|
|
874
|
-
}
|
|
1502
|
+
};
|
|
875
1503
|
const buildHeaders = react.useCallback(
|
|
876
1504
|
(requestHeaders) => {
|
|
877
1505
|
const headers = new Headers(requestHeaders);
|
|
@@ -884,7 +1512,7 @@ function useProtectedFetch(options = {}) {
|
|
|
884
1512
|
});
|
|
885
1513
|
return headers;
|
|
886
1514
|
},
|
|
887
|
-
[
|
|
1515
|
+
[customHeaders]
|
|
888
1516
|
);
|
|
889
1517
|
const protectedFetch = react.useCallback(
|
|
890
1518
|
async (url, init) => {
|
|
@@ -935,6 +1563,7 @@ function useProtectedFetch(options = {}) {
|
|
|
935
1563
|
};
|
|
936
1564
|
}
|
|
937
1565
|
|
|
1566
|
+
exports.CONFIG_PRESETS = CONFIG_PRESETS;
|
|
938
1567
|
exports.DeviceLogin = DeviceLogin;
|
|
939
1568
|
exports.KeycloakConfigBuilder = KeycloakConfigBuilder;
|
|
940
1569
|
exports.KeycloakProvider = KeycloakProvider;
|
|
@@ -942,15 +1571,19 @@ exports.LoginButton = LoginButton;
|
|
|
942
1571
|
exports.LogoutButton = LogoutButton;
|
|
943
1572
|
exports.ProtectedRoute = ProtectedRoute;
|
|
944
1573
|
exports.SessionMonitor = SessionMonitor;
|
|
1574
|
+
exports.TokenLifecycleManager = TokenLifecycleManager;
|
|
945
1575
|
exports.UserProfile = UserProfile;
|
|
946
1576
|
exports.defaultConfig = defaultConfig;
|
|
947
1577
|
exports.fetchUserInfo = fetchUserInfo;
|
|
948
1578
|
exports.getConfigFromEnv = getConfigFromEnv;
|
|
1579
|
+
exports.getConfigPreset = getConfigPreset;
|
|
1580
|
+
exports.mergeWithPreset = mergeWithPreset;
|
|
949
1581
|
exports.pollForToken = pollForToken;
|
|
950
1582
|
exports.requestDeviceCode = requestDeviceCode;
|
|
951
1583
|
exports.useAuth = useAuth;
|
|
952
1584
|
exports.useProtectedFetch = useProtectedFetch;
|
|
953
1585
|
exports.useRoles = useRoles;
|
|
954
1586
|
exports.useScopes = useScopes;
|
|
1587
|
+
exports.useTokenLifecycle = useTokenLifecycle;
|
|
955
1588
|
//# sourceMappingURL=index.js.map
|
|
956
1589
|
//# sourceMappingURL=index.js.map
|