update frontend

This commit is contained in:
Your Name 2025-12-11 19:07:07 -05:00
parent ef5e361d40
commit 50c0375be4
1 changed files with 160 additions and 133 deletions

View File

@ -1,212 +1,239 @@
import React, { import React, { createContext, useContext, useEffect, useRef, useState } from "react";
createContext,
useContext,
useEffect,
useState,
} from "react";
const AuthContext = createContext(null); const AuthContext = createContext(null);
/* ===== Keycloak config ===== */ // Keycloak config
const KC_BASE = "https://web.sociowire.com:8443"; const KC_BASE = "https://web.sociowire.com:8443";
const KC_REALM = "sociowire"; const KC_REALM = "sociowire";
const KC_CLIENT_ID = "sociowire-frontend"; const KC_CLIENT_ID = "sociowire-frontend";
/* ===== Storage keys ===== */
const TOKEN_KEY = "sociowire:access_token"; const TOKEN_KEY = "sociowire:access_token";
const REFRESH_KEY = "sociowire:refresh_token"; const REFRESH_KEY = "sociowire:refresh_token";
const EXPIRES_AT_KEY = "sociowire:expires_at";
const USERNAME_KEY = "sociowire:username"; 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 }) { 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 [authenticated, setAuthenticated] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [token, setToken] = useState(null); const [token, setToken] = useState(null);
const [lastError, setLastError] = useState(""); const [lastError, setLastError] = useState("");
/* ============================== const refreshRef = useRef(null);
REFRESH TOKEN (helper) const usernameRef = useRef("");
============================== */
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);
// persist helper
function persistAuth({ accessToken, refreshToken, user }) {
setToken(accessToken);
setAuthenticated(true);
setUsername(user);
usernameRef.current = user;
try { 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(TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_KEY, newRefresh); if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken);
localStorage.setItem( localStorage.setItem(USERNAME_KEY, user);
EXPIRES_AT_KEY, } catch {}
String(Date.now() + expiresIn * 1000) }
);
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; return true;
} catch (e) { } catch (e) {
console.error("[AUTH] refresh error", e); console.warn("[AUTH] silent refresh failed:", e);
clearAuth("Session expired. Please login again.");
return false; return false;
} }
} }
/* ============================== // Boot: restore session
INIT AUTH (APP LOAD)
============================== */
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const storedToken = localStorage.getItem(TOKEN_KEY); // si on a un token/refresh en storage => restore/refresh
const storedRefresh = localStorage.getItem(REFRESH_KEY); const ok = await trySilentRefreshIfNeeded();
const storedUser = localStorage.getItem(USERNAME_KEY) || ""; if (!ok) {
const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0"); // fallback: si token sans refresh, on garde loggé mais ça expirera vite
const storedToken = localStorage.getItem(TOKEN_KEY);
const now = Date.now(); const storedUser = localStorage.getItem(USERNAME_KEY) || "";
const needsRefresh = !storedToken || now > expiresAt - 60_000; if (storedToken) {
setToken(storedToken);
if (storedRefresh && needsRefresh) {
const ok = await refreshWithToken(storedRefresh);
if (ok) {
setUsername(storedUser); 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 { } finally {
setLoading(false); setBooting(false);
} }
})(); })();
}, []); }, []);
/* ============================== // Auto-refresh loop (silencieux)
BACKGROUND REFRESH
============================== */
useEffect(() => { useEffect(() => {
if (!authenticated) return; if (!authenticated) return;
const t = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const refresh = localStorage.getItem(REFRESH_KEY); const t = localStorage.getItem(TOKEN_KEY);
const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0"); const rt = localStorage.getItem(REFRESH_KEY);
if (!t || !rt) return;
if (!refresh) return; if (!willExpireSoon(t, 90)) return;
if (Date.now() > expiresAt - 60_000) {
await refreshWithToken(refresh); const { accessToken, refreshToken } = await refreshWithKeycloak(rt);
} const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || "";
} catch { persistAuth({ accessToken, refreshToken, user: u });
/* ignore */ } 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]); }, [authenticated]);
/* ============================== // Login via password grant
LOGIN
============================== */
async function login(user, pass) { async function login(user, pass) {
setLastError(""); setLastError("");
const u = (user || "").trim();
if (!u || !pass) {
setLastError("Username and password required.");
return;
}
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("grant_type", "password"); params.set("grant_type", "password");
params.set("client_id", KC_CLIENT_ID); params.set("client_id", KC_CLIENT_ID);
params.set("username", user); params.set("username", u);
params.set("password", pass); params.set("password", pass);
try { try {
const res = await fetch( setLoading(true);
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
}
);
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) { 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; return;
} }
const data = await res.json(); const data = JSON.parse(text);
const accessToken = data.access_token; const accessToken = data.access_token;
const refreshToken = data.refresh_token; const refreshToken = data.refresh_token;
const expiresIn = Number(data.expires_in || 0);
if (!accessToken || !refreshToken) { if (!accessToken) {
setLastError("Invalid token response"); clearAuth("No access token in response.");
return; return;
} }
setToken(accessToken); persistAuth({ accessToken, refreshToken, user: u });
setAuthenticated(true); setLastError("");
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);
} catch (e) { } catch (e) {
console.error("[AUTH] login error", e); console.error("[AUTH] network error", e);
setLastError("Network error"); clearAuth("Network error during login.");
} finally {
setLoading(false);
} }
} }
/* ==============================
LOGOUT
============================== */
function logout() { function logout() {
setAuthenticated(false); clearAuth("");
setToken(null);
setUsername("");
setLastError("");
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
localStorage.removeItem(EXPIRES_AT_KEY);
localStorage.removeItem(USERNAME_KEY);
} }
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
booting,
loading, loading,
authenticated, authenticated,
username, username,
@ -223,6 +250,6 @@ export function AuthProvider({ children }) {
export function useAuth() { export function useAuth() {
const ctx = useContext(AuthContext); const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx; return ctx;
} }