import React, { createContext, useContext, useEffect, useRef, useState } from "react"; const AuthContext = createContext(null); // Keycloak config const KC_BASE = "https://web.sociowire.com:8443"; const KC_REALM = "sociowire"; const KC_CLIENT_ID = "sociowire-frontend"; const TOKEN_KEY = "sociowire:access_token"; const REFRESH_KEY = "sociowire:refresh_token"; 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, }; } export function AuthProvider({ children }) { const [booting, setBooting] = useState(true); const [loading, setLoading] = useState(false); const [authenticated, setAuthenticated] = useState(false); const [username, setUsername] = useState(""); const [token, setToken] = useState(null); const [lastError, setLastError] = useState(""); const usernameRef = useRef(""); function persistAuth({ accessToken, refreshToken, user }) { setToken(accessToken); setAuthenticated(true); setUsername(user); usernameRef.current = user; try { localStorage.setItem(TOKEN_KEY, accessToken); 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; if (!willExpireSoon(storedToken, 30)) { setToken(storedToken); setUsername(storedUser); usernameRef.current = storedUser; setAuthenticated(true); return true; } 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; } } useEffect(() => { (async () => { try { const ok = await trySilentRefreshIfNeeded(); if (!ok) { const storedToken = localStorage.getItem(TOKEN_KEY); const storedUser = localStorage.getItem(USERNAME_KEY) || ""; if (storedToken) { setToken(storedToken); setUsername(storedUser); usernameRef.current = storedUser; setAuthenticated(true); } } } finally { setBooting(false); } })(); }, []); useEffect(() => { if (!authenticated) return; const interval = setInterval(async () => { try { const t = localStorage.getItem(TOKEN_KEY); const rt = localStorage.getItem(REFRESH_KEY); if (!t || !rt) return; 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."); } }, 60_000); return () => clearInterval(interval); }, [authenticated]); 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("password", pass); try { 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) { 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 = JSON.parse(text); const accessToken = data.access_token; const refreshToken = data.refresh_token; if (!accessToken) { clearAuth("No access token in response."); return; } persistAuth({ accessToken, refreshToken, user: u }); setLastError(""); } catch (e) { console.error("[AUTH] network error", e); clearAuth("Network error during login."); } finally { setLoading(false); } } function logout() { clearAuth(""); } return ( {children} ); } export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used inside "); return ctx; }