Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 6,669 Bytes
f6c5cad d543fc1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
const AuthContext = createContext(null);
const TOKEN_KEY = 'wss_token';
const REFRESH_KEY = 'wss_refresh_token';
// Parse the expiry time from a JWT without any library
function getTokenExpiry(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp ? payload.exp * 1000 : null; // convert to ms
} catch {
return null;
}
}
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem(TOKEN_KEY));
const [refreshToken, setRefreshToken] = useState(localStorage.getItem(REFRESH_KEY));
const [loading, setLoading] = useState(true);
const proactiveRefreshRef = useRef(null); // holds the setTimeout handle
// ββ Core logout β clear everything βββββββββββββββββββββββββββββββββββββββ
const doLogout = useCallback(() => {
if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
setToken(null);
setRefreshToken(null);
setUser(null);
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
}, []);
// ββ Refresh access token (uses body refresh_token + HttpOnly cookie fallback)
const refreshAccessToken = useCallback(async () => {
const storedRefresh = localStorage.getItem(REFRESH_KEY);
try {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // send HttpOnly cookie as fallback
body: JSON.stringify({ refresh_token: storedRefresh || '' }),
});
if (res.ok) {
const data = await res.json();
setToken(data.access_token);
localStorage.setItem(TOKEN_KEY, data.access_token);
if (data.refresh_token) {
setRefreshToken(data.refresh_token);
localStorage.setItem(REFRESH_KEY, data.refresh_token);
}
return data.access_token;
}
// Refresh failed β do NOT auto-logout. Just return null and let callers decide.
return null;
} catch {
return null;
}
}, []);
// ββ Schedule a proactive refresh 5 minutes before the access token expires ββ
const scheduleProactiveRefresh = useCallback((accessToken) => {
if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
const expiry = getTokenExpiry(accessToken);
if (!expiry) return;
const now = Date.now();
const refreshAt = expiry - 5 * 60 * 1000; // 5 min before expiry
const delay = Math.max(refreshAt - now, 10_000); // min 10s
proactiveRefreshRef.current = setTimeout(async () => {
const newToken = await refreshAccessToken();
if (newToken) {
scheduleProactiveRefresh(newToken); // reschedule for the new token
}
// If refresh failed but user is still browsing, keep them logged in
// until they actually hit a 401 (e.g. backend was briefly down)
}, delay);
}, [refreshAccessToken]);
// ββ Fetch user profile from backend βββββββββββββββββββββββββββββββββββββββ
const fetchProfile = useCallback(async (activeToken, retryCount = 0) => {
try {
const res = await fetch('/api/auth/profile', {
headers: { 'Authorization': `Bearer ${activeToken}` },
});
if (res.ok) {
const data = await res.json();
setUser(data.user);
setLoading(false);
scheduleProactiveRefresh(activeToken);
return;
}
if (res.status === 401) {
// Try to silently refresh before giving up
const newToken = await refreshAccessToken();
if (newToken) {
const retryRes = await fetch('/api/auth/profile', {
headers: { 'Authorization': `Bearer ${newToken}` },
});
if (retryRes.ok) {
const data = await retryRes.json();
setUser(data.user);
setLoading(false);
scheduleProactiveRefresh(newToken);
return;
}
}
// Only logout if both the access token AND refresh failed (401 Unauthorized)
doLogout();
setLoading(false);
return;
}
// If backend is offline (502, 504), retry max 2 times then finish loading
if (res.status >= 500 && retryCount < 2) {
setTimeout(() => fetchProfile(activeToken, retryCount + 1), 1500);
return;
}
setLoading(false);
} catch {
if (retryCount < 2) {
setTimeout(() => fetchProfile(activeToken, retryCount + 1), 1500);
return;
}
setLoading(false);
}
}, [refreshAccessToken, scheduleProactiveRefresh, doLogout]);
// ββ Sync token changes to localStorage & fetch profile ββββββββββββββββββββ
useEffect(() => {
if (token) {
localStorage.setItem(TOKEN_KEY, token);
fetchProfile(token);
} else {
localStorage.removeItem(TOKEN_KEY);
setUser(null);
setLoading(false);
}
}, [token]); // intentionally only re-run when token changes
// ββ Login β called after successful credentials check βββββββββββββββββββββ
const login = useCallback((newToken, newRefreshToken, userData) => {
if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
setToken(newToken);
setRefreshToken(newRefreshToken);
setUser(userData);
localStorage.setItem(TOKEN_KEY, newToken);
if (newRefreshToken) localStorage.setItem(REFRESH_KEY, newRefreshToken);
scheduleProactiveRefresh(newToken);
}, [scheduleProactiveRefresh]);
const logout = useCallback(() => {
doLogout();
}, [doLogout]);
const reloadUser = useCallback(() => {
if (token) {
fetchProfile(token);
}
}, [token, fetchProfile]);
// ββ Cleanup proactive refresh on unmount ββββββββββββββββββββββββββββββββββ
useEffect(() => {
return () => {
if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
};
}, []);
return (
<AuthContext.Provider value={{ user, token, refreshToken, loading, login, logout, refreshAccessToken, reloadUser }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
|