diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx
index 717fa95..883d3a6 100644
--- a/src/auth/AuthContext.jsx
+++ b/src/auth/AuthContext.jsx
@@ -1,343 +1,244 @@
-import React, { createContext, useContext, useEffect, useRef, useState } from "react";
+import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
+import { clearAuth, guessUsernameFromToken, jwtExpSeconds, loadAuth, saveAuth } from "./authStorage";
-const AuthContext = createContext(null);
-const API_BASE = "/api";
+const AuthCtx = createContext(null);
-const TOKEN_KEY = "sociowire:access_token";
-const REFRESH_KEY = "sociowire:refresh_token";
-const USERNAME_KEY = "sociowire:username";
-
-function decodeJwtPayload(token) {
- try {
- const parts = String(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 apiBase() {
+ // même domaine par défaut
+ return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
}
-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 apiFetch(path, opts = {}) {
+ const url = apiBase() + path;
+ const res = await fetch(url, {
+ ...opts,
+ headers: {
+ "Content-Type": "application/json",
+ ...(opts.headers || {}),
+ },
+ });
+ return res;
}
-async function apiRefresh(refreshToken) {
- const res = await fetch(`${API_BASE}/refresh`, {
+async function whoami(accessToken) {
+ const res = await apiFetch("/api/whoami", {
+ method: "GET",
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
+ });
+ const j = await res.json().catch(() => null);
+ return { ok: res.ok, status: res.status, json: j };
+}
+
+async function refreshTokens(refreshToken) {
+ const res = await apiFetch("/api/refresh", {
method: "POST",
- headers: { "Content-Type": "application/json" },
- credentials: "include",
body: JSON.stringify({ refresh_token: refreshToken }),
});
-
- const text = await res.text().catch(() => "");
- 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,
- };
+ const j = await res.json().catch(() => null);
+ return { ok: res.ok, status: res.status, json: j };
}
-async function apiMe(accessToken) {
- const res = await fetch(`${API_BASE}/me`, {
- method: "GET",
- headers: { Authorization: `Bearer ${accessToken}` },
- credentials: "include",
- });
-
- const text = await res.text().catch(() => "");
- if (!res.ok) throw new Error(`${res.status} ${text || "me failed"}`);
-
- const data = JSON.parse(text || "{}");
- return data;
-}
-
-async function apiResendVerify(accessToken) {
- const res = await fetch(`${API_BASE}/resend-verification`, {
- method: "POST",
- headers: { Authorization: `Bearer ${accessToken}` },
- credentials: "include",
- });
-
- const text = await res.text().catch(() => "");
- if (!res.ok) throw new Error(`${res.status} ${text || "resend failed"}`);
-
- return text ? JSON.parse(text) : { ok: true };
+function nowSec() {
+ return Math.floor(Date.now() / 1000);
}
export function AuthProvider({ children }) {
- const [booting, setBooting] = useState(true);
- const [loading, setLoading] = useState(false);
+ const [status, setStatus] = useState("loading"); // loading | anon | auth
+ const [user, setUser] = useState(null); // { username }
+ const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
+ const [error, setError] = useState("");
+ const refreshTimer = useRef(null);
- const [authenticated, setAuthenticated] = useState(false);
- const [username, setUsername] = useState("");
- const [token, setToken] = useState(null);
-
- const [emailVerified, setEmailVerified] = useState(true);
- const [needsEmailVerification, setNeedsEmailVerification] = useState(false);
-
- const [lastError, setLastError] = useState("");
- const [lastInfo, setLastInfo] = 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 setAnon(msg = "") {
+ setStatus("anon");
+ setUser(null);
+ setTokens(null);
+ setError(msg || "");
+ clearAuth();
}
- function clearAuth(msg = "") {
- setAuthenticated(false);
- setToken(null);
- setUsername("");
- usernameRef.current = "";
-
- setEmailVerified(true);
- setNeedsEmailVerification(false);
-
- setLastError(msg || "");
- try {
- localStorage.removeItem(TOKEN_KEY);
- localStorage.removeItem(REFRESH_KEY);
- localStorage.removeItem(USERNAME_KEY);
- } catch {}
+ function setAuthFromTokens(t, usernameGuess = "") {
+ setTokens(t);
+ saveAuth({ ...t, obtained_at: Date.now() });
+ const u = usernameGuess || guessUsernameFromToken(t?.access_token) || "";
+ setUser(u ? { username: u } : null);
+ setStatus("auth");
+ setError("");
}
- async function syncWithBackend(accessToken, fallbackUser = "") {
- // Confirme que le backend accepte vraiment le token
- const me = await apiMe(accessToken);
+ async function ensureFreshSession(existing) {
+ if (!existing?.access_token) return { ok: false, reason: "no_access_token" };
- const u =
- me.username ||
- me.preferred_username ||
- me.user ||
- fallbackUser ||
- localStorage.getItem(USERNAME_KEY) ||
- "";
+ const exp = jwtExpSeconds(existing.access_token);
+ const skew = 60; // seconds
+ const isExpiredOrSoon = !exp || exp <= (nowSec() + skew);
- const verified = me.email_verified !== false; // default true si absent
- setEmailVerified(verified);
- setNeedsEmailVerification(!verified);
+ // 1) refresh si nécessaire
+ if (isExpiredOrSoon) {
+ if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" };
- setAuthenticated(true);
- setToken(accessToken);
- setUsername(u);
- usernameRef.current = u;
+ const rr = await refreshTokens(existing.refresh_token);
+ if (!rr.ok || !rr.json?.access_token) {
+ return { ok: false, reason: `refresh_failed_${rr.status}` };
+ }
- // garde username en storage pour UI
- try { localStorage.setItem(USERNAME_KEY, u); } catch {}
+ const merged = {
+ ...existing,
+ ...rr.json,
+ };
- if (!verified) {
- setLastInfo("Check your inbox and verify your email to unlock posting.");
+ // certains serveurs renvoient pas refresh_token à chaque fois
+ if (!merged.refresh_token) merged.refresh_token = existing.refresh_token;
+
+ setAuthFromTokens(merged);
+ existing = merged;
} else {
- setLastInfo("");
+ // access token encore bon
+ setAuthFromTokens(existing);
}
- return { me, verified };
+ // 2) confirmer avec backend (whoami)
+ const w = await whoami(existing.access_token);
+ if (w.ok && w.json?.authenticated) {
+ const uname = w.json.username || guessUsernameFromToken(existing.access_token) || "";
+ setUser(uname ? { username: uname } : null);
+ setStatus("auth");
+ setError("");
+ return { ok: true };
+ }
+
+ // Si whoami fail, on considère désync (token invalide pour backend)
+ return { ok: false, reason: `whoami_${w.status}` };
}
- async function bootRestore() {
- const storedToken = (() => {
- try { return localStorage.getItem(TOKEN_KEY); } catch { return null; }
- })();
- const storedRefresh = (() => {
- try { return localStorage.getItem(REFRESH_KEY); } catch { return null; }
- })();
- const storedUser = (() => {
- try { return localStorage.getItem(USERNAME_KEY) || ""; } catch { return ""; }
- })();
-
- if (!storedToken) return;
-
- // 1) essaie /me direct
- try {
- await syncWithBackend(storedToken, storedUser);
- return;
- } catch (e) {
- // 2) si /me refuse, tente refresh si possible
- const msg = String(e?.message || "");
- const is401 = msg.startsWith("401") || msg.includes(" 401 ");
- if (!is401 || !storedRefresh) {
- clearAuth("Session expired. Please login again.");
- return;
- }
+ function scheduleAutoRefresh(t) {
+ if (refreshTimer.current) {
+ clearInterval(refreshTimer.current);
+ refreshTimer.current = null;
}
+ // check aux 30s, refresh quand exp < 90s
+ refreshTimer.current = setInterval(async () => {
+ const cur = loadAuth();
+ if (!cur?.access_token) return;
- try {
- const { accessToken, refreshToken } = await apiRefresh(storedRefresh);
- persistAuth({ accessToken, refreshToken, user: storedUser });
- await syncWithBackend(accessToken, storedUser);
- } catch (e) {
- console.warn("[AUTH] restore refresh failed:", e);
- clearAuth("Session expired. Please login again.");
- }
- }
+ const exp = jwtExpSeconds(cur.access_token);
+ if (!exp) return;
- useEffect(() => {
- (async () => {
- try {
- await bootRestore();
- } finally {
- setBooting(false);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- // Auto refresh (et resync backend)
- useEffect(() => {
- if (!authenticated) return;
-
- const interval = setInterval(async () => {
- try {
- const t = (() => { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } })();
- const rt = (() => { try { return localStorage.getItem(REFRESH_KEY); } catch { return null; } })();
- if (!t || !rt) return;
-
- if (!willExpireSoon(t, 90)) {
- // même si pas proche d’expirer, on peut vérifier /me 1 fois de temps en temps
+ if (exp <= nowSec() + 90) {
+ if (!cur.refresh_token) {
+ setAnon("Session expirée (pas de refresh_token).");
return;
}
-
- const { accessToken, refreshToken } = await apiRefresh(rt);
- const u = (() => {
- try { return localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; } catch { return usernameRef.current || ""; }
- })();
-
- persistAuth({ accessToken, refreshToken, user: u });
- await syncWithBackend(accessToken, u);
- } catch (e) {
- console.warn("[AUTH] auto refresh failed:", e);
- clearAuth("Session expired. Please login again.");
+ const rr = await refreshTokens(cur.refresh_token);
+ if (!rr.ok || !rr.json?.access_token) {
+ setAnon("Session expirée. Please login again.");
+ return;
+ }
+ const merged = { ...cur, ...rr.json };
+ if (!merged.refresh_token) merged.refresh_token = cur.refresh_token;
+ setAuthFromTokens(merged);
}
- }, 60_000);
+ }, 30000);
+ }
- return () => clearInterval(interval);
- }, [authenticated]);
-
- async function login(user, pass) {
- setLastError("");
- setLastInfo("");
- const u = (user || "").trim();
- if (!u || !pass) {
- setLastError("Username and password required.");
+ async function restore() {
+ setStatus("loading");
+ setError("");
+ const saved = loadAuth();
+ if (!saved?.access_token) {
+ setAnon("");
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().catch(() => "");
- if (!res.ok) {
- // backend peut renvoyer un JSON {error:"email_not_verified"}
- try {
- const obj = JSON.parse(text || "{}");
- if (res.status === 403 && obj?.error === "email_not_verified") {
- setLastError("Email not verified. Check your inbox (spam) and verify, then login again.");
- return;
- }
- } catch {}
- 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 });
-
- // IMPORTANT: resync with backend right after login
- try {
- await syncWithBackend(accessToken, u);
- } catch (e) {
- console.warn("[AUTH] /me failed after login:", e);
- clearAuth("Login desynced. Please login again.");
- return;
- }
-
- setLastError("");
- } catch (e) {
- console.error("[AUTH] network error", e);
- clearAuth("Network error during login.");
- setLastError("Network error during login.");
- } finally {
- setLoading(false);
+ const r = await ensureFreshSession(saved);
+ if (!r.ok) {
+ setAnon("Login desynced. Please login again.");
+ return;
}
+
+ scheduleAutoRefresh(saved);
+ }
+
+ async function login(username, password) {
+ setError("");
+ setStatus("loading");
+
+ const res = await apiFetch("/api/login", {
+ method: "POST",
+ body: JSON.stringify({ username, password }),
+ });
+
+ const j = await res.json().catch(() => null);
+ if (!res.ok || !j?.access_token) {
+ setAnon(j?.error_description || j?.error || "Login failed");
+ return { ok: false, status: res.status, json: j };
+ }
+
+ const t = {
+ ...j,
+ obtained_at: Date.now(),
+ };
+
+ setAuthFromTokens(t);
+ scheduleAutoRefresh(t);
+
+ // confirm whoami (si ça fail -> desync)
+ const w = await whoami(t.access_token);
+ if (!w.ok || !w.json?.authenticated) {
+ setAnon("Login desynced. Please login again.");
+ return { ok: false, status: w.status, json: w.json };
+ }
+
+ const uname = w.json.username || guessUsernameFromToken(t.access_token) || "";
+ setUser(uname ? { username: uname } : null);
+ setStatus("auth");
+ return { ok: true };
}
function logout() {
- clearAuth("");
+ if (refreshTimer.current) clearInterval(refreshTimer.current);
+ refreshTimer.current = null;
+ setAnon("");
}
- async function resendVerificationEmail() {
- setLastError("");
- setLastInfo("");
- const t = token || (() => { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } })();
- if (!t) {
- setLastError("Not logged in.");
- return;
- }
- try {
- await apiResendVerify(t);
- setLastInfo("Verification email sent. Check inbox/spam.");
- } catch (e) {
- console.error(e);
- setLastError("Could not resend verification email.");
- }
- }
+ useEffect(() => {
+ restore();
- return (
-
- {children}
-
- );
+ const onStorage = (e) => {
+ if (e.key === "sociowire:auth") restore();
+ };
+ window.addEventListener("storage", onStorage);
+ return () => window.removeEventListener("storage", onStorage);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (status === "auth" && tokens?.access_token) {
+ scheduleAutoRefresh(tokens);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [status]);
+
+ const value = useMemo(() => {
+ return {
+ status,
+ user,
+ tokens,
+ error,
+
+ isAuthed: status === "auth",
+ accessToken: tokens?.access_token || "",
+
+ login,
+ logout,
+ restore,
+ setError,
+ };
+ }, [status, user, tokens, error]);
+
+ return {children};
}
export function useAuth() {
- const ctx = useContext(AuthContext);
- if (!ctx) throw new Error("useAuth must be used inside ");
- return ctx;
+ return useContext(AuthCtx);
}
diff --git a/src/auth/AuthContext.jsx.bak.1766173632 b/src/auth/AuthContext.jsx.bak.1766173632
new file mode 100644
index 0000000..717fa95
--- /dev/null
+++ b/src/auth/AuthContext.jsx.bak.1766173632
@@ -0,0 +1,343 @@
+import React, { createContext, useContext, useEffect, useRef, useState } from "react";
+
+const AuthContext = createContext(null);
+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 = String(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 apiRefresh(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().catch(() => "");
+ 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,
+ };
+}
+
+async function apiMe(accessToken) {
+ const res = await fetch(`${API_BASE}/me`, {
+ method: "GET",
+ headers: { Authorization: `Bearer ${accessToken}` },
+ credentials: "include",
+ });
+
+ const text = await res.text().catch(() => "");
+ if (!res.ok) throw new Error(`${res.status} ${text || "me failed"}`);
+
+ const data = JSON.parse(text || "{}");
+ return data;
+}
+
+async function apiResendVerify(accessToken) {
+ const res = await fetch(`${API_BASE}/resend-verification`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${accessToken}` },
+ credentials: "include",
+ });
+
+ const text = await res.text().catch(() => "");
+ if (!res.ok) throw new Error(`${res.status} ${text || "resend failed"}`);
+
+ return text ? JSON.parse(text) : { ok: true };
+}
+
+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 [emailVerified, setEmailVerified] = useState(true);
+ const [needsEmailVerification, setNeedsEmailVerification] = useState(false);
+
+ const [lastError, setLastError] = useState("");
+ const [lastInfo, setLastInfo] = 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 = "";
+
+ setEmailVerified(true);
+ setNeedsEmailVerification(false);
+
+ setLastError(msg || "");
+ try {
+ localStorage.removeItem(TOKEN_KEY);
+ localStorage.removeItem(REFRESH_KEY);
+ localStorage.removeItem(USERNAME_KEY);
+ } catch {}
+ }
+
+ async function syncWithBackend(accessToken, fallbackUser = "") {
+ // Confirme que le backend accepte vraiment le token
+ const me = await apiMe(accessToken);
+
+ const u =
+ me.username ||
+ me.preferred_username ||
+ me.user ||
+ fallbackUser ||
+ localStorage.getItem(USERNAME_KEY) ||
+ "";
+
+ const verified = me.email_verified !== false; // default true si absent
+ setEmailVerified(verified);
+ setNeedsEmailVerification(!verified);
+
+ setAuthenticated(true);
+ setToken(accessToken);
+ setUsername(u);
+ usernameRef.current = u;
+
+ // garde username en storage pour UI
+ try { localStorage.setItem(USERNAME_KEY, u); } catch {}
+
+ if (!verified) {
+ setLastInfo("Check your inbox and verify your email to unlock posting.");
+ } else {
+ setLastInfo("");
+ }
+
+ return { me, verified };
+ }
+
+ async function bootRestore() {
+ const storedToken = (() => {
+ try { return localStorage.getItem(TOKEN_KEY); } catch { return null; }
+ })();
+ const storedRefresh = (() => {
+ try { return localStorage.getItem(REFRESH_KEY); } catch { return null; }
+ })();
+ const storedUser = (() => {
+ try { return localStorage.getItem(USERNAME_KEY) || ""; } catch { return ""; }
+ })();
+
+ if (!storedToken) return;
+
+ // 1) essaie /me direct
+ try {
+ await syncWithBackend(storedToken, storedUser);
+ return;
+ } catch (e) {
+ // 2) si /me refuse, tente refresh si possible
+ const msg = String(e?.message || "");
+ const is401 = msg.startsWith("401") || msg.includes(" 401 ");
+ if (!is401 || !storedRefresh) {
+ clearAuth("Session expired. Please login again.");
+ return;
+ }
+ }
+
+ try {
+ const { accessToken, refreshToken } = await apiRefresh(storedRefresh);
+ persistAuth({ accessToken, refreshToken, user: storedUser });
+ await syncWithBackend(accessToken, storedUser);
+ } catch (e) {
+ console.warn("[AUTH] restore refresh failed:", e);
+ clearAuth("Session expired. Please login again.");
+ }
+ }
+
+ useEffect(() => {
+ (async () => {
+ try {
+ await bootRestore();
+ } finally {
+ setBooting(false);
+ }
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // Auto refresh (et resync backend)
+ useEffect(() => {
+ if (!authenticated) return;
+
+ const interval = setInterval(async () => {
+ try {
+ const t = (() => { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } })();
+ const rt = (() => { try { return localStorage.getItem(REFRESH_KEY); } catch { return null; } })();
+ if (!t || !rt) return;
+
+ if (!willExpireSoon(t, 90)) {
+ // même si pas proche d’expirer, on peut vérifier /me 1 fois de temps en temps
+ return;
+ }
+
+ const { accessToken, refreshToken } = await apiRefresh(rt);
+ const u = (() => {
+ try { return localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; } catch { return usernameRef.current || ""; }
+ })();
+
+ persistAuth({ accessToken, refreshToken, user: u });
+ await syncWithBackend(accessToken, 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("");
+ setLastInfo("");
+ 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().catch(() => "");
+ if (!res.ok) {
+ // backend peut renvoyer un JSON {error:"email_not_verified"}
+ try {
+ const obj = JSON.parse(text || "{}");
+ if (res.status === 403 && obj?.error === "email_not_verified") {
+ setLastError("Email not verified. Check your inbox (spam) and verify, then login again.");
+ return;
+ }
+ } catch {}
+ 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 });
+
+ // IMPORTANT: resync with backend right after login
+ try {
+ await syncWithBackend(accessToken, u);
+ } catch (e) {
+ console.warn("[AUTH] /me failed after login:", e);
+ clearAuth("Login desynced. Please login again.");
+ return;
+ }
+
+ setLastError("");
+ } catch (e) {
+ console.error("[AUTH] network error", e);
+ clearAuth("Network error during login.");
+ setLastError("Network error during login.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function logout() {
+ clearAuth("");
+ }
+
+ async function resendVerificationEmail() {
+ setLastError("");
+ setLastInfo("");
+ const t = token || (() => { try { return localStorage.getItem(TOKEN_KEY); } catch { return null; } })();
+ if (!t) {
+ setLastError("Not logged in.");
+ return;
+ }
+ try {
+ await apiResendVerify(t);
+ setLastInfo("Verification email sent. Check inbox/spam.");
+ } catch (e) {
+ console.error(e);
+ setLastError("Could not resend verification email.");
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error("useAuth must be used inside ");
+ return ctx;
+}
diff --git a/src/auth/authStorage.js b/src/auth/authStorage.js
new file mode 100644
index 0000000..6f0af7e
--- /dev/null
+++ b/src/auth/authStorage.js
@@ -0,0 +1,57 @@
+const KEY = "sociowire:auth";
+
+export function loadAuth() {
+ try {
+ const raw = localStorage.getItem(KEY);
+ if (!raw) return null;
+ const v = JSON.parse(raw);
+ if (!v || typeof v !== "object") return null;
+ return v;
+ } catch {
+ return null;
+ }
+}
+
+export function saveAuth(v) {
+ try {
+ localStorage.setItem(KEY, JSON.stringify(v || {}));
+ } catch {}
+}
+
+export function clearAuth() {
+ try {
+ localStorage.removeItem(KEY);
+ } catch {}
+}
+
+export function decodeJwtPayload(token) {
+ try {
+ if (!token || typeof token !== "string") return null;
+ const parts = token.split(".");
+ if (parts.length < 2) return null;
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
+ const pad = b64.length % 4 ? "=".repeat(4 - (b64.length % 4)) : "";
+ const json = atob(b64 + pad);
+ return JSON.parse(json);
+ } catch {
+ return null;
+ }
+}
+
+export function jwtExpSeconds(token) {
+ const p = decodeJwtPayload(token);
+ if (!p || !p.exp) return 0;
+ const exp = Number(p.exp);
+ return Number.isFinite(exp) ? exp : 0;
+}
+
+export function guessUsernameFromToken(token) {
+ const p = decodeJwtPayload(token);
+ if (!p) return "";
+ return (
+ (typeof p.preferred_username === "string" && p.preferred_username) ||
+ (typeof p.email === "string" && p.email) ||
+ (typeof p.sub === "string" && p.sub) ||
+ ""
+ );
+}