UNPKG

@joouis/msal-react-utility

Version:

The utility package for @azure/msal-react.

269 lines (255 loc) 7.88 kB
// src/hooks/useAutoSetActiveAccount.ts import React from "react"; import { useMsal } from "@azure/msal-react"; // src/globalConfig.ts var MSALConfigSingleton = class _MSALConfigSingleton { static instance; config = {}; constructor() { } static getInstance() { if (!_MSALConfigSingleton.instance) { _MSALConfigSingleton.instance = new _MSALConfigSingleton(); } return _MSALConfigSingleton.instance; } setDefaultSilentRequest(config) { this.config.defaultSilentRequest = config; } getDefaultSilentRequest() { return this.config.defaultSilentRequest; } setDefaultTenantId(tenantId) { this.config.defaultTenantId = tenantId; } getDefaultTenantId() { return this.config.defaultTenantId; } reset() { this.config = {}; } }; var msalConfig = MSALConfigSingleton.getInstance(); // src/hooks/useAutoSetActiveAccount.ts var useAutoSetActiveAccount = () => { const { instance, accounts } = useMsal(); React.useEffect(() => { const activeAccount = instance.getActiveAccount(); const defaultTenantId = msalConfig.getDefaultTenantId(); if (defaultTenantId && activeAccount?.tenantId !== defaultTenantId) { const validAccount = accounts.find((a) => a.tenantId === defaultTenantId); if (validAccount) { instance.setActiveAccount(validAccount); } } else if (!activeAccount && accounts[0]) { instance.setActiveAccount(accounts[0]); } }, [instance, accounts]); }; // src/hooks/useEventCallback.ts import { useCallback, useLayoutEffect, useRef } from "react"; function useEventCallback(handler) { const handlerRef = useRef(handler); useLayoutEffect(() => { handlerRef.current = handler; }); return useCallback((...args) => { const handle = handlerRef.current; return handle(...args); }, []); } // src/hooks/useFetchWithStatus.ts import React2 from "react"; // src/hooks/useFetchWithToken.ts import { useCallback as useCallback2 } from "react"; // src/hooks/useGetToken.ts import { useRef as useRef2, useEffect } from "react"; import { useMsal as useMsal2 } from "@azure/msal-react"; import { InteractionRequiredAuthError, BrowserAuthError, BrowserAuthErrorCodes, InteractionStatus } from "@azure/msal-browser"; // src/utilities/sleep.ts var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // src/utilities/parseJwtToken.ts var parseJwtToken = (token) => { if (!token) { throw new Error("Token is empty"); } const base64Url = token.split(".")[1]; if (!base64Url) { throw new Error("Invalid token format"); } const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const jsonPayload = decodeURIComponent( atob(base64).split("").map((c) => `%${("00" + c.charCodeAt(0).toString(16)).slice(-2)}`).join("") ); return JSON.parse(jsonPayload); }; // src/inteface.ts var TokenType = /* @__PURE__ */ ((TokenType2) => { TokenType2["id"] = "id"; TokenType2["access"] = "access"; return TokenType2; })(TokenType || {}); var RequestInProgressError = class extends Error { constructor() { super("Request is in progress!"); } }; // src/hooks/useGetToken.ts var useGetToken = (defaultRequestConfigs) => { useAutoSetActiveAccount(); const { instance, inProgress } = useMsal2(); const inProgressRef = useRef2(inProgress); useEffect(() => { inProgressRef.current = inProgress; }, [inProgress]); const getToken = useEventCallback(async (opts) => { const { tokenType = "access" /* access */, requestConfigs } = opts || {}; while (inProgressRef.current !== InteractionStatus.None) { await sleep(100); } const configs = { scopes: ["User.Read"], prompt: "select_account", ...msalConfig.getDefaultSilentRequest(), ...defaultRequestConfigs, ...requestConfigs }; try { const activeAccount = instance.getActiveAccount(); if (!activeAccount) { await instance.loginRedirect(); } const resp = await instance.acquireTokenSilent({ account: activeAccount, ...configs }); if (tokenType === "access") { return resp.accessToken; } if (!resp.idToken) { throw new Error("ID token is not available"); } const idTokenExp = parseJwtToken(resp.idToken).exp; if (idTokenExp && idTokenExp * 1e3 - Date.now() < 2 * 60 * 1e3) { return await getToken({ tokenType: "id" /* id */, requestConfigs: { ...configs, forceRefresh: true } }); } return resp.idToken; } catch (error) { console.error(`[getToken] ${error}`); if (error instanceof InteractionRequiredAuthError) { await instance.acquireTokenRedirect(configs); } else if (error instanceof BrowserAuthError && error.errorCode === BrowserAuthErrorCodes.interactionInProgress) { await sleep(12e4); } else { throw error; } } }); return getToken; }; // src/hooks/useFetchWithToken.ts var useFetchWithToken = (tokenRequestConfigs) => { const getToken = useGetToken(tokenRequestConfigs); return useCallback2( async (input, init, getTokenOpts) => { try { const token = await getToken(getTokenOpts); if (!token) { throw new Error("Failed to fetch token"); } return fetch(input, { ...init, headers: { Authorization: `Bearer ${token}`, // User can override token ...init?.headers } }); } catch (error) { console.error(`[useFetchWithToken] ${error}`); const { name, message } = error; return new Response(name, { status: 401, statusText: message }); } }, [getToken] ); }; // src/utilities/getResponseData.ts var getResponseData = async (response) => { let data; const contentType = response.headers.get("Content-Type") || ""; if (contentType.includes("application/json")) { data = await response.json(); } else if (contentType.includes("text/")) { data = await response.text(); } else if (contentType.includes("application/octet-stream")) { data = await response.arrayBuffer(); } else if (contentType.includes("application/xml") || contentType.includes("text/xml")) { data = await response.text(); } else if (contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("application/pdf")) { data = await response.blob(); } else { try { data = await response.json(); } catch (e) { data = await response.text(); } } return data; }; // src/hooks/useFetchWithStatus.ts var useFetchWithStatus = (input, init) => { const [isLoading, setIsLoading] = React2.useState(false); const isLoadingRef = React2.useRef(false); const fetchWithToken = useFetchWithToken(); const _fetch = useEventCallback(async (payload, getTokenOpts) => { if (isLoadingRef.current) { throw new RequestInProgressError(); } setIsLoading(true); isLoadingRef.current = true; const requestInit = !init && !payload ? void 0 : { ...init, ...payload }; try { const response = await fetchWithToken(input, requestInit, getTokenOpts); if (!response.ok) { throw new Error(`Failed to fetch data: ${response.statusText}`, { cause: response }); } const data = await getResponseData(response); return data; } catch (error) { throw error; } finally { setIsLoading(false); isLoadingRef.current = false; } }); return { isLoading, _fetch }; }; export { RequestInProgressError, TokenType, getResponseData, msalConfig, sleep, useAutoSetActiveAccount, useEventCallback, useFetchWithStatus, useFetchWithToken, useGetToken };