antaeus.keycloak.react 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,956 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactOidcContext = require('react-oidc-context');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var reactRouterDom = require('react-router-dom');
7
+ var qrcode_react = require('qrcode.react');
8
+ var oidcClientTs = require('oidc-client-ts');
9
+
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ // src/components/KeycloakProvider.tsx
12
+
13
+ // src/config/defaults.ts
14
+ var defaultConfig = {
15
+ redirectUri: typeof window !== "undefined" ? window.location.origin + "/" : "/",
16
+ postLogoutRedirectUri: typeof window !== "undefined" ? window.location.origin + "/" : "/",
17
+ scope: "openid profile email offline_access",
18
+ features: {
19
+ deviceFlow: false,
20
+ sessionMonitor: false,
21
+ autoRefresh: true
22
+ },
23
+ silentRenew: {
24
+ enabled: true,
25
+ silentRedirectUri: typeof window !== "undefined" ? window.location.origin + "/silent-renew.html" : "/silent-renew.html",
26
+ thresholdSeconds: 300
27
+ // 5 minutes
28
+ },
29
+ advanced: {
30
+ responseType: "code",
31
+ codeChallengeMethod: "S256",
32
+ loadUserInfo: true,
33
+ automaticSilentSignIn: false
34
+ },
35
+ events: {}
36
+ };
37
+
38
+ // src/config/KeycloakConfig.ts
39
+ var KeycloakConfigBuilder = class {
40
+ /**
41
+ * Builds OIDC client configuration from Keycloak config
42
+ *
43
+ * @param userConfig - User-provided Keycloak configuration
44
+ * @returns OIDC client settings ready for react-oidc-context
45
+ * @throws Error if required fields are missing or invalid
46
+ */
47
+ static build(userConfig) {
48
+ this.validate(userConfig);
49
+ const config = this.mergeWithDefaults(userConfig);
50
+ const oidcSettings = {
51
+ authority: config.authority,
52
+ client_id: config.clientId,
53
+ redirect_uri: config.redirectUri,
54
+ post_logout_redirect_uri: config.postLogoutRedirectUri,
55
+ response_type: config.advanced.responseType,
56
+ scope: config.scope,
57
+ automaticSilentRenew: config.silentRenew.enabled,
58
+ loadUserInfo: config.advanced.loadUserInfo,
59
+ silent_redirect_uri: config.silentRenew.silentRedirectUri
60
+ };
61
+ return oidcSettings;
62
+ }
63
+ /**
64
+ * Validates required configuration fields
65
+ *
66
+ * @param config - Configuration to validate
67
+ * @throws Error if validation fails
68
+ */
69
+ static validate(config) {
70
+ if (!config.authority) {
71
+ throw new Error("Keycloak authority is required");
72
+ }
73
+ if (!config.clientId) {
74
+ throw new Error("Keycloak clientId is required");
75
+ }
76
+ try {
77
+ new URL(config.authority);
78
+ } catch {
79
+ throw new Error("Keycloak authority must be a valid URL");
80
+ }
81
+ }
82
+ /**
83
+ * Merges user configuration with defaults
84
+ *
85
+ * @param config - User configuration
86
+ * @returns Merged configuration with all defaults applied
87
+ */
88
+ static mergeWithDefaults(config) {
89
+ return {
90
+ authority: config.authority,
91
+ clientId: config.clientId,
92
+ redirectUri: config.redirectUri ?? defaultConfig.redirectUri,
93
+ postLogoutRedirectUri: config.postLogoutRedirectUri ?? defaultConfig.postLogoutRedirectUri,
94
+ scope: config.scope ?? defaultConfig.scope,
95
+ features: {
96
+ ...defaultConfig.features,
97
+ ...config.features
98
+ },
99
+ silentRenew: {
100
+ ...defaultConfig.silentRenew,
101
+ ...config.silentRenew
102
+ },
103
+ advanced: {
104
+ ...defaultConfig.advanced,
105
+ ...config.advanced
106
+ },
107
+ events: {
108
+ ...defaultConfig.events,
109
+ ...config.events
110
+ }
111
+ };
112
+ }
113
+ };
114
+ function getConfigFromEnv() {
115
+ if (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) === "undefined") {
116
+ return {};
117
+ }
118
+ const env = undefined;
119
+ if (!env) {
120
+ return {};
121
+ }
122
+ return {
123
+ authority: env.VITE_KEYCLOAK_AUTHORITY,
124
+ clientId: env.VITE_KEYCLOAK_CLIENT_ID,
125
+ features: {
126
+ deviceFlow: env.VITE_ENABLE_DEVICE_FLOW === "true",
127
+ sessionMonitor: env.VITE_ENABLE_SESSION_MONITOR === "true"
128
+ }
129
+ };
130
+ }
131
+ var KeycloakProvider = ({
132
+ config,
133
+ children,
134
+ debug = false
135
+ }) => {
136
+ const oidcConfig = KeycloakConfigBuilder.build(config);
137
+ const log = react.useCallback(
138
+ (level, message, ...args) => {
139
+ if (debug) {
140
+ const prefix = "[Keycloak]";
141
+ switch (level) {
142
+ case "info":
143
+ console.log(prefix, message, ...args);
144
+ break;
145
+ case "warn":
146
+ console.warn(prefix, message, ...args);
147
+ break;
148
+ case "error":
149
+ console.error(prefix, message, ...args);
150
+ break;
151
+ }
152
+ }
153
+ },
154
+ [debug]
155
+ );
156
+ const onSigninCallback = react.useCallback(
157
+ (_user) => {
158
+ log("info", "Sign in successful");
159
+ window.history.replaceState({}, document.title, window.location.pathname);
160
+ if (_user) {
161
+ config.events?.onLogin?.(_user);
162
+ }
163
+ },
164
+ [config.events, log]
165
+ );
166
+ const onSignoutCallback = react.useCallback(() => {
167
+ log("info", "Sign out successful");
168
+ config.events?.onLogout?.();
169
+ }, [config.events, log]);
170
+ const onRemoveUser = react.useCallback(() => {
171
+ log("info", "User removed");
172
+ }, [log]);
173
+ return /* @__PURE__ */ jsxRuntime.jsx(
174
+ reactOidcContext.AuthProvider,
175
+ {
176
+ ...oidcConfig,
177
+ onSigninCallback,
178
+ onSignoutCallback,
179
+ onRemoveUser,
180
+ children
181
+ }
182
+ );
183
+ };
184
+ var useAuth = reactOidcContext.useAuth;
185
+ function useRoles() {
186
+ const auth = useAuth();
187
+ const roles = react.useMemo(() => {
188
+ if (!auth.user?.profile) {
189
+ return [];
190
+ }
191
+ const realmAccess = auth.user.profile.realm_access;
192
+ return realmAccess?.roles || [];
193
+ }, [auth.user]);
194
+ const hasRole = react.useMemo(
195
+ () => (role) => {
196
+ return roles.includes(role);
197
+ },
198
+ [roles]
199
+ );
200
+ const hasAnyRole = react.useMemo(
201
+ () => (requiredRoles) => {
202
+ return requiredRoles.some((role) => roles.includes(role));
203
+ },
204
+ [roles]
205
+ );
206
+ const hasAllRoles = react.useMemo(
207
+ () => (requiredRoles) => {
208
+ return requiredRoles.every((role) => roles.includes(role));
209
+ },
210
+ [roles]
211
+ );
212
+ const getClientRoles = react.useMemo(
213
+ () => (clientId) => {
214
+ if (!auth.user?.profile) {
215
+ return [];
216
+ }
217
+ const resourceAccess = auth.user.profile.resource_access;
218
+ return resourceAccess?.[clientId]?.roles || [];
219
+ },
220
+ [auth.user]
221
+ );
222
+ return {
223
+ roles,
224
+ hasRole,
225
+ hasAnyRole,
226
+ hasAllRoles,
227
+ getClientRoles
228
+ };
229
+ }
230
+ function useScopes() {
231
+ const auth = useAuth();
232
+ const scopes = react.useMemo(() => {
233
+ if (!auth.user?.scope) return [];
234
+ return auth.user.scope.split(" ").filter((s) => s.length > 0);
235
+ }, [auth.user]);
236
+ const hasScope = react.useMemo(
237
+ () => (scope) => {
238
+ return scopes.includes(scope);
239
+ },
240
+ [scopes]
241
+ );
242
+ const hasAnyScope = react.useMemo(
243
+ () => (requiredScopes) => {
244
+ return requiredScopes.some((scope) => scopes.includes(scope));
245
+ },
246
+ [scopes]
247
+ );
248
+ const hasAllScopes = react.useMemo(
249
+ () => (requiredScopes) => {
250
+ return requiredScopes.every((scope) => scopes.includes(scope));
251
+ },
252
+ [scopes]
253
+ );
254
+ return {
255
+ scopes,
256
+ hasScope,
257
+ hasAnyScope,
258
+ hasAllScopes
259
+ };
260
+ }
261
+ var ProtectedRoute = ({
262
+ children,
263
+ requiredRoles = [],
264
+ requireAllRoles = false,
265
+ requiredScopes = [],
266
+ requireAllScopes = false,
267
+ policy,
268
+ loadingComponent,
269
+ unauthorizedComponent,
270
+ onUnauthorized
271
+ }) => {
272
+ const auth = useAuth();
273
+ const { hasAnyRole, hasAllRoles } = useRoles();
274
+ const { hasAnyScope, hasAllScopes } = useScopes();
275
+ const location = reactRouterDom.useLocation();
276
+ const isAuthenticated = !auth.isLoading && auth.isAuthenticated;
277
+ const policyAccess = policy && auth.user ? policy(auth.user, { location }) : null;
278
+ const roleAccess = requiredRoles.length > 0 ? requireAllRoles ? hasAllRoles(requiredRoles) : hasAnyRole(requiredRoles) : null;
279
+ const scopeAccess = requiredScopes.length > 0 ? requireAllScopes ? hasAllScopes(requiredScopes) : hasAnyScope(requiredScopes) : null;
280
+ const unauthorizedReason = !isAuthenticated ? "not-authenticated" : policyAccess === false ? "policy-denied" : roleAccess === false ? "insufficient-roles" : scopeAccess === false ? "insufficient-scopes" : null;
281
+ react.useEffect(() => {
282
+ if (unauthorizedReason) {
283
+ onUnauthorized?.(unauthorizedReason);
284
+ }
285
+ }, [unauthorizedReason, onUnauthorized]);
286
+ react.useEffect(() => {
287
+ if (!auth.isLoading && !auth.isAuthenticated) {
288
+ auth.signinRedirect({
289
+ redirect_uri: window.location.origin + location.pathname + location.search
290
+ });
291
+ }
292
+ }, [auth.isLoading, auth.isAuthenticated, auth, location.pathname, location.search]);
293
+ if (auth.isLoading) {
294
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent || /* @__PURE__ */ jsxRuntime.jsx(DefaultLoadingComponent, {}) });
295
+ }
296
+ if (!isAuthenticated) {
297
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent || /* @__PURE__ */ jsxRuntime.jsx(DefaultLoadingComponent, {}) });
298
+ }
299
+ if (unauthorizedReason) {
300
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: unauthorizedComponent || /* @__PURE__ */ jsxRuntime.jsx(DefaultUnauthorizedComponent, {}) });
301
+ }
302
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
303
+ };
304
+ var DefaultLoadingComponent = () => /* @__PURE__ */ jsxRuntime.jsxs(
305
+ "div",
306
+ {
307
+ style: {
308
+ display: "flex",
309
+ justifyContent: "center",
310
+ alignItems: "center",
311
+ minHeight: "200px",
312
+ flexDirection: "column",
313
+ gap: "1rem"
314
+ },
315
+ children: [
316
+ /* @__PURE__ */ jsxRuntime.jsx(
317
+ "div",
318
+ {
319
+ style: {
320
+ border: "3px solid #f3f3f3",
321
+ borderTop: "3px solid #3498db",
322
+ borderRadius: "50%",
323
+ width: "40px",
324
+ height: "40px",
325
+ animation: "spin 1s linear infinite"
326
+ }
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#666", margin: 0 }, children: "Loading..." }),
330
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
331
+ @keyframes spin {
332
+ 0% { transform: rotate(0deg); }
333
+ 100% { transform: rotate(360deg); }
334
+ }
335
+ ` })
336
+ ]
337
+ }
338
+ );
339
+ var DefaultUnauthorizedComponent = () => /* @__PURE__ */ jsxRuntime.jsxs(
340
+ "div",
341
+ {
342
+ style: {
343
+ display: "flex",
344
+ justifyContent: "center",
345
+ alignItems: "center",
346
+ minHeight: "200px",
347
+ flexDirection: "column",
348
+ gap: "1rem",
349
+ padding: "2rem",
350
+ textAlign: "center"
351
+ },
352
+ children: [
353
+ /* @__PURE__ */ jsxRuntime.jsx(
354
+ "div",
355
+ {
356
+ style: {
357
+ fontSize: "4rem",
358
+ color: "#e74c3c"
359
+ },
360
+ children: "\u{1F512}"
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { margin: 0, color: "#333" }, children: "Access Denied" }),
364
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#666", margin: 0 }, children: "You do not have permission to access this page." })
365
+ ]
366
+ }
367
+ );
368
+ var LoginButton = ({
369
+ children = "Login",
370
+ className = "",
371
+ variant = "primary",
372
+ size = "medium",
373
+ showIcon = true,
374
+ onLoginStart,
375
+ style,
376
+ disabled = false
377
+ }) => {
378
+ const auth = useAuth();
379
+ if (auth.isAuthenticated) {
380
+ return null;
381
+ }
382
+ const handleClick = () => {
383
+ onLoginStart?.();
384
+ auth.signinRedirect();
385
+ };
386
+ const baseClass = "kc-button kc-login-button";
387
+ const variantClass = `kc-button--${variant}`;
388
+ const sizeClass = `kc-button--${size}`;
389
+ const classes = `${baseClass} ${variantClass} ${sizeClass} ${className}`.trim();
390
+ return /* @__PURE__ */ jsxRuntime.jsxs(
391
+ "button",
392
+ {
393
+ type: "button",
394
+ className: classes,
395
+ onClick: handleClick,
396
+ disabled: disabled || auth.isLoading,
397
+ style,
398
+ children: [
399
+ showIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-button__icon", children: "\u{1F510}" }),
400
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-button__text", children })
401
+ ]
402
+ }
403
+ );
404
+ };
405
+ var LogoutButton = ({
406
+ children = "Logout",
407
+ className = "",
408
+ variant = "secondary",
409
+ size = "medium",
410
+ showIcon = true,
411
+ onLogoutStart,
412
+ style,
413
+ disabled = false
414
+ }) => {
415
+ const auth = useAuth();
416
+ if (!auth.isAuthenticated) {
417
+ return null;
418
+ }
419
+ const handleClick = () => {
420
+ onLogoutStart?.();
421
+ auth.signoutRedirect();
422
+ };
423
+ const baseClass = "kc-button kc-logout-button";
424
+ const variantClass = `kc-button--${variant}`;
425
+ const sizeClass = `kc-button--${size}`;
426
+ const classes = `${baseClass} ${variantClass} ${sizeClass} ${className}`.trim();
427
+ return /* @__PURE__ */ jsxRuntime.jsxs(
428
+ "button",
429
+ {
430
+ type: "button",
431
+ className: classes,
432
+ onClick: handleClick,
433
+ disabled: disabled || auth.isLoading,
434
+ style,
435
+ children: [
436
+ showIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-button__icon", children: "\u{1F6AA}" }),
437
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-button__text", children })
438
+ ]
439
+ }
440
+ );
441
+ };
442
+ var UserProfile = ({
443
+ className = "",
444
+ showAvatar = true,
445
+ showEmail = true,
446
+ showRoles = false,
447
+ avatarSize = 40,
448
+ style,
449
+ onClick
450
+ }) => {
451
+ const auth = useAuth();
452
+ const { roles } = useRoles();
453
+ if (!auth.isAuthenticated || !auth.user) {
454
+ return null;
455
+ }
456
+ const profile = auth.user.profile;
457
+ const name = profile.name || profile.preferred_username || "User";
458
+ const email = profile.email;
459
+ const initials = getInitials(name);
460
+ const baseClass = "kc-user-profile";
461
+ const clickableClass = onClick ? "kc-user-profile--clickable" : "";
462
+ const classes = `${baseClass} ${clickableClass} ${className}`.trim();
463
+ const handleClick = () => {
464
+ if (onClick) {
465
+ onClick();
466
+ }
467
+ };
468
+ return /* @__PURE__ */ jsxRuntime.jsxs(
469
+ "div",
470
+ {
471
+ className: classes,
472
+ style,
473
+ onClick: handleClick,
474
+ role: onClick ? "button" : void 0,
475
+ tabIndex: onClick ? 0 : void 0,
476
+ onKeyPress: onClick ? (e) => e.key === "Enter" && handleClick() : void 0,
477
+ children: [
478
+ showAvatar && /* @__PURE__ */ jsxRuntime.jsx(
479
+ "div",
480
+ {
481
+ className: "kc-user-profile__avatar",
482
+ style: {
483
+ width: avatarSize,
484
+ height: avatarSize,
485
+ fontSize: avatarSize * 0.4
486
+ },
487
+ children: initials
488
+ }
489
+ ),
490
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-user-profile__info", children: [
491
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-user-profile__name", children: name }),
492
+ showEmail && email && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-user-profile__email", children: email }),
493
+ showRoles && roles.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-user-profile__roles", children: [
494
+ roles.slice(0, 3).map((role) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-user-profile__role-badge", children: role }, role)),
495
+ roles.length > 3 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "kc-user-profile__role-badge", children: [
496
+ "+",
497
+ roles.length - 3
498
+ ] })
499
+ ] })
500
+ ] })
501
+ ]
502
+ }
503
+ );
504
+ };
505
+ function getInitials(name) {
506
+ const parts = name.trim().split(" ");
507
+ if (parts.length === 1) {
508
+ return parts[0].substring(0, 2).toUpperCase();
509
+ }
510
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
511
+ }
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
+
632
+ // src/utils/deviceFlow.ts
633
+ async function requestDeviceCode(apiBaseUrl) {
634
+ const url = `${apiBaseUrl}/api/keycloak/device/authorize`;
635
+ try {
636
+ const response = await fetch(url, {
637
+ method: "POST",
638
+ headers: {
639
+ "Content-Type": "application/json"
640
+ }
641
+ });
642
+ if (!response.ok) {
643
+ const errorText = await response.text();
644
+ throw new Error(
645
+ `Device authorization failed: ${response.status} ${response.statusText}. ${errorText}`
646
+ );
647
+ }
648
+ const data = await response.json();
649
+ return data;
650
+ } catch (error) {
651
+ if (error instanceof Error) {
652
+ throw new Error(`Failed to request device code: ${error.message}`);
653
+ }
654
+ throw error;
655
+ }
656
+ }
657
+ async function pollForToken(apiBaseUrl, deviceCode) {
658
+ const url = `${apiBaseUrl}/api/keycloak/device/token`;
659
+ try {
660
+ const response = await fetch(url, {
661
+ method: "POST",
662
+ headers: {
663
+ "Content-Type": "application/json"
664
+ },
665
+ body: JSON.stringify({ device_code: deviceCode })
666
+ });
667
+ const data = await response.json();
668
+ if ("error" in data) {
669
+ const errorData = data;
670
+ if (errorData.error === "authorization_pending" || errorData.error === "slow_down") {
671
+ return null;
672
+ }
673
+ throw new Error(
674
+ errorData.error_description || errorData.error || "Token polling failed"
675
+ );
676
+ }
677
+ if (!response.ok) {
678
+ throw new Error(`Token polling failed with status ${response.status}`);
679
+ }
680
+ return data;
681
+ } catch (error) {
682
+ if (error instanceof Error) {
683
+ throw error;
684
+ }
685
+ throw new Error("Token polling failed");
686
+ }
687
+ }
688
+ async function fetchUserInfo(apiBaseUrl, accessToken) {
689
+ const url = `${apiBaseUrl}/api/keycloak/device/userinfo`;
690
+ try {
691
+ const response = await fetch(url, {
692
+ method: "POST",
693
+ headers: {
694
+ "Content-Type": "application/json"
695
+ },
696
+ body: JSON.stringify({ access_token: accessToken })
697
+ });
698
+ if (!response.ok) {
699
+ throw new Error(`Failed to fetch user info: ${response.statusText}`);
700
+ }
701
+ return await response.json();
702
+ } catch (error) {
703
+ if (error instanceof Error) {
704
+ throw new Error(`Failed to fetch user info: ${error.message}`);
705
+ }
706
+ throw error;
707
+ }
708
+ }
709
+ var DeviceLogin = ({
710
+ apiBaseUrl,
711
+ qrCodeSize = 256,
712
+ pollingInterval = 5,
713
+ className = "",
714
+ onSuccess,
715
+ onError,
716
+ style
717
+ }) => {
718
+ const auth = useAuth();
719
+ const [deviceData, setDeviceData] = react.useState(null);
720
+ const [error, setError] = react.useState(null);
721
+ const [loading, setLoading] = react.useState(true);
722
+ const [timeRemaining, setTimeRemaining] = react.useState(null);
723
+ const pollTimerRef = react.useRef(null);
724
+ const userManagerRef = react.useRef(null);
725
+ react.useEffect(() => {
726
+ const initDeviceFlow = async () => {
727
+ setLoading(true);
728
+ setError(null);
729
+ try {
730
+ const response = await requestDeviceCode(apiBaseUrl);
731
+ setDeviceData(response);
732
+ setTimeRemaining(response.expires_in);
733
+ } catch (err) {
734
+ const error2 = err;
735
+ setError(error2.message);
736
+ onError?.(error2);
737
+ } finally {
738
+ setLoading(false);
739
+ }
740
+ };
741
+ initDeviceFlow();
742
+ }, [apiBaseUrl, onError]);
743
+ react.useEffect(() => {
744
+ if (!deviceData || timeRemaining === null) return;
745
+ const interval = setInterval(() => {
746
+ setTimeRemaining((prev) => {
747
+ if (prev === null || prev <= 1) {
748
+ clearInterval(interval);
749
+ setError("Device code expired. Please refresh to try again.");
750
+ return 0;
751
+ }
752
+ return prev - 1;
753
+ });
754
+ }, 1e3);
755
+ return () => clearInterval(interval);
756
+ }, [deviceData, timeRemaining]);
757
+ react.useEffect(() => {
758
+ if (!deviceData) return;
759
+ userManagerRef.current = auth.userManager;
760
+ const poll = async () => {
761
+ try {
762
+ const tokenResponse = await pollForToken(apiBaseUrl, deviceData.device_code);
763
+ if (tokenResponse) {
764
+ if (pollTimerRef.current) {
765
+ clearInterval(pollTimerRef.current);
766
+ }
767
+ const userInfo = await fetchUserInfo(apiBaseUrl, tokenResponse.access_token);
768
+ const user = new oidcClientTs.User({
769
+ id_token: tokenResponse.id_token || "",
770
+ access_token: tokenResponse.access_token,
771
+ token_type: tokenResponse.token_type,
772
+ profile: userInfo,
773
+ expires_at: Math.floor(Date.now() / 1e3) + tokenResponse.expires_in,
774
+ scope: tokenResponse.scope,
775
+ refresh_token: tokenResponse.refresh_token
776
+ });
777
+ if (userManagerRef.current) {
778
+ await userManagerRef.current.storeUser(user);
779
+ }
780
+ onSuccess?.(user);
781
+ }
782
+ } catch (err) {
783
+ if (!err.message.includes("authorization_pending")) {
784
+ if (pollTimerRef.current) {
785
+ clearInterval(pollTimerRef.current);
786
+ }
787
+ const error2 = err;
788
+ setError(error2.message);
789
+ onError?.(error2);
790
+ }
791
+ }
792
+ };
793
+ pollTimerRef.current = setInterval(poll, pollingInterval * 1e3);
794
+ return () => {
795
+ if (pollTimerRef.current) {
796
+ clearInterval(pollTimerRef.current);
797
+ }
798
+ };
799
+ }, [deviceData, apiBaseUrl, pollingInterval, onSuccess, onError, auth]);
800
+ if (loading) {
801
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `kc-device-login kc-device-login--loading ${className}`, style, children: [
802
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-device-login__spinner" }),
803
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Initializing device flow..." })
804
+ ] });
805
+ }
806
+ if (error) {
807
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `kc-device-login kc-device-login--error ${className}`, style, children: [
808
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-device-login__error-icon", children: "\u26A0\uFE0F" }),
809
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Error" }),
810
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: error }),
811
+ /* @__PURE__ */ jsxRuntime.jsx(
812
+ "button",
813
+ {
814
+ className: "kc-button kc-button--primary",
815
+ onClick: () => window.location.reload(),
816
+ children: "Retry"
817
+ }
818
+ )
819
+ ] });
820
+ }
821
+ if (!deviceData) {
822
+ return null;
823
+ }
824
+ const formatTime = (seconds) => {
825
+ const mins = Math.floor(seconds / 60);
826
+ const secs = seconds % 60;
827
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
828
+ };
829
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `kc-device-login ${className}`, style, children: [
830
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "kc-device-login__title", children: "Device Login" }),
831
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-device-login__qr", children: /* @__PURE__ */ jsxRuntime.jsx(qrcode_react.QRCodeSVG, { value: deviceData.verification_uri_complete, size: qrCodeSize }) }),
832
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-device-login__instructions", children: [
833
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Scan the QR code with your phone or visit:" }),
834
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "kc-device-login__url", children: deviceData.verification_uri }),
835
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "And enter code:" }),
836
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "kc-device-login__user-code", children: deviceData.user_code })
837
+ ] }),
838
+ timeRemaining !== null && timeRemaining > 0 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "kc-device-login__expiry", children: [
839
+ "Code expires in ",
840
+ formatTime(timeRemaining)
841
+ ] }),
842
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-device-login__status", children: [
843
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-device-login__spinner kc-device-login__spinner--small" }),
844
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Waiting for authorization..." })
845
+ ] })
846
+ ] });
847
+ };
848
+ function useProtectedFetch(options = {}) {
849
+ const auth = useAuth();
850
+ const {
851
+ baseUrl = "",
852
+ onUnauthorized,
853
+ retryOn401 = true,
854
+ headers: customHeaders = {}
855
+ } = options;
856
+ const buildUrl = react.useCallback(
857
+ (url) => {
858
+ if (url.startsWith("http://") || url.startsWith("https://")) {
859
+ return url;
860
+ }
861
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
862
+ const path = url.startsWith("/") ? url : `/${url}`;
863
+ return base + path;
864
+ },
865
+ [baseUrl]
866
+ );
867
+ const getAuthHeader = react.useCallback(() => {
868
+ if (!auth.user?.access_token) {
869
+ return {};
870
+ }
871
+ return {
872
+ Authorization: `Bearer ${auth.user.access_token}`
873
+ };
874
+ }, [auth.user]);
875
+ const buildHeaders = react.useCallback(
876
+ (requestHeaders) => {
877
+ const headers = new Headers(requestHeaders);
878
+ const authHeader = getAuthHeader();
879
+ if (authHeader.Authorization) {
880
+ headers.set("Authorization", authHeader.Authorization);
881
+ }
882
+ Object.entries(customHeaders).forEach(([key, value]) => {
883
+ headers.set(key, value);
884
+ });
885
+ return headers;
886
+ },
887
+ [getAuthHeader, customHeaders]
888
+ );
889
+ const protectedFetch = react.useCallback(
890
+ async (url, init) => {
891
+ const fullUrl = buildUrl(url);
892
+ const headers = buildHeaders(init?.headers);
893
+ const response = await fetch(fullUrl, {
894
+ ...init,
895
+ headers
896
+ });
897
+ if (response.status === 401) {
898
+ onUnauthorized?.();
899
+ if (retryOn401 && auth.user) {
900
+ try {
901
+ await auth.signinSilent();
902
+ const newHeaders = buildHeaders(init?.headers);
903
+ const retryResponse = await fetch(fullUrl, {
904
+ ...init,
905
+ headers: newHeaders
906
+ });
907
+ return retryResponse;
908
+ } catch (error) {
909
+ return response;
910
+ }
911
+ }
912
+ }
913
+ return response;
914
+ },
915
+ [buildUrl, buildHeaders, onUnauthorized, retryOn401, auth]
916
+ );
917
+ const fetchJson = react.useCallback(
918
+ async (url, init) => {
919
+ const response = await protectedFetch(url, init);
920
+ if (!response.ok) {
921
+ const errorText = await response.text();
922
+ throw new Error(
923
+ `HTTP ${response.status}: ${response.statusText}. ${errorText}`
924
+ );
925
+ }
926
+ const data = await response.json();
927
+ return data;
928
+ },
929
+ [protectedFetch]
930
+ );
931
+ return {
932
+ fetchJson,
933
+ fetch: protectedFetch,
934
+ isLoading: auth.isLoading
935
+ };
936
+ }
937
+
938
+ exports.DeviceLogin = DeviceLogin;
939
+ exports.KeycloakConfigBuilder = KeycloakConfigBuilder;
940
+ exports.KeycloakProvider = KeycloakProvider;
941
+ exports.LoginButton = LoginButton;
942
+ exports.LogoutButton = LogoutButton;
943
+ exports.ProtectedRoute = ProtectedRoute;
944
+ exports.SessionMonitor = SessionMonitor;
945
+ exports.UserProfile = UserProfile;
946
+ exports.defaultConfig = defaultConfig;
947
+ exports.fetchUserInfo = fetchUserInfo;
948
+ exports.getConfigFromEnv = getConfigFromEnv;
949
+ exports.pollForToken = pollForToken;
950
+ exports.requestDeviceCode = requestDeviceCode;
951
+ exports.useAuth = useAuth;
952
+ exports.useProtectedFetch = useProtectedFetch;
953
+ exports.useRoles = useRoles;
954
+ exports.useScopes = useScopes;
955
+ //# sourceMappingURL=index.js.map
956
+ //# sourceMappingURL=index.js.map