mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: stop plugins from blocking or stalling sign-out
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import * as browserNavigation from '@/lib/browser-navigation';
|
||||
import { authHooks, clearAllHooks } from '@/lib/plugin-hooks';
|
||||
import { useAuthStore, LOGOUT_HOOK_BUDGET_MS } from '../auth-store';
|
||||
import { useAccountStore } from '../account-store';
|
||||
|
||||
describe('sign-out with plugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
clearAllHooks();
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
|
||||
vi.spyOn(browserNavigation, 'replaceWindowLocation').mockImplementation(() => {});
|
||||
useAccountStore.setState({ accounts: [], activeAccountId: null, defaultAccountId: null });
|
||||
useAuthStore.setState({ isAuthenticated: true, authMode: 'basic', client: null, activeAccountId: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('signs out even when a plugin answers false before sign-out', async () => {
|
||||
const before = vi.fn().mockReturnValue(false);
|
||||
const after = vi.fn();
|
||||
authHooks.onBeforeLogout.register('p', before);
|
||||
authHooks.onAfterLogout.register('p', after);
|
||||
|
||||
await useAuthStore.getState().logout();
|
||||
|
||||
expect(before).toHaveBeenCalledWith({ accountId: 'all' });
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false);
|
||||
expect(after).toHaveBeenCalledWith({ accountId: 'all' });
|
||||
});
|
||||
|
||||
it('does not wait on a plugin that never answers', async () => {
|
||||
vi.useFakeTimers();
|
||||
authHooks.onBeforeLogout.register('p', () => new Promise(() => {}));
|
||||
|
||||
const done = useAuthStore.getState().logout();
|
||||
await vi.advanceTimersByTimeAsync(LOGOUT_HOOK_BUDGET_MS + 10);
|
||||
await done;
|
||||
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false);
|
||||
});
|
||||
});
|
||||
+18
-16
@@ -1243,6 +1243,20 @@ function performFullLogout(set: (state: Partial<AuthState>) => void): void {
|
||||
broadcastSignOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* How long sign-out waits for plugins to tidy up. Plugins observe sign-out;
|
||||
* they cannot stop it or hold it up, or a plugin could keep the user signed
|
||||
* in on a shared computer.
|
||||
*/
|
||||
export const LOGOUT_HOOK_BUDGET_MS = 2000;
|
||||
|
||||
function beforeLogoutHooks(accountId: string): Promise<void> {
|
||||
return Promise.race([
|
||||
authHooks.onBeforeLogout.emit({ accountId }),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, LOGOUT_HOOK_BUDGET_MS)),
|
||||
]);
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -2080,13 +2094,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
const userInitiated = options?.endProviderSession !== false;
|
||||
const endProviderSession = userInitiated && !wasDemoMode && shouldEndProviderSession(account);
|
||||
|
||||
const ok = await authHooks.onBeforeLogout.intercept({
|
||||
accountId: accountId ?? 'all',
|
||||
});
|
||||
|
||||
if(!ok){
|
||||
return;
|
||||
}
|
||||
await beforeLogoutHooks(accountId ?? 'all');
|
||||
|
||||
// From here on nothing may renew this slot's session; the cleanup
|
||||
// below reopens it once the credentials are gone.
|
||||
@@ -2111,7 +2119,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
await useSettingsStore.getState().flushSync();
|
||||
useSettingsStore.getState().disableSync();
|
||||
|
||||
await authHooks.onAfterLogout.emit({
|
||||
void authHooks.onAfterLogout.emit({
|
||||
accountId: accountId ?? 'all',
|
||||
});
|
||||
|
||||
@@ -2234,13 +2242,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
},
|
||||
|
||||
logoutAll: async () => {
|
||||
const ok = await authHooks.onBeforeLogout.intercept({
|
||||
accountId: 'all'
|
||||
});
|
||||
|
||||
if(!ok){
|
||||
return;
|
||||
}
|
||||
await beforeLogoutHooks('all');
|
||||
|
||||
const accountStore = useAccountStore.getState();
|
||||
const allAccounts = [...accountStore.accounts];
|
||||
@@ -2272,7 +2274,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
accountStore.removeAccount(account.id);
|
||||
}
|
||||
|
||||
await authHooks.onAfterLogout.emit({
|
||||
void authHooks.onAfterLogout.emit({
|
||||
accountId: 'all'
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user