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