@chemmangat/msal-next 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +351 -1
- package/README.md +580 -526
- package/SECURITY.md +422 -110
- package/TROUBLESHOOTING.md +380 -188
- package/dist/index.d.mts +332 -8
- package/dist/index.d.ts +332 -8
- package/dist/index.js +2382 -2
- package/dist/index.mjs +2320 -2
- package/dist/server.js +89 -1
- package/dist/server.mjs +86 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2382 @@
|
|
|
1
|
-
'use strict';var msalReact=require('@azure/msal-react'),msalBrowser=require('@azure/msal-browser'),react=require('react'),jsxRuntime=require('react/jsx-runtime'),navigation=require('next/navigation'),server=require('next/server');function H(r,e){try{let t=JSON.parse(r);return e(t)?t:(console.warn("[Validation] JSON validation failed"),null)}catch(t){return console.error("[Validation] JSON parse error:",t),null}}function W(r){return typeof r=="object"&&r!==null&&typeof r.homeAccountId=="string"&&r.homeAccountId.length>0&&typeof r.username=="string"&&r.username.length>0&&(r.name===void 0||typeof r.name=="string")}function w(r){return r instanceof Error?r.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]"):"An unexpected error occurred"}function z(r,e){try{let t=new URL(r);return e.some(o=>{let i=new URL(o);return t.origin===i.origin})}catch{return false}}function ae(r){return /^[a-zA-Z0-9._-]+$/.test(r)}function Ce(r){return Array.isArray(r)&&r.every(ae)}function j(r){if(r.msalConfig)return r.msalConfig;let{clientId:e,tenantId:t,authorityType:o="common",redirectUri:i,postLogoutRedirectUri:a,cacheLocation:g="sessionStorage",storeAuthStateInCookie:u=false,navigateToLoginRequestUrl:l=false,enableLogging:n=false,loggerCallback:h,allowedRedirectUris:f}=r;if(!e)throw new Error("@chemmangat/msal-next: clientId is required");let c=()=>{if(o==="tenant"){if(!t)throw new Error('@chemmangat/msal-next: tenantId is required when authorityType is "tenant"');return `https://login.microsoftonline.com/${t}`}return `https://login.microsoftonline.com/${o}`},s=typeof window<"u"?window.location.origin:"http://localhost:3000",m=i||s;if(f&&f.length>0){if(!z(m,f))throw new Error(`@chemmangat/msal-next: redirectUri "${m}" is not in the allowed list`);let d=a||m;if(!z(d,f))throw new Error(`@chemmangat/msal-next: postLogoutRedirectUri "${d}" is not in the allowed list`)}return {auth:{clientId:e,authority:c(),redirectUri:m,postLogoutRedirectUri:a||m,navigateToLoginRequestUrl:l},cache:{cacheLocation:g,storeAuthStateInCookie:u},system:{loggerOptions:{loggerCallback:h||((d,y,R)=>{if(!(R||!n))switch(d){case msalBrowser.LogLevel.Error:console.error("[MSAL]",y);break;case msalBrowser.LogLevel.Warning:console.warn("[MSAL]",y);break;case msalBrowser.LogLevel.Info:console.info("[MSAL]",y);break;case msalBrowser.LogLevel.Verbose:console.debug("[MSAL]",y);break}}),logLevel:n?msalBrowser.LogLevel.Verbose:msalBrowser.LogLevel.Error}}}}var ce=null;function Ue(){return ce}function J({children:r,loadingComponent:e,onInitialized:t,...o}){let[i,a]=react.useState(null),g=react.useRef(null);return react.useEffect(()=>{if(typeof window>"u"||g.current)return;(async()=>{try{let l=j(o),n=new msalBrowser.PublicClientApplication(l);await n.initialize();try{let c=await n.handleRedirectPromise();c&&(o.enableLogging&&console.log("[MSAL] Redirect authentication successful"),c.account&&n.setActiveAccount(c.account),window.location.hash&&window.history.replaceState(null,"",window.location.pathname+window.location.search));}catch(c){c?.errorCode==="no_token_request_cache_error"?o.enableLogging&&console.log("[MSAL] No pending redirect found (this is normal)"):c?.errorCode==="user_cancelled"?o.enableLogging&&console.log("[MSAL] User cancelled authentication"):console.error("[MSAL] Redirect handling error:",c),window.location.hash&&(window.location.hash.includes("code=")||window.location.hash.includes("error="))&&window.history.replaceState(null,"",window.location.pathname+window.location.search);}let h=n.getAllAccounts();h.length>0&&!n.getActiveAccount()&&n.setActiveAccount(h[0]);let f=o.enableLogging||!1;n.addEventCallback(c=>{if(c.eventType===msalBrowser.EventType.LOGIN_SUCCESS){let s=c.payload;s?.account&&n.setActiveAccount(s.account),f&&console.log("[MSAL] Login successful:",s.account?.username);}if(c.eventType===msalBrowser.EventType.LOGIN_FAILURE&&console.error("[MSAL] Login failed:",c.error),c.eventType===msalBrowser.EventType.LOGOUT_SUCCESS&&(n.setActiveAccount(null),f&&console.log("[MSAL] Logout successful")),c.eventType===msalBrowser.EventType.ACQUIRE_TOKEN_SUCCESS){let s=c.payload;s?.account&&!n.getActiveAccount()&&n.setActiveAccount(s.account);}c.eventType===msalBrowser.EventType.ACQUIRE_TOKEN_FAILURE&&f&&console.error("[MSAL] Token acquisition failed:",c.error);}),g.current=n,ce=n,a(n),t&&t(n);}catch(l){throw console.error("[MSAL] Initialization failed:",l),l}})();},[]),typeof window>"u"?jsxRuntime.jsx(jsxRuntime.Fragment,{children:e||jsxRuntime.jsx("div",{children:"Loading authentication..."})}):i?jsxRuntime.jsx(msalReact.MsalProvider,{instance:i,children:r}):jsxRuntime.jsx(jsxRuntime.Fragment,{children:e||jsxRuntime.jsx("div",{children:"Loading authentication..."})})}var Ne=react.createContext(void 0);function Fe({children:r,protection:e,...t}){return jsxRuntime.jsx(Ne.Provider,{value:e,children:jsxRuntime.jsx(J,{...t,children:r})})}var K=new Map;function A(r=["User.Read"]){let{instance:e,accounts:t,inProgress:o}=msalReact.useMsal(),i=msalReact.useAccount(t[0]||null),a=react.useMemo(()=>t.length>0,[t]),g=react.useCallback(async(c=r)=>{if(o!==msalBrowser.InteractionStatus.None){console.warn("[MSAL] Interaction already in progress");return}try{let s={scopes:c,prompt:"select_account"};await e.loginRedirect(s);}catch(s){if(s?.errorCode==="user_cancelled"){console.log("[MSAL] User cancelled login");return}throw console.error("[MSAL] Login redirect failed:",s),s}},[e,r,o]),u=react.useCallback(async()=>{try{await e.logoutRedirect({account:i||void 0});}catch(c){throw console.error("[MSAL] Logout redirect failed:",c),c}},[e,i]),l=react.useCallback(async(c=r)=>{if(!i)throw new Error("[MSAL] No active account. Please login first.");try{let s={scopes:c,account:i,forceRefresh:!1};return (await e.acquireTokenSilent(s)).accessToken}catch(s){throw console.error("[MSAL] Silent token acquisition failed:",s),s}},[e,i,r]),n=react.useCallback(async(c=r)=>{if(!i)throw new Error("[MSAL] No active account. Please login first.");try{let s={scopes:c,account:i};await e.acquireTokenRedirect(s);}catch(s){throw console.error("[MSAL] Token redirect acquisition failed:",s),s}},[e,i,r]),h=react.useCallback(async(c=r)=>{let s=`${i?.homeAccountId||"anonymous"}-${c.sort().join(",")}`,m=K.get(s);if(m)return m;let p=(async()=>{try{return await l(c)}catch{throw console.warn("[MSAL] Silent token acquisition failed, falling back to redirect"),await n(c),new Error("[MSAL] Redirecting for token acquisition")}finally{K.delete(s);}})();return K.set(s,p),p},[l,n,r,i]),f=react.useCallback(async()=>{e.setActiveAccount(null),await e.clearCache();},[e]);return {account:i,accounts:t,isAuthenticated:a,inProgress:o!==msalBrowser.InteractionStatus.None,loginRedirect:g,logoutRedirect:u,acquireToken:h,acquireTokenSilent:l,acquireTokenRedirect:n,clearSession:f}}function $e({text:r="Sign in with Microsoft",variant:e="dark",size:t="medium",scopes:o,className:i="",style:a,onSuccess:g,onError:u}){let{loginRedirect:l,inProgress:n}=A(),[h,f]=react.useState(false),c=async()=>{f(true);try{await l(o),g?.();}catch(y){u?.(y);}finally{setTimeout(()=>f(false),500);}},s={small:{padding:"8px 16px",fontSize:"14px",height:"36px"},medium:{padding:"10px 20px",fontSize:"15px",height:"41px"},large:{padding:"12px 24px",fontSize:"16px",height:"48px"}},m={dark:{backgroundColor:"#2F2F2F",color:"#FFFFFF",border:"1px solid #8C8C8C"},light:{backgroundColor:"#FFFFFF",color:"#5E5E5E",border:"1px solid #8C8C8C"}},p=n||h,d={display:"inline-flex",alignItems:"center",justifyContent:"center",gap:"12px",fontFamily:'"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',fontWeight:600,borderRadius:"2px",cursor:p?"not-allowed":"pointer",transition:"all 0.2s ease",opacity:p?.6:1,...m[e],...s[t],...a};return jsxRuntime.jsxs("button",{onClick:c,disabled:p,className:i,style:d,"aria-label":r,children:[jsxRuntime.jsx(ze,{}),jsxRuntime.jsx("span",{children:r})]})}function ze(){return jsxRuntime.jsxs("svg",{width:"21",height:"21",viewBox:"0 0 21 21",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[jsxRuntime.jsx("rect",{width:"10",height:"10",fill:"#F25022"}),jsxRuntime.jsx("rect",{x:"11",width:"10",height:"10",fill:"#7FBA00"}),jsxRuntime.jsx("rect",{y:"11",width:"10",height:"10",fill:"#00A4EF"}),jsxRuntime.jsx("rect",{x:"11",y:"11",width:"10",height:"10",fill:"#FFB900"})]})}function qe({text:r="Sign out",variant:e="dark",size:t="medium",className:o="",style:i,onSuccess:a,onError:g}){let{logoutRedirect:u,inProgress:l}=A(),n=async()=>{try{await u(),a?.();}catch(s){g?.(s);}},h={small:{padding:"8px 16px",fontSize:"14px",height:"36px"},medium:{padding:"10px 20px",fontSize:"15px",height:"41px"},large:{padding:"12px 24px",fontSize:"16px",height:"48px"}},c={display:"inline-flex",alignItems:"center",justifyContent:"center",gap:"12px",fontFamily:'"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',fontWeight:600,borderRadius:"2px",cursor:l?"not-allowed":"pointer",transition:"all 0.2s ease",opacity:l?.6:1,...{dark:{backgroundColor:"#2F2F2F",color:"#FFFFFF",border:"1px solid #8C8C8C"},light:{backgroundColor:"#FFFFFF",color:"#5E5E5E",border:"1px solid #8C8C8C"}}[e],...h[t],...i};return jsxRuntime.jsxs("button",{onClick:n,disabled:l,className:o,style:c,"aria-label":r,children:[jsxRuntime.jsx(Be,{}),jsxRuntime.jsx("span",{children:r})]})}function Be(){return jsxRuntime.jsxs("svg",{width:"21",height:"21",viewBox:"0 0 21 21",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[jsxRuntime.jsx("rect",{width:"10",height:"10",fill:"#F25022"}),jsxRuntime.jsx("rect",{x:"11",width:"10",height:"10",fill:"#7FBA00"}),jsxRuntime.jsx("rect",{y:"11",width:"10",height:"10",fill:"#00A4EF"}),jsxRuntime.jsx("rect",{x:"11",y:"11",width:"10",height:"10",fill:"#FFB900"})]})}function N(){let{acquireToken:r}=A(),e=react.useCallback(async(u,l={})=>{let{scopes:n=["User.Read"],version:h="v1.0",debug:f=false,...c}=l;try{let s=await r(n),m=`https://graph.microsoft.com/${h}`,p=u.startsWith("http")?u:`${m}${u.startsWith("/")?u:`/${u}`}`;f&&console.log("[GraphAPI] Request:",{url:p,method:c.method||"GET"});let d=await fetch(p,{...c,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json",...c.headers}});if(!d.ok){let R=await d.text(),M=`Graph API error (${d.status}): ${R}`;throw new Error(M)}if(d.status===204||d.headers.get("content-length")==="0")return null;let y=await d.json();return f&&console.log("[GraphAPI] Response:",y),y}catch(s){let m=w(s);throw console.error("[GraphAPI] Request failed:",m),new Error(m)}},[r]),t=react.useCallback((u,l={})=>e(u,{...l,method:"GET"}),[e]),o=react.useCallback((u,l,n={})=>e(u,{...n,method:"POST",body:l?JSON.stringify(l):void 0}),[e]),i=react.useCallback((u,l,n={})=>e(u,{...n,method:"PUT",body:l?JSON.stringify(l):void 0}),[e]),a=react.useCallback((u,l,n={})=>e(u,{...n,method:"PATCH",body:l?JSON.stringify(l):void 0}),[e]),g=react.useCallback((u,l={})=>e(u,{...l,method:"DELETE"}),[e]);return {get:t,post:o,put:i,patch:a,delete:g,request:e}}var x=new Map,_e=300*1e3,me=100;function Ve(){if(x.size>me){let r=Array.from(x.entries());r.sort((t,o)=>t[1].timestamp-o[1].timestamp),r.slice(0,x.size-me).forEach(([t])=>{let o=x.get(t);o?.data.photo&&URL.revokeObjectURL(o.data.photo),x.delete(t);});}}function Q(){let{isAuthenticated:r,account:e}=A(),t=N(),[o,i]=react.useState(null),[a,g]=react.useState(false),[u,l]=react.useState(null),n=react.useCallback(async()=>{if(!r||!e){i(null);return}let f=e.homeAccountId,c=x.get(f);if(c&&Date.now()-c.timestamp<_e){i(c.data);return}g(true),l(null);try{let s=await t.get("/me",{scopes:["User.Read"]}),m;try{let d=await t.get("/me/photo/$value",{scopes:["User.Read"],headers:{"Content-Type":"image/jpeg"}});d&&(m=URL.createObjectURL(d));}catch{console.debug("[UserProfile] Photo not available");}let p={id:s.id,displayName:s.displayName,givenName:s.givenName,surname:s.surname,userPrincipalName:s.userPrincipalName,mail:s.mail,jobTitle:s.jobTitle,officeLocation:s.officeLocation,mobilePhone:s.mobilePhone,businessPhones:s.businessPhones,photo:m};x.set(f,{data:p,timestamp:Date.now()}),Ve(),i(p);}catch(s){let p=w(s),d=new Error(p);l(d),console.error("[UserProfile] Failed to fetch profile:",p);}finally{g(false);}},[r,e,t]),h=react.useCallback(()=>{if(e){let f=x.get(e.homeAccountId);f?.data.photo&&URL.revokeObjectURL(f.data.photo),x.delete(e.homeAccountId);}o?.photo&&URL.revokeObjectURL(o.photo),i(null);},[e,o]);return react.useEffect(()=>(n(),()=>{o?.photo&&URL.revokeObjectURL(o.photo);}),[n]),react.useEffect(()=>()=>{o?.photo&&URL.revokeObjectURL(o.photo);},[o?.photo]),{profile:o,loading:a,error:u,refetch:n,clearCache:h}}function We({size:r=40,className:e="",style:t,showTooltip:o=true,fallbackImage:i}){let{profile:a,loading:g}=Q(),[u,l]=react.useState(null),[n,h]=react.useState(false);react.useEffect(()=>{a?.photo&&l(a.photo);},[a?.photo]);let f=()=>{if(!a)return "?";let{givenName:m,surname:p,displayName:d}=a;if(m&&p)return `${m[0]}${p[0]}`.toUpperCase();if(d){let y=d.split(" ");return y.length>=2?`${y[0][0]}${y[y.length-1][0]}`.toUpperCase():d.substring(0,2).toUpperCase()}return "?"},c={width:`${r}px`,height:`${r}px`,borderRadius:"50%",display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:`${r*.4}px`,fontWeight:600,fontFamily:'"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',backgroundColor:"#0078D4",color:"#FFFFFF",overflow:"hidden",userSelect:"none",...t},s=a?.displayName||"User";return g?jsxRuntime.jsx("div",{className:e,style:{...c,backgroundColor:"#E1E1E1"},"aria-label":"Loading user avatar",children:jsxRuntime.jsx("span",{style:{fontSize:`${r*.3}px`},children:"..."})}):u&&!n?jsxRuntime.jsx("div",{className:e,style:c,title:o?s:void 0,"aria-label":`${s} avatar`,children:jsxRuntime.jsx("img",{src:u,alt:s,style:{width:"100%",height:"100%",objectFit:"cover"},onError:()=>{h(true),i&&l(i);}})}):jsxRuntime.jsx("div",{className:e,style:c,title:o?s:void 0,"aria-label":`${s} avatar`,children:f()})}function je({className:r="",style:e,showDetails:t=false,renderLoading:o,renderAuthenticated:i,renderUnauthenticated:a}){let{isAuthenticated:g,inProgress:u,account:l}=A(),n={display:"inline-flex",alignItems:"center",gap:"8px",padding:"8px 12px",borderRadius:"4px",fontFamily:'"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',fontSize:"14px",fontWeight:500,...e};if(u)return o?jsxRuntime.jsx(jsxRuntime.Fragment,{children:o()}):jsxRuntime.jsxs("div",{className:r,style:{...n,backgroundColor:"#FFF4CE",color:"#8A6D3B"},role:"status","aria-live":"polite",children:[jsxRuntime.jsx(X,{color:"#FFA500"}),jsxRuntime.jsx("span",{children:"Loading..."})]});if(g){let h=l?.username||l?.name||"User";return i?jsxRuntime.jsx(jsxRuntime.Fragment,{children:i(h)}):jsxRuntime.jsxs("div",{className:r,style:{...n,backgroundColor:"#D4EDDA",color:"#155724"},role:"status","aria-live":"polite",children:[jsxRuntime.jsx(X,{color:"#28A745"}),jsxRuntime.jsx("span",{children:t?`Authenticated as ${h}`:"Authenticated"})]})}return a?jsxRuntime.jsx(jsxRuntime.Fragment,{children:a()}):jsxRuntime.jsxs("div",{className:r,style:{...n,backgroundColor:"#F8D7DA",color:"#721C24"},role:"status","aria-live":"polite",children:[jsxRuntime.jsx(X,{color:"#DC3545"}),jsxRuntime.jsx("span",{children:"Not authenticated"})]})}function X({color:r}){return jsxRuntime.jsx("svg",{width:"8",height:"8",viewBox:"0 0 8 8",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:jsxRuntime.jsx("circle",{cx:"4",cy:"4",r:"4",fill:r})})}function re({children:r,loadingComponent:e,fallbackComponent:t,scopes:o,onAuthRequired:i}){let{isAuthenticated:a,inProgress:g,loginRedirect:u}=A();return react.useEffect(()=>{!a&&!g&&(i?.(),(async()=>{try{await u(o);}catch(n){console.error("[AuthGuard] Authentication failed:",n);}})());},[a,g,o,u,i]),g?jsxRuntime.jsx(jsxRuntime.Fragment,{children:e||jsxRuntime.jsx("div",{children:"Authenticating..."})}):a?jsxRuntime.jsx(jsxRuntime.Fragment,{children:r}):jsxRuntime.jsx(jsxRuntime.Fragment,{children:t||jsxRuntime.jsx("div",{children:"Redirecting to login..."})})}var ne=class extends react.Component{constructor(t){super(t);this.reset=()=>{this.setState({hasError:false,error:null});};this.state={hasError:false,error:null};}static getDerivedStateFromError(t){return {hasError:true,error:t}}componentDidCatch(t,o){let{onError:i,debug:a}=this.props;a&&(console.error("[ErrorBoundary] Caught error:",t),console.error("[ErrorBoundary] Error info:",o)),i?.(t,o);}render(){let{hasError:t,error:o}=this.state,{children:i,fallback:a}=this.props;return t&&o?a?a(o,this.reset):jsxRuntime.jsxs("div",{style:{padding:"20px",margin:"20px",border:"1px solid #DC3545",borderRadius:"4px",backgroundColor:"#F8D7DA",color:"#721C24",fontFamily:'"Segoe UI", Tahoma, Geneva, Verdana, sans-serif'},children:[jsxRuntime.jsx("h2",{style:{margin:"0 0 10px 0",fontSize:"18px"},children:"Authentication Error"}),jsxRuntime.jsx("p",{style:{margin:"0 0 10px 0"},children:o.message}),jsxRuntime.jsx("button",{onClick:this.reset,style:{padding:"8px 16px",backgroundColor:"#DC3545",color:"#FFFFFF",border:"none",borderRadius:"4px",cursor:"pointer",fontSize:"14px",fontWeight:600},children:"Try Again"})]}):i}};var v=new Map,Xe=300*1e3,Ae=100;function Ye(r){r?v.delete(r):v.clear();}function et(){if(v.size>Ae){let r=Array.from(v.entries());r.sort((t,o)=>t[1].timestamp-o[1].timestamp),r.slice(0,v.size-Ae).forEach(([t])=>v.delete(t));}}function tt(){let{isAuthenticated:r,account:e}=A(),t=N(),[o,i]=react.useState([]),[a,g]=react.useState([]),[u,l]=react.useState(false),[n,h]=react.useState(null),f=react.useCallback(async()=>{if(!r||!e){i([]),g([]);return}let d=e.homeAccountId,y=v.get(d);if(y&&Date.now()-y.timestamp<Xe){i(y.roles),g(y.groups);return}l(true),h(null);try{let M=e.idTokenClaims?.roles||[],$=(await t.get("/me/memberOf",{scopes:["User.Read","Directory.Read.All"]})).value.map(se=>se.id);v.set(d,{roles:M,groups:$,timestamp:Date.now()}),et(),i(M),g($);}catch(R){let V=w(R),$=new Error(V);h($),console.error("[Roles] Failed to fetch roles/groups:",V);let Re=e.idTokenClaims?.roles||[];i(Re);}finally{l(false);}},[r,e,t]),c=react.useCallback(d=>o.includes(d),[o]),s=react.useCallback(d=>a.includes(d),[a]),m=react.useCallback(d=>d.some(y=>o.includes(y)),[o]),p=react.useCallback(d=>d.every(y=>o.includes(y)),[o]);return react.useEffect(()=>(f(),()=>{e&&Ye(e.homeAccountId);}),[f,e]),{roles:o,groups:a,loading:u,error:n,hasRole:c,hasGroup:s,hasAnyRole:m,hasAllRoles:p,refetch:f}}function rt(r,e={}){let{displayName:t,...o}=e,i=a=>jsxRuntime.jsx(re,{...o,children:jsxRuntime.jsx(r,{...a})});return i.displayName=t||`withAuth(${r.displayName||r.name||"Component"})`,i}async function Pe(r,e={}){let{maxRetries:t=3,initialDelay:o=1e3,maxDelay:i=1e4,backoffMultiplier:a=2,debug:g=false}=e,u,l=o;for(let n=0;n<=t;n++)try{return g&&n>0&&console.log(`[TokenRetry] Attempt ${n+1}/${t+1}`),await r()}catch(h){if(u=h,n===t){g&&console.error("[TokenRetry] All retry attempts failed");break}if(!ot(h))throw g&&console.log("[TokenRetry] Non-retryable error, aborting"),h;g&&console.warn(`[TokenRetry] Attempt ${n+1} failed, retrying in ${l}ms...`),await nt(l),l=Math.min(l*a,i);}throw u}function ot(r){let e=r.message.toLowerCase();return !!(e.includes("network")||e.includes("timeout")||e.includes("fetch")||e.includes("connection")||e.includes("500")||e.includes("502")||e.includes("503")||e.includes("429")||e.includes("rate limit")||e.includes("token")&&e.includes("expired"))}function nt(r){return new Promise(e=>setTimeout(e,r))}function it(r,e={}){return (...t)=>Pe(()=>r(...t),e)}var B=class{constructor(e={}){this.logHistory=[];this.performanceTimings=new Map;this.config={enabled:e.enabled??false,prefix:e.prefix??"[MSAL-Next]",showTimestamp:e.showTimestamp??true,level:e.level??"info",enablePerformance:e.enablePerformance??false,enableNetworkLogs:e.enableNetworkLogs??false,maxHistorySize:e.maxHistorySize??100};}shouldLog(e){if(!this.config.enabled)return false;let t=["error","warn","info","debug"],o=t.indexOf(this.config.level);return t.indexOf(e)<=o}formatMessage(e,t,o){let i=this.config.showTimestamp?`[${new Date().toISOString()}]`:"",a=this.config.prefix,g=`[${e.toUpperCase()}]`,u=`${i} ${a} ${g} ${t}`;return o!==void 0&&(u+=`
|
|
2
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/client.ts
|
|
22
|
+
var client_exports = {};
|
|
23
|
+
__export(client_exports, {
|
|
24
|
+
AuthGuard: () => AuthGuard,
|
|
25
|
+
AuthStatus: () => AuthStatus,
|
|
26
|
+
ErrorBoundary: () => ErrorBoundary,
|
|
27
|
+
MSALProvider: () => MSALProvider,
|
|
28
|
+
MicrosoftSignInButton: () => MicrosoftSignInButton,
|
|
29
|
+
MsalAuthProvider: () => MsalAuthProvider,
|
|
30
|
+
MsalError: () => MsalError,
|
|
31
|
+
ProtectedPage: () => ProtectedPage,
|
|
32
|
+
SignOutButton: () => SignOutButton,
|
|
33
|
+
UserAvatar: () => UserAvatar,
|
|
34
|
+
createAuthMiddleware: () => createAuthMiddleware,
|
|
35
|
+
createMissingEnvVarError: () => createMissingEnvVarError,
|
|
36
|
+
createMsalConfig: () => createMsalConfig,
|
|
37
|
+
createRetryWrapper: () => createRetryWrapper,
|
|
38
|
+
createScopedLogger: () => createScopedLogger,
|
|
39
|
+
displayValidationResults: () => displayValidationResults,
|
|
40
|
+
getDebugLogger: () => getDebugLogger,
|
|
41
|
+
getMsalInstance: () => getMsalInstance,
|
|
42
|
+
isValidAccountData: () => isValidAccountData,
|
|
43
|
+
isValidRedirectUri: () => isValidRedirectUri,
|
|
44
|
+
isValidScope: () => isValidScope,
|
|
45
|
+
retryWithBackoff: () => retryWithBackoff,
|
|
46
|
+
safeJsonParse: () => safeJsonParse,
|
|
47
|
+
sanitizeError: () => sanitizeError,
|
|
48
|
+
useAccount: () => import_msal_react3.useAccount,
|
|
49
|
+
useGraphApi: () => useGraphApi,
|
|
50
|
+
useIsAuthenticated: () => import_msal_react3.useIsAuthenticated,
|
|
51
|
+
useMsal: () => import_msal_react3.useMsal,
|
|
52
|
+
useMsalAuth: () => useMsalAuth,
|
|
53
|
+
useRoles: () => useRoles,
|
|
54
|
+
useTokenRefresh: () => useTokenRefresh,
|
|
55
|
+
useUserProfile: () => useUserProfile,
|
|
56
|
+
validateConfig: () => validateConfig,
|
|
57
|
+
validateScopes: () => validateScopes,
|
|
58
|
+
withAuth: () => withAuth,
|
|
59
|
+
withPageAuth: () => withPageAuth,
|
|
60
|
+
wrapMsalError: () => wrapMsalError
|
|
61
|
+
});
|
|
62
|
+
module.exports = __toCommonJS(client_exports);
|
|
63
|
+
|
|
64
|
+
// src/components/MsalAuthProvider.tsx
|
|
65
|
+
var import_msal_react2 = require("@azure/msal-react");
|
|
66
|
+
var import_msal_browser3 = require("@azure/msal-browser");
|
|
67
|
+
var import_react3 = require("react");
|
|
68
|
+
|
|
69
|
+
// src/utils/createMsalConfig.ts
|
|
70
|
+
var import_msal_browser = require("@azure/msal-browser");
|
|
71
|
+
|
|
72
|
+
// src/utils/validation.ts
|
|
73
|
+
function safeJsonParse(jsonString, validator) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(jsonString);
|
|
76
|
+
if (validator(parsed)) {
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
console.warn("[Validation] JSON validation failed");
|
|
80
|
+
return null;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("[Validation] JSON parse error:", error);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isValidAccountData(data) {
|
|
87
|
+
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");
|
|
88
|
+
}
|
|
89
|
+
function sanitizeError(error) {
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
const message = error.message;
|
|
92
|
+
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]");
|
|
93
|
+
return sanitized;
|
|
94
|
+
}
|
|
95
|
+
return "An unexpected error occurred";
|
|
96
|
+
}
|
|
97
|
+
function isValidRedirectUri(uri, allowedOrigins) {
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL(uri);
|
|
100
|
+
return allowedOrigins.some((allowed) => {
|
|
101
|
+
const allowedUrl = new URL(allowed);
|
|
102
|
+
return url.origin === allowedUrl.origin;
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function isValidScope(scope) {
|
|
109
|
+
return /^[a-zA-Z0-9._-]+$/.test(scope);
|
|
110
|
+
}
|
|
111
|
+
function validateScopes(scopes) {
|
|
112
|
+
return Array.isArray(scopes) && scopes.every(isValidScope);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/utils/createMsalConfig.ts
|
|
116
|
+
function createMsalConfig(config) {
|
|
117
|
+
if (config.msalConfig) {
|
|
118
|
+
return config.msalConfig;
|
|
119
|
+
}
|
|
120
|
+
const {
|
|
121
|
+
clientId,
|
|
122
|
+
tenantId,
|
|
123
|
+
authorityType = "common",
|
|
124
|
+
redirectUri,
|
|
125
|
+
postLogoutRedirectUri,
|
|
126
|
+
cacheLocation = "sessionStorage",
|
|
127
|
+
storeAuthStateInCookie = false,
|
|
128
|
+
navigateToLoginRequestUrl = false,
|
|
129
|
+
enableLogging = false,
|
|
130
|
+
loggerCallback,
|
|
131
|
+
allowedRedirectUris
|
|
132
|
+
} = config;
|
|
133
|
+
if (!clientId) {
|
|
134
|
+
throw new Error("@chemmangat/msal-next: clientId is required");
|
|
135
|
+
}
|
|
136
|
+
const getAuthority = () => {
|
|
137
|
+
if (authorityType === "tenant") {
|
|
138
|
+
if (!tenantId) {
|
|
139
|
+
throw new Error('@chemmangat/msal-next: tenantId is required when authorityType is "tenant"');
|
|
140
|
+
}
|
|
141
|
+
return `https://login.microsoftonline.com/${tenantId}`;
|
|
142
|
+
}
|
|
143
|
+
return `https://login.microsoftonline.com/${authorityType}`;
|
|
144
|
+
};
|
|
145
|
+
const defaultRedirectUri = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
|
|
146
|
+
const finalRedirectUri = redirectUri || defaultRedirectUri;
|
|
147
|
+
if (allowedRedirectUris && allowedRedirectUris.length > 0) {
|
|
148
|
+
if (!isValidRedirectUri(finalRedirectUri, allowedRedirectUris)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`@chemmangat/msal-next: redirectUri "${finalRedirectUri}" is not in the allowed list`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const finalPostLogoutUri = postLogoutRedirectUri || finalRedirectUri;
|
|
154
|
+
if (!isValidRedirectUri(finalPostLogoutUri, allowedRedirectUris)) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`@chemmangat/msal-next: postLogoutRedirectUri "${finalPostLogoutUri}" is not in the allowed list`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const msalConfig = {
|
|
161
|
+
auth: {
|
|
162
|
+
clientId,
|
|
163
|
+
authority: getAuthority(),
|
|
164
|
+
redirectUri: finalRedirectUri,
|
|
165
|
+
postLogoutRedirectUri: postLogoutRedirectUri || finalRedirectUri,
|
|
166
|
+
navigateToLoginRequestUrl
|
|
167
|
+
},
|
|
168
|
+
cache: {
|
|
169
|
+
cacheLocation,
|
|
170
|
+
storeAuthStateInCookie
|
|
171
|
+
},
|
|
172
|
+
system: {
|
|
173
|
+
loggerOptions: {
|
|
174
|
+
loggerCallback: loggerCallback || ((level, message, containsPii) => {
|
|
175
|
+
if (containsPii || !enableLogging) return;
|
|
176
|
+
switch (level) {
|
|
177
|
+
case import_msal_browser.LogLevel.Error:
|
|
178
|
+
console.error("[MSAL]", message);
|
|
179
|
+
break;
|
|
180
|
+
case import_msal_browser.LogLevel.Warning:
|
|
181
|
+
console.warn("[MSAL]", message);
|
|
182
|
+
break;
|
|
183
|
+
case import_msal_browser.LogLevel.Info:
|
|
184
|
+
console.info("[MSAL]", message);
|
|
185
|
+
break;
|
|
186
|
+
case import_msal_browser.LogLevel.Verbose:
|
|
187
|
+
console.debug("[MSAL]", message);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}),
|
|
191
|
+
logLevel: enableLogging ? import_msal_browser.LogLevel.Verbose : import_msal_browser.LogLevel.Error
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return msalConfig;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/utils/configValidator.ts
|
|
199
|
+
var validationCache = null;
|
|
200
|
+
var hasDisplayedResults = false;
|
|
201
|
+
function validateConfig(config) {
|
|
202
|
+
if (process.env.NODE_ENV !== "development") {
|
|
203
|
+
return { valid: true, warnings: [], errors: [] };
|
|
204
|
+
}
|
|
205
|
+
if (validationCache) {
|
|
206
|
+
return validationCache;
|
|
207
|
+
}
|
|
208
|
+
const warnings = [];
|
|
209
|
+
const errors = [];
|
|
210
|
+
if (!config.clientId) {
|
|
211
|
+
errors.push({
|
|
212
|
+
field: "clientId",
|
|
213
|
+
message: "Client ID is missing",
|
|
214
|
+
fix: `Add NEXT_PUBLIC_AZURE_AD_CLIENT_ID to your .env.local file.
|
|
215
|
+
|
|
216
|
+
Get it from: Azure Portal \u2192 App registrations \u2192 Your app \u2192 Application (client) ID`
|
|
217
|
+
});
|
|
218
|
+
} else if (isPlaceholderValue(config.clientId)) {
|
|
219
|
+
warnings.push({
|
|
220
|
+
field: "clientId",
|
|
221
|
+
message: "Client ID appears to be a placeholder",
|
|
222
|
+
fix: `Replace the placeholder with your actual Application (client) ID from Azure Portal.
|
|
223
|
+
|
|
224
|
+
Current value: ${config.clientId}
|
|
225
|
+
Expected format: 12345678-1234-1234-1234-123456789012 (GUID)`
|
|
226
|
+
});
|
|
227
|
+
} else if (!isValidGuid(config.clientId)) {
|
|
228
|
+
warnings.push({
|
|
229
|
+
field: "clientId",
|
|
230
|
+
message: "Client ID format is invalid",
|
|
231
|
+
fix: `Client ID should be a GUID (UUID) format.
|
|
232
|
+
|
|
233
|
+
Current value: ${config.clientId}
|
|
234
|
+
Expected format: 12345678-1234-1234-1234-123456789012
|
|
235
|
+
|
|
236
|
+
Get the correct value from: Azure Portal \u2192 App registrations \u2192 Your app`
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (config.tenantId) {
|
|
240
|
+
if (isPlaceholderValue(config.tenantId)) {
|
|
241
|
+
warnings.push({
|
|
242
|
+
field: "tenantId",
|
|
243
|
+
message: "Tenant ID appears to be a placeholder",
|
|
244
|
+
fix: `Replace the placeholder with your actual Directory (tenant) ID from Azure Portal.
|
|
245
|
+
|
|
246
|
+
Current value: ${config.tenantId}
|
|
247
|
+
Expected format: 87654321-4321-4321-4321-210987654321 (GUID)
|
|
248
|
+
|
|
249
|
+
Or remove tenantId and use authorityType: 'common' for multi-tenant apps.`
|
|
250
|
+
});
|
|
251
|
+
} else if (!isValidGuid(config.tenantId)) {
|
|
252
|
+
warnings.push({
|
|
253
|
+
field: "tenantId",
|
|
254
|
+
message: "Tenant ID format is invalid",
|
|
255
|
+
fix: `Tenant ID should be a GUID (UUID) format.
|
|
256
|
+
|
|
257
|
+
Current value: ${config.tenantId}
|
|
258
|
+
Expected format: 87654321-4321-4321-4321-210987654321
|
|
259
|
+
|
|
260
|
+
Get the correct value from: Azure Portal \u2192 Azure Active Directory \u2192 Properties \u2192 Tenant ID`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (config.redirectUri) {
|
|
265
|
+
if (isProductionUrl(config.redirectUri) && !config.redirectUri.startsWith("https://")) {
|
|
266
|
+
errors.push({
|
|
267
|
+
field: "redirectUri",
|
|
268
|
+
message: "Production redirect URI must use HTTPS",
|
|
269
|
+
fix: `Change your redirect URI to use HTTPS in production.
|
|
270
|
+
|
|
271
|
+
Current value: ${config.redirectUri}
|
|
272
|
+
Should be: ${config.redirectUri.replace("http://", "https://")}
|
|
273
|
+
|
|
274
|
+
HTTP is only allowed for localhost development.`
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (!isValidUrl(config.redirectUri)) {
|
|
278
|
+
errors.push({
|
|
279
|
+
field: "redirectUri",
|
|
280
|
+
message: "Redirect URI is not a valid URL",
|
|
281
|
+
fix: `Provide a valid URL for redirectUri.
|
|
282
|
+
|
|
283
|
+
Current value: ${config.redirectUri}
|
|
284
|
+
Expected format: https://yourdomain.com or http://localhost:3000`
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (config.scopes && config.scopes.length > 0) {
|
|
289
|
+
const invalidScopes = config.scopes.filter((scope) => !isValidScope2(scope));
|
|
290
|
+
if (invalidScopes.length > 0) {
|
|
291
|
+
warnings.push({
|
|
292
|
+
field: "scopes",
|
|
293
|
+
message: "Some scopes have invalid format",
|
|
294
|
+
fix: `Invalid scopes: ${invalidScopes.join(", ")}
|
|
295
|
+
|
|
296
|
+
Scopes should be in format: "Resource.Permission" (e.g., "User.Read", "Mail.Read")
|
|
297
|
+
|
|
298
|
+
Common scopes:
|
|
299
|
+
\u2022 User.Read - Read user profile
|
|
300
|
+
\u2022 Mail.Read - Read user mail
|
|
301
|
+
\u2022 Calendars.Read - Read user calendars
|
|
302
|
+
\u2022 Files.Read - Read user files`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (typeof window !== "undefined") {
|
|
307
|
+
const clientIdFromEnv = process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID;
|
|
308
|
+
const tenantIdFromEnv = process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID;
|
|
309
|
+
if (!clientIdFromEnv) {
|
|
310
|
+
warnings.push({
|
|
311
|
+
field: "environment",
|
|
312
|
+
message: "NEXT_PUBLIC_AZURE_AD_CLIENT_ID not found in environment",
|
|
313
|
+
fix: `Add NEXT_PUBLIC_AZURE_AD_CLIENT_ID to your .env.local file:
|
|
314
|
+
|
|
315
|
+
NEXT_PUBLIC_AZURE_AD_CLIENT_ID=your-client-id-here
|
|
316
|
+
|
|
317
|
+
Then restart your development server.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!tenantIdFromEnv && config.authorityType === "tenant") {
|
|
321
|
+
warnings.push({
|
|
322
|
+
field: "environment",
|
|
323
|
+
message: 'NEXT_PUBLIC_AZURE_AD_TENANT_ID not found but authorityType is "tenant"',
|
|
324
|
+
fix: `Either:
|
|
325
|
+
1. Add NEXT_PUBLIC_AZURE_AD_TENANT_ID to your .env.local file, OR
|
|
326
|
+
2. Change authorityType to 'common' for multi-tenant support
|
|
327
|
+
|
|
328
|
+
For single-tenant apps:
|
|
329
|
+
NEXT_PUBLIC_AZURE_AD_TENANT_ID=your-tenant-id-here`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const result = {
|
|
334
|
+
valid: errors.length === 0,
|
|
335
|
+
warnings,
|
|
336
|
+
errors
|
|
337
|
+
};
|
|
338
|
+
validationCache = result;
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
function displayValidationResults(result) {
|
|
342
|
+
if (hasDisplayedResults || process.env.NODE_ENV !== "development") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
hasDisplayedResults = true;
|
|
346
|
+
if (result.valid && result.warnings.length === 0) {
|
|
347
|
+
console.log("\u2705 MSAL configuration validated successfully");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.group("\u{1F50D} MSAL Configuration Validation");
|
|
351
|
+
if (result.errors.length > 0) {
|
|
352
|
+
console.group("\u274C Errors (must fix)");
|
|
353
|
+
result.errors.forEach((error) => {
|
|
354
|
+
console.error(`
|
|
355
|
+
${error.field}:`);
|
|
356
|
+
console.error(` ${error.message}`);
|
|
357
|
+
console.error(`
|
|
358
|
+
Fix:
|
|
359
|
+
${error.fix.split("\n").join("\n ")}`);
|
|
360
|
+
});
|
|
361
|
+
console.groupEnd();
|
|
362
|
+
}
|
|
363
|
+
if (result.warnings.length > 0) {
|
|
364
|
+
console.group("\u26A0\uFE0F Warnings (should fix)");
|
|
365
|
+
result.warnings.forEach((warning) => {
|
|
366
|
+
console.warn(`
|
|
367
|
+
${warning.field}:`);
|
|
368
|
+
console.warn(` ${warning.message}`);
|
|
369
|
+
console.warn(`
|
|
370
|
+
Fix:
|
|
371
|
+
${warning.fix.split("\n").join("\n ")}`);
|
|
372
|
+
});
|
|
373
|
+
console.groupEnd();
|
|
374
|
+
}
|
|
375
|
+
console.groupEnd();
|
|
376
|
+
}
|
|
377
|
+
function isPlaceholderValue(value) {
|
|
378
|
+
const placeholders = [
|
|
379
|
+
"your-client-id",
|
|
380
|
+
"your-tenant-id",
|
|
381
|
+
"your-client-id-here",
|
|
382
|
+
"your-tenant-id-here",
|
|
383
|
+
"client-id",
|
|
384
|
+
"tenant-id",
|
|
385
|
+
"replace-me",
|
|
386
|
+
"changeme",
|
|
387
|
+
"placeholder",
|
|
388
|
+
"example",
|
|
389
|
+
"xxx",
|
|
390
|
+
"000"
|
|
391
|
+
];
|
|
392
|
+
const lowerValue = value.toLowerCase();
|
|
393
|
+
return placeholders.some((placeholder) => lowerValue.includes(placeholder));
|
|
394
|
+
}
|
|
395
|
+
function isValidGuid(value) {
|
|
396
|
+
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
397
|
+
return guidRegex.test(value);
|
|
398
|
+
}
|
|
399
|
+
function isProductionUrl(url) {
|
|
400
|
+
return !url.includes("localhost") && !url.includes("127.0.0.1");
|
|
401
|
+
}
|
|
402
|
+
function isValidUrl(url) {
|
|
403
|
+
try {
|
|
404
|
+
new URL(url);
|
|
405
|
+
return true;
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function isValidScope2(scope) {
|
|
411
|
+
return /^([a-zA-Z0-9]+\.[a-zA-Z0-9]+|https?:\/\/.+)$/.test(scope);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/errors/MsalError.ts
|
|
415
|
+
var MSAL_ERROR_SOLUTIONS = {
|
|
416
|
+
// Redirect URI mismatch
|
|
417
|
+
"AADSTS50011": {
|
|
418
|
+
message: "Redirect URI mismatch",
|
|
419
|
+
fix: `Your redirect URI doesn't match what's configured in Azure AD.
|
|
420
|
+
|
|
421
|
+
Fix:
|
|
422
|
+
1. Go to Azure Portal \u2192 Azure Active Directory \u2192 App registrations
|
|
423
|
+
2. Select your app \u2192 Authentication
|
|
424
|
+
3. Under "Single-page application", add your redirect URI:
|
|
425
|
+
\u2022 http://localhost:3000 (for development)
|
|
426
|
+
\u2022 https://yourdomain.com (for production)
|
|
427
|
+
4. Click "Save"
|
|
428
|
+
|
|
429
|
+
Current redirect URI: ${typeof window !== "undefined" ? window.location.origin : "unknown"}`,
|
|
430
|
+
docs: "https://learn.microsoft.com/en-us/azure/active-directory/develop/reply-url"
|
|
431
|
+
},
|
|
432
|
+
// Consent required
|
|
433
|
+
"AADSTS65001": {
|
|
434
|
+
message: "Admin consent required",
|
|
435
|
+
fix: `Your app requires admin consent for the requested permissions.
|
|
436
|
+
|
|
437
|
+
Fix:
|
|
438
|
+
1. Go to Azure Portal \u2192 Azure Active Directory \u2192 App registrations
|
|
439
|
+
2. Select your app \u2192 API permissions
|
|
440
|
+
3. Click "Grant admin consent for [Your Organization]"
|
|
441
|
+
4. Confirm the consent
|
|
442
|
+
|
|
443
|
+
Alternatively, ask your Azure AD administrator to grant consent.`,
|
|
444
|
+
docs: "https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-admin-consent"
|
|
445
|
+
},
|
|
446
|
+
// Invalid client
|
|
447
|
+
"AADSTS700016": {
|
|
448
|
+
message: "Invalid client application",
|
|
449
|
+
fix: `The application ID (client ID) is not found in the directory.
|
|
450
|
+
|
|
451
|
+
Fix:
|
|
452
|
+
1. Verify your NEXT_PUBLIC_AZURE_AD_CLIENT_ID in .env.local
|
|
453
|
+
2. Ensure the app registration exists in Azure Portal
|
|
454
|
+
3. Check that you're using the correct tenant
|
|
455
|
+
|
|
456
|
+
Current client ID: Check your environment variables`,
|
|
457
|
+
docs: "https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes"
|
|
458
|
+
},
|
|
459
|
+
// Invalid tenant
|
|
460
|
+
"AADSTS90002": {
|
|
461
|
+
message: "Invalid tenant",
|
|
462
|
+
fix: `The tenant ID is invalid or not found.
|
|
463
|
+
|
|
464
|
+
Fix:
|
|
465
|
+
1. Verify your NEXT_PUBLIC_AZURE_AD_TENANT_ID in .env.local
|
|
466
|
+
2. Ensure you're using the correct tenant ID (GUID format)
|
|
467
|
+
3. For multi-tenant apps, use authorityType: 'common' instead
|
|
468
|
+
|
|
469
|
+
Current tenant ID: Check your environment variables`,
|
|
470
|
+
docs: "https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-national-cloud"
|
|
471
|
+
},
|
|
472
|
+
// User cancelled
|
|
473
|
+
"user_cancelled": {
|
|
474
|
+
message: "User cancelled authentication",
|
|
475
|
+
fix: "The user closed the authentication window or cancelled the login process. This is normal user behavior.",
|
|
476
|
+
docs: void 0
|
|
477
|
+
},
|
|
478
|
+
// No token in cache
|
|
479
|
+
"no_token_request_cache_error": {
|
|
480
|
+
message: "No cached token request",
|
|
481
|
+
fix: `This usually happens when the page is refreshed during authentication.
|
|
482
|
+
|
|
483
|
+
This is normal and will be handled automatically. If the issue persists:
|
|
484
|
+
1. Clear your browser cache and cookies
|
|
485
|
+
2. Try logging in again
|
|
486
|
+
3. Ensure cookies are enabled in your browser`,
|
|
487
|
+
docs: void 0
|
|
488
|
+
},
|
|
489
|
+
// Interaction required
|
|
490
|
+
"interaction_required": {
|
|
491
|
+
message: "User interaction required",
|
|
492
|
+
fix: `The token cannot be acquired silently and requires user interaction.
|
|
493
|
+
|
|
494
|
+
This is normal behavior. The app will redirect you to sign in.`,
|
|
495
|
+
docs: void 0
|
|
496
|
+
},
|
|
497
|
+
// Consent required
|
|
498
|
+
"consent_required": {
|
|
499
|
+
message: "User consent required",
|
|
500
|
+
fix: `Additional consent is required for the requested permissions.
|
|
501
|
+
|
|
502
|
+
This is normal behavior. You'll be prompted to grant consent.`,
|
|
503
|
+
docs: void 0
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var MsalError = class _MsalError extends Error {
|
|
507
|
+
constructor(error) {
|
|
508
|
+
const errorInfo = _MsalError.parseError(error);
|
|
509
|
+
super(errorInfo.message);
|
|
510
|
+
this.name = "MsalError";
|
|
511
|
+
this.code = errorInfo.code;
|
|
512
|
+
this.fix = errorInfo.fix;
|
|
513
|
+
this.docs = errorInfo.docs;
|
|
514
|
+
this.originalError = error;
|
|
515
|
+
if (Error.captureStackTrace) {
|
|
516
|
+
Error.captureStackTrace(this, _MsalError);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Parse error and extract actionable information
|
|
521
|
+
*/
|
|
522
|
+
static parseError(error) {
|
|
523
|
+
if (error && typeof error === "object") {
|
|
524
|
+
const err = error;
|
|
525
|
+
const errorCode = err.errorCode || err.error || err.code;
|
|
526
|
+
if (errorCode && MSAL_ERROR_SOLUTIONS[errorCode]) {
|
|
527
|
+
const solution = MSAL_ERROR_SOLUTIONS[errorCode];
|
|
528
|
+
return {
|
|
529
|
+
message: solution.message,
|
|
530
|
+
code: errorCode,
|
|
531
|
+
fix: solution.fix,
|
|
532
|
+
docs: solution.docs
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const errorMessage = err.errorMessage || err.message || String(error);
|
|
536
|
+
for (const [code, solution] of Object.entries(MSAL_ERROR_SOLUTIONS)) {
|
|
537
|
+
if (errorMessage.includes(code)) {
|
|
538
|
+
return {
|
|
539
|
+
message: solution.message,
|
|
540
|
+
code,
|
|
541
|
+
fix: solution.fix,
|
|
542
|
+
docs: solution.docs
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
message: errorMessage || "Authentication error occurred",
|
|
548
|
+
code: errorCode
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
message: "An unexpected authentication error occurred"
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Format error for console logging with colors (development only)
|
|
557
|
+
*/
|
|
558
|
+
toConsoleString() {
|
|
559
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
560
|
+
if (!isDev) {
|
|
561
|
+
return this.message;
|
|
562
|
+
}
|
|
563
|
+
let output = `
|
|
564
|
+
\u{1F6A8} MSAL Authentication Error
|
|
565
|
+
`;
|
|
566
|
+
output += `
|
|
567
|
+
Error: ${this.message}
|
|
568
|
+
`;
|
|
569
|
+
if (this.code) {
|
|
570
|
+
output += `Code: ${this.code}
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
if (this.fix) {
|
|
574
|
+
output += `
|
|
575
|
+
\u{1F4A1} How to fix:
|
|
576
|
+
${this.fix}
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
if (this.docs) {
|
|
580
|
+
output += `
|
|
581
|
+
\u{1F4DA} Documentation: ${this.docs}
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
return output;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Check if error is a user cancellation (not a real error)
|
|
588
|
+
*/
|
|
589
|
+
isUserCancellation() {
|
|
590
|
+
return this.code === "user_cancelled";
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Check if error requires user interaction
|
|
594
|
+
*/
|
|
595
|
+
requiresInteraction() {
|
|
596
|
+
return this.code === "interaction_required" || this.code === "consent_required";
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function wrapMsalError(error) {
|
|
600
|
+
if (error instanceof MsalError) {
|
|
601
|
+
return error;
|
|
602
|
+
}
|
|
603
|
+
return new MsalError(error);
|
|
604
|
+
}
|
|
605
|
+
function createMissingEnvVarError(varName) {
|
|
606
|
+
const error = {
|
|
607
|
+
errorCode: "missing_env_var",
|
|
608
|
+
errorMessage: `Missing environment variable: ${varName}`,
|
|
609
|
+
message: `${varName} not found`
|
|
610
|
+
};
|
|
611
|
+
const fix = `Environment variable ${varName} is not set.
|
|
612
|
+
|
|
613
|
+
Fix:
|
|
614
|
+
1. Copy .env.local.example to .env.local (if it exists)
|
|
615
|
+
2. Add the following to your .env.local file:
|
|
616
|
+
|
|
617
|
+
${varName}=your-value-here
|
|
618
|
+
|
|
619
|
+
3. Get the value from Azure Portal:
|
|
620
|
+
\u2022 Go to Azure Active Directory \u2192 App registrations
|
|
621
|
+
\u2022 Select your app
|
|
622
|
+
\u2022 Copy the ${varName.includes("CLIENT_ID") ? "Application (client) ID" : "Directory (tenant) ID"}
|
|
623
|
+
|
|
624
|
+
4. Restart your development server
|
|
625
|
+
|
|
626
|
+
Note: Environment variables starting with NEXT_PUBLIC_ are exposed to the browser.`;
|
|
627
|
+
return new MsalError({
|
|
628
|
+
...error,
|
|
629
|
+
fix,
|
|
630
|
+
docs: "https://nextjs.org/docs/basic-features/environment-variables"
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/hooks/useTokenRefresh.ts
|
|
635
|
+
var import_react2 = require("react");
|
|
636
|
+
|
|
637
|
+
// src/hooks/useMsalAuth.ts
|
|
638
|
+
var import_msal_react = require("@azure/msal-react");
|
|
639
|
+
var import_msal_browser2 = require("@azure/msal-browser");
|
|
640
|
+
var import_react = require("react");
|
|
641
|
+
var pendingTokenRequests = /* @__PURE__ */ new Map();
|
|
642
|
+
function useMsalAuth(defaultScopes = ["User.Read"]) {
|
|
643
|
+
const { instance, accounts, inProgress } = (0, import_msal_react.useMsal)();
|
|
644
|
+
const account = (0, import_msal_react.useAccount)(accounts[0] || null);
|
|
645
|
+
const isAuthenticated = (0, import_react.useMemo)(() => accounts.length > 0, [accounts]);
|
|
646
|
+
const loginRedirect = (0, import_react.useCallback)(
|
|
647
|
+
async (scopes = defaultScopes) => {
|
|
648
|
+
if (inProgress !== import_msal_browser2.InteractionStatus.None) {
|
|
649
|
+
console.warn("[MSAL] Interaction already in progress");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const request = {
|
|
654
|
+
scopes,
|
|
655
|
+
prompt: "select_account"
|
|
656
|
+
};
|
|
657
|
+
await instance.loginRedirect(request);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
const msalError = wrapMsalError(error);
|
|
660
|
+
if (msalError.isUserCancellation()) {
|
|
661
|
+
console.log("[MSAL] User cancelled login");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (process.env.NODE_ENV === "development") {
|
|
665
|
+
console.error(msalError.toConsoleString());
|
|
666
|
+
} else {
|
|
667
|
+
console.error("[MSAL] Login redirect failed:", msalError.message);
|
|
668
|
+
}
|
|
669
|
+
throw msalError;
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
[instance, defaultScopes, inProgress]
|
|
673
|
+
);
|
|
674
|
+
const logoutRedirect = (0, import_react.useCallback)(async () => {
|
|
675
|
+
try {
|
|
676
|
+
await instance.logoutRedirect({
|
|
677
|
+
account: account || void 0
|
|
678
|
+
});
|
|
679
|
+
} catch (error) {
|
|
680
|
+
const msalError = wrapMsalError(error);
|
|
681
|
+
if (process.env.NODE_ENV === "development") {
|
|
682
|
+
console.error(msalError.toConsoleString());
|
|
683
|
+
} else {
|
|
684
|
+
console.error("[MSAL] Logout redirect failed:", msalError.message);
|
|
685
|
+
}
|
|
686
|
+
throw msalError;
|
|
687
|
+
}
|
|
688
|
+
}, [instance, account]);
|
|
689
|
+
const acquireTokenSilent = (0, import_react.useCallback)(
|
|
690
|
+
async (scopes = defaultScopes) => {
|
|
691
|
+
if (!account) {
|
|
692
|
+
throw new Error("[MSAL] No active account. Please login first.");
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const request = {
|
|
696
|
+
scopes,
|
|
697
|
+
account,
|
|
698
|
+
forceRefresh: false
|
|
699
|
+
};
|
|
700
|
+
const response = await instance.acquireTokenSilent(request);
|
|
701
|
+
return response.accessToken;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
const msalError = wrapMsalError(error);
|
|
704
|
+
if (process.env.NODE_ENV === "development") {
|
|
705
|
+
console.error(msalError.toConsoleString());
|
|
706
|
+
} else {
|
|
707
|
+
console.error("[MSAL] Silent token acquisition failed:", msalError.message);
|
|
708
|
+
}
|
|
709
|
+
throw msalError;
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
[instance, account, defaultScopes]
|
|
713
|
+
);
|
|
714
|
+
const acquireTokenRedirect = (0, import_react.useCallback)(
|
|
715
|
+
async (scopes = defaultScopes) => {
|
|
716
|
+
if (!account) {
|
|
717
|
+
throw new Error("[MSAL] No active account. Please login first.");
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
const request = {
|
|
721
|
+
scopes,
|
|
722
|
+
account
|
|
723
|
+
};
|
|
724
|
+
await instance.acquireTokenRedirect(request);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
const msalError = wrapMsalError(error);
|
|
727
|
+
if (process.env.NODE_ENV === "development") {
|
|
728
|
+
console.error(msalError.toConsoleString());
|
|
729
|
+
} else {
|
|
730
|
+
console.error("[MSAL] Token redirect acquisition failed:", msalError.message);
|
|
731
|
+
}
|
|
732
|
+
throw msalError;
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
[instance, account, defaultScopes]
|
|
736
|
+
);
|
|
737
|
+
const acquireToken = (0, import_react.useCallback)(
|
|
738
|
+
async (scopes = defaultScopes) => {
|
|
739
|
+
const requestKey = `${account?.homeAccountId || "anonymous"}-${scopes.sort().join(",")}`;
|
|
740
|
+
const pendingRequest = pendingTokenRequests.get(requestKey);
|
|
741
|
+
if (pendingRequest) {
|
|
742
|
+
return pendingRequest;
|
|
743
|
+
}
|
|
744
|
+
const tokenRequest = (async () => {
|
|
745
|
+
try {
|
|
746
|
+
return await acquireTokenSilent(scopes);
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.warn("[MSAL] Silent token acquisition failed, falling back to redirect");
|
|
749
|
+
await acquireTokenRedirect(scopes);
|
|
750
|
+
throw new Error("[MSAL] Redirecting for token acquisition");
|
|
751
|
+
} finally {
|
|
752
|
+
pendingTokenRequests.delete(requestKey);
|
|
753
|
+
}
|
|
754
|
+
})();
|
|
755
|
+
pendingTokenRequests.set(requestKey, tokenRequest);
|
|
756
|
+
return tokenRequest;
|
|
757
|
+
},
|
|
758
|
+
[acquireTokenSilent, acquireTokenRedirect, defaultScopes, account]
|
|
759
|
+
);
|
|
760
|
+
const clearSession = (0, import_react.useCallback)(async () => {
|
|
761
|
+
instance.setActiveAccount(null);
|
|
762
|
+
await instance.clearCache();
|
|
763
|
+
}, [instance]);
|
|
764
|
+
return {
|
|
765
|
+
account,
|
|
766
|
+
accounts,
|
|
767
|
+
isAuthenticated,
|
|
768
|
+
inProgress: inProgress !== import_msal_browser2.InteractionStatus.None,
|
|
769
|
+
loginRedirect,
|
|
770
|
+
logoutRedirect,
|
|
771
|
+
acquireToken,
|
|
772
|
+
acquireTokenSilent,
|
|
773
|
+
acquireTokenRedirect,
|
|
774
|
+
clearSession
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/hooks/useTokenRefresh.ts
|
|
779
|
+
function useTokenRefresh(options = {}) {
|
|
780
|
+
const {
|
|
781
|
+
enabled = true,
|
|
782
|
+
refreshBeforeExpiry = 300,
|
|
783
|
+
// 5 minutes
|
|
784
|
+
scopes = ["User.Read"],
|
|
785
|
+
onRefresh,
|
|
786
|
+
onError
|
|
787
|
+
} = options;
|
|
788
|
+
const { isAuthenticated, account, acquireTokenSilent } = useMsalAuth();
|
|
789
|
+
const intervalRef = (0, import_react2.useRef)(null);
|
|
790
|
+
const lastRefreshRef = (0, import_react2.useRef)(null);
|
|
791
|
+
const expiresInRef = (0, import_react2.useRef)(null);
|
|
792
|
+
const refresh = (0, import_react2.useCallback)(async () => {
|
|
793
|
+
if (!isAuthenticated || !account) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
await acquireTokenSilent(scopes);
|
|
798
|
+
lastRefreshRef.current = /* @__PURE__ */ new Date();
|
|
799
|
+
const expiresIn = 3600;
|
|
800
|
+
expiresInRef.current = expiresIn;
|
|
801
|
+
onRefresh?.(expiresIn);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.error("[TokenRefresh] Failed to refresh token:", error);
|
|
804
|
+
onError?.(error);
|
|
805
|
+
}
|
|
806
|
+
}, [isAuthenticated, account, acquireTokenSilent, scopes, onRefresh, onError]);
|
|
807
|
+
(0, import_react2.useEffect)(() => {
|
|
808
|
+
if (!enabled || !isAuthenticated) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
refresh();
|
|
812
|
+
intervalRef.current = setInterval(() => {
|
|
813
|
+
if (!expiresInRef.current) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
|
|
817
|
+
const remainingTime = expiresInRef.current - timeSinceRefresh;
|
|
818
|
+
expiresInRef.current = Math.max(0, remainingTime);
|
|
819
|
+
if (remainingTime <= refreshBeforeExpiry && remainingTime > 0) {
|
|
820
|
+
refresh();
|
|
821
|
+
}
|
|
822
|
+
}, 6e4);
|
|
823
|
+
return () => {
|
|
824
|
+
if (intervalRef.current) {
|
|
825
|
+
clearInterval(intervalRef.current);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}, [enabled, isAuthenticated, refreshBeforeExpiry, refresh]);
|
|
829
|
+
const isExpiringSoon = expiresInRef.current !== null && expiresInRef.current <= refreshBeforeExpiry;
|
|
830
|
+
return {
|
|
831
|
+
expiresIn: expiresInRef.current,
|
|
832
|
+
isExpiringSoon,
|
|
833
|
+
refresh,
|
|
834
|
+
lastRefresh: lastRefreshRef.current
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/components/TokenRefreshManager.tsx
|
|
839
|
+
function TokenRefreshManager({
|
|
840
|
+
enabled,
|
|
841
|
+
refreshBeforeExpiry = 300,
|
|
842
|
+
scopes = ["User.Read"],
|
|
843
|
+
enableLogging = false
|
|
844
|
+
}) {
|
|
845
|
+
useTokenRefresh({
|
|
846
|
+
enabled,
|
|
847
|
+
refreshBeforeExpiry,
|
|
848
|
+
scopes,
|
|
849
|
+
onRefresh: (expiresIn) => {
|
|
850
|
+
if (enableLogging) {
|
|
851
|
+
console.log(`[TokenRefresh] Token refreshed successfully. Expires in ${expiresIn} seconds.`);
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
onError: (error) => {
|
|
855
|
+
if (enableLogging) {
|
|
856
|
+
console.error("[TokenRefresh] Failed to refresh token:", error);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/components/MsalAuthProvider.tsx
|
|
864
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
865
|
+
var globalMsalInstance = null;
|
|
866
|
+
function getMsalInstance() {
|
|
867
|
+
return globalMsalInstance;
|
|
868
|
+
}
|
|
869
|
+
function MsalAuthProvider({
|
|
870
|
+
children,
|
|
871
|
+
loadingComponent,
|
|
872
|
+
onInitialized,
|
|
873
|
+
autoRefreshToken = false,
|
|
874
|
+
refreshBeforeExpiry = 300,
|
|
875
|
+
...config
|
|
876
|
+
}) {
|
|
877
|
+
const [msalInstance, setMsalInstance] = (0, import_react3.useState)(null);
|
|
878
|
+
const instanceRef = (0, import_react3.useRef)(null);
|
|
879
|
+
const { scopes = ["User.Read"], enableLogging = false } = config;
|
|
880
|
+
(0, import_react3.useEffect)(() => {
|
|
881
|
+
if (typeof window === "undefined") {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (instanceRef.current) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const initializeMsal = async () => {
|
|
888
|
+
try {
|
|
889
|
+
if (process.env.NODE_ENV === "development") {
|
|
890
|
+
const validationResult = validateConfig(config);
|
|
891
|
+
displayValidationResults(validationResult);
|
|
892
|
+
}
|
|
893
|
+
const msalConfig = createMsalConfig(config);
|
|
894
|
+
const instance = new import_msal_browser3.PublicClientApplication(msalConfig);
|
|
895
|
+
await instance.initialize();
|
|
896
|
+
try {
|
|
897
|
+
const response = await instance.handleRedirectPromise();
|
|
898
|
+
if (response) {
|
|
899
|
+
if (config.enableLogging) {
|
|
900
|
+
console.log("[MSAL] Redirect authentication successful");
|
|
901
|
+
}
|
|
902
|
+
if (response.account) {
|
|
903
|
+
instance.setActiveAccount(response.account);
|
|
904
|
+
}
|
|
905
|
+
if (window.location.hash) {
|
|
906
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch (redirectError) {
|
|
910
|
+
const msalError = wrapMsalError(redirectError);
|
|
911
|
+
if (msalError.code === "no_token_request_cache_error") {
|
|
912
|
+
if (config.enableLogging) {
|
|
913
|
+
console.log("[MSAL] No pending redirect found (this is normal)");
|
|
914
|
+
}
|
|
915
|
+
} else if (msalError.isUserCancellation()) {
|
|
916
|
+
if (config.enableLogging) {
|
|
917
|
+
console.log("[MSAL] User cancelled authentication");
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
if (process.env.NODE_ENV === "development") {
|
|
921
|
+
console.error(msalError.toConsoleString());
|
|
922
|
+
} else {
|
|
923
|
+
console.error("[MSAL] Redirect handling error:", msalError.message);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (window.location.hash && (window.location.hash.includes("code=") || window.location.hash.includes("error="))) {
|
|
927
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const accounts = instance.getAllAccounts();
|
|
931
|
+
if (accounts.length > 0 && !instance.getActiveAccount()) {
|
|
932
|
+
instance.setActiveAccount(accounts[0]);
|
|
933
|
+
}
|
|
934
|
+
const loggingEnabled = config.enableLogging || false;
|
|
935
|
+
instance.addEventCallback((event) => {
|
|
936
|
+
if (event.eventType === import_msal_browser3.EventType.LOGIN_SUCCESS) {
|
|
937
|
+
const payload = event.payload;
|
|
938
|
+
if (payload?.account) {
|
|
939
|
+
instance.setActiveAccount(payload.account);
|
|
940
|
+
}
|
|
941
|
+
if (loggingEnabled) {
|
|
942
|
+
console.log("[MSAL] Login successful:", payload.account?.username);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (event.eventType === import_msal_browser3.EventType.LOGIN_FAILURE) {
|
|
946
|
+
console.error("[MSAL] Login failed:", event.error);
|
|
947
|
+
}
|
|
948
|
+
if (event.eventType === import_msal_browser3.EventType.LOGOUT_SUCCESS) {
|
|
949
|
+
instance.setActiveAccount(null);
|
|
950
|
+
if (loggingEnabled) {
|
|
951
|
+
console.log("[MSAL] Logout successful");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (event.eventType === import_msal_browser3.EventType.ACQUIRE_TOKEN_SUCCESS) {
|
|
955
|
+
const payload = event.payload;
|
|
956
|
+
if (payload?.account && !instance.getActiveAccount()) {
|
|
957
|
+
instance.setActiveAccount(payload.account);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (event.eventType === import_msal_browser3.EventType.ACQUIRE_TOKEN_FAILURE) {
|
|
961
|
+
if (loggingEnabled) {
|
|
962
|
+
console.error("[MSAL] Token acquisition failed:", event.error);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
instanceRef.current = instance;
|
|
967
|
+
globalMsalInstance = instance;
|
|
968
|
+
setMsalInstance(instance);
|
|
969
|
+
if (onInitialized) {
|
|
970
|
+
onInitialized(instance);
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
console.error("[MSAL] Initialization failed:", error);
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
initializeMsal();
|
|
978
|
+
}, []);
|
|
979
|
+
if (typeof window === "undefined") {
|
|
980
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: loadingComponent || /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "Loading authentication..." }) });
|
|
981
|
+
}
|
|
982
|
+
if (!msalInstance) {
|
|
983
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: loadingComponent || /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "Loading authentication..." }) });
|
|
984
|
+
}
|
|
985
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_msal_react2.MsalProvider, { instance: msalInstance, children: [
|
|
986
|
+
autoRefreshToken && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
987
|
+
TokenRefreshManager,
|
|
988
|
+
{
|
|
989
|
+
enabled: autoRefreshToken,
|
|
990
|
+
refreshBeforeExpiry,
|
|
991
|
+
scopes,
|
|
992
|
+
enableLogging
|
|
993
|
+
}
|
|
994
|
+
),
|
|
995
|
+
children
|
|
996
|
+
] });
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/components/MSALProvider.tsx
|
|
1000
|
+
var import_react4 = require("react");
|
|
1001
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1002
|
+
var ProtectionConfigContext = (0, import_react4.createContext)(void 0);
|
|
1003
|
+
function MSALProvider({ children, protection, ...props }) {
|
|
1004
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ProtectionConfigContext.Provider, { value: protection, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MsalAuthProvider, { ...props, children }) });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/components/MicrosoftSignInButton.tsx
|
|
1008
|
+
var import_react5 = require("react");
|
|
1009
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1010
|
+
function MicrosoftSignInButton({
|
|
1011
|
+
text = "Sign in with Microsoft",
|
|
1012
|
+
variant = "dark",
|
|
1013
|
+
size = "medium",
|
|
1014
|
+
scopes,
|
|
1015
|
+
className = "",
|
|
1016
|
+
style,
|
|
1017
|
+
onSuccess,
|
|
1018
|
+
onError
|
|
1019
|
+
}) {
|
|
1020
|
+
const { loginRedirect, inProgress, isAuthenticated } = useMsalAuth();
|
|
1021
|
+
const [isLoading, setIsLoading] = (0, import_react5.useState)(false);
|
|
1022
|
+
const timeoutRef = (0, import_react5.useRef)(null);
|
|
1023
|
+
(0, import_react5.useEffect)(() => {
|
|
1024
|
+
return () => {
|
|
1025
|
+
if (timeoutRef.current) {
|
|
1026
|
+
clearTimeout(timeoutRef.current);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}, []);
|
|
1030
|
+
(0, import_react5.useEffect)(() => {
|
|
1031
|
+
if (isAuthenticated && isLoading) {
|
|
1032
|
+
setIsLoading(false);
|
|
1033
|
+
}
|
|
1034
|
+
}, [isAuthenticated, isLoading]);
|
|
1035
|
+
const handleClick = async () => {
|
|
1036
|
+
if (inProgress || isLoading || isAuthenticated) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
setIsLoading(true);
|
|
1040
|
+
try {
|
|
1041
|
+
await loginRedirect(scopes);
|
|
1042
|
+
onSuccess?.();
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
onError?.(error);
|
|
1045
|
+
setIsLoading(false);
|
|
1046
|
+
}
|
|
1047
|
+
timeoutRef.current = setTimeout(() => {
|
|
1048
|
+
setIsLoading(false);
|
|
1049
|
+
}, 3e3);
|
|
1050
|
+
};
|
|
1051
|
+
const sizeStyles = {
|
|
1052
|
+
small: {
|
|
1053
|
+
padding: "8px 16px",
|
|
1054
|
+
fontSize: "14px",
|
|
1055
|
+
height: "36px"
|
|
1056
|
+
},
|
|
1057
|
+
medium: {
|
|
1058
|
+
padding: "10px 20px",
|
|
1059
|
+
fontSize: "15px",
|
|
1060
|
+
height: "41px"
|
|
1061
|
+
},
|
|
1062
|
+
large: {
|
|
1063
|
+
padding: "12px 24px",
|
|
1064
|
+
fontSize: "16px",
|
|
1065
|
+
height: "48px"
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const variantStyles = {
|
|
1069
|
+
dark: {
|
|
1070
|
+
backgroundColor: "#2F2F2F",
|
|
1071
|
+
color: "#FFFFFF",
|
|
1072
|
+
border: "1px solid #8C8C8C"
|
|
1073
|
+
},
|
|
1074
|
+
light: {
|
|
1075
|
+
backgroundColor: "#FFFFFF",
|
|
1076
|
+
color: "#5E5E5E",
|
|
1077
|
+
border: "1px solid #8C8C8C"
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
const isDisabled = inProgress || isLoading;
|
|
1081
|
+
const baseStyles = {
|
|
1082
|
+
display: "inline-flex",
|
|
1083
|
+
alignItems: "center",
|
|
1084
|
+
justifyContent: "center",
|
|
1085
|
+
gap: "12px",
|
|
1086
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
1087
|
+
fontWeight: 600,
|
|
1088
|
+
borderRadius: "2px",
|
|
1089
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
1090
|
+
transition: "all 0.2s ease",
|
|
1091
|
+
opacity: isDisabled ? 0.6 : 1,
|
|
1092
|
+
...variantStyles[variant],
|
|
1093
|
+
...sizeStyles[size],
|
|
1094
|
+
...style
|
|
1095
|
+
};
|
|
1096
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1097
|
+
"button",
|
|
1098
|
+
{
|
|
1099
|
+
onClick: handleClick,
|
|
1100
|
+
disabled: isDisabled,
|
|
1101
|
+
className,
|
|
1102
|
+
style: baseStyles,
|
|
1103
|
+
"aria-label": text,
|
|
1104
|
+
children: [
|
|
1105
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicrosoftLogo, {}),
|
|
1106
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: text })
|
|
1107
|
+
]
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function MicrosoftLogo() {
|
|
1112
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "21", height: "21", viewBox: "0 0 21 21", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1113
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "10", height: "10", fill: "#F25022" }),
|
|
1114
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "11", width: "10", height: "10", fill: "#7FBA00" }),
|
|
1115
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { y: "11", width: "10", height: "10", fill: "#00A4EF" }),
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "11", y: "11", width: "10", height: "10", fill: "#FFB900" })
|
|
1117
|
+
] });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/components/SignOutButton.tsx
|
|
1121
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1122
|
+
function SignOutButton({
|
|
1123
|
+
text = "Sign out",
|
|
1124
|
+
variant = "dark",
|
|
1125
|
+
size = "medium",
|
|
1126
|
+
className = "",
|
|
1127
|
+
style,
|
|
1128
|
+
onSuccess,
|
|
1129
|
+
onError
|
|
1130
|
+
}) {
|
|
1131
|
+
const { logoutRedirect, inProgress } = useMsalAuth();
|
|
1132
|
+
const handleClick = async () => {
|
|
1133
|
+
try {
|
|
1134
|
+
await logoutRedirect();
|
|
1135
|
+
onSuccess?.();
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
onError?.(error);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
const sizeStyles = {
|
|
1141
|
+
small: {
|
|
1142
|
+
padding: "8px 16px",
|
|
1143
|
+
fontSize: "14px",
|
|
1144
|
+
height: "36px"
|
|
1145
|
+
},
|
|
1146
|
+
medium: {
|
|
1147
|
+
padding: "10px 20px",
|
|
1148
|
+
fontSize: "15px",
|
|
1149
|
+
height: "41px"
|
|
1150
|
+
},
|
|
1151
|
+
large: {
|
|
1152
|
+
padding: "12px 24px",
|
|
1153
|
+
fontSize: "16px",
|
|
1154
|
+
height: "48px"
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
const variantStyles = {
|
|
1158
|
+
dark: {
|
|
1159
|
+
backgroundColor: "#2F2F2F",
|
|
1160
|
+
color: "#FFFFFF",
|
|
1161
|
+
border: "1px solid #8C8C8C"
|
|
1162
|
+
},
|
|
1163
|
+
light: {
|
|
1164
|
+
backgroundColor: "#FFFFFF",
|
|
1165
|
+
color: "#5E5E5E",
|
|
1166
|
+
border: "1px solid #8C8C8C"
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
const baseStyles = {
|
|
1170
|
+
display: "inline-flex",
|
|
1171
|
+
alignItems: "center",
|
|
1172
|
+
justifyContent: "center",
|
|
1173
|
+
gap: "12px",
|
|
1174
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
1175
|
+
fontWeight: 600,
|
|
1176
|
+
borderRadius: "2px",
|
|
1177
|
+
cursor: inProgress ? "not-allowed" : "pointer",
|
|
1178
|
+
transition: "all 0.2s ease",
|
|
1179
|
+
opacity: inProgress ? 0.6 : 1,
|
|
1180
|
+
...variantStyles[variant],
|
|
1181
|
+
...sizeStyles[size],
|
|
1182
|
+
...style
|
|
1183
|
+
};
|
|
1184
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1185
|
+
"button",
|
|
1186
|
+
{
|
|
1187
|
+
onClick: handleClick,
|
|
1188
|
+
disabled: inProgress,
|
|
1189
|
+
className,
|
|
1190
|
+
style: baseStyles,
|
|
1191
|
+
"aria-label": text,
|
|
1192
|
+
children: [
|
|
1193
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MicrosoftLogo2, {}),
|
|
1194
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: text })
|
|
1195
|
+
]
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
function MicrosoftLogo2() {
|
|
1200
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "21", height: "21", viewBox: "0 0 21 21", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1201
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: "10", height: "10", fill: "#F25022" }),
|
|
1202
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "11", width: "10", height: "10", fill: "#7FBA00" }),
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { y: "11", width: "10", height: "10", fill: "#00A4EF" }),
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: "11", y: "11", width: "10", height: "10", fill: "#FFB900" })
|
|
1205
|
+
] });
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/components/UserAvatar.tsx
|
|
1209
|
+
var import_react8 = require("react");
|
|
1210
|
+
|
|
1211
|
+
// src/hooks/useUserProfile.ts
|
|
1212
|
+
var import_react7 = require("react");
|
|
1213
|
+
|
|
1214
|
+
// src/hooks/useGraphApi.ts
|
|
1215
|
+
var import_react6 = require("react");
|
|
1216
|
+
function useGraphApi() {
|
|
1217
|
+
const { acquireToken } = useMsalAuth();
|
|
1218
|
+
const request = (0, import_react6.useCallback)(
|
|
1219
|
+
async (endpoint, options = {}) => {
|
|
1220
|
+
const {
|
|
1221
|
+
scopes = ["User.Read"],
|
|
1222
|
+
version = "v1.0",
|
|
1223
|
+
debug = false,
|
|
1224
|
+
...fetchOptions
|
|
1225
|
+
} = options;
|
|
1226
|
+
try {
|
|
1227
|
+
const token = await acquireToken(scopes);
|
|
1228
|
+
const baseUrl = `https://graph.microsoft.com/${version}`;
|
|
1229
|
+
const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
|
1230
|
+
if (debug) {
|
|
1231
|
+
console.log("[GraphAPI] Request:", { url, method: fetchOptions.method || "GET" });
|
|
1232
|
+
}
|
|
1233
|
+
const response = await fetch(url, {
|
|
1234
|
+
...fetchOptions,
|
|
1235
|
+
headers: {
|
|
1236
|
+
"Authorization": `Bearer ${token}`,
|
|
1237
|
+
"Content-Type": "application/json",
|
|
1238
|
+
...fetchOptions.headers
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
if (!response.ok) {
|
|
1242
|
+
const errorText = await response.text();
|
|
1243
|
+
const errorMessage = `Graph API error (${response.status}): ${errorText}`;
|
|
1244
|
+
throw new Error(errorMessage);
|
|
1245
|
+
}
|
|
1246
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const data = await response.json();
|
|
1250
|
+
if (debug) {
|
|
1251
|
+
console.log("[GraphAPI] Response:", data);
|
|
1252
|
+
}
|
|
1253
|
+
return data;
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
const sanitizedMessage = sanitizeError(error);
|
|
1256
|
+
console.error("[GraphAPI] Request failed:", sanitizedMessage);
|
|
1257
|
+
throw new Error(sanitizedMessage);
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
[acquireToken]
|
|
1261
|
+
);
|
|
1262
|
+
const get = (0, import_react6.useCallback)(
|
|
1263
|
+
(endpoint, options = {}) => {
|
|
1264
|
+
return request(endpoint, { ...options, method: "GET" });
|
|
1265
|
+
},
|
|
1266
|
+
[request]
|
|
1267
|
+
);
|
|
1268
|
+
const post = (0, import_react6.useCallback)(
|
|
1269
|
+
(endpoint, body, options = {}) => {
|
|
1270
|
+
return request(endpoint, {
|
|
1271
|
+
...options,
|
|
1272
|
+
method: "POST",
|
|
1273
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
[request]
|
|
1277
|
+
);
|
|
1278
|
+
const put = (0, import_react6.useCallback)(
|
|
1279
|
+
(endpoint, body, options = {}) => {
|
|
1280
|
+
return request(endpoint, {
|
|
1281
|
+
...options,
|
|
1282
|
+
method: "PUT",
|
|
1283
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1284
|
+
});
|
|
1285
|
+
},
|
|
1286
|
+
[request]
|
|
1287
|
+
);
|
|
1288
|
+
const patch = (0, import_react6.useCallback)(
|
|
1289
|
+
(endpoint, body, options = {}) => {
|
|
1290
|
+
return request(endpoint, {
|
|
1291
|
+
...options,
|
|
1292
|
+
method: "PATCH",
|
|
1293
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1294
|
+
});
|
|
1295
|
+
},
|
|
1296
|
+
[request]
|
|
1297
|
+
);
|
|
1298
|
+
const deleteRequest = (0, import_react6.useCallback)(
|
|
1299
|
+
(endpoint, options = {}) => {
|
|
1300
|
+
return request(endpoint, { ...options, method: "DELETE" });
|
|
1301
|
+
},
|
|
1302
|
+
[request]
|
|
1303
|
+
);
|
|
1304
|
+
return {
|
|
1305
|
+
get,
|
|
1306
|
+
post,
|
|
1307
|
+
put,
|
|
1308
|
+
patch,
|
|
1309
|
+
delete: deleteRequest,
|
|
1310
|
+
request
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/hooks/useUserProfile.ts
|
|
1315
|
+
var profileCache = /* @__PURE__ */ new Map();
|
|
1316
|
+
var CACHE_DURATION = 5 * 60 * 1e3;
|
|
1317
|
+
var MAX_CACHE_SIZE = 100;
|
|
1318
|
+
function enforceCacheLimit() {
|
|
1319
|
+
if (profileCache.size > MAX_CACHE_SIZE) {
|
|
1320
|
+
const entries = Array.from(profileCache.entries());
|
|
1321
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
1322
|
+
const toRemove = entries.slice(0, profileCache.size - MAX_CACHE_SIZE);
|
|
1323
|
+
toRemove.forEach(([key]) => {
|
|
1324
|
+
const cached = profileCache.get(key);
|
|
1325
|
+
if (cached?.data.photo) {
|
|
1326
|
+
URL.revokeObjectURL(cached.data.photo);
|
|
1327
|
+
}
|
|
1328
|
+
profileCache.delete(key);
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function useUserProfile() {
|
|
1333
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
1334
|
+
const graph = useGraphApi();
|
|
1335
|
+
const [profile, setProfile] = (0, import_react7.useState)(null);
|
|
1336
|
+
const [loading, setLoading] = (0, import_react7.useState)(false);
|
|
1337
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
1338
|
+
const fetchProfile = (0, import_react7.useCallback)(async () => {
|
|
1339
|
+
if (!isAuthenticated || !account) {
|
|
1340
|
+
setProfile(null);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const cacheKey = account.homeAccountId;
|
|
1344
|
+
const cached = profileCache.get(cacheKey);
|
|
1345
|
+
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
|
1346
|
+
setProfile(cached.data);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
setLoading(true);
|
|
1350
|
+
setError(null);
|
|
1351
|
+
try {
|
|
1352
|
+
const userData = await graph.get("/me", {
|
|
1353
|
+
scopes: ["User.Read"]
|
|
1354
|
+
});
|
|
1355
|
+
let photoUrl;
|
|
1356
|
+
try {
|
|
1357
|
+
const photoBlob = await graph.get("/me/photo/$value", {
|
|
1358
|
+
scopes: ["User.Read"],
|
|
1359
|
+
headers: {
|
|
1360
|
+
"Content-Type": "image/jpeg"
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
if (photoBlob) {
|
|
1364
|
+
photoUrl = URL.createObjectURL(photoBlob);
|
|
1365
|
+
}
|
|
1366
|
+
} catch (photoError) {
|
|
1367
|
+
console.debug("[UserProfile] Photo not available");
|
|
1368
|
+
}
|
|
1369
|
+
const profileData = {
|
|
1370
|
+
id: userData.id,
|
|
1371
|
+
displayName: userData.displayName,
|
|
1372
|
+
givenName: userData.givenName,
|
|
1373
|
+
surname: userData.surname,
|
|
1374
|
+
userPrincipalName: userData.userPrincipalName,
|
|
1375
|
+
mail: userData.mail,
|
|
1376
|
+
jobTitle: userData.jobTitle,
|
|
1377
|
+
department: userData.department,
|
|
1378
|
+
companyName: userData.companyName,
|
|
1379
|
+
officeLocation: userData.officeLocation,
|
|
1380
|
+
mobilePhone: userData.mobilePhone,
|
|
1381
|
+
businessPhones: userData.businessPhones,
|
|
1382
|
+
preferredLanguage: userData.preferredLanguage,
|
|
1383
|
+
employeeId: userData.employeeId,
|
|
1384
|
+
employeeHireDate: userData.employeeHireDate,
|
|
1385
|
+
employeeType: userData.employeeType,
|
|
1386
|
+
country: userData.country,
|
|
1387
|
+
city: userData.city,
|
|
1388
|
+
state: userData.state,
|
|
1389
|
+
streetAddress: userData.streetAddress,
|
|
1390
|
+
postalCode: userData.postalCode,
|
|
1391
|
+
usageLocation: userData.usageLocation,
|
|
1392
|
+
manager: userData.manager,
|
|
1393
|
+
aboutMe: userData.aboutMe,
|
|
1394
|
+
birthday: userData.birthday,
|
|
1395
|
+
interests: userData.interests,
|
|
1396
|
+
skills: userData.skills,
|
|
1397
|
+
schools: userData.schools,
|
|
1398
|
+
pastProjects: userData.pastProjects,
|
|
1399
|
+
responsibilities: userData.responsibilities,
|
|
1400
|
+
mySite: userData.mySite,
|
|
1401
|
+
faxNumber: userData.faxNumber,
|
|
1402
|
+
accountEnabled: userData.accountEnabled,
|
|
1403
|
+
ageGroup: userData.ageGroup,
|
|
1404
|
+
userType: userData.userType,
|
|
1405
|
+
photo: photoUrl,
|
|
1406
|
+
...userData
|
|
1407
|
+
// Include any additional fields from the API
|
|
1408
|
+
};
|
|
1409
|
+
profileCache.set(cacheKey, {
|
|
1410
|
+
data: profileData,
|
|
1411
|
+
timestamp: Date.now()
|
|
1412
|
+
});
|
|
1413
|
+
enforceCacheLimit();
|
|
1414
|
+
setProfile(profileData);
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
const error2 = err;
|
|
1417
|
+
const sanitizedMessage = sanitizeError(error2);
|
|
1418
|
+
const sanitizedError = new Error(sanitizedMessage);
|
|
1419
|
+
setError(sanitizedError);
|
|
1420
|
+
console.error("[UserProfile] Failed to fetch profile:", sanitizedMessage);
|
|
1421
|
+
} finally {
|
|
1422
|
+
setLoading(false);
|
|
1423
|
+
}
|
|
1424
|
+
}, [isAuthenticated, account, graph]);
|
|
1425
|
+
const clearCache = (0, import_react7.useCallback)(() => {
|
|
1426
|
+
if (account) {
|
|
1427
|
+
const cached = profileCache.get(account.homeAccountId);
|
|
1428
|
+
if (cached?.data.photo) {
|
|
1429
|
+
URL.revokeObjectURL(cached.data.photo);
|
|
1430
|
+
}
|
|
1431
|
+
profileCache.delete(account.homeAccountId);
|
|
1432
|
+
}
|
|
1433
|
+
if (profile?.photo) {
|
|
1434
|
+
URL.revokeObjectURL(profile.photo);
|
|
1435
|
+
}
|
|
1436
|
+
setProfile(null);
|
|
1437
|
+
}, [account, profile]);
|
|
1438
|
+
(0, import_react7.useEffect)(() => {
|
|
1439
|
+
fetchProfile();
|
|
1440
|
+
return () => {
|
|
1441
|
+
if (profile?.photo) {
|
|
1442
|
+
URL.revokeObjectURL(profile.photo);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}, [fetchProfile]);
|
|
1446
|
+
(0, import_react7.useEffect)(() => {
|
|
1447
|
+
return () => {
|
|
1448
|
+
if (profile?.photo) {
|
|
1449
|
+
URL.revokeObjectURL(profile.photo);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}, [profile?.photo]);
|
|
1453
|
+
return {
|
|
1454
|
+
profile,
|
|
1455
|
+
loading,
|
|
1456
|
+
error,
|
|
1457
|
+
refetch: fetchProfile,
|
|
1458
|
+
clearCache
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/components/UserAvatar.tsx
|
|
1463
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1464
|
+
function UserAvatar({
|
|
1465
|
+
size = 40,
|
|
1466
|
+
className = "",
|
|
1467
|
+
style,
|
|
1468
|
+
showTooltip = true,
|
|
1469
|
+
fallbackImage
|
|
1470
|
+
}) {
|
|
1471
|
+
const { profile, loading } = useUserProfile();
|
|
1472
|
+
const [photoUrl, setPhotoUrl] = (0, import_react8.useState)(null);
|
|
1473
|
+
const [photoError, setPhotoError] = (0, import_react8.useState)(false);
|
|
1474
|
+
(0, import_react8.useEffect)(() => {
|
|
1475
|
+
if (profile?.photo) {
|
|
1476
|
+
setPhotoUrl(profile.photo);
|
|
1477
|
+
}
|
|
1478
|
+
}, [profile?.photo]);
|
|
1479
|
+
const getInitials = () => {
|
|
1480
|
+
if (!profile) return "?";
|
|
1481
|
+
const { givenName, surname, displayName: displayName2 } = profile;
|
|
1482
|
+
if (givenName && surname) {
|
|
1483
|
+
return `${givenName[0]}${surname[0]}`.toUpperCase();
|
|
1484
|
+
}
|
|
1485
|
+
if (displayName2) {
|
|
1486
|
+
const parts = displayName2.split(" ");
|
|
1487
|
+
if (parts.length >= 2) {
|
|
1488
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
1489
|
+
}
|
|
1490
|
+
return displayName2.substring(0, 2).toUpperCase();
|
|
1491
|
+
}
|
|
1492
|
+
return "?";
|
|
1493
|
+
};
|
|
1494
|
+
const baseStyles = {
|
|
1495
|
+
width: `${size}px`,
|
|
1496
|
+
height: `${size}px`,
|
|
1497
|
+
borderRadius: "50%",
|
|
1498
|
+
display: "inline-flex",
|
|
1499
|
+
alignItems: "center",
|
|
1500
|
+
justifyContent: "center",
|
|
1501
|
+
fontSize: `${size * 0.4}px`,
|
|
1502
|
+
fontWeight: 600,
|
|
1503
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
1504
|
+
backgroundColor: "#0078D4",
|
|
1505
|
+
color: "#FFFFFF",
|
|
1506
|
+
overflow: "hidden",
|
|
1507
|
+
userSelect: "none",
|
|
1508
|
+
...style
|
|
1509
|
+
};
|
|
1510
|
+
const displayName = profile?.displayName || "User";
|
|
1511
|
+
if (loading) {
|
|
1512
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1513
|
+
"div",
|
|
1514
|
+
{
|
|
1515
|
+
className,
|
|
1516
|
+
style: { ...baseStyles, backgroundColor: "#E1E1E1" },
|
|
1517
|
+
"aria-label": "Loading user avatar",
|
|
1518
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: `${size * 0.3}px` }, children: "..." })
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (photoUrl && !photoError) {
|
|
1523
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1524
|
+
"div",
|
|
1525
|
+
{
|
|
1526
|
+
className,
|
|
1527
|
+
style: baseStyles,
|
|
1528
|
+
title: showTooltip ? displayName : void 0,
|
|
1529
|
+
"aria-label": `${displayName} avatar`,
|
|
1530
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1531
|
+
"img",
|
|
1532
|
+
{
|
|
1533
|
+
src: photoUrl,
|
|
1534
|
+
alt: displayName,
|
|
1535
|
+
style: { width: "100%", height: "100%", objectFit: "cover" },
|
|
1536
|
+
onError: () => {
|
|
1537
|
+
setPhotoError(true);
|
|
1538
|
+
if (fallbackImage) {
|
|
1539
|
+
setPhotoUrl(fallbackImage);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
)
|
|
1544
|
+
}
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1548
|
+
"div",
|
|
1549
|
+
{
|
|
1550
|
+
className,
|
|
1551
|
+
style: baseStyles,
|
|
1552
|
+
title: showTooltip ? displayName : void 0,
|
|
1553
|
+
"aria-label": `${displayName} avatar`,
|
|
1554
|
+
children: getInitials()
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/components/AuthStatus.tsx
|
|
1560
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1561
|
+
function AuthStatus({
|
|
1562
|
+
className = "",
|
|
1563
|
+
style,
|
|
1564
|
+
showDetails = false,
|
|
1565
|
+
renderLoading,
|
|
1566
|
+
renderAuthenticated,
|
|
1567
|
+
renderUnauthenticated
|
|
1568
|
+
}) {
|
|
1569
|
+
const { isAuthenticated, inProgress, account } = useMsalAuth();
|
|
1570
|
+
const baseStyles = {
|
|
1571
|
+
display: "inline-flex",
|
|
1572
|
+
alignItems: "center",
|
|
1573
|
+
gap: "8px",
|
|
1574
|
+
padding: "8px 12px",
|
|
1575
|
+
borderRadius: "4px",
|
|
1576
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
|
1577
|
+
fontSize: "14px",
|
|
1578
|
+
fontWeight: 500,
|
|
1579
|
+
...style
|
|
1580
|
+
};
|
|
1581
|
+
if (inProgress) {
|
|
1582
|
+
if (renderLoading) {
|
|
1583
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: renderLoading() });
|
|
1584
|
+
}
|
|
1585
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1586
|
+
"div",
|
|
1587
|
+
{
|
|
1588
|
+
className,
|
|
1589
|
+
style: { ...baseStyles, backgroundColor: "#FFF4CE", color: "#8A6D3B" },
|
|
1590
|
+
role: "status",
|
|
1591
|
+
"aria-live": "polite",
|
|
1592
|
+
children: [
|
|
1593
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatusIndicator, { color: "#FFA500" }),
|
|
1594
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Loading..." })
|
|
1595
|
+
]
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (isAuthenticated) {
|
|
1600
|
+
const username = account?.username || account?.name || "User";
|
|
1601
|
+
if (renderAuthenticated) {
|
|
1602
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: renderAuthenticated(username) });
|
|
1603
|
+
}
|
|
1604
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1605
|
+
"div",
|
|
1606
|
+
{
|
|
1607
|
+
className,
|
|
1608
|
+
style: { ...baseStyles, backgroundColor: "#D4EDDA", color: "#155724" },
|
|
1609
|
+
role: "status",
|
|
1610
|
+
"aria-live": "polite",
|
|
1611
|
+
children: [
|
|
1612
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatusIndicator, { color: "#28A745" }),
|
|
1613
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: showDetails ? `Authenticated as ${username}` : "Authenticated" })
|
|
1614
|
+
]
|
|
1615
|
+
}
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
if (renderUnauthenticated) {
|
|
1619
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: renderUnauthenticated() });
|
|
1620
|
+
}
|
|
1621
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1622
|
+
"div",
|
|
1623
|
+
{
|
|
1624
|
+
className,
|
|
1625
|
+
style: { ...baseStyles, backgroundColor: "#F8D7DA", color: "#721C24" },
|
|
1626
|
+
role: "status",
|
|
1627
|
+
"aria-live": "polite",
|
|
1628
|
+
children: [
|
|
1629
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatusIndicator, { color: "#DC3545" }),
|
|
1630
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Not authenticated" })
|
|
1631
|
+
]
|
|
1632
|
+
}
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
function StatusIndicator({ color }) {
|
|
1636
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 8 8", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "4", cy: "4", r: "4", fill: color }) });
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/components/AuthGuard.tsx
|
|
1640
|
+
var import_react9 = require("react");
|
|
1641
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1642
|
+
function AuthGuard({
|
|
1643
|
+
children,
|
|
1644
|
+
loadingComponent,
|
|
1645
|
+
fallbackComponent,
|
|
1646
|
+
scopes,
|
|
1647
|
+
onAuthRequired
|
|
1648
|
+
}) {
|
|
1649
|
+
const { isAuthenticated, inProgress, loginRedirect } = useMsalAuth();
|
|
1650
|
+
(0, import_react9.useEffect)(() => {
|
|
1651
|
+
if (!isAuthenticated && !inProgress) {
|
|
1652
|
+
onAuthRequired?.();
|
|
1653
|
+
const login = async () => {
|
|
1654
|
+
try {
|
|
1655
|
+
await loginRedirect(scopes);
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
console.error("[AuthGuard] Authentication failed:", error);
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
login();
|
|
1661
|
+
}
|
|
1662
|
+
}, [isAuthenticated, inProgress, scopes, loginRedirect, onAuthRequired]);
|
|
1663
|
+
if (inProgress) {
|
|
1664
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: loadingComponent || /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: "Authenticating..." }) });
|
|
1665
|
+
}
|
|
1666
|
+
if (!isAuthenticated) {
|
|
1667
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: fallbackComponent || /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: "Redirecting to login..." }) });
|
|
1668
|
+
}
|
|
1669
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/components/ErrorBoundary.tsx
|
|
1673
|
+
var import_react10 = require("react");
|
|
1674
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1675
|
+
var ErrorBoundary = class extends import_react10.Component {
|
|
1676
|
+
constructor(props) {
|
|
1677
|
+
super(props);
|
|
1678
|
+
this.reset = () => {
|
|
1679
|
+
this.setState({
|
|
1680
|
+
hasError: false,
|
|
1681
|
+
error: null
|
|
1682
|
+
});
|
|
1683
|
+
};
|
|
1684
|
+
this.state = {
|
|
1685
|
+
hasError: false,
|
|
1686
|
+
error: null
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
static getDerivedStateFromError(error) {
|
|
1690
|
+
return {
|
|
1691
|
+
hasError: true,
|
|
1692
|
+
error
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
componentDidCatch(error, errorInfo) {
|
|
1696
|
+
const { onError, debug } = this.props;
|
|
1697
|
+
if (debug) {
|
|
1698
|
+
console.error("[ErrorBoundary] Caught error:", error);
|
|
1699
|
+
console.error("[ErrorBoundary] Error info:", errorInfo);
|
|
1700
|
+
}
|
|
1701
|
+
onError?.(error, errorInfo);
|
|
1702
|
+
}
|
|
1703
|
+
render() {
|
|
1704
|
+
const { hasError, error } = this.state;
|
|
1705
|
+
const { children, fallback } = this.props;
|
|
1706
|
+
if (hasError && error) {
|
|
1707
|
+
if (fallback) {
|
|
1708
|
+
return fallback(error, this.reset);
|
|
1709
|
+
}
|
|
1710
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1711
|
+
"div",
|
|
1712
|
+
{
|
|
1713
|
+
style: {
|
|
1714
|
+
padding: "20px",
|
|
1715
|
+
margin: "20px",
|
|
1716
|
+
border: "1px solid #DC3545",
|
|
1717
|
+
borderRadius: "4px",
|
|
1718
|
+
backgroundColor: "#F8D7DA",
|
|
1719
|
+
color: "#721C24",
|
|
1720
|
+
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif'
|
|
1721
|
+
},
|
|
1722
|
+
children: [
|
|
1723
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: { margin: "0 0 10px 0", fontSize: "18px" }, children: "Authentication Error" }),
|
|
1724
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: { margin: "0 0 10px 0" }, children: error.message }),
|
|
1725
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1726
|
+
"button",
|
|
1727
|
+
{
|
|
1728
|
+
onClick: this.reset,
|
|
1729
|
+
style: {
|
|
1730
|
+
padding: "8px 16px",
|
|
1731
|
+
backgroundColor: "#DC3545",
|
|
1732
|
+
color: "#FFFFFF",
|
|
1733
|
+
border: "none",
|
|
1734
|
+
borderRadius: "4px",
|
|
1735
|
+
cursor: "pointer",
|
|
1736
|
+
fontSize: "14px",
|
|
1737
|
+
fontWeight: 600
|
|
1738
|
+
},
|
|
1739
|
+
children: "Try Again"
|
|
1740
|
+
}
|
|
1741
|
+
)
|
|
1742
|
+
]
|
|
1743
|
+
}
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
return children;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
// src/hooks/useRoles.ts
|
|
1751
|
+
var import_react11 = require("react");
|
|
1752
|
+
var rolesCache = /* @__PURE__ */ new Map();
|
|
1753
|
+
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
1754
|
+
var MAX_CACHE_SIZE2 = 100;
|
|
1755
|
+
function clearRolesCache(accountId) {
|
|
1756
|
+
if (accountId) {
|
|
1757
|
+
rolesCache.delete(accountId);
|
|
1758
|
+
} else {
|
|
1759
|
+
rolesCache.clear();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function enforceCacheLimit2() {
|
|
1763
|
+
if (rolesCache.size > MAX_CACHE_SIZE2) {
|
|
1764
|
+
const entries = Array.from(rolesCache.entries());
|
|
1765
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
1766
|
+
const toRemove = entries.slice(0, rolesCache.size - MAX_CACHE_SIZE2);
|
|
1767
|
+
toRemove.forEach(([key]) => rolesCache.delete(key));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function useRoles() {
|
|
1771
|
+
const { isAuthenticated, account } = useMsalAuth();
|
|
1772
|
+
const graph = useGraphApi();
|
|
1773
|
+
const [roles, setRoles] = (0, import_react11.useState)([]);
|
|
1774
|
+
const [groups, setGroups] = (0, import_react11.useState)([]);
|
|
1775
|
+
const [loading, setLoading] = (0, import_react11.useState)(false);
|
|
1776
|
+
const [error, setError] = (0, import_react11.useState)(null);
|
|
1777
|
+
const fetchRolesAndGroups = (0, import_react11.useCallback)(async () => {
|
|
1778
|
+
if (!isAuthenticated || !account) {
|
|
1779
|
+
setRoles([]);
|
|
1780
|
+
setGroups([]);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const cacheKey = account.homeAccountId;
|
|
1784
|
+
const cached = rolesCache.get(cacheKey);
|
|
1785
|
+
if (cached && Date.now() - cached.timestamp < CACHE_DURATION2) {
|
|
1786
|
+
setRoles(cached.roles);
|
|
1787
|
+
setGroups(cached.groups);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
setLoading(true);
|
|
1791
|
+
setError(null);
|
|
1792
|
+
try {
|
|
1793
|
+
const idTokenClaims = account.idTokenClaims;
|
|
1794
|
+
const tokenRoles = idTokenClaims?.roles || [];
|
|
1795
|
+
const groupsResponse = await graph.get("/me/memberOf", {
|
|
1796
|
+
scopes: ["User.Read", "Directory.Read.All"]
|
|
1797
|
+
});
|
|
1798
|
+
const userGroups = groupsResponse.value.map((group) => group.id);
|
|
1799
|
+
rolesCache.set(cacheKey, {
|
|
1800
|
+
roles: tokenRoles,
|
|
1801
|
+
groups: userGroups,
|
|
1802
|
+
timestamp: Date.now()
|
|
1803
|
+
});
|
|
1804
|
+
enforceCacheLimit2();
|
|
1805
|
+
setRoles(tokenRoles);
|
|
1806
|
+
setGroups(userGroups);
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
const error2 = err;
|
|
1809
|
+
const sanitizedMessage = sanitizeError(error2);
|
|
1810
|
+
const sanitizedError = new Error(sanitizedMessage);
|
|
1811
|
+
setError(sanitizedError);
|
|
1812
|
+
console.error("[Roles] Failed to fetch roles/groups:", sanitizedMessage);
|
|
1813
|
+
const idTokenClaims = account.idTokenClaims;
|
|
1814
|
+
const tokenRoles = idTokenClaims?.roles || [];
|
|
1815
|
+
setRoles(tokenRoles);
|
|
1816
|
+
} finally {
|
|
1817
|
+
setLoading(false);
|
|
1818
|
+
}
|
|
1819
|
+
}, [isAuthenticated, account, graph]);
|
|
1820
|
+
const hasRole = (0, import_react11.useCallback)(
|
|
1821
|
+
(role) => {
|
|
1822
|
+
return roles.includes(role);
|
|
1823
|
+
},
|
|
1824
|
+
[roles]
|
|
1825
|
+
);
|
|
1826
|
+
const hasGroup = (0, import_react11.useCallback)(
|
|
1827
|
+
(groupId) => {
|
|
1828
|
+
return groups.includes(groupId);
|
|
1829
|
+
},
|
|
1830
|
+
[groups]
|
|
1831
|
+
);
|
|
1832
|
+
const hasAnyRole = (0, import_react11.useCallback)(
|
|
1833
|
+
(checkRoles) => {
|
|
1834
|
+
return checkRoles.some((role) => roles.includes(role));
|
|
1835
|
+
},
|
|
1836
|
+
[roles]
|
|
1837
|
+
);
|
|
1838
|
+
const hasAllRoles = (0, import_react11.useCallback)(
|
|
1839
|
+
(checkRoles) => {
|
|
1840
|
+
return checkRoles.every((role) => roles.includes(role));
|
|
1841
|
+
},
|
|
1842
|
+
[roles]
|
|
1843
|
+
);
|
|
1844
|
+
(0, import_react11.useEffect)(() => {
|
|
1845
|
+
fetchRolesAndGroups();
|
|
1846
|
+
return () => {
|
|
1847
|
+
if (account) {
|
|
1848
|
+
clearRolesCache(account.homeAccountId);
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
}, [fetchRolesAndGroups, account]);
|
|
1852
|
+
return {
|
|
1853
|
+
roles,
|
|
1854
|
+
groups,
|
|
1855
|
+
loading,
|
|
1856
|
+
error,
|
|
1857
|
+
hasRole,
|
|
1858
|
+
hasGroup,
|
|
1859
|
+
hasAnyRole,
|
|
1860
|
+
hasAllRoles,
|
|
1861
|
+
refetch: fetchRolesAndGroups
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/utils/withAuth.tsx
|
|
1866
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1867
|
+
function withAuth(Component2, options = {}) {
|
|
1868
|
+
const { displayName, ...guardProps } = options;
|
|
1869
|
+
const WrappedComponent = (props) => {
|
|
1870
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AuthGuard, { ...guardProps, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Component2, { ...props }) });
|
|
1871
|
+
};
|
|
1872
|
+
WrappedComponent.displayName = displayName || `withAuth(${Component2.displayName || Component2.name || "Component"})`;
|
|
1873
|
+
return WrappedComponent;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/utils/tokenRetry.ts
|
|
1877
|
+
async function retryWithBackoff(fn, config = {}) {
|
|
1878
|
+
const {
|
|
1879
|
+
maxRetries = 3,
|
|
1880
|
+
initialDelay = 1e3,
|
|
1881
|
+
maxDelay = 1e4,
|
|
1882
|
+
backoffMultiplier = 2,
|
|
1883
|
+
debug = false
|
|
1884
|
+
} = config;
|
|
1885
|
+
let lastError;
|
|
1886
|
+
let delay = initialDelay;
|
|
1887
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1888
|
+
try {
|
|
1889
|
+
if (debug && attempt > 0) {
|
|
1890
|
+
console.log(`[TokenRetry] Attempt ${attempt + 1}/${maxRetries + 1}`);
|
|
1891
|
+
}
|
|
1892
|
+
return await fn();
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
lastError = error;
|
|
1895
|
+
if (attempt === maxRetries) {
|
|
1896
|
+
if (debug) {
|
|
1897
|
+
console.error("[TokenRetry] All retry attempts failed");
|
|
1898
|
+
}
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
if (!isRetryableError(error)) {
|
|
1902
|
+
if (debug) {
|
|
1903
|
+
console.log("[TokenRetry] Non-retryable error, aborting");
|
|
1904
|
+
}
|
|
1905
|
+
throw error;
|
|
1906
|
+
}
|
|
1907
|
+
if (debug) {
|
|
1908
|
+
console.warn(`[TokenRetry] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
1909
|
+
}
|
|
1910
|
+
await sleep(delay);
|
|
1911
|
+
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
throw lastError;
|
|
1915
|
+
}
|
|
1916
|
+
function isRetryableError(error) {
|
|
1917
|
+
const message = error.message.toLowerCase();
|
|
1918
|
+
if (message.includes("network") || message.includes("timeout") || message.includes("fetch") || message.includes("connection")) {
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
if (message.includes("500") || message.includes("502") || message.includes("503")) {
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
if (message.includes("429") || message.includes("rate limit")) {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
if (message.includes("token") && message.includes("expired")) {
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
function sleep(ms) {
|
|
1933
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1934
|
+
}
|
|
1935
|
+
function createRetryWrapper(fn, config = {}) {
|
|
1936
|
+
return (...args) => {
|
|
1937
|
+
return retryWithBackoff(() => fn(...args), config);
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/utils/debugLogger.ts
|
|
1942
|
+
var DebugLogger = class {
|
|
1943
|
+
constructor(config = {}) {
|
|
1944
|
+
this.logHistory = [];
|
|
1945
|
+
this.performanceTimings = /* @__PURE__ */ new Map();
|
|
1946
|
+
this.config = {
|
|
1947
|
+
enabled: config.enabled ?? false,
|
|
1948
|
+
prefix: config.prefix ?? "[MSAL-Next]",
|
|
1949
|
+
showTimestamp: config.showTimestamp ?? true,
|
|
1950
|
+
level: config.level ?? "info",
|
|
1951
|
+
enablePerformance: config.enablePerformance ?? false,
|
|
1952
|
+
enableNetworkLogs: config.enableNetworkLogs ?? false,
|
|
1953
|
+
maxHistorySize: config.maxHistorySize ?? 100
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
shouldLog(level) {
|
|
1957
|
+
if (!this.config.enabled) return false;
|
|
1958
|
+
const levels = ["error", "warn", "info", "debug"];
|
|
1959
|
+
const currentLevelIndex = levels.indexOf(this.config.level);
|
|
1960
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
1961
|
+
return messageLevelIndex <= currentLevelIndex;
|
|
1962
|
+
}
|
|
1963
|
+
formatMessage(level, message, data) {
|
|
1964
|
+
const timestamp = this.config.showTimestamp ? `[${(/* @__PURE__ */ new Date()).toISOString()}]` : "";
|
|
1965
|
+
const prefix = this.config.prefix;
|
|
1966
|
+
const levelStr = `[${level.toUpperCase()}]`;
|
|
1967
|
+
let formatted = `${timestamp} ${prefix} ${levelStr} ${message}`;
|
|
1968
|
+
if (data !== void 0) {
|
|
1969
|
+
formatted += "\n" + JSON.stringify(data, null, 2);
|
|
1970
|
+
}
|
|
1971
|
+
return formatted;
|
|
1972
|
+
}
|
|
1973
|
+
addToHistory(level, message, data) {
|
|
1974
|
+
if (this.logHistory.length >= this.config.maxHistorySize) {
|
|
1975
|
+
this.logHistory.shift();
|
|
1976
|
+
}
|
|
1977
|
+
this.logHistory.push({
|
|
1978
|
+
timestamp: Date.now(),
|
|
1979
|
+
level,
|
|
1980
|
+
message,
|
|
1981
|
+
data
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
error(message, data) {
|
|
1985
|
+
if (this.shouldLog("error")) {
|
|
1986
|
+
console.error(this.formatMessage("error", message, data));
|
|
1987
|
+
this.addToHistory("error", message, data);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
warn(message, data) {
|
|
1991
|
+
if (this.shouldLog("warn")) {
|
|
1992
|
+
console.warn(this.formatMessage("warn", message, data));
|
|
1993
|
+
this.addToHistory("warn", message, data);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
info(message, data) {
|
|
1997
|
+
if (this.shouldLog("info")) {
|
|
1998
|
+
console.info(this.formatMessage("info", message, data));
|
|
1999
|
+
this.addToHistory("info", message, data);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
debug(message, data) {
|
|
2003
|
+
if (this.shouldLog("debug")) {
|
|
2004
|
+
console.debug(this.formatMessage("debug", message, data));
|
|
2005
|
+
this.addToHistory("debug", message, data);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
group(label) {
|
|
2009
|
+
if (this.config.enabled) {
|
|
2010
|
+
console.group(`${this.config.prefix} ${label}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
groupEnd() {
|
|
2014
|
+
if (this.config.enabled) {
|
|
2015
|
+
console.groupEnd();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Start performance timing for an operation
|
|
2020
|
+
*/
|
|
2021
|
+
startTiming(operation) {
|
|
2022
|
+
if (this.config.enablePerformance) {
|
|
2023
|
+
this.performanceTimings.set(operation, {
|
|
2024
|
+
operation,
|
|
2025
|
+
startTime: performance.now()
|
|
2026
|
+
});
|
|
2027
|
+
this.debug(`\u23F1\uFE0F Started: ${operation}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* End performance timing for an operation
|
|
2032
|
+
*/
|
|
2033
|
+
endTiming(operation) {
|
|
2034
|
+
if (this.config.enablePerformance) {
|
|
2035
|
+
const timing = this.performanceTimings.get(operation);
|
|
2036
|
+
if (timing) {
|
|
2037
|
+
timing.endTime = performance.now();
|
|
2038
|
+
timing.duration = timing.endTime - timing.startTime;
|
|
2039
|
+
this.info(`\u23F1\uFE0F Completed: ${operation} (${timing.duration.toFixed(2)}ms)`);
|
|
2040
|
+
return timing.duration;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return void 0;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Log network request
|
|
2047
|
+
*/
|
|
2048
|
+
logRequest(method, url, options) {
|
|
2049
|
+
if (this.config.enableNetworkLogs) {
|
|
2050
|
+
this.debug(`\u{1F310} ${method} ${url}`, options);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Log network response
|
|
2055
|
+
*/
|
|
2056
|
+
logResponse(method, url, status, data) {
|
|
2057
|
+
if (this.config.enableNetworkLogs) {
|
|
2058
|
+
const statusEmoji = status >= 200 && status < 300 ? "\u2705" : "\u274C";
|
|
2059
|
+
this.debug(`${statusEmoji} ${method} ${url} - ${status}`, data);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Get log history
|
|
2064
|
+
*/
|
|
2065
|
+
getHistory() {
|
|
2066
|
+
return [...this.logHistory];
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Get performance timings
|
|
2070
|
+
*/
|
|
2071
|
+
getPerformanceTimings() {
|
|
2072
|
+
return Array.from(this.performanceTimings.values());
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Clear log history
|
|
2076
|
+
*/
|
|
2077
|
+
clearHistory() {
|
|
2078
|
+
this.logHistory = [];
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Clear performance timings
|
|
2082
|
+
*/
|
|
2083
|
+
clearTimings() {
|
|
2084
|
+
this.performanceTimings.clear();
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Export logs as JSON
|
|
2088
|
+
*/
|
|
2089
|
+
exportLogs() {
|
|
2090
|
+
return JSON.stringify({
|
|
2091
|
+
config: this.config,
|
|
2092
|
+
history: this.logHistory,
|
|
2093
|
+
performanceTimings: Array.from(this.performanceTimings.values()),
|
|
2094
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2095
|
+
}, null, 2);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Download logs as a file
|
|
2099
|
+
*/
|
|
2100
|
+
downloadLogs(filename = "msal-next-debug-logs.json") {
|
|
2101
|
+
if (typeof window === "undefined") return;
|
|
2102
|
+
const logs = this.exportLogs();
|
|
2103
|
+
const blob = new Blob([logs], { type: "application/json" });
|
|
2104
|
+
const url = URL.createObjectURL(blob);
|
|
2105
|
+
const a = document.createElement("a");
|
|
2106
|
+
a.href = url;
|
|
2107
|
+
a.download = filename;
|
|
2108
|
+
a.click();
|
|
2109
|
+
URL.revokeObjectURL(url);
|
|
2110
|
+
}
|
|
2111
|
+
setEnabled(enabled) {
|
|
2112
|
+
this.config.enabled = enabled;
|
|
2113
|
+
}
|
|
2114
|
+
setLevel(level) {
|
|
2115
|
+
if (level) {
|
|
2116
|
+
this.config.level = level;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
var globalLogger = null;
|
|
2121
|
+
function getDebugLogger(config) {
|
|
2122
|
+
if (!globalLogger) {
|
|
2123
|
+
globalLogger = new DebugLogger(config);
|
|
2124
|
+
} else if (config) {
|
|
2125
|
+
if (config.enabled !== void 0) {
|
|
2126
|
+
globalLogger.setEnabled(config.enabled);
|
|
2127
|
+
}
|
|
2128
|
+
if (config.level) {
|
|
2129
|
+
globalLogger.setLevel(config.level);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return globalLogger;
|
|
2133
|
+
}
|
|
2134
|
+
function createScopedLogger(scope, config) {
|
|
2135
|
+
return new DebugLogger({
|
|
2136
|
+
...config,
|
|
2137
|
+
prefix: `[MSAL-Next:${scope}]`
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/protection/ProtectedPage.tsx
|
|
2142
|
+
var import_react12 = require("react");
|
|
2143
|
+
var import_navigation = require("next/navigation");
|
|
2144
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
2145
|
+
function ProtectedPage({
|
|
2146
|
+
children,
|
|
2147
|
+
config,
|
|
2148
|
+
defaultRedirectTo = "/login",
|
|
2149
|
+
defaultLoading,
|
|
2150
|
+
defaultUnauthorized,
|
|
2151
|
+
debug = false
|
|
2152
|
+
}) {
|
|
2153
|
+
const router = (0, import_navigation.useRouter)();
|
|
2154
|
+
const { isAuthenticated, account, inProgress } = useMsalAuth();
|
|
2155
|
+
const [isValidating, setIsValidating] = (0, import_react12.useState)(true);
|
|
2156
|
+
const [isAuthorized, setIsAuthorized] = (0, import_react12.useState)(false);
|
|
2157
|
+
(0, import_react12.useEffect)(() => {
|
|
2158
|
+
async function checkAuth() {
|
|
2159
|
+
if (debug) {
|
|
2160
|
+
console.log("[ProtectedPage] Checking auth...", {
|
|
2161
|
+
isAuthenticated,
|
|
2162
|
+
inProgress,
|
|
2163
|
+
config
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
if (inProgress) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (!config.required) {
|
|
2170
|
+
setIsAuthorized(true);
|
|
2171
|
+
setIsValidating(false);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (!isAuthenticated || !account) {
|
|
2175
|
+
if (debug) {
|
|
2176
|
+
console.log("[ProtectedPage] Not authenticated, redirecting...");
|
|
2177
|
+
}
|
|
2178
|
+
const redirectPath = config.redirectTo || defaultRedirectTo;
|
|
2179
|
+
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
|
2180
|
+
router.push(`${redirectPath}?returnUrl=${returnUrl}`);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (config.roles && config.roles.length > 0) {
|
|
2184
|
+
const userRoles = account.idTokenClaims?.roles || [];
|
|
2185
|
+
const hasRequiredRole = config.roles.some((role) => userRoles.includes(role));
|
|
2186
|
+
if (!hasRequiredRole) {
|
|
2187
|
+
if (debug) {
|
|
2188
|
+
console.log("[ProtectedPage] Missing required role", {
|
|
2189
|
+
required: config.roles,
|
|
2190
|
+
user: userRoles
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
setIsAuthorized(false);
|
|
2194
|
+
setIsValidating(false);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (config.validate) {
|
|
2199
|
+
try {
|
|
2200
|
+
const isValid = await config.validate(account);
|
|
2201
|
+
if (!isValid) {
|
|
2202
|
+
if (debug) {
|
|
2203
|
+
console.log("[ProtectedPage] Custom validation failed");
|
|
2204
|
+
}
|
|
2205
|
+
setIsAuthorized(false);
|
|
2206
|
+
setIsValidating(false);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
console.error("[ProtectedPage] Validation error:", error);
|
|
2211
|
+
setIsAuthorized(false);
|
|
2212
|
+
setIsValidating(false);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (debug) {
|
|
2217
|
+
console.log("[ProtectedPage] Authorization successful");
|
|
2218
|
+
}
|
|
2219
|
+
setIsAuthorized(true);
|
|
2220
|
+
setIsValidating(false);
|
|
2221
|
+
}
|
|
2222
|
+
checkAuth();
|
|
2223
|
+
}, [isAuthenticated, account, inProgress, config, router, defaultRedirectTo, debug]);
|
|
2224
|
+
if (isValidating || inProgress) {
|
|
2225
|
+
if (config.loading) {
|
|
2226
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: config.loading });
|
|
2227
|
+
}
|
|
2228
|
+
if (defaultLoading) {
|
|
2229
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: defaultLoading });
|
|
2230
|
+
}
|
|
2231
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" }) });
|
|
2232
|
+
}
|
|
2233
|
+
if (!isAuthorized) {
|
|
2234
|
+
if (config.unauthorized) {
|
|
2235
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: config.unauthorized });
|
|
2236
|
+
}
|
|
2237
|
+
if (defaultUnauthorized) {
|
|
2238
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: defaultUnauthorized });
|
|
2239
|
+
}
|
|
2240
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "text-center", children: [
|
|
2241
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h1", { className: "text-2xl font-bold text-gray-900 mb-2", children: "Access Denied" }),
|
|
2242
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-gray-600", children: "You don't have permission to access this page." })
|
|
2243
|
+
] }) });
|
|
2244
|
+
}
|
|
2245
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children });
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// src/protection/withPageAuth.tsx
|
|
2249
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2250
|
+
function withPageAuth(Component2, authConfig, globalConfig) {
|
|
2251
|
+
const WrappedComponent = (props) => {
|
|
2252
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2253
|
+
ProtectedPage,
|
|
2254
|
+
{
|
|
2255
|
+
config: authConfig,
|
|
2256
|
+
defaultRedirectTo: globalConfig?.defaultRedirectTo,
|
|
2257
|
+
defaultLoading: globalConfig?.defaultLoading,
|
|
2258
|
+
defaultUnauthorized: globalConfig?.defaultUnauthorized,
|
|
2259
|
+
debug: globalConfig?.debug,
|
|
2260
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Component2, { ...props })
|
|
2261
|
+
}
|
|
2262
|
+
);
|
|
2263
|
+
};
|
|
2264
|
+
WrappedComponent.displayName = `withPageAuth(${Component2.displayName || Component2.name || "Component"})`;
|
|
2265
|
+
return WrappedComponent;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/middleware/createAuthMiddleware.ts
|
|
2269
|
+
var import_server = require("next/server");
|
|
2270
|
+
function createAuthMiddleware(config = {}) {
|
|
2271
|
+
const {
|
|
2272
|
+
protectedRoutes = [],
|
|
2273
|
+
publicOnlyRoutes = [],
|
|
2274
|
+
loginPath = "/login",
|
|
2275
|
+
redirectAfterLogin = "/",
|
|
2276
|
+
sessionCookie = "msal.account",
|
|
2277
|
+
isAuthenticated: customAuthCheck,
|
|
2278
|
+
debug = false
|
|
2279
|
+
} = config;
|
|
2280
|
+
return async function authMiddleware(request) {
|
|
2281
|
+
const { pathname } = request.nextUrl;
|
|
2282
|
+
if (debug) {
|
|
2283
|
+
console.log("[AuthMiddleware] Processing:", pathname);
|
|
2284
|
+
}
|
|
2285
|
+
let authenticated = false;
|
|
2286
|
+
if (customAuthCheck) {
|
|
2287
|
+
authenticated = await customAuthCheck(request);
|
|
2288
|
+
} else {
|
|
2289
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
2290
|
+
authenticated = !!sessionData?.value;
|
|
2291
|
+
}
|
|
2292
|
+
if (debug) {
|
|
2293
|
+
console.log("[AuthMiddleware] Authenticated:", authenticated);
|
|
2294
|
+
}
|
|
2295
|
+
const isProtectedRoute = protectedRoutes.some(
|
|
2296
|
+
(route) => pathname.startsWith(route)
|
|
2297
|
+
);
|
|
2298
|
+
const isPublicOnlyRoute = publicOnlyRoutes.some(
|
|
2299
|
+
(route) => pathname.startsWith(route)
|
|
2300
|
+
);
|
|
2301
|
+
if (isProtectedRoute && !authenticated) {
|
|
2302
|
+
if (debug) {
|
|
2303
|
+
console.log("[AuthMiddleware] Redirecting to login");
|
|
2304
|
+
}
|
|
2305
|
+
const url = request.nextUrl.clone();
|
|
2306
|
+
url.pathname = loginPath;
|
|
2307
|
+
url.searchParams.set("returnUrl", pathname);
|
|
2308
|
+
return import_server.NextResponse.redirect(url);
|
|
2309
|
+
}
|
|
2310
|
+
if (isPublicOnlyRoute && authenticated) {
|
|
2311
|
+
if (debug) {
|
|
2312
|
+
console.log("[AuthMiddleware] Redirecting to home");
|
|
2313
|
+
}
|
|
2314
|
+
const returnUrl = request.nextUrl.searchParams.get("returnUrl");
|
|
2315
|
+
const url = request.nextUrl.clone();
|
|
2316
|
+
url.pathname = returnUrl || redirectAfterLogin;
|
|
2317
|
+
url.searchParams.delete("returnUrl");
|
|
2318
|
+
return import_server.NextResponse.redirect(url);
|
|
2319
|
+
}
|
|
2320
|
+
const response = import_server.NextResponse.next();
|
|
2321
|
+
if (authenticated) {
|
|
2322
|
+
response.headers.set("x-msal-authenticated", "true");
|
|
2323
|
+
try {
|
|
2324
|
+
const sessionData = request.cookies.get(sessionCookie);
|
|
2325
|
+
if (sessionData?.value) {
|
|
2326
|
+
const account = safeJsonParse(sessionData.value, isValidAccountData);
|
|
2327
|
+
if (account?.username) {
|
|
2328
|
+
response.headers.set("x-msal-username", account.username);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
if (debug) {
|
|
2333
|
+
console.warn("[AuthMiddleware] Failed to parse session data");
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return response;
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/client.ts
|
|
2342
|
+
var import_msal_react3 = require("@azure/msal-react");
|
|
2343
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2344
|
+
0 && (module.exports = {
|
|
2345
|
+
AuthGuard,
|
|
2346
|
+
AuthStatus,
|
|
2347
|
+
ErrorBoundary,
|
|
2348
|
+
MSALProvider,
|
|
2349
|
+
MicrosoftSignInButton,
|
|
2350
|
+
MsalAuthProvider,
|
|
2351
|
+
MsalError,
|
|
2352
|
+
ProtectedPage,
|
|
2353
|
+
SignOutButton,
|
|
2354
|
+
UserAvatar,
|
|
2355
|
+
createAuthMiddleware,
|
|
2356
|
+
createMissingEnvVarError,
|
|
2357
|
+
createMsalConfig,
|
|
2358
|
+
createRetryWrapper,
|
|
2359
|
+
createScopedLogger,
|
|
2360
|
+
displayValidationResults,
|
|
2361
|
+
getDebugLogger,
|
|
2362
|
+
getMsalInstance,
|
|
2363
|
+
isValidAccountData,
|
|
2364
|
+
isValidRedirectUri,
|
|
2365
|
+
isValidScope,
|
|
2366
|
+
retryWithBackoff,
|
|
2367
|
+
safeJsonParse,
|
|
2368
|
+
sanitizeError,
|
|
2369
|
+
useAccount,
|
|
2370
|
+
useGraphApi,
|
|
2371
|
+
useIsAuthenticated,
|
|
2372
|
+
useMsal,
|
|
2373
|
+
useMsalAuth,
|
|
2374
|
+
useRoles,
|
|
2375
|
+
useTokenRefresh,
|
|
2376
|
+
useUserProfile,
|
|
2377
|
+
validateConfig,
|
|
2378
|
+
validateScopes,
|
|
2379
|
+
withAuth,
|
|
2380
|
+
withPageAuth,
|
|
2381
|
+
wrapMsalError
|
|
2382
|
+
});
|