mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix(client): revoke server session on user-initiated logout
api.logout() (POST /auth/logout) existed but was never called, leaving the bearer token valid server-side after a client-local logout. Add a small logout() helper that fires the revocation best-effort — fire-and-forget with its rejection swallowed — then runs clearAuth() synchronously, so a slow, offline, or rejecting server can never block or delay the local logout. Wire it into the settings Log Out button. Tests pin both paths: logout is called, and local logout still completes when the request rejects or never settles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* User-initiated logout.
|
||||
*
|
||||
* Revokes the session server-side (`POST /auth/logout`) and tears down local
|
||||
* auth state. The revocation is strictly best-effort: it is fire-and-forget and
|
||||
* its failure is swallowed, so a slow, offline, or rejecting server can never
|
||||
* block or delay the local logout — `clearAuth()` always runs synchronously.
|
||||
*/
|
||||
|
||||
import type { ApiClient } from "./api";
|
||||
import { clearAuth } from "@stores/auth.store";
|
||||
|
||||
export function logout(api: Pick<ApiClient, "logout">): void {
|
||||
// Best-effort server-side token revocation; never awaited, never allowed to
|
||||
// throw. clearAuth() below is the authoritative local teardown.
|
||||
void api.logout().catch(() => {});
|
||||
clearAuth();
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { createSettingsOverlay } from "@components/SettingsOverlay";
|
||||
import { createToastContainer } from "@components/Toast";
|
||||
import type { ToastContainer } from "@components/Toast";
|
||||
import { initToast, teardownToast, showToast } from "@lib/toast";
|
||||
import { logout } from "@lib/logout";
|
||||
import { authStore, clearAuth, updateUser } from "@stores/auth.store";
|
||||
import { closeSettings, uiStore } from "@stores/ui.store";
|
||||
import { updatePresence } from "@stores/members.store";
|
||||
@@ -303,7 +304,7 @@ export function createMainPage(options: MainPageOptions): MountableComponent {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
onLogout: () => clearAuth(),
|
||||
onLogout: () => logout(api),
|
||||
onDeleteAccount: async (password) => {
|
||||
await api.deleteAccount(password);
|
||||
clearAuth();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// clearAuth pulls in voice/livekit/notification teardown; stub it so this test
|
||||
// stays a pure unit test of the best-effort revoke-then-teardown ordering.
|
||||
const clearAuth = vi.fn();
|
||||
vi.mock("@stores/auth.store", () => ({
|
||||
clearAuth: () => clearAuth(),
|
||||
}));
|
||||
|
||||
import { logout } from "../../src/lib/logout";
|
||||
|
||||
function makeApi(logoutImpl: () => Promise<void>): { logout: ReturnType<typeof vi.fn> } {
|
||||
return { logout: vi.fn(logoutImpl) };
|
||||
}
|
||||
|
||||
describe("logout", () => {
|
||||
beforeEach(() => {
|
||||
clearAuth.mockClear();
|
||||
});
|
||||
|
||||
it("calls POST /auth/logout and then clears local auth", () => {
|
||||
const api = makeApi(() => Promise.resolve());
|
||||
|
||||
logout(api);
|
||||
|
||||
expect(api.logout).toHaveBeenCalledTimes(1);
|
||||
expect(clearAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("still logs out locally when the revocation request rejects", async () => {
|
||||
const api = makeApi(() => Promise.reject(new Error("network down")));
|
||||
|
||||
// Must not throw despite the rejected request...
|
||||
expect(() => logout(api)).not.toThrow();
|
||||
// ...and the local teardown must have happened regardless of the outcome.
|
||||
expect(clearAuth).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Let the rejected promise settle: the swallowing .catch() must prevent an
|
||||
// unhandled rejection and must not re-trigger teardown.
|
||||
await Promise.resolve();
|
||||
expect(clearAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not await the request before local logout (best-effort, non-blocking)", () => {
|
||||
// A request that never settles must not delay or block clearAuth.
|
||||
const api = makeApi(() => new Promise<void>(() => {}));
|
||||
|
||||
logout(api);
|
||||
|
||||
expect(clearAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -224,18 +224,19 @@ locks it.
|
||||
|
||||
| Trigger | Target behavior |
|
||||
|---------|-----------------|
|
||||
| User logout | `clearAuth()` → leave voice, disconnect WS, delete stored credential for the host, → connect page |
|
||||
| User logout | best-effort `POST /auth/logout` (fire-and-forget) → `clearAuth()` → leave voice, disconnect WS, delete stored credential for the host, → connect page |
|
||||
| 401 anywhere | Same as logout, with "Your session expired — sign in again." |
|
||||
| WS `BANNED` | Transient-error → connect page, no reconnect |
|
||||
| Cert reject | Disconnect → connect page |
|
||||
|
||||
> **⚠ Current gap — server session not revoked on logout.** `api.logout()`
|
||||
> (`POST /auth/logout`, `api.ts:211`) is defined but never called; logout is
|
||||
> client-local only (`MainPage.ts:298` → `clearAuth()`), so the bearer token
|
||||
> stays valid server-side until it expires. Target: user-initiated logout should
|
||||
> `POST /auth/logout` (best-effort, before tearing down) so the session is
|
||||
> actually revoked. The credential *is* deleted locally (`main.ts:491-515`), but
|
||||
> the server token is not.
|
||||
> **✓ Resolved 2026-07-20 — server session revoked on logout.** User-initiated
|
||||
> logout now calls `api.logout()` (`POST /auth/logout`) via the `logout()` helper
|
||||
> (`src/lib/logout.ts`), wired into the settings Log Out button
|
||||
> (`MainPage.ts` → `logout(api)`). The revocation is strictly best-effort:
|
||||
> fire-and-forget with its rejection swallowed, so a slow/offline/rejecting
|
||||
> server never blocks or delays the local teardown — `clearAuth()` always runs
|
||||
> synchronously. The credential is still deleted locally (`main.ts`), and the
|
||||
> server token is now invalidated too.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user