diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index 86c8c49..a8a5836 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -7,12 +7,15 @@ import React, { const AuthContext = createContext(null); -// Config Keycloak pour le login inline +/* ===== Keycloak config ===== */ const KC_BASE = "https://web.sociowire.com:8443"; const KC_REALM = "sociowire"; const KC_CLIENT_ID = "sociowire-frontend"; +/* ===== Storage keys ===== */ const TOKEN_KEY = "sociowire:access_token"; +const REFRESH_KEY = "sociowire:refresh_token"; +const EXPIRES_AT_KEY = "sociowire:expires_at"; const USERNAME_KEY = "sociowire:username"; export function AuthProvider({ children }) { @@ -22,38 +25,126 @@ export function AuthProvider({ children }) { const [token, setToken] = useState(null); const [lastError, setLastError] = useState(""); - // Charge token/username depuis localStorage au démarrage - useEffect(() => { + /* ============================== + REFRESH TOKEN (helper) + ============================== */ + async function refreshWithToken(refreshToken) { + const params = new URLSearchParams(); + params.set("grant_type", "refresh_token"); + params.set("client_id", KC_CLIENT_ID); + params.set("refresh_token", refreshToken); + try { - const storedToken = localStorage.getItem(TOKEN_KEY); - const storedUser = localStorage.getItem(USERNAME_KEY) || ""; - if (storedToken) { - setToken(storedToken); - setUsername(storedUser); - setAuthenticated(true); - } else { - setAuthenticated(false); + const res = await fetch( + `${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + } + ); + + if (!res.ok) { + console.error("[AUTH] refresh failed", res.status); + logout(); + return false; } + + const data = await res.json(); + const accessToken = data.access_token; + const newRefresh = data.refresh_token || refreshToken; + const expiresIn = Number(data.expires_in || 0); + + if (!accessToken) { + logout(); + return false; + } + + setToken(accessToken); + setAuthenticated(true); + + localStorage.setItem(TOKEN_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, newRefresh); + localStorage.setItem( + EXPIRES_AT_KEY, + String(Date.now() + expiresIn * 1000) + ); + + return true; } catch (e) { - console.warn("[AUTH] localStorage error", e); - } finally { - setLoading(false); + console.error("[AUTH] refresh error", e); + return false; } + } + + /* ============================== + INIT AUTH (APP LOAD) + ============================== */ + useEffect(() => { + (async () => { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0"); + + const now = Date.now(); + const needsRefresh = !storedToken || now > expiresAt - 60_000; + + if (storedRefresh && needsRefresh) { + const ok = await refreshWithToken(storedRefresh); + if (ok) { + setUsername(storedUser); + return; + } + } + + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + setAuthenticated(true); + } + } catch (e) { + console.warn("[AUTH] init error", e); + } finally { + setLoading(false); + } + })(); }, []); - // Login via password grant sur Keycloak + /* ============================== + BACKGROUND REFRESH + ============================== */ + useEffect(() => { + if (!authenticated) return; + + const t = setInterval(async () => { + try { + const refresh = localStorage.getItem(REFRESH_KEY); + const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0"); + + if (!refresh) return; + if (Date.now() > expiresAt - 60_000) { + await refreshWithToken(refresh); + } + } catch { + /* ignore */ + } + }, 30_000); + + return () => clearInterval(t); + }, [authenticated]); + + /* ============================== + LOGIN + ============================== */ async function login(user, pass) { setLastError(""); - const u = (user || "").trim(); - if (!u || !pass) { - setLastError("Username and password required."); - return; - } const params = new URLSearchParams(); params.set("grant_type", "password"); params.set("client_id", KC_CLIENT_ID); - params.set("username", u); + params.set("username", user); params.set("password", pass); try { @@ -61,89 +152,56 @@ export function AuthProvider({ children }) { `${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString(), } ); if (!res.ok) { - const text = await res.text(); - console.error("[AUTH] login failed", res.status, text); - - let msg = "Invalid credentials or auth error."; - try { - const j = JSON.parse(text); - if (j.error_description) { - msg = j.error_description; - } else if (j.error) { - msg = j.error; - } - } catch (e) { - // ignore parse error - } - - setLastError(`${res.status} ${msg}`); - setAuthenticated(false); - setToken(null); - setUsername(""); - try { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USERNAME_KEY); - } catch (e) { - // ignore - } + setLastError("Invalid credentials"); return; } const data = await res.json(); const accessToken = data.access_token; - if (!accessToken) { - setLastError("No access token in response."); - setAuthenticated(false); - setToken(null); - setUsername(""); + const refreshToken = data.refresh_token; + const expiresIn = Number(data.expires_in || 0); + + if (!accessToken || !refreshToken) { + setLastError("Invalid token response"); return; } setToken(accessToken); setAuthenticated(true); - setUsername(u); - setLastError(""); + setUsername(user); - try { - localStorage.setItem(TOKEN_KEY, accessToken); - localStorage.setItem(USERNAME_KEY, u); - } catch (e) { - console.warn("[AUTH] failed to persist token", e); - } + localStorage.setItem(TOKEN_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem( + EXPIRES_AT_KEY, + String(Date.now() + expiresIn * 1000) + ); + localStorage.setItem(USERNAME_KEY, user); } catch (e) { - console.error("[AUTH] network error", e); - setLastError("Network error during login."); - setAuthenticated(false); - setToken(null); - setUsername(""); - try { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USERNAME_KEY); - } catch (e2) { - // ignore - } + console.error("[AUTH] login error", e); + setLastError("Network error"); } } + /* ============================== + LOGOUT + ============================== */ function logout() { setAuthenticated(false); setToken(null); setUsername(""); setLastError(""); - try { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(USERNAME_KEY); - } catch (e) { - // ignore - } + + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(EXPIRES_AT_KEY); + localStorage.removeItem(USERNAME_KEY); } return ( @@ -165,8 +223,6 @@ export function AuthProvider({ children }) { export function useAuth() { const ctx = useContext(AuthContext); - if (!ctx) { - throw new Error("useAuth must be used inside "); - } + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); return ctx; -} +} \ No newline at end of file