import React, { createContext, useContext, useEffect, useRef, useState } from "react"; const AuthContext = createContext(null); // Backend API (same origin) const API_BASE = "/api"; const TOKEN_KEY = "sociowire:access_token"; const REFRESH_KEY = "sociowire:refresh_token"; const USERNAME_KEY = "sociowire:username"; 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 backendRefresh(refreshToken) { const res = await fetch(`${API_BASE}/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ refresh_token: refreshToken }), }); const text = await res.text(); if (!res.ok) { throw new Error(`${res.status} ${text}`); } 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 backendRefresh(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 backendRefresh(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; } try { setLoading(true); const res = await fetch(`${API_BASE}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ username: u, password: pass }), }); const text = await res.text(); if (!res.ok) { console.error("[AUTH] /api/login failed", res.status, text); setLastError(`${res.status} login failed`); return; } const data = JSON.parse(text); const accessToken = data.access_token; const refreshToken = data.refresh_token; if (!accessToken) { setLastError("No access token in response."); return; } persistAuth({ accessToken, refreshToken, user: u }); setLastError(""); } catch (e) { console.error("[AUTH] network error", e); setLastError("Network error during login."); 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; }