feat: implement logout hook and add a new plugin api method to perform logout
This commit is contained in:
@@ -41,7 +41,7 @@ describe('auth-store logout redirects', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('redirects full logout to the locale login page', () => {
|
||||
it('redirects full logout to the locale login page', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const replaceSpy = vi.spyOn(browserNavigation, 'replaceWindowLocation').mockImplementation(() => {});
|
||||
@@ -49,7 +49,7 @@ describe('auth-store logout redirects', () => {
|
||||
window.history.pushState({}, '', '/fr/calendar');
|
||||
useAuthStore.setState({ isAuthenticated: true, authMode: 'basic' });
|
||||
|
||||
useAuthStore.getState().logout();
|
||||
await useAuthStore.getState().logout();
|
||||
|
||||
expect(replaceSpy).toHaveBeenCalledWith('/fr/login');
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/session?slot=0', { method: 'DELETE', keepalive: true });
|
||||
@@ -169,7 +169,7 @@ describe('auth-store logout redirects', () => {
|
||||
|
||||
// Refresh goes in flight, then the user signs out before it settles.
|
||||
const pending = useAuthStore.getState().refreshAccessToken();
|
||||
useAuthStore.getState().logout();
|
||||
await useAuthStore.getState().logout();
|
||||
resolveInFlight!({ ok: false, status: 503, json: async () => ({}) });
|
||||
await pending;
|
||||
|
||||
|
||||
+30
-4
@@ -17,6 +17,7 @@ import { replaceWindowLocation, getPathPrefix, getLocaleFromPath, apiFetch } fro
|
||||
import { notifyParent } from '@/lib/iframe-bridge';
|
||||
import { snapshotAccount, restoreAccount, clearAllStores, evictAccount, evictAll } from '@/lib/account-state-manager';
|
||||
import type { Identity } from '@/lib/jmap/types';
|
||||
import { authHooks } from '@/lib/plugin-hooks';
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
@@ -42,8 +43,8 @@ interface AuthState {
|
||||
loginWithServerSso: (code: string, state: string) => Promise<boolean>;
|
||||
loginDemo: () => Promise<boolean>;
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
logout: () => void;
|
||||
logoutAll: () => void;
|
||||
logout: () => Promise<void>;
|
||||
logoutAll: () => Promise<void>;
|
||||
removeAccount: (accountId: string) => void;
|
||||
switchAccount: (accountId: string) => Promise<void>;
|
||||
checkAuth: () => Promise<void>;
|
||||
@@ -1196,7 +1197,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
return promise;
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
logout: async () => {
|
||||
const state = get();
|
||||
const wasDemoMode = state.isDemoMode;
|
||||
const wasOAuth = state.authMode === 'oauth';
|
||||
@@ -1206,6 +1207,15 @@ export const useAuthStore = create<AuthState>()(
|
||||
const slot = account?.cookieSlot ?? 0;
|
||||
|
||||
// Stop refresh timers immediately
|
||||
|
||||
const ok = await authHooks.onBeforeLogout.intercept({
|
||||
accountId: accountId ?? 'all',
|
||||
});
|
||||
|
||||
if(!ok){
|
||||
return;
|
||||
}
|
||||
|
||||
clearRefreshTimer(accountId ?? undefined);
|
||||
|
||||
// Disconnect and null out the client BEFORE clearing stores so the
|
||||
@@ -1223,6 +1233,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
useSettingsStore.getState().disableSync();
|
||||
|
||||
await authHooks.onAfterLogout.emit({
|
||||
accountId: accountId ?? 'all',
|
||||
});
|
||||
|
||||
// Check if there are remaining accounts to switch to
|
||||
const remainingAccounts = accountStore.accounts;
|
||||
|
||||
@@ -1320,7 +1334,15 @@ export const useAuthStore = create<AuthState>()(
|
||||
}
|
||||
},
|
||||
|
||||
logoutAll: () => {
|
||||
logoutAll: async () => {
|
||||
const ok = await authHooks.onBeforeLogout.intercept({
|
||||
accountId: 'all'
|
||||
});
|
||||
|
||||
if(!ok){
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect all clients
|
||||
for (const c of clients.values()) {
|
||||
c.disconnect();
|
||||
@@ -1338,6 +1360,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
accountStore.removeAccount(account.id);
|
||||
}
|
||||
|
||||
await authHooks.onAfterLogout.emit({
|
||||
accountId: 'all'
|
||||
});
|
||||
|
||||
// Background cookie/token cleanup
|
||||
apiFetch('/api/auth/session?all=true', { method: 'DELETE', keepalive: true }).catch(() => {});
|
||||
apiFetch('/api/auth/token?all=true', { method: 'DELETE', keepalive: true }).catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user