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