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;
}