From 4c2d185be4b90b022142cc0952388b1794d506ba Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 21 Mar 2026 14:02:42 +0100
Subject: [PATCH] fix: refactor logout to use synchronous flow with full page
redirect
- Rewrite logout() from async to synchronous to prevent React re-renders with stale state
- Replace router.push('/login') with redirectToLogin() (window.location.replace) in all page auth guards for reliable navigation in Edge/Safari
- Add performFullLogout() helper that clears auth state, feature stores, and localStorage
- Fix persist middleware partialize to return {} when not authenticated, preventing state resurrection
- Use keepalive fetch for background cookie/token cleanup so redirect fires immediately
- Remove unused useRouter imports from page.tsx and contacts/page.tsx
- Simplify all page logout handlers to directly call logout()
Fixes #63
---
app/[locale]/calendar/page.tsx | 6 +-
app/[locale]/contacts/page.tsx | 10 +-
app/[locale]/files/page.tsx | 8 +-
app/[locale]/page.tsx | 15 +-
app/[locale]/settings/page.tsx | 10 +-
components/layout/account-switcher.tsx | 4 -
stores/auth-store.ts | 284 ++++++++-----------------
7 files changed, 104 insertions(+), 233 deletions(-)
diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx
index 3ff546ed..56276b30 100644
--- a/app/[locale]/calendar/page.tsx
+++ b/app/[locale]/calendar/page.tsx
@@ -11,7 +11,7 @@ import {
} from "date-fns";
import { useCalendarStore } from "@/stores/calendar-store";
import { isCalendarViewMode } from "@/stores/calendar-store";
-import { useAuthStore } from "@/stores/auth-store";
+import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useIdentityStore } from "@/stores/identity-store";
@@ -105,7 +105,7 @@ export default function CalendarPage() {
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
- router.push("/login");
+ redirectToLogin();
} else if (client && !supportsCalendar) {
router.push("/");
}
@@ -721,7 +721,7 @@ export default function CalendarPage() {
collapsed
quota={quota}
isPushConnected={isPushConnected}
- onLogout={() => { logout(); if (!useAuthStore.getState().isAuthenticated) router.push('/login'); }}
+ onLogout={logout}
onManageApps={handleManageApps}
onInlineApp={handleInlineApp}
onCloseInlineApp={closeInlineApp}
diff --git a/app/[locale]/contacts/page.tsx b/app/[locale]/contacts/page.tsx
index 1b8bc214..2c232552 100644
--- a/app/[locale]/contacts/page.tsx
+++ b/app/[locale]/contacts/page.tsx
@@ -1,7 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
-import { useRouter } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
import { ArrowLeft, Users } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -16,7 +15,7 @@ import { ContactsSidebar, type ContactCategory } from "@/components/contacts/con
import { ContactImportDialog } from "@/components/contacts/contact-import-dialog";
import { exportContacts } from "@/components/contacts/contact-export";
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
-import { useAuthStore } from "@/stores/auth-store";
+import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { toast } from "@/stores/toast-store";
import { cn } from "@/lib/utils";
@@ -39,7 +38,6 @@ type View =
| "bulk-add-to-group";
export default function ContactsPage() {
- const router = useRouter();
const t = useTranslations("contacts");
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
@@ -109,9 +107,9 @@ export default function ContactsPage() {
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
- router.push("/login");
+ redirectToLogin();
}
- }, [initialCheckDone, isAuthenticated, authLoading, router]);
+ }, [initialCheckDone, isAuthenticated, authLoading]);
useEffect(() => {
if (client && supportsSync && !hasFetched.current) {
@@ -594,7 +592,7 @@ export default function ContactsPage() {
collapsed
quota={quota}
isPushConnected={isPushConnected}
- onLogout={() => { logout(); if (!useAuthStore.getState().isAuthenticated) router.push('/login'); }}
+ onLogout={logout}
onManageApps={handleManageApps}
onInlineApp={handleInlineApp}
onCloseInlineApp={closeInlineApp}
diff --git a/app/[locale]/files/page.tsx b/app/[locale]/files/page.tsx
index e7b45c82..aade290f 100644
--- a/app/[locale]/files/page.tsx
+++ b/app/[locale]/files/page.tsx
@@ -7,7 +7,7 @@ import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
-import { useAuthStore } from "@/stores/auth-store";
+import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useFileStore } from "@/stores/file-store";
import { toast } from "@/stores/toast-store";
@@ -112,9 +112,9 @@ export default function FilesPage() {
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
- router.push("/login");
+ redirectToLogin();
}
- }, [initialCheckDone, isAuthenticated, authLoading, router]);
+ }, [initialCheckDone, isAuthenticated, authLoading]);
// Initialize JMAP files client
useEffect(() => {
@@ -357,7 +357,7 @@ export default function FilesPage() {
collapsed
quota={quota}
isPushConnected={isPushConnected}
- onLogout={() => { logout(); if (!useAuthStore.getState().isAuthenticated) router.push('/login'); }}
+ onLogout={logout}
onManageApps={handleManageApps}
onInlineApp={handleInlineApp}
onCloseInlineApp={closeInlineApp}
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index c0b39e5a..0f5852ff 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -1,7 +1,6 @@
"use client";
import { useEffect, useState, useRef, useMemo, useCallback } from "react";
-import { useRouter } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
import { Sidebar } from "@/components/layout/sidebar";
import { EmailList } from "@/components/email/email-list";
@@ -13,7 +12,7 @@ import { MobileHeader, MobileViewerHeader } from "@/components/layout/mobile-hea
import { ThreadGroup, Email } from "@/lib/jmap/types";
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
import { useEmailStore } from "@/stores/email-store";
-import { useAuthStore } from "@/stores/auth-store";
+import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useIdentityStore } from "@/stores/identity-store";
import { useUIStore } from "@/stores/ui-store";
@@ -48,7 +47,6 @@ import { Button } from "@/components/ui/button";
import { useConfig } from "@/hooks/use-config";
export default function Home() {
- const router = useRouter();
const t = useTranslations();
const tCommon = useTranslations('common');
const { appName } = useConfig();
@@ -285,9 +283,9 @@ export default function Home() {
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
- router.push('/login');
+ redirectToLogin();
}
- }, [initialCheckDone, isAuthenticated, authLoading, router]);
+ }, [initialCheckDone, isAuthenticated, authLoading]);
// Load mailboxes and emails when authenticated (only if not already loaded)
useEffect(() => {
@@ -768,12 +766,7 @@ export default function Home() {
}
};
- const handleLogout = () => {
- logout();
- if (!useAuthStore.getState().isAuthenticated) {
- router.push('/login');
- }
- };
+ const handleLogout = logout;
const handleSearch = async (query: string) => {
if (!client) return;
diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx
index 8b70b6cd..8ccd3179 100644
--- a/app/[locale]/settings/page.tsx
+++ b/app/[locale]/settings/page.tsx
@@ -44,7 +44,7 @@ import { FilesSettingsComponent } from '@/components/settings/files-settings';
import { ContactsSettings } from '@/components/settings/contacts-settings';
import { SmimeSettings } from '@/components/settings/smime-settings';
import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings';
-import { useAuthStore } from '@/stores/auth-store';
+import { useAuthStore, redirectToLogin } from '@/stores/auth-store';
import { useEmailStore } from '@/stores/email-store';
import { useIsDesktop } from '@/hooks/use-media-query';
import { NavigationRail } from '@/components/layout/navigation-rail';
@@ -122,9 +122,9 @@ export default function SettingsPage() {
useEffect(() => {
if (initialCheckDone && !isAuthenticated && !authLoading) {
try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ }
- router.push('/login');
+ redirectToLogin();
}
- }, [initialCheckDone, isAuthenticated, authLoading, router]);
+ }, [initialCheckDone, isAuthenticated, authLoading]);
if (!isAuthenticated) {
return null;
@@ -286,7 +286,7 @@ export default function SettingsPage() {
{/* Logout */}