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 */}