fix: proper token refreshing BEFORE sending req
This commit is contained in:
@ -4,65 +4,100 @@ import { useAuth } from "@/store/auth";
|
|||||||
import Axios, { type AxiosResponse } from "axios";
|
import Axios, { type AxiosResponse } from "axios";
|
||||||
import { refreshTokenApi } from "./refresh";
|
import { refreshTokenApi } from "./refresh";
|
||||||
|
|
||||||
|
import { isExpired } from "react-jwt";
|
||||||
|
|
||||||
export const axios = Axios.create({
|
export const axios = Axios.create({
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let isRefreshing = false;
|
||||||
|
let refreshQueue: ((token: string | null) => void)[] = [];
|
||||||
|
|
||||||
|
const waitForTokenRefresh = () => {
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
refreshQueue.push((token: string | null) => resolve(token));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const processRefreshQueue = async (token: string | null) => {
|
||||||
|
refreshQueue.forEach((resolve) => resolve(token));
|
||||||
|
refreshQueue = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshToken = async (
|
||||||
|
accountId: string,
|
||||||
|
refreshToken: string
|
||||||
|
): Promise<{ access: string; refresh: string }> => {
|
||||||
|
const db = useDbStore.getState().db;
|
||||||
|
const loadAccounts = useAuth.getState().loadAccounts;
|
||||||
|
const requireSignIn = useAuth.getState().requireSignIn;
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
console.log("No database connection available.");
|
||||||
|
return Promise.reject("No database connection available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await refreshTokenApi(refreshToken);
|
||||||
|
|
||||||
|
await updateAccountTokens(db, {
|
||||||
|
accountId: accountId,
|
||||||
|
access: response.access,
|
||||||
|
refresh: response.refresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
processRefreshQueue(response.access);
|
||||||
|
|
||||||
|
return { access: response.access, refresh: response.refresh };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Token refresh failed:", err);
|
||||||
|
await deleteAccount(db, accountId);
|
||||||
|
requireSignIn?.();
|
||||||
|
processRefreshQueue(null);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
localStorage.removeItem("refreshing");
|
||||||
|
loadAccounts?.();
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
(request) => {
|
async (request) => {
|
||||||
const account = useAuth.getState().activeAccount;
|
const account = useAuth.getState().activeAccount;
|
||||||
if (account?.access) {
|
let token: string | null = account?.access ?? null;
|
||||||
request.headers["Authorization"] = `Bearer ${account.access}`;
|
|
||||||
|
if (!token || !isExpired(token)) {
|
||||||
|
request.headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
try {
|
||||||
|
const { access } = await refreshToken(
|
||||||
|
account!.accountId,
|
||||||
|
account!.refresh
|
||||||
|
);
|
||||||
|
token = access;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Token refresh failed:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token = await waitForTokenRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("No token available");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.headers["Authorization"] = `Bearer ${token}`;
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => Promise.reject(error)
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error) => {
|
|
||||||
const originalRequest = error.config;
|
|
||||||
|
|
||||||
if (error.response.status === 401 && !originalRequest._retry) {
|
|
||||||
originalRequest._retry = true;
|
|
||||||
|
|
||||||
const account = useAuth.getState().activeAccount;
|
|
||||||
if (!account?.refresh) {
|
|
||||||
return Promise.reject(new Error("Unauthorized. No refresh token"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDbStore.getState().db;
|
|
||||||
if (!db) {
|
|
||||||
return Promise.reject(new Error("No database connection"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await refreshTokenApi(account.refresh);
|
|
||||||
|
|
||||||
updateAccountTokens(db, {
|
|
||||||
accountId: account.accountId,
|
|
||||||
access: response.access,
|
|
||||||
refresh: response.refresh,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("token refresh failed:", err);
|
|
||||||
await deleteAccount(db, account.accountId);
|
|
||||||
const loadAccounts = useAuth.getState().loadAccounts;
|
|
||||||
loadAccounts?.();
|
|
||||||
const requireSignIn = useAuth.getState().requireSignIn;
|
|
||||||
requireSignIn?.();
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const handleApiError = async (response: AxiosResponse) => {
|
export const handleApiError = async (response: AxiosResponse) => {
|
||||||
|
Reference in New Issue
Block a user