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:
parent
7fec93e05c
commit
1599d0e690
3 changed files with 47 additions and 3 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue