Update package layout
This commit is contained in:
77
lib/provider/token/TokenProvider.tsx
Normal file
77
lib/provider/token/TokenProvider.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useRef } from "react";
|
||||
import { defaultTokenData, fetchToken, parseToken, type TokenData } from "./TokenUtils";
|
||||
|
||||
export interface TokenState {
|
||||
getToken: () => Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
const initialState: TokenState = {
|
||||
getToken: () => new Promise(() => {})
|
||||
}
|
||||
|
||||
const TokenContext = createContext<TokenState>(initialState);
|
||||
|
||||
|
||||
export default function TokenProvider({
|
||||
apiUrl,
|
||||
children
|
||||
}: Readonly<{
|
||||
apiUrl: string;
|
||||
children: React.ReactNode;
|
||||
}>){
|
||||
const tokenRef = useRef<TokenData>(defaultTokenData);
|
||||
const refreshPromise = useRef<Promise<string | null | undefined>>(null);
|
||||
|
||||
const getToken = useCallback(async () => {
|
||||
if(refreshPromise.current){
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
const { accessToken, expires } = tokenRef.current;
|
||||
|
||||
const isExpired = Date.now() > (expires - 5000); //Give a 5 second buffer
|
||||
|
||||
if(!accessToken || isExpired){
|
||||
refreshPromise.current = (async () => {
|
||||
try {
|
||||
const rawToken = (await fetchToken(apiUrl)).token;
|
||||
const parsedToken = parseToken(rawToken);
|
||||
tokenRef.current = parsedToken;
|
||||
return rawToken;
|
||||
}
|
||||
catch(error){
|
||||
tokenRef.current = defaultTokenData;
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
refreshPromise.current = null;
|
||||
}
|
||||
})();
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}, [apiUrl]);
|
||||
|
||||
const value: TokenState = useMemo(() => ({
|
||||
getToken
|
||||
}), [getToken]);
|
||||
|
||||
return (
|
||||
<TokenContext.Provider value={value}>
|
||||
{children}
|
||||
</TokenContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useToken(){
|
||||
const context = useContext(TokenContext);
|
||||
|
||||
if(!context){
|
||||
throw new Error("useTOken must be called inside a TokenProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
52
lib/provider/token/TokenUtils.ts
Normal file
52
lib/provider/token/TokenUtils.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
interface TokenResponse {
|
||||
token: string | null | undefined;
|
||||
}
|
||||
|
||||
interface TokenBody {
|
||||
token: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface LoginBody {
|
||||
login: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TokenData {
|
||||
accessToken: string | null | undefined;
|
||||
issued: number;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export const defaultTokenData: TokenData = {
|
||||
accessToken: "",
|
||||
issued: 0,
|
||||
expires: 0
|
||||
}
|
||||
|
||||
export async function fetchToken(apiUrl: string){
|
||||
const res = await fetch(`${apiUrl}/refresh`);
|
||||
return await res.json() as TokenResponse;
|
||||
}
|
||||
|
||||
export function parseToken(rawToken: string | null | undefined): TokenData {
|
||||
if(!rawToken){
|
||||
return defaultTokenData;
|
||||
}
|
||||
|
||||
const payloads = rawToken.split(".");
|
||||
if(payloads.length !== 3){
|
||||
return defaultTokenData;
|
||||
}
|
||||
|
||||
const payload = payloads[1];
|
||||
|
||||
const tokenBody = JSON.parse(atob(payload)) as TokenBody;
|
||||
|
||||
return ({
|
||||
accessToken: rawToken,
|
||||
issued: tokenBody.iat * 1000,
|
||||
expires: tokenBody.exp * 1000
|
||||
});
|
||||
}
|
||||
1
lib/provider/token/index.ts
Normal file
1
lib/provider/token/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as TokenProvider, useToken } from "./TokenProvider";
|
||||
Reference in New Issue
Block a user