@chemmangat/msal-next 1.2.1 → 2.0.1
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 +551 -290
- package/SECURITY.md +152 -0
- package/dist/index.d.mts +632 -2
- package/dist/index.d.ts +632 -2
- package/dist/index.js +1092 -102
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1026 -41
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +54 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +91 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +88 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +77 -61
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
import { MsalProvider, useMsal, useAccount } from '@azure/msal-react';
|
|
2
|
+
export { useAccount, useIsAuthenticated, useMsal } from '@azure/msal-react';
|
|
3
|
+
import { LogLevel, InteractionStatus, PublicClientApplication, EventType } from '@azure/msal-browser';
|
|
4
|
+
import { useState, useRef, useEffect, useMemo, useCallback, Component } from 'react';
|
|
5
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
6
|
+
import { NextResponse } from 'next/server';
|
|
2
7
|
|
|
3
|
-
// src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
// src/utils/validation.ts
|
|
9
|
+
function safeJsonParse(jsonString, validator) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(jsonString);
|
|
12
|
+
if (validator(parsed)) {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
console.warn("[Validation] JSON validation failed");
|
|
16
|
+
return null;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("[Validation] JSON parse error:", error);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isValidAccountData(data) {
|
|
23
|
+
return typeof data === "object" && data !== null && typeof data.homeAccountId === "string" && data.homeAccountId.length > 0 && typeof data.username === "string" && data.username.length > 0 && (data.name === void 0 || typeof data.name === "string");
|
|
24
|
+
}
|
|
25
|
+
function sanitizeError(error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
const message = error.message;
|
|
28
|
+
const sanitized = message.replace(/[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g, "[TOKEN_REDACTED]").replace(/[a-f0-9]{32,}/gi, "[SECRET_REDACTED]").replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]");
|
|
29
|
+
return sanitized;
|
|
30
|
+
}
|
|
31
|
+
return "An unexpected error occurred";
|
|
32
|
+
}
|
|
33
|
+
function isValidRedirectUri(uri, allowedOrigins) {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(uri);
|
|
36
|
+
return allowedOrigins.some((allowed) => {
|
|
37
|
+
const allowedUrl = new URL(allowed);
|
|
38
|
+
return url.origin === allowedUrl.origin;
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isValidScope(scope) {
|
|
45
|
+
return /^[a-zA-Z0-9._-]+$/.test(scope);
|
|
46
|
+
}
|
|
47
|
+
function validateScopes(scopes) {
|
|
48
|
+
return Array.isArray(scopes) && scopes.every(isValidScope);
|
|
49
|
+
}
|
|
7
50
|
|
|
8
51
|
// src/utils/createMsalConfig.ts
|
|
9
|
-
import { LogLevel } from "@azure/msal-browser";
|
|
10
52
|
function createMsalConfig(config) {
|
|
11
53
|
if (config.msalConfig) {
|
|
12
54
|
return config.msalConfig;
|
|
@@ -21,7 +63,8 @@ function createMsalConfig(config) {
|
|
|
21
63
|
storeAuthStateInCookie = false,
|
|
22
64
|
navigateToLoginRequestUrl = true,
|
|
23
65
|
enableLogging = false,
|
|
24
|
-
loggerCallback
|
|
66
|
+
loggerCallback,
|
|
67
|
+
allowedRedirectUris
|
|
25
68
|
} = config;
|
|
26
69
|
if (!clientId) {
|
|
27
70
|
throw new Error("@chemmangat/msal-next: clientId is required");
|
|
@@ -37,6 +80,19 @@ function createMsalConfig(config) {
|
|
|
37
80
|
};
|
|
38
81
|
const defaultRedirectUri = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
|
|
39
82
|
const finalRedirectUri = redirectUri || defaultRedirectUri;
|
|
83
|
+
if (allowedRedirectUris && allowedRedirectUris.length > 0) {
|
|
84
|
+
if (!isValidRedirectUri(finalRedirectUri, allowedRedirectUris)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`@chemmangat/msal-next: redirectUri "${finalRedirectUri}" is not in the allowed list`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const finalPostLogoutUri = postLogoutRedirectUri || finalRedirectUri;
|
|
90
|
+
if (!isValidRedirectUri(finalPostLogoutUri, allowedRedirectUris)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`@chemmangat/msal-next: postLogoutRedirectUri "${finalPostLogoutUri}" is not in the allowed list`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
40
96
|
const msalConfig = {
|
|
41
97
|
auth: {
|
|
42
98
|
clientId,
|
|
@@ -74,9 +130,6 @@ function createMsalConfig(config) {
|
|
|
74
130
|
};
|
|
75
131
|
return msalConfig;
|
|
76
132
|
}
|
|
77
|
-
|
|
78
|
-
// src/components/MsalAuthProvider.tsx
|
|
79
|
-
import { Fragment, jsx } from "react/jsx-runtime";
|
|
80
133
|
var globalMsalInstance = null;
|
|
81
134
|
function getMsalInstance() {
|
|
82
135
|
return globalMsalInstance;
|
|
@@ -138,14 +191,11 @@ function MsalAuthProvider({ children, loadingComponent, onInitialized, ...config
|
|
|
138
191
|
}
|
|
139
192
|
return /* @__PURE__ */ jsx(MsalProvider, { instance: msalInstance, children });
|
|
140
193
|
}
|
|
141
|
-
|
|
142
|
-
// src/hooks/useMsalAuth.ts
|
|
143
|
-
import { useMsal, useAccount } from "@azure/msal-react";
|
|
144
|
-
import { InteractionStatus } from "@azure/msal-browser";
|
|
145
|
-
import { useCallback, useMemo } from "react";
|
|
194
|
+
var pendingTokenRequests = /* @__PURE__ */ new Map();
|
|
146
195
|
function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
147
196
|
const { instance, accounts, inProgress } = useMsal();
|
|
148
197
|
const account = useAccount(accounts[0] || null);
|
|
198
|
+
const popupInProgressRef = useRef(false);
|
|
149
199
|
const isAuthenticated = useMemo(() => accounts.length > 0, [accounts]);
|
|
150
200
|
const loginPopup = useCallback(
|
|
151
201
|
async (scopes = defaultScopes) => {
|
|
@@ -205,7 +255,8 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
205
255
|
try {
|
|
206
256
|
const request = {
|
|
207
257
|
scopes,
|
|
208
|
-
account
|
|
258
|
+
account,
|
|
259
|
+
forceRefresh: false
|
|
209
260
|
};
|
|
210
261
|
const response = await instance.acquireTokenSilent(request);
|
|
211
262
|
return response.accessToken;
|
|
@@ -221,7 +272,11 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
221
272
|
if (!account) {
|
|
222
273
|
throw new Error("[MSAL] No active account. Please login first.");
|
|
223
274
|
}
|
|
275
|
+
if (popupInProgressRef.current) {
|
|
276
|
+
throw new Error("[MSAL] Popup already in progress. Please wait.");
|
|
277
|
+
}
|
|
224
278
|
try {
|
|
279
|
+
popupInProgressRef.current = true;
|
|
225
280
|
const request = {
|
|
226
281
|
scopes,
|
|
227
282
|
account
|
|
@@ -231,6 +286,8 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
231
286
|
} catch (error) {
|
|
232
287
|
console.error("[MSAL] Token popup acquisition failed:", error);
|
|
233
288
|
throw error;
|
|
289
|
+
} finally {
|
|
290
|
+
popupInProgressRef.current = false;
|
|
234
291
|
}
|
|
235
292
|
},
|
|
236
293
|
[instance, account, defaultScopes]
|
|
@@ -255,14 +312,25 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
255
312
|
);
|
|
256
313
|
const acquireToken = useCallback(
|
|
257
314
|
async (scopes = defaultScopes) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return await acquireTokenPopup(scopes);
|
|
315
|
+
const requestKey = `${account?.homeAccountId || "anonymous"}-${scopes.sort().join(",")}`;
|
|
316
|
+
const pendingRequest = pendingTokenRequests.get(requestKey);
|
|
317
|
+
if (pendingRequest) {
|
|
318
|
+
return pendingRequest;
|
|
263
319
|
}
|
|
320
|
+
const tokenRequest = (async () => {
|
|
321
|
+
try {
|
|
322
|
+
return await acquireTokenSilent(scopes);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.warn("[MSAL] Silent token acquisition failed, falling back to popup");
|
|
325
|
+
return await acquireTokenPopup(scopes);
|
|
326
|
+
} finally {
|
|
327
|
+
pendingTokenRequests.delete(requestKey);
|
|
328
|
+
}
|
|
329
|
+
})();
|
|
330
|
+
pendingTokenRequests.set(requestKey, tokenRequest);
|
|
331
|
+
return tokenRequest;
|
|
264
332
|
},
|
|
265
|
-
[acquireTokenSilent, acquireTokenPopup, defaultScopes]
|
|
333
|
+
[acquireTokenSilent, acquireTokenPopup, defaultScopes, account]
|
|
266
334
|
);
|
|
267
335
|
const clearSession = useCallback(async () => {
|
|
268
336
|
instance.setActiveAccount(null);
|
|
@@ -284,9 +352,6 @@ function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
|
284
352
|
clearSession
|
|
285
353
|
};
|
|
286
354
|
}
|
|
287
|
-
|
|
288
|
-
// src/components/MicrosoftSignInButton.tsx
|
|
289
|
-
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
290
355
|
function MicrosoftSignInButton({
|
|
291
356
|
text = "Sign in with Microsoft",
|
|
292
357
|
variant = "dark",
|
|
@@ -364,30 +429,950 @@ function MicrosoftSignInButton({
|
|
|
364
429
|
style: baseStyles,
|
|
365
430
|
"aria-label": text,
|
|
366
431
|
children: [
|
|
367
|
-
/* @__PURE__ */
|
|
368
|
-
/* @__PURE__ */
|
|
432
|
+
/* @__PURE__ */ jsx(MicrosoftLogo, {}),
|
|
433
|
+
/* @__PURE__ */ jsx("span", { children: text })
|
|
369
434
|
]
|
|
370
435
|
}
|
|
371
436
|
);
|
|
372
437
|
}
|
|
373
438
|
function MicrosoftLogo() {
|
|
374
439
|
return /* @__PURE__ */ jsxs("svg", { width: "21", height: "21", viewBox: "0 0 21 21", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
375
|
-
/* @__PURE__ */
|
|
376
|
-
/* @__PURE__ */
|
|
377
|
-
/* @__PURE__ */
|
|
378
|
-
/* @__PURE__ */
|
|
440
|
+
/* @__PURE__ */ jsx("rect", { width: "10", height: "10", fill: "#F25022" }),
|
|
441
|
+
/* @__PURE__ */ jsx("rect", { x: "11", width: "10", height: "10", fill: "#7FBA00" }),
|
|
442
|
+
/* @__PURE__ */ jsx("rect", { y: "11", width: "10", height: "10", fill: "#00A4EF" }),
|
|
443
|
+
/* @__PURE__ */ jsx("rect", { x: "11", y: "11", width: "10", height: "10", fill: "#FFB900" })
|
|
379
444
|
] });
|
|
380
445
|
}
|
|
446
|
+
function SignOutButton({
|
|
447
|
+
text = "Sign out",
|
|
448
|
+
variant = "dark",
|
|
449
|
+
size = "medium",
|
|
450
|
+
useRedirect = false,
|
|
451
|
+
className = "",
|
|
452
|
+
style,
|
|
453
|
+
onSuccess,
|
|
454
|
+
onError
|
|
455
|
+
}) {
|
|
456
|
+
const { logoutPopup, logoutRedirect, inProgress } = useMsalAuth();
|
|
457
|
+
const handleClick = async () => {
|
|
458
|
+
try {
|
|
459
|
+
if (useRedirect) {
|
|
460
|
+
await logoutRedirect();
|
|
461
|
+
} else {
|
|
462
|
+
await logoutPopup();
|
|
463
|
+
}
|
|
464
|
+
onSuccess?.();
|
|
465
|
+
} catch (error) {
|
|
466
|
+
onError?.(error);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const sizeStyles = {
|
|
470
|
+
small: {
|
|
471
|
+
padding: "8px 16px",
|
|
472
|
+
fontSize: "14px",
|
|
473
|
+
height: "36px"
|
|
474
|
+
},
|
|
475
|
+
medium: {
|
|
476
|
+
padding: "10px 20px",
|
|
477
|
+
fontSize: "15px",
|
|
478
|
+
height: "41px"
|
|
479
|
+
},
|
|
480
|
+
large: {
|
|
481
|
+
padding: "12px 24px",
|
|
482
|
+
fontSize: "16px",
|
|
483
|
+
height: "48px"
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const variantStyles = {
|
|
487
|
+
dark: {
|
|
488
|
+
backgroundColor: "#2F2F2F",
|
|
489
|
+
color: "#FFFFFF",
|
|
490
|
+
border: "1px solid #8C8C8C"
|
|
491
|
+
},
|
|
492
|
+
light: {
|
|
493
|
+
backgroundColor: "#FFFFFF",
|
|
494
|
+
color: "#5E5E5E",
|
|
495
|
+
border: "1px solid #8C8C8C"
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const baseStyles = {
|
|
499
|
+
display: "inline-flex",
|
|
500
|
+
alignItems: "center",
|
|
501
|
+
justifyContent: "center",
|
|
502
|
+
gap: "12px",
|
|
503
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
504
|
+
fontWeight: 600,
|
|
505
|
+
borderRadius: "2px",
|
|
506
|
+
cursor: inProgress ? "not-allowed" : "pointer",
|
|
507
|
+
transition: "all 0.2s ease",
|
|
508
|
+
opacity: inProgress ? 0.6 : 1,
|
|
509
|
+
...variantStyles[variant],
|
|
510
|
+
...sizeStyles[size],
|
|
511
|
+
...style
|
|
512
|
+
};
|
|
513
|
+
return /* @__PURE__ */ jsxs(
|
|
514
|
+
"button",
|
|
515
|
+
{
|
|
516
|
+
onClick: handleClick,
|
|
517
|
+
disabled: inProgress,
|
|
518
|
+
className,
|
|
519
|
+
style: baseStyles,
|
|
520
|
+
"aria-label": text,
|
|
521
|
+
children: [
|
|
522
|
+
/* @__PURE__ */ jsx(MicrosoftLogo2, {}),
|
|
523
|
+
/* @__PURE__ */ jsx("span", { children: text })
|
|
524
|
+
]
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function MicrosoftLogo2() {
|
|
529
|
+
return /* @__PURE__ */ jsxs("svg", { width: "21", height: "21", viewBox: "0 0 21 21", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
530
|
+
/* @__PURE__ */ jsx("rect", { width: "10", height: "10", fill: "#F25022" }),
|
|
531
|
+
/* @__PURE__ */ jsx("rect", { x: "11", width: "10", height: "10", fill: "#7FBA00" }),
|
|
532
|
+
/* @__PURE__ */ jsx("rect", { y: "11", width: "10", height: "10", fill: "#00A4EF" }),
|
|
533
|
+
/* @__PURE__ */ jsx("rect", { x: "11", y: "11", width: "10", height: "10", fill: "#FFB900" })
|
|
534
|
+
] });
|
|
535
|
+
}
|
|
536
|
+
function useGraphApi() {
|
|
537
|
+
const { acquireToken } = useMsalAuth();
|
|
538
|
+
const request = useCallback(
|
|
539
|
+
async (endpoint, options = {}) => {
|
|
540
|
+
const {
|
|
541
|
+
scopes = ["User.Read"],
|
|
542
|
+
version = "v1.0",
|
|
543
|
+
debug = false,
|
|
544
|
+
...fetchOptions
|
|
545
|
+
} = options;
|
|
546
|
+
try {
|
|
547
|
+
const token = await acquireToken(scopes);
|
|
548
|
+
const baseUrl = `https://graph.microsoft.com/${version}`;
|
|
549
|
+
const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
|
550
|
+
if (debug) {
|
|
551
|
+
console.log("[GraphAPI] Request:", { url, method: fetchOptions.method || "GET" });
|
|
552
|
+
}
|
|
553
|
+
const response = await fetch(url, {
|
|
554
|
+
...fetchOptions,
|
|
555
|
+
headers: {
|
|
556
|
+
"Authorization": `Bearer ${token}`,
|
|
557
|
+
"Content-Type": "application/json",
|
|
558
|
+
...fetchOptions.headers
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
const errorText = await response.text();
|
|
563
|
+
const errorMessage = `Graph API error (${response.status}): ${errorText}`;
|
|
564
|
+
throw new Error(errorMessage);
|
|
565
|
+
}
|
|
566
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
const data = await response.json();
|
|
570
|
+
if (debug) {
|
|
571
|
+
console.log("[GraphAPI] Response:", data);
|
|
572
|
+
}
|
|
573
|
+
return data;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
const sanitizedMessage = sanitizeError(error);
|
|
576
|
+
console.error("[GraphAPI] Request failed:", sanitizedMessage);
|
|
577
|
+
throw new Error(sanitizedMessage);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
[acquireToken]
|
|
581
|
+
);
|
|
582
|
+
const get = useCallback(
|
|
583
|
+
(endpoint, options = {}) => {
|
|
584
|
+
return request(endpoint, { ...options, method: "GET" });
|
|
585
|
+
},
|
|
586
|
+
[request]
|
|
587
|
+
);
|
|
588
|
+
const post = useCallback(
|
|
589
|
+
(endpoint, body, options = {}) => {
|
|
590
|
+
return request(endpoint, {
|
|
591
|
+
...options,
|
|
592
|
+
method: "POST",
|
|
593
|
+
body: body ? JSON.stringify(body) : void 0
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
[request]
|
|
597
|
+
);
|
|
598
|
+
const put = useCallback(
|
|
599
|
+
(endpoint, body, options = {}) => {
|
|
600
|
+
return request(endpoint, {
|
|
601
|
+
...options,
|
|
602
|
+
method: "PUT",
|
|
603
|
+
body: body ? JSON.stringify(body) : void 0
|
|
604
|
+
});
|
|
605
|
+
},
|
|
606
|
+
[request]
|
|
607
|
+
);
|
|
608
|
+
const patch = useCallback(
|
|
609
|
+
(endpoint, body, options = {}) => {
|
|
610
|
+
return request(endpoint, {
|
|
611
|
+
...options,
|
|
612
|
+
method: "PATCH",
|
|
613
|
+
body: body ? JSON.stringify(body) : void 0
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
[request]
|
|
617
|
+
);
|
|
618
|
+
const deleteRequest = useCallback(
|
|
619
|
+
(endpoint, options = {}) => {
|
|
620
|
+
return request(endpoint, { ...options, method: "DELETE" });
|
|
621
|
+
},
|
|
622
|
+
[request]
|
|
623
|
+
);
|
|
624
|
+
return {
|
|
625
|
+
get,
|
|
626
|
+
post,
|
|
627
|
+
put,
|
|
628
|
+
patch,
|
|
629
|
+
delete: deleteRequest,
|
|
630
|
+
request
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/hooks/useUserProfile.ts
|
|
635
|
+
var profileCache = /* @__PURE__ */ new Map();
|
|
636
|
+
var CACHE_DURATION = 5 * 60 * 1e3;
|
|
637
|
+
var MAX_CACHE_SIZE = 100;
|
|
638
|
+
function enforceCacheLimit() {
|
|
639
|
+
if (profileCache.size > MAX_CACHE_SIZE) {
|
|
640
|
+
const entries = Array.from(profileCache.entries());
|
|
641
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
642
|
+
const toRemove = entries.slice(0, profileCache.size - MAX_CACHE_SIZE);
|
|
643
|
+
toRemove.forEach(([key]) => {
|
|
644
|
+
const cached = profileCache.get(key);
|
|
645
|
+
if (cached?.data.photo) {
|
|
646
|
+
URL.revokeObjectURL(cached.data.photo);
|
|
647
|
+
}
|
|
648
|
+
profileCache.delete(key);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function useUserProfile() {
|
|
653
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
654
|
+
const graph = useGraphApi();
|
|
655
|
+
const [profile, setProfile] = useState(null);
|
|
656
|
+
const [loading, setLoading] = useState(false);
|
|
657
|
+
const [error, setError] = useState(null);
|
|
658
|
+
const fetchProfile = useCallback(async () => {
|
|
659
|
+
if (!isAuthenticated || !account) {
|
|
660
|
+
setProfile(null);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const cacheKey = account.homeAccountId;
|
|
664
|
+
const cached = profileCache.get(cacheKey);
|
|
665
|
+
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
|
666
|
+
setProfile(cached.data);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
setLoading(true);
|
|
670
|
+
setError(null);
|
|
671
|
+
try {
|
|
672
|
+
const userData = await graph.get("/me", {
|
|
673
|
+
scopes: ["User.Read"]
|
|
674
|
+
});
|
|
675
|
+
let photoUrl;
|
|
676
|
+
try {
|
|
677
|
+
const photoBlob = await graph.get("/me/photo/$value", {
|
|
678
|
+
scopes: ["User.Read"],
|
|
679
|
+
headers: {
|
|
680
|
+
"Content-Type": "image/jpeg"
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
if (photoBlob) {
|
|
684
|
+
photoUrl = URL.createObjectURL(photoBlob);
|
|
685
|
+
}
|
|
686
|
+
} catch (photoError) {
|
|
687
|
+
console.debug("[UserProfile] Photo not available");
|
|
688
|
+
}
|
|
689
|
+
const profileData = {
|
|
690
|
+
id: userData.id,
|
|
691
|
+
displayName: userData.displayName,
|
|
692
|
+
givenName: userData.givenName,
|
|
693
|
+
surname: userData.surname,
|
|
694
|
+
userPrincipalName: userData.userPrincipalName,
|
|
695
|
+
mail: userData.mail,
|
|
696
|
+
jobTitle: userData.jobTitle,
|
|
697
|
+
officeLocation: userData.officeLocation,
|
|
698
|
+
mobilePhone: userData.mobilePhone,
|
|
699
|
+
businessPhones: userData.businessPhones,
|
|
700
|
+
photo: photoUrl
|
|
701
|
+
};
|
|
702
|
+
profileCache.set(cacheKey, {
|
|
703
|
+
data: profileData,
|
|
704
|
+
timestamp: Date.now()
|
|
705
|
+
});
|
|
706
|
+
enforceCacheLimit();
|
|
707
|
+
setProfile(profileData);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
const error2 = err;
|
|
710
|
+
const sanitizedMessage = sanitizeError(error2);
|
|
711
|
+
const sanitizedError = new Error(sanitizedMessage);
|
|
712
|
+
setError(sanitizedError);
|
|
713
|
+
console.error("[UserProfile] Failed to fetch profile:", sanitizedMessage);
|
|
714
|
+
} finally {
|
|
715
|
+
setLoading(false);
|
|
716
|
+
}
|
|
717
|
+
}, [isAuthenticated, account, graph]);
|
|
718
|
+
const clearCache = useCallback(() => {
|
|
719
|
+
if (account) {
|
|
720
|
+
const cached = profileCache.get(account.homeAccountId);
|
|
721
|
+
if (cached?.data.photo) {
|
|
722
|
+
URL.revokeObjectURL(cached.data.photo);
|
|
723
|
+
}
|
|
724
|
+
profileCache.delete(account.homeAccountId);
|
|
725
|
+
}
|
|
726
|
+
if (profile?.photo) {
|
|
727
|
+
URL.revokeObjectURL(profile.photo);
|
|
728
|
+
}
|
|
729
|
+
setProfile(null);
|
|
730
|
+
}, [account, profile]);
|
|
731
|
+
useEffect(() => {
|
|
732
|
+
fetchProfile();
|
|
733
|
+
return () => {
|
|
734
|
+
if (profile?.photo) {
|
|
735
|
+
URL.revokeObjectURL(profile.photo);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}, [fetchProfile]);
|
|
739
|
+
useEffect(() => {
|
|
740
|
+
return () => {
|
|
741
|
+
if (profile?.photo) {
|
|
742
|
+
URL.revokeObjectURL(profile.photo);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}, [profile?.photo]);
|
|
746
|
+
return {
|
|
747
|
+
profile,
|
|
748
|
+
loading,
|
|
749
|
+
error,
|
|
750
|
+
refetch: fetchProfile,
|
|
751
|
+
clearCache
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function UserAvatar({
|
|
755
|
+
size = 40,
|
|
756
|
+
className = "",
|
|
757
|
+
style,
|
|
758
|
+
showTooltip = true,
|
|
759
|
+
fallbackImage
|
|
760
|
+
}) {
|
|
761
|
+
const { profile, loading } = useUserProfile();
|
|
762
|
+
const [photoUrl, setPhotoUrl] = useState(null);
|
|
763
|
+
const [photoError, setPhotoError] = useState(false);
|
|
764
|
+
useEffect(() => {
|
|
765
|
+
if (profile?.photo) {
|
|
766
|
+
setPhotoUrl(profile.photo);
|
|
767
|
+
}
|
|
768
|
+
}, [profile?.photo]);
|
|
769
|
+
const getInitials = () => {
|
|
770
|
+
if (!profile) return "?";
|
|
771
|
+
const { givenName, surname, displayName: displayName2 } = profile;
|
|
772
|
+
if (givenName && surname) {
|
|
773
|
+
return `${givenName[0]}${surname[0]}`.toUpperCase();
|
|
774
|
+
}
|
|
775
|
+
if (displayName2) {
|
|
776
|
+
const parts = displayName2.split(" ");
|
|
777
|
+
if (parts.length >= 2) {
|
|
778
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
779
|
+
}
|
|
780
|
+
return displayName2.substring(0, 2).toUpperCase();
|
|
781
|
+
}
|
|
782
|
+
return "?";
|
|
783
|
+
};
|
|
784
|
+
const baseStyles = {
|
|
785
|
+
width: `${size}px`,
|
|
786
|
+
height: `${size}px`,
|
|
787
|
+
borderRadius: "50%",
|
|
788
|
+
display: "inline-flex",
|
|
789
|
+
alignItems: "center",
|
|
790
|
+
justifyContent: "center",
|
|
791
|
+
fontSize: `${size * 0.4}px`,
|
|
792
|
+
fontWeight: 600,
|
|
793
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
794
|
+
backgroundColor: "#0078D4",
|
|
795
|
+
color: "#FFFFFF",
|
|
796
|
+
overflow: "hidden",
|
|
797
|
+
userSelect: "none",
|
|
798
|
+
...style
|
|
799
|
+
};
|
|
800
|
+
const displayName = profile?.displayName || "User";
|
|
801
|
+
if (loading) {
|
|
802
|
+
return /* @__PURE__ */ jsx(
|
|
803
|
+
"div",
|
|
804
|
+
{
|
|
805
|
+
className,
|
|
806
|
+
style: { ...baseStyles, backgroundColor: "#E1E1E1" },
|
|
807
|
+
"aria-label": "Loading user avatar",
|
|
808
|
+
children: /* @__PURE__ */ jsx("span", { style: { fontSize: `${size * 0.3}px` }, children: "..." })
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (photoUrl && !photoError) {
|
|
813
|
+
return /* @__PURE__ */ jsx(
|
|
814
|
+
"div",
|
|
815
|
+
{
|
|
816
|
+
className,
|
|
817
|
+
style: baseStyles,
|
|
818
|
+
title: showTooltip ? displayName : void 0,
|
|
819
|
+
"aria-label": `${displayName} avatar`,
|
|
820
|
+
children: /* @__PURE__ */ jsx(
|
|
821
|
+
"img",
|
|
822
|
+
{
|
|
823
|
+
src: photoUrl,
|
|
824
|
+
alt: displayName,
|
|
825
|
+
style: { width: "100%", height: "100%", objectFit: "cover" },
|
|
826
|
+
onError: () => {
|
|
827
|
+
setPhotoError(true);
|
|
828
|
+
if (fallbackImage) {
|
|
829
|
+
setPhotoUrl(fallbackImage);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
return /* @__PURE__ */ jsx(
|
|
838
|
+
"div",
|
|
839
|
+
{
|
|
840
|
+
className,
|
|
841
|
+
style: baseStyles,
|
|
842
|
+
title: showTooltip ? displayName : void 0,
|
|
843
|
+
"aria-label": `${displayName} avatar`,
|
|
844
|
+
children: getInitials()
|
|
845
|
+
}
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
function AuthStatus({
|
|
849
|
+
className = "",
|
|
850
|
+
style,
|
|
851
|
+
showDetails = false,
|
|
852
|
+
renderLoading,
|
|
853
|
+
renderAuthenticated,
|
|
854
|
+
renderUnauthenticated
|
|
855
|
+
}) {
|
|
856
|
+
const { isAuthenticated, inProgress, account } = useMsalAuth();
|
|
857
|
+
const baseStyles = {
|
|
858
|
+
display: "inline-flex",
|
|
859
|
+
alignItems: "center",
|
|
860
|
+
gap: "8px",
|
|
861
|
+
padding: "8px 12px",
|
|
862
|
+
borderRadius: "4px",
|
|
863
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
864
|
+
fontSize: "14px",
|
|
865
|
+
fontWeight: 500,
|
|
866
|
+
...style
|
|
867
|
+
};
|
|
868
|
+
if (inProgress) {
|
|
869
|
+
if (renderLoading) {
|
|
870
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
|
|
871
|
+
}
|
|
872
|
+
return /* @__PURE__ */ jsxs(
|
|
873
|
+
"div",
|
|
874
|
+
{
|
|
875
|
+
className,
|
|
876
|
+
style: { ...baseStyles, backgroundColor: "#FFF4CE", color: "#8A6D3B" },
|
|
877
|
+
role: "status",
|
|
878
|
+
"aria-live": "polite",
|
|
879
|
+
children: [
|
|
880
|
+
/* @__PURE__ */ jsx(StatusIndicator, { color: "#FFA500" }),
|
|
881
|
+
/* @__PURE__ */ jsx("span", { children: "Loading..." })
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (isAuthenticated) {
|
|
887
|
+
const username = account?.username || account?.name || "User";
|
|
888
|
+
if (renderAuthenticated) {
|
|
889
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderAuthenticated(username) });
|
|
890
|
+
}
|
|
891
|
+
return /* @__PURE__ */ jsxs(
|
|
892
|
+
"div",
|
|
893
|
+
{
|
|
894
|
+
className,
|
|
895
|
+
style: { ...baseStyles, backgroundColor: "#D4EDDA", color: "#155724" },
|
|
896
|
+
role: "status",
|
|
897
|
+
"aria-live": "polite",
|
|
898
|
+
children: [
|
|
899
|
+
/* @__PURE__ */ jsx(StatusIndicator, { color: "#28A745" }),
|
|
900
|
+
/* @__PURE__ */ jsx("span", { children: showDetails ? `Authenticated as ${username}` : "Authenticated" })
|
|
901
|
+
]
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (renderUnauthenticated) {
|
|
906
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderUnauthenticated() });
|
|
907
|
+
}
|
|
908
|
+
return /* @__PURE__ */ jsxs(
|
|
909
|
+
"div",
|
|
910
|
+
{
|
|
911
|
+
className,
|
|
912
|
+
style: { ...baseStyles, backgroundColor: "#F8D7DA", color: "#721C24" },
|
|
913
|
+
role: "status",
|
|
914
|
+
"aria-live": "polite",
|
|
915
|
+
children: [
|
|
916
|
+
/* @__PURE__ */ jsx(StatusIndicator, { color: "#DC3545" }),
|
|
917
|
+
/* @__PURE__ */ jsx("span", { children: "Not authenticated" })
|
|
918
|
+
]
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
function StatusIndicator({ color }) {
|
|
923
|
+
return /* @__PURE__ */ jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx("circle", { cx: "4", cy: "4", r: "4", fill: color }) });
|
|
924
|
+
}
|
|
925
|
+
function AuthGuard({
|
|
926
|
+
children,
|
|
927
|
+
loadingComponent,
|
|
928
|
+
fallbackComponent,
|
|
929
|
+
useRedirect = true,
|
|
930
|
+
scopes,
|
|
931
|
+
onAuthRequired
|
|
932
|
+
}) {
|
|
933
|
+
const { isAuthenticated, inProgress, loginRedirect, loginPopup } = useMsalAuth();
|
|
934
|
+
useEffect(() => {
|
|
935
|
+
if (!isAuthenticated && !inProgress) {
|
|
936
|
+
onAuthRequired?.();
|
|
937
|
+
const login = async () => {
|
|
938
|
+
try {
|
|
939
|
+
if (useRedirect) {
|
|
940
|
+
await loginRedirect(scopes);
|
|
941
|
+
} else {
|
|
942
|
+
await loginPopup(scopes);
|
|
943
|
+
}
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.error("[AuthGuard] Authentication failed:", error);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
login();
|
|
949
|
+
}
|
|
950
|
+
}, [isAuthenticated, inProgress, useRedirect, scopes, loginRedirect, loginPopup, onAuthRequired]);
|
|
951
|
+
if (inProgress) {
|
|
952
|
+
return /* @__PURE__ */ jsx(Fragment, { children: loadingComponent || /* @__PURE__ */ jsx("div", { children: "Authenticating..." }) });
|
|
953
|
+
}
|
|
954
|
+
if (!isAuthenticated) {
|
|
955
|
+
return /* @__PURE__ */ jsx(Fragment, { children: fallbackComponent || /* @__PURE__ */ jsx("div", { children: "Redirecting to login..." }) });
|
|
956
|
+
}
|
|
957
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
958
|
+
}
|
|
959
|
+
var ErrorBoundary = class extends Component {
|
|
960
|
+
constructor(props) {
|
|
961
|
+
super(props);
|
|
962
|
+
this.reset = () => {
|
|
963
|
+
this.setState({
|
|
964
|
+
hasError: false,
|
|
965
|
+
error: null
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
this.state = {
|
|
969
|
+
hasError: false,
|
|
970
|
+
error: null
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
static getDerivedStateFromError(error) {
|
|
974
|
+
return {
|
|
975
|
+
hasError: true,
|
|
976
|
+
error
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
componentDidCatch(error, errorInfo) {
|
|
980
|
+
const { onError, debug } = this.props;
|
|
981
|
+
if (debug) {
|
|
982
|
+
console.error("[ErrorBoundary] Caught error:", error);
|
|
983
|
+
console.error("[ErrorBoundary] Error info:", errorInfo);
|
|
984
|
+
}
|
|
985
|
+
onError?.(error, errorInfo);
|
|
986
|
+
}
|
|
987
|
+
render() {
|
|
988
|
+
const { hasError, error } = this.state;
|
|
989
|
+
const { children, fallback } = this.props;
|
|
990
|
+
if (hasError && error) {
|
|
991
|
+
if (fallback) {
|
|
992
|
+
return fallback(error, this.reset);
|
|
993
|
+
}
|
|
994
|
+
return /* @__PURE__ */ jsxs(
|
|
995
|
+
"div",
|
|
996
|
+
{
|
|
997
|
+
style: {
|
|
998
|
+
padding: "20px",
|
|
999
|
+
margin: "20px",
|
|
1000
|
+
border: "1px solid #DC3545",
|
|
1001
|
+
borderRadius: "4px",
|
|
1002
|
+
backgroundColor: "#F8D7DA",
|
|
1003
|
+
color: "#721C24",
|
|
1004
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif'
|
|
1005
|
+
},
|
|
1006
|
+
children: [
|
|
1007
|
+
/* @__PURE__ */ jsx("h2", { style: { margin: "0 0 10px 0", fontSize: "18px" }, children: "Authentication Error" }),
|
|
1008
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "0 0 10px 0" }, children: error.message }),
|
|
1009
|
+
/* @__PURE__ */ jsx(
|
|
1010
|
+
"button",
|
|
1011
|
+
{
|
|
1012
|
+
onClick: this.reset,
|
|
1013
|
+
style: {
|
|
1014
|
+
padding: "8px 16px",
|
|
1015
|
+
backgroundColor: "#DC3545",
|
|
1016
|
+
color: "#FFFFFF",
|
|
1017
|
+
border: "none",
|
|
1018
|
+
borderRadius: "4px",
|
|
1019
|
+
cursor: "pointer",
|
|
1020
|
+
fontSize: "14px",
|
|
1021
|
+
fontWeight: 600
|
|
1022
|
+
},
|
|
1023
|
+
children: "Try Again"
|
|
1024
|
+
}
|
|
1025
|
+
)
|
|
1026
|
+
]
|
|
1027
|
+
}
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return children;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
var rolesCache = /* @__PURE__ */ new Map();
|
|
1034
|
+
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
1035
|
+
var MAX_CACHE_SIZE2 = 100;
|
|
1036
|
+
function clearRolesCache(accountId) {
|
|
1037
|
+
if (accountId) {
|
|
1038
|
+
rolesCache.delete(accountId);
|
|
1039
|
+
} else {
|
|
1040
|
+
rolesCache.clear();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function enforceCacheLimit2() {
|
|
1044
|
+
if (rolesCache.size > MAX_CACHE_SIZE2) {
|
|
1045
|
+
const entries = Array.from(rolesCache.entries());
|
|
1046
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
1047
|
+
const toRemove = entries.slice(0, rolesCache.size - MAX_CACHE_SIZE2);
|
|
1048
|
+
toRemove.forEach(([key]) => rolesCache.delete(key));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function useRoles() {
|
|
1052
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
1053
|
+
const graph = useGraphApi();
|
|
1054
|
+
const [roles, setRoles] = useState([]);
|
|
1055
|
+
const [groups, setGroups] = useState([]);
|
|
1056
|
+
const [loading, setLoading] = useState(false);
|
|
1057
|
+
const [error, setError] = useState(null);
|
|
1058
|
+
const fetchRolesAndGroups = useCallback(async () => {
|
|
1059
|
+
if (!isAuthenticated || !account) {
|
|
1060
|
+
setRoles([]);
|
|
1061
|
+
setGroups([]);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const cacheKey = account.homeAccountId;
|
|
1065
|
+
const cached = rolesCache.get(cacheKey);
|
|
1066
|
+
if (cached && Date.now() - cached.timestamp < CACHE_DURATION2) {
|
|
1067
|
+
setRoles(cached.roles);
|
|
1068
|
+
setGroups(cached.groups);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
setLoading(true);
|
|
1072
|
+
setError(null);
|
|
1073
|
+
try {
|
|
1074
|
+
const idTokenClaims = account.idTokenClaims;
|
|
1075
|
+
const tokenRoles = idTokenClaims?.roles || [];
|
|
1076
|
+
const groupsResponse = await graph.get("/me/memberOf", {
|
|
1077
|
+
scopes: ["User.Read", "Directory.Read.All"]
|
|
1078
|
+
});
|
|
1079
|
+
const userGroups = groupsResponse.value.map((group) => group.id);
|
|
1080
|
+
rolesCache.set(cacheKey, {
|
|
1081
|
+
roles: tokenRoles,
|
|
1082
|
+
groups: userGroups,
|
|
1083
|
+
timestamp: Date.now()
|
|
1084
|
+
});
|
|
1085
|
+
enforceCacheLimit2();
|
|
1086
|
+
setRoles(tokenRoles);
|
|
1087
|
+
setGroups(userGroups);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
const error2 = err;
|
|
1090
|
+
const sanitizedMessage = sanitizeError(error2);
|
|
1091
|
+
const sanitizedError = new Error(sanitizedMessage);
|
|
1092
|
+
setError(sanitizedError);
|
|
1093
|
+
console.error("[Roles] Failed to fetch roles/groups:", sanitizedMessage);
|
|
1094
|
+
const idTokenClaims = account.idTokenClaims;
|
|
1095
|
+
const tokenRoles = idTokenClaims?.roles || [];
|
|
1096
|
+
setRoles(tokenRoles);
|
|
1097
|
+
} finally {
|
|
1098
|
+
setLoading(false);
|
|
1099
|
+
}
|
|
1100
|
+
}, [isAuthenticated, account, graph]);
|
|
1101
|
+
const hasRole = useCallback(
|
|
1102
|
+
(role) => {
|
|
1103
|
+
return roles.includes(role);
|
|
1104
|
+
},
|
|
1105
|
+
[roles]
|
|
1106
|
+
);
|
|
1107
|
+
const hasGroup = useCallback(
|
|
1108
|
+
(groupId) => {
|
|
1109
|
+
return groups.includes(groupId);
|
|
1110
|
+
},
|
|
1111
|
+
[groups]
|
|
1112
|
+
);
|
|
1113
|
+
const hasAnyRole = useCallback(
|
|
1114
|
+
(checkRoles) => {
|
|
1115
|
+
return checkRoles.some((role) => roles.includes(role));
|
|
1116
|
+
},
|
|
1117
|
+
[roles]
|
|
1118
|
+
);
|
|
1119
|
+
const hasAllRoles = useCallback(
|
|
1120
|
+
(checkRoles) => {
|
|
1121
|
+
return checkRoles.every((role) => roles.includes(role));
|
|
1122
|
+
},
|
|
1123
|
+
[roles]
|
|
1124
|
+
);
|
|
1125
|
+
useEffect(() => {
|
|
1126
|
+
fetchRolesAndGroups();
|
|
1127
|
+
return () => {
|
|
1128
|
+
if (account) {
|
|
1129
|
+
clearRolesCache(account.homeAccountId);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}, [fetchRolesAndGroups, account]);
|
|
1133
|
+
return {
|
|
1134
|
+
roles,
|
|
1135
|
+
groups,
|
|
1136
|
+
loading,
|
|
1137
|
+
error,
|
|
1138
|
+
hasRole,
|
|
1139
|
+
hasGroup,
|
|
1140
|
+
hasAnyRole,
|
|
1141
|
+
hasAllRoles,
|
|
1142
|
+
refetch: fetchRolesAndGroups
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function withAuth(Component2, options = {}) {
|
|
1146
|
+
const { displayName, ...guardProps } = options;
|
|
1147
|
+
const WrappedComponent = (props) => {
|
|
1148
|
+
return /* @__PURE__ */ jsx(AuthGuard, { ...guardProps, children: /* @__PURE__ */ jsx(Component2, { ...props }) });
|
|
1149
|
+
};
|
|
1150
|
+
WrappedComponent.displayName = displayName || `withAuth(${Component2.displayName || Component2.name || "Component"})`;
|
|
1151
|
+
return WrappedComponent;
|
|
1152
|
+
}
|
|
381
1153
|
|
|
382
|
-
// src/
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1154
|
+
// src/utils/tokenRetry.ts
|
|
1155
|
+
async function retryWithBackoff(fn, config = {}) {
|
|
1156
|
+
const {
|
|
1157
|
+
maxRetries = 3,
|
|
1158
|
+
initialDelay = 1e3,
|
|
1159
|
+
maxDelay = 1e4,
|
|
1160
|
+
backoffMultiplier = 2,
|
|
1161
|
+
debug = false
|
|
1162
|
+
} = config;
|
|
1163
|
+
let lastError;
|
|
1164
|
+
let delay = initialDelay;
|
|
1165
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1166
|
+
try {
|
|
1167
|
+
if (debug && attempt > 0) {
|
|
1168
|
+
console.log(`[TokenRetry] Attempt ${attempt + 1}/${maxRetries + 1}`);
|
|
1169
|
+
}
|
|
1170
|
+
return await fn();
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
lastError = error;
|
|
1173
|
+
if (attempt === maxRetries) {
|
|
1174
|
+
if (debug) {
|
|
1175
|
+
console.error("[TokenRetry] All retry attempts failed");
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
if (!isRetryableError(error)) {
|
|
1180
|
+
if (debug) {
|
|
1181
|
+
console.log("[TokenRetry] Non-retryable error, aborting");
|
|
1182
|
+
}
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
if (debug) {
|
|
1186
|
+
console.warn(`[TokenRetry] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
1187
|
+
}
|
|
1188
|
+
await sleep(delay);
|
|
1189
|
+
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
throw lastError;
|
|
1193
|
+
}
|
|
1194
|
+
function isRetryableError(error) {
|
|
1195
|
+
const message = error.message.toLowerCase();
|
|
1196
|
+
if (message.includes("network") || message.includes("timeout") || message.includes("fetch") || message.includes("connection")) {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
if (message.includes("500") || message.includes("502") || message.includes("503")) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
if (message.includes("429") || message.includes("rate limit")) {
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
if (message.includes("token") && message.includes("expired")) {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
function sleep(ms) {
|
|
1211
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1212
|
+
}
|
|
1213
|
+
function createRetryWrapper(fn, config = {}) {
|
|
1214
|
+
return (...args) => {
|
|
1215
|
+
return retryWithBackoff(() => fn(...args), config);
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/utils/debugLogger.ts
|
|
1220
|
+
var DebugLogger = class {
|
|
1221
|
+
constructor(config = {}) {
|
|
1222
|
+
this.config = {
|
|
1223
|
+
enabled: config.enabled ?? false,
|
|
1224
|
+
prefix: config.prefix ?? "[MSAL-Next]",
|
|
1225
|
+
showTimestamp: config.showTimestamp ?? true,
|
|
1226
|
+
level: config.level ?? "info"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
shouldLog(level) {
|
|
1230
|
+
if (!this.config.enabled) return false;
|
|
1231
|
+
const levels = ["error", "warn", "info", "debug"];
|
|
1232
|
+
const currentLevelIndex = levels.indexOf(this.config.level);
|
|
1233
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
1234
|
+
return messageLevelIndex <= currentLevelIndex;
|
|
1235
|
+
}
|
|
1236
|
+
formatMessage(level, message, data) {
|
|
1237
|
+
const timestamp = this.config.showTimestamp ? `[${(/* @__PURE__ */ new Date()).toISOString()}]` : "";
|
|
1238
|
+
const prefix = this.config.prefix;
|
|
1239
|
+
const levelStr = `[${level.toUpperCase()}]`;
|
|
1240
|
+
let formatted = `${timestamp} ${prefix} ${levelStr} ${message}`;
|
|
1241
|
+
if (data !== void 0) {
|
|
1242
|
+
formatted += "\n" + JSON.stringify(data, null, 2);
|
|
1243
|
+
}
|
|
1244
|
+
return formatted;
|
|
1245
|
+
}
|
|
1246
|
+
error(message, data) {
|
|
1247
|
+
if (this.shouldLog("error")) {
|
|
1248
|
+
console.error(this.formatMessage("error", message, data));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
warn(message, data) {
|
|
1252
|
+
if (this.shouldLog("warn")) {
|
|
1253
|
+
console.warn(this.formatMessage("warn", message, data));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
info(message, data) {
|
|
1257
|
+
if (this.shouldLog("info")) {
|
|
1258
|
+
console.info(this.formatMessage("info", message, data));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
debug(message, data) {
|
|
1262
|
+
if (this.shouldLog("debug")) {
|
|
1263
|
+
console.debug(this.formatMessage("debug", message, data));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
group(label) {
|
|
1267
|
+
if (this.config.enabled) {
|
|
1268
|
+
console.group(`${this.config.prefix} ${label}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
groupEnd() {
|
|
1272
|
+
if (this.config.enabled) {
|
|
1273
|
+
console.groupEnd();
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
setEnabled(enabled) {
|
|
1277
|
+
this.config.enabled = enabled;
|
|
1278
|
+
}
|
|
1279
|
+
setLevel(level) {
|
|
1280
|
+
if (level) {
|
|
1281
|
+
this.config.level = level;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
392
1284
|
};
|
|
1285
|
+
var globalLogger = null;
|
|
1286
|
+
function getDebugLogger(config) {
|
|
1287
|
+
if (!globalLogger) {
|
|
1288
|
+
globalLogger = new DebugLogger(config);
|
|
1289
|
+
} else if (config) {
|
|
1290
|
+
if (config.enabled !== void 0) {
|
|
1291
|
+
globalLogger.setEnabled(config.enabled);
|
|
1292
|
+
}
|
|
1293
|
+
if (config.level) {
|
|
1294
|
+
globalLogger.setLevel(config.level);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return globalLogger;
|
|
1298
|
+
}
|
|
1299
|
+
function createScopedLogger(scope, config) {
|
|
1300
|
+
return new DebugLogger({
|
|
1301
|
+
...config,
|
|
1302
|
+
prefix: `[MSAL-Next:${scope}]`
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
function createAuthMiddleware(config = {}) {
|
|
1306
|
+
const {
|
|
1307
|
+
protectedRoutes = [],
|
|
1308
|
+
publicOnlyRoutes = [],
|
|
1309
|
+
loginPath = "/login",
|
|
1310
|
+
redirectAfterLogin = "/",
|
|
1311
|
+
sessionCookie = "msal.account",
|
|
1312
|
+
isAuthenticated: customAuthCheck,
|
|
1313
|
+
debug = false
|
|
1314
|
+
} = config;
|
|
1315
|
+
return async function authMiddleware(request) {
|
|
1316
|
+
const { pathname } = request.nextUrl;
|
|
1317
|
+
if (debug) {
|
|
1318
|
+
console.log("[AuthMiddleware] Processing:", pathname);
|
|
1319
|
+
}
|
|
1320
|
+
let authenticated = false;
|
|
1321
|
+
if (customAuthCheck) {
|
|
1322
|
+
authenticated = await customAuthCheck(request);
|
|
1323
|
+
} else {
|
|
1324
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
1325
|
+
authenticated = !!sessionData?.value;
|
|
1326
|
+
}
|
|
1327
|
+
if (debug) {
|
|
1328
|
+
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
1329
|
+
}
|
|
1330
|
+
const isProtectedRoute = protectedRoutes.some(
|
|
1331
|
+
(route) => pathname.startsWith(route)
|
|
1332
|
+
);
|
|
1333
|
+
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
1334
|
+
(route) => pathname.startsWith(route)
|
|
1335
|
+
);
|
|
1336
|
+
if (isProtectedRoute && !authenticated) {
|
|
1337
|
+
if (debug) {
|
|
1338
|
+
console.log("[AuthMiddleware] Redirecting to login");
|
|
1339
|
+
}
|
|
1340
|
+
const url = request.nextUrl.clone();
|
|
1341
|
+
url.pathname = loginPath;
|
|
1342
|
+
url.searchParams.set("returnUrl", pathname);
|
|
1343
|
+
return NextResponse.redirect(url);
|
|
1344
|
+
}
|
|
1345
|
+
if (isPublicOnlyRoute && authenticated) {
|
|
1346
|
+
if (debug) {
|
|
1347
|
+
console.log("[AuthMiddleware] Redirecting to home");
|
|
1348
|
+
}
|
|
1349
|
+
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
1350
|
+
const url = request.nextUrl.clone();
|
|
1351
|
+
url.pathname = returnUrl || redirectAfterLogin;
|
|
1352
|
+
url.searchParams.delete("returnUrl");
|
|
1353
|
+
return NextResponse.redirect(url);
|
|
1354
|
+
}
|
|
1355
|
+
const response = NextResponse.next();
|
|
1356
|
+
if (authenticated) {
|
|
1357
|
+
response.headers.set("x-msal-authenticated", "true");
|
|
1358
|
+
try {
|
|
1359
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
1360
|
+
if (sessionData?.value) {
|
|
1361
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
1362
|
+
if (account?.username) {
|
|
1363
|
+
response.headers.set("x-msal-username", account.username);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
if (debug) {
|
|
1368
|
+
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return response;
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export { AuthGuard, AuthStatus, ErrorBoundary, MicrosoftSignInButton, MsalAuthProvider, SignOutButton, UserAvatar, createAuthMiddleware, createMsalConfig, createRetryWrapper, createScopedLogger, getDebugLogger, getMsalInstance, isValidAccountData, isValidRedirectUri, isValidScope, retryWithBackoff, safeJsonParse, sanitizeError, useGraphApi, useMsalAuth, useRoles, useUserProfile, validateScopes, withAuth };
|
|
1377
|
+
//# sourceMappingURL=index.mjs.map
|
|
393
1378
|
//# sourceMappingURL=index.mjs.map
|