235 lines
6.2 KiB
Plaintext
235 lines
6.2 KiB
Plaintext
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
|
|
const AuthContext = createContext(null);
|
|
|
|
// Keycloak config
|
|
|
|
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 refreshViaBackend(refreshToken) {
|
|
const res = await fetch("/api/refresh", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
});
|
|
|
|
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(` `);
|
|
}
|
|
|
|
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 refreshViaBackend(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 refreshViaBackend(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/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) {
|
|
let msg = "Invalid credentials or auth error.";
|
|
try {
|
|
const j = JSON.parse(text);
|
|
msg = j.error_description || j.error || msg;
|
|
} catch {}
|
|
clearAuth(` `);
|
|
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 (
|
|
<AuthContext.Provider
|
|
value={{
|
|
booting,
|
|
loading,
|
|
authenticated,
|
|
username,
|
|
token,
|
|
login,
|
|
logout,
|
|
lastError,
|
|
}}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
|
|
return ctx;
|
|
}
|