From 50c0375be488a0c202ea2cc0272ee449e5735baf Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 11 Dec 2025 19:07:07 -0500 Subject: [PATCH] update frontend --- src/auth/AuthContext.jsx | 293 +++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 133 deletions(-) diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index a8a5836..552007b 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -1,212 +1,239 @@ -import React, { - createContext, - useContext, - useEffect, - useState, -} from "react"; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; const AuthContext = createContext(null); -/* ===== Keycloak config ===== */ +// 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"; +// --- helpers --- +function decodeJwtPayload(token) { + try { + const parts = token.split("."); + if (parts.length < 2) return null; + const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json); + } catch { + return null; + } +} + +function willExpireSoon(token, withinSeconds = 90) { + const p = decodeJwtPayload(token); + if (!p || !p.exp) return true; + const now = Math.floor(Date.now() / 1000); + return p.exp - now <= withinSeconds; +} + +async function refreshWithKeycloak(refreshToken) { + const params = new URLSearchParams(); + params.set("grant_type", "refresh_token"); + params.set("client_id", KC_CLIENT_ID); + params.set("refresh_token", refreshToken); + + 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(), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = text; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || text; + } catch {} + throw new Error(`${res.status} ${msg}`); + } + + const data = JSON.parse(text); + if (!data.access_token) throw new Error("No access_token in refresh response"); + return { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, // parfois KC renvoie un nouveau refresh + }; +} + export function AuthProvider({ children }) { - const [loading, setLoading] = useState(true); + const [booting, setBooting] = useState(true); // UX: restore session + const [loading, setLoading] = useState(false); // login in progress const [authenticated, setAuthenticated] = useState(false); const [username, setUsername] = useState(""); const [token, setToken] = useState(null); const [lastError, setLastError] = useState(""); - /* ============================== - 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); + const refreshRef = useRef(null); + const usernameRef = useRef(""); + // persist helper + function persistAuth({ accessToken, refreshToken, user }) { + setToken(accessToken); + setAuthenticated(true); + setUsername(user); + usernameRef.current = user; try { - 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) - ); + if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem(USERNAME_KEY, user); + } catch {} + } + function clearAuth(msg = "") { + setAuthenticated(false); + setToken(null); + setUsername(""); + usernameRef.current = ""; + setLastError(msg); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch {} + } + + async function trySilentRefreshIfNeeded() { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + + if (!storedToken || !storedRefresh) return false; + + // si token encore bon, on le charge tel quel + if (!willExpireSoon(storedToken, 30)) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + return true; + } + + // sinon refresh + const { accessToken, refreshToken } = await refreshWithKeycloak(storedRefresh); + persistAuth({ accessToken, refreshToken, user: storedUser }); return true; } catch (e) { - console.error("[AUTH] refresh error", e); + console.warn("[AUTH] silent refresh failed:", e); + clearAuth("Session expired. Please login again."); return false; } } - /* ============================== - INIT AUTH (APP LOAD) - ============================== */ + // Boot: restore session 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) { + // si on a un token/refresh en storage => restore/refresh + const ok = await trySilentRefreshIfNeeded(); + if (!ok) { + // fallback: si token sans refresh, on garde “loggé” mais ça expirera vite + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); setUsername(storedUser); - return; + usernameRef.current = storedUser; + setAuthenticated(true); } } - - if (storedToken) { - setToken(storedToken); - setUsername(storedUser); - setAuthenticated(true); - } - } catch (e) { - console.warn("[AUTH] init error", e); } finally { - setLoading(false); + setBooting(false); } })(); }, []); - /* ============================== - BACKGROUND REFRESH - ============================== */ + // Auto-refresh loop (silencieux) useEffect(() => { if (!authenticated) return; - const t = setInterval(async () => { + const interval = setInterval(async () => { try { - const refresh = localStorage.getItem(REFRESH_KEY); - const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0"); + const t = localStorage.getItem(TOKEN_KEY); + const rt = localStorage.getItem(REFRESH_KEY); + if (!t || !rt) return; - if (!refresh) return; - if (Date.now() > expiresAt - 60_000) { - await refreshWithToken(refresh); - } - } catch { - /* ignore */ + if (!willExpireSoon(t, 90)) return; + + const { accessToken, refreshToken } = await refreshWithKeycloak(rt); + const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; + persistAuth({ accessToken, refreshToken, user: u }); + } catch (e) { + console.warn("[AUTH] auto refresh failed:", e); + clearAuth("Session expired. Please login again."); } - }, 30_000); + }, 60_000); - return () => clearInterval(t); + return () => clearInterval(interval); }, [authenticated]); - /* ============================== - LOGIN - ============================== */ + // Login via password grant 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", user); + params.set("username", u); params.set("password", pass); try { - 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(), - } - ); + setLoading(true); + 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(), + }); + + const text = await res.text(); if (!res.ok) { - setLastError("Invalid credentials"); + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || msg; + } catch {} + clearAuth(`${res.status} ${msg}`); return; } - const data = await res.json(); + const data = JSON.parse(text); const accessToken = data.access_token; const refreshToken = data.refresh_token; - const expiresIn = Number(data.expires_in || 0); - if (!accessToken || !refreshToken) { - setLastError("Invalid token response"); + if (!accessToken) { + clearAuth("No access token in response."); return; } - setToken(accessToken); - setAuthenticated(true); - setUsername(user); - - 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); + persistAuth({ accessToken, refreshToken, user: u }); + setLastError(""); } catch (e) { - console.error("[AUTH] login error", e); - setLastError("Network error"); + console.error("[AUTH] network error", e); + clearAuth("Network error during login."); + } finally { + setLoading(false); } } - /* ============================== - LOGOUT - ============================== */ function logout() { - setAuthenticated(false); - setToken(null); - setUsername(""); - setLastError(""); - - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(REFRESH_KEY); - localStorage.removeItem(EXPIRES_AT_KEY); - localStorage.removeItem(USERNAME_KEY); + clearAuth(""); } return ( "); return ctx; } \ No newline at end of file