fix: prevent false session logouts and wire up remember-me

- Narrow the unhandledrejection error handler: removed "unauthorized"
  and "auth/" patterns which were too broad and cleared sessions on
  unrelated API 401s or any URL containing "auth/". Now only matches
  specific Supabase strings (invalid refresh token, jwt expired, etc.)
- Wire up the Remember Me checkbox in Login — was purely decorative
  before. Defaults to checked, stores aethex_remember_me in localStorage
- Authentik SSO callback now sets a 30-day cookie so SSO sessions
  survive browser restarts (AuthContext promotes it to localStorage)
- AuthContext clears local session on load if remember-me flag is absent
  (respects user's choice to not stay logged in)
- signOut now removes aethex_remember_me from localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AeThex 2026-04-12 08:15:47 +00:00
parent 7fec93e05c
commit 1599d0e690
3 changed files with 47 additions and 3 deletions

View file

@ -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");

View file

@ -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<string | null>(null);
const [discordLinkedEmail, setDiscordLinkedEmail] = useState<string | null>(
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() {
<input
type="checkbox"
className="rounded border-border/50"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
<span className="text-muted-foreground">
Remember me

View file

@ -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) {