feat: implement cross-tab coordination for token refresh and enhance logout handling
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m57s

This commit is contained in:
2026-03-27 23:02:29 -04:00
parent 89097a390e
commit a4e23aad11
6 changed files with 54 additions and 11 deletions

View File

@@ -2,7 +2,7 @@
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useBackendEvents } from '@/common/backendEvents'
import { currentUserId, logoutUser, suppressForceLogout } from '@/stores/auth'
import { currentUserId, logoutParent, logoutUser, suppressForceLogout } from '@/stores/auth'
import { eventBus } from '@/common/eventBus'
const userId = ref(currentUserId.value)
@@ -20,6 +20,7 @@ function handleForceLogout(event: { payload?: { reason?: string } }) {
suppressForceLogout.value = false
return
}
logoutParent()
logoutUser()
if (event?.payload?.reason === 'account_deleted') {
router.push('/')

View File

@@ -5,16 +5,20 @@ import BackendEventsListener from '../BackendEventsListener.vue'
// ── Hoisted mocks ────────────────────────────────────────────────────────────
const { mockLogoutUser, mockSuppressForceLogout, mockCurrentUserId } = vi.hoisted(() => ({
mockLogoutUser: vi.fn(),
mockSuppressForceLogout: { value: false },
mockCurrentUserId: { value: 'user-1' },
}))
const { mockLogoutUser, mockLogoutParent, mockSuppressForceLogout, mockCurrentUserId } = vi.hoisted(
() => ({
mockLogoutUser: vi.fn(),
mockLogoutParent: vi.fn(),
mockSuppressForceLogout: { value: false },
mockCurrentUserId: { value: 'user-1' },
}),
)
let capturedForceLogoutHandler: ((event: any) => void) | null = null
vi.mock('@/stores/auth', () => ({
currentUserId: mockCurrentUserId,
logoutParent: mockLogoutParent,
logoutUser: mockLogoutUser,
suppressForceLogout: mockSuppressForceLogout,
}))
@@ -70,6 +74,7 @@ describe('BackendEventsListener force_logout', () => {
fireForceLogout('password_reset')
await flushPromises()
expect(mockLogoutParent).toHaveBeenCalledOnce()
expect(mockLogoutUser).toHaveBeenCalledOnce()
expect(pushSpy).toHaveBeenCalledWith({ name: 'Login' })
})
@@ -81,6 +86,7 @@ describe('BackendEventsListener force_logout', () => {
fireForceLogout('account_deleted')
await flushPromises()
expect(mockLogoutParent).toHaveBeenCalledOnce()
expect(mockLogoutUser).toHaveBeenCalledOnce()
expect(pushSpy).toHaveBeenCalledWith('/')
})
@@ -92,6 +98,7 @@ describe('BackendEventsListener force_logout', () => {
fireForceLogout('something_else')
await flushPromises()
expect(mockLogoutParent).toHaveBeenCalledOnce()
expect(mockLogoutUser).toHaveBeenCalledOnce()
expect(pushSpy).toHaveBeenCalledWith({ name: 'Login' })
})
@@ -103,6 +110,7 @@ describe('BackendEventsListener force_logout', () => {
fireForceLogout()
await flushPromises()
expect(mockLogoutParent).toHaveBeenCalledOnce()
expect(mockLogoutUser).toHaveBeenCalledOnce()
expect(pushSpy).toHaveBeenCalledWith({ name: 'Login' })
})
@@ -115,6 +123,7 @@ describe('BackendEventsListener force_logout', () => {
fireForceLogout('password_reset')
await flushPromises()
expect(mockLogoutParent).not.toHaveBeenCalled()
expect(mockLogoutUser).not.toHaveBeenCalled()
expect(pushSpy).not.toHaveBeenCalled()
})
@@ -142,6 +151,7 @@ describe('BackendEventsListener force_logout', () => {
// Second event: should go through normally
fireForceLogout('password_reset')
await flushPromises()
expect(mockLogoutParent).toHaveBeenCalledOnce()
expect(mockLogoutUser).toHaveBeenCalledOnce()
expect(pushSpy).toHaveBeenCalledWith({ name: 'Login' })
})

View File

@@ -224,6 +224,7 @@ function executeMenuItem(index: number) {
async function signOut() {
try {
await fetch('/api/auth/logout', { method: 'POST' })
logoutParent()
logoutUser()
router.push('/')
} catch {