update frontend
This commit is contained in:
parent
ef5e361d40
commit
50c0375be4
|
|
@ -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";
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [token, setToken] = useState(null);
|
||||
const [lastError, setLastError] = useState("");
|
||||
// --- 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
REFRESH TOKEN (helper)
|
||||
============================== */
|
||||
async function refreshWithToken(refreshToken) {
|
||||
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);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`,
|
||||
{
|
||||
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) {
|
||||
console.error("[AUTH] refresh failed", res.status);
|
||||
logout();
|
||||
return false;
|
||||
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 = 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;
|
||||
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 [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("");
|
||||
|
||||
const refreshRef = useRef(null);
|
||||
const usernameRef = useRef("");
|
||||
|
||||
// persist helper
|
||||
function persistAuth({ accessToken, refreshToken, user }) {
|
||||
setToken(accessToken);
|
||||
setAuthenticated(true);
|
||||
|
||||
setUsername(user);
|
||||
usernameRef.current = user;
|
||||
try {
|
||||
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.error("[AUTH] refresh error", e);
|
||||
return false;
|
||||
}
|
||||
if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken);
|
||||
localStorage.setItem(USERNAME_KEY, user);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
INIT AUTH (APP LOAD)
|
||||
============================== */
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
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) || "";
|
||||
const expiresAt = Number(localStorage.getItem(EXPIRES_AT_KEY) || "0");
|
||||
|
||||
const now = Date.now();
|
||||
const needsRefresh = !storedToken || now > expiresAt - 60_000;
|
||||
if (!storedToken || !storedRefresh) return false;
|
||||
|
||||
if (storedRefresh && needsRefresh) {
|
||||
const ok = await refreshWithToken(storedRefresh);
|
||||
if (ok) {
|
||||
// si token encore bon, on le charge tel quel
|
||||
if (!willExpireSoon(storedToken, 30)) {
|
||||
setToken(storedToken);
|
||||
setUsername(storedUser);
|
||||
return;
|
||||
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.warn("[AUTH] silent refresh failed:", e);
|
||||
clearAuth("Session expired. Please login again.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Boot: restore session
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// 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);
|
||||
usernameRef.current = 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 */
|
||||
}
|
||||
}, 30_000);
|
||||
if (!willExpireSoon(t, 90)) return;
|
||||
|
||||
return () => clearInterval(t);
|
||||
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.");
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
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`,
|
||||
{
|
||||
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);
|
||||
} catch (e) {
|
||||
console.error("[AUTH] login error", e);
|
||||
setLastError("Network error");
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
LOGOUT
|
||||
============================== */
|
||||
function logout() {
|
||||
setAuthenticated(false);
|
||||
setToken(null);
|
||||
setUsername("");
|
||||
persistAuth({ accessToken, refreshToken, user: u });
|
||||
setLastError("");
|
||||
} catch (e) {
|
||||
console.error("[AUTH] network error", e);
|
||||
clearAuth("Network error during login.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_KEY);
|
||||
localStorage.removeItem(EXPIRES_AT_KEY);
|
||||
localStorage.removeItem(USERNAME_KEY);
|
||||
function logout() {
|
||||
clearAuth("");
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
booting,
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
|
|
@ -223,6 +250,6 @@ export function AuthProvider({ children }) {
|
|||
|
||||
export function useAuth() {
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue