diff --git a/client/contexts/AuthContext.tsx b/client/contexts/AuthContext.tsx index d953b583..2fd80757 100644 --- a/client/contexts/AuthContext.tsx +++ b/client/contexts/AuthContext.tsx @@ -200,6 +200,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // - IndexedDB (where Supabase stores sessions) // Clearing these breaks session persistence across page reloads/redirects! + // If the server set the SSO remember-me cookie (Authentik login), promote + // it to localStorage so the session survives across browser restarts. + if (document.cookie.includes("aethex_sso_remember=1")) { + window.localStorage.setItem("aethex_remember_me", "1"); + } + storageClearedRef.current = true; } catch { storageClearedRef.current = true; @@ -224,6 +230,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ data: { session }, } = await supabase.auth.getSession(); + // If "remember me" was NOT checked when the user last signed in, clear + // the persisted session so closing the browser actually logs them out. + // SSO (Authentik) logins always set this flag, so this only affects + // email/password logins where the user explicitly unchecked it. + if (session?.user) { + const rememberMe = window.localStorage.getItem("aethex_remember_me"); + if (rememberMe === null) { + // No flag — user didn't ask to be remembered; clear local session. + await supabase.auth.signOut({ scope: "local" }); + sessionRestored = true; + setLoading(false); + return; + } + } + // If no session but tokens exist, the session might not have restored yet // Wait for onAuthStateChange to trigger if (!session && hasAuthTokens()) { @@ -992,13 +1013,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ return; } - // Only clear session for actual auth errors + // Only clear session for actual Supabase auth errors — be very specific. + // "unauthorized" and "auth/" were removed: they're too broad and match + // normal API 401s or any URL containing "auth/", which falsely logs users out. const authErrorPatterns = [ "invalid refresh token", + "refresh_token_not_found", "session expired", + "token_expired", "revoked", - "unauthorized", - "auth/", + "jwt expired", ]; if (authErrorPatterns.some((pattern) => messageStr.includes(pattern))) { @@ -1043,6 +1067,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // Step 2: Clear localStorage and IndexedDB console.log("Clearing localStorage and IndexedDB..."); if (typeof window !== "undefined") { + window.localStorage.removeItem("aethex_remember_me"); try { window.localStorage.removeItem("onboarding_complete"); window.localStorage.removeItem("aethex_onboarding_progress_v1"); diff --git a/client/pages/Login.tsx b/client/pages/Login.tsx index a0b13653..e70fbc9b 100644 --- a/client/pages/Login.tsx +++ b/client/pages/Login.tsx @@ -65,6 +65,7 @@ export default function Login() { const [fullName, setFullName] = useState(""); const [showReset, setShowReset] = useState(false); const [resetEmail, setResetEmail] = useState(""); + const [rememberMe, setRememberMe] = useState(true); const [errorFromUrl, setErrorFromUrl] = useState(null); const [discordLinkedEmail, setDiscordLinkedEmail] = useState( null, @@ -175,6 +176,12 @@ export default function Login() { }); } else { await signIn(email, password); + // Store remember-me preference — read by AuthContext on next page load + if (rememberMe) { + localStorage.setItem("aethex_remember_me", "1"); + } else { + localStorage.removeItem("aethex_remember_me"); + } toastInfo({ title: "Signing you in", description: "Redirecting...", @@ -560,6 +567,8 @@ export default function Login() { setRememberMe(e.target.checked)} /> Remember me diff --git a/server/index.ts b/server/index.ts index a6b22b59..03591f93 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8116,6 +8116,16 @@ export function createServer() { res.clearCookie("ak_verifier", { path: "/" }); res.clearCookie("ak_redirect", { path: "/" }); + // Set remember-me cookie so the client-side AuthContext keeps this SSO + // session alive across browser restarts — SSO users always want to stay logged in. + res.cookie("aethex_sso_remember", "1", { + httpOnly: false, // readable by JS so AuthContext can copy to localStorage + secure: true, + sameSite: "lax", + maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days + path: "/", + }); + console.log("[Authentik] Redirecting user to Supabase action link → dashboard"); return res.redirect(302, linkData.properties.action_link); } catch (e: any) {