diff --git a/src/pages/appeal.tsx b/src/pages/appeal.tsx index 08d3d95..329c67c 100644 --- a/src/pages/appeal.tsx +++ b/src/pages/appeal.tsx @@ -31,8 +31,16 @@ const AppealPage: React.FC = () => { // Use useRef to store the interval ID so we can clear it const refreshIntervalRef = useRef(null); + // Use ref to prevent concurrent token refreshes + const isRefreshingRef = useRef(false); + const refreshPromiseRef = useRef | null>(null); async function refreshTokenIfNeeded() { + // If already refreshing, wait for that promise to complete + if (isRefreshingRef.current && refreshPromiseRef.current) { + return refreshPromiseRef.current; + } + const savedToken = sessionStorage.getItem("kc_token"); const refreshToken = sessionStorage.getItem("kc_refresh_token"); @@ -46,34 +54,45 @@ const AppealPage: React.FC = () => { // If token expires within 60 seconds, refresh it if (expiresAt - now < 60000) { if (refreshToken) { - const refreshRes = await fetch("/api/v2/keycloak/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - if (refreshRes.ok) { - const newTokens = await refreshRes.json(); - sessionStorage.setItem("kc_token", newTokens.access_token); - - if (newTokens.refresh_token) { - sessionStorage.setItem( - "kc_refresh_token", - newTokens.refresh_token, - ); - } - - // Set ID token for post logout - if (newTokens.id_token) { - sessionStorage.setItem("kc_id_token", newTokens.id_token); + // Set flag and create promise to prevent concurrent refreshes + isRefreshingRef.current = true; + refreshPromiseRef.current = (async () => { + try { + const refreshRes = await fetch("/api/v2/keycloak/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + if (refreshRes.ok) { + const newTokens = await refreshRes.json(); + sessionStorage.setItem("kc_token", newTokens.access_token); + + if (newTokens.refresh_token) { + sessionStorage.setItem( + "kc_refresh_token", + newTokens.refresh_token, + ); + } + + // Set ID token for post logout + if (newTokens.id_token) { + sessionStorage.setItem("kc_id_token", newTokens.id_token); + } + + window.dispatchEvent(new Event("tokensUpdated")); + return newTokens.access_token; + } + sessionStorage.removeItem("kc_token"); + sessionStorage.removeItem("kc_refresh_token"); + return null; + } finally { + isRefreshingRef.current = false; + refreshPromiseRef.current = null; } + })(); - window.dispatchEvent(new Event("tokensUpdated")); - return newTokens.access_token; - } - sessionStorage.removeItem("kc_token"); - sessionStorage.removeItem("kc_refresh_token"); - return null; + return await refreshPromiseRef.current; } sessionStorage.removeItem("kc_token"); return null; @@ -176,9 +195,12 @@ const AppealPage: React.FC = () => { async function checkBan() { try { + // Get auth headers once and reuse them for all requests in this function + const headers = await getAuthHeaders(); + // Check ban status (no Discord ID needed - comes from token) const banRes = await fetch("/api/v2/users/banned", { - headers: await getAuthHeaders(), + headers, }); if (!banRes.ok) { @@ -187,32 +209,25 @@ const AppealPage: React.FC = () => { const currentBanStatus = await banRes.json(); - if (!currentBanStatus.banned) { - setBanStatus("not_banned"); - - // Even if not banned, check for appeal history - const appealsRes = await fetch("/api/v2/appeals/latest", { - headers: await getAuthHeaders(), - }); - - if (appealsRes.ok) { - const appeal = await appealsRes.json(); - setLatestAppeal(appeal); - } - - setLoading(false); - return; - } - - // If banned, check for existing appeals + // Check for appeal history (works for both banned and not banned users) const appealsRes = await fetch("/api/v2/appeals/latest", { - headers: await getAuthHeaders(), + headers, }); + let appeal = null; if (appealsRes.ok) { - const appeal = await appealsRes.json(); + appeal = await appealsRes.json(); setLatestAppeal(appeal); + } + + if (!currentBanStatus.banned) { + setBanStatus("not_banned"); + setLoading(false); + return; + } + // User is banned - determine appeal status + if (appealsRes.ok && appeal) { // Check if this appeal is for the current ban if (appeal.status === "ACCEPTED") { // Previous appeal was accepted, they can make a new appeal for current ban