@joouis/msal-react-utility
Version:
The utility package for @azure/msal-react.
269 lines (255 loc) • 7.88 kB
JavaScript
// 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
};