diff --git a/frontend/apps/web/src/Layout.tsx b/frontend/apps/web/src/Layout.tsx
index 35177c1..d11cef4 100644
--- a/frontend/apps/web/src/Layout.tsx
+++ b/frontend/apps/web/src/Layout.tsx
@@ -193,33 +193,34 @@ export function Layout({ children }: PropsWithChildren) {
)}
{BRAND_TITLE}
- {data?.user && (
-
- {data.user.display_name}
- {data.user.is_superuser ? ' · superuser' : ''}
-
- )}
-
+
+ {data?.user && (
+
+ {data.user.display_name}
+
+ )}
- {canInstall && (
-
- )}
+ {canInstall && (
+
+ )}
{showFilter && (
diff --git a/frontend/apps/web/src/components/SettingsModal.tsx b/frontend/apps/web/src/components/SettingsModal.tsx
index 95fd746..33331aa 100644
--- a/frontend/apps/web/src/components/SettingsModal.tsx
+++ b/frontend/apps/web/src/components/SettingsModal.tsx
@@ -4,20 +4,37 @@
// the filter / delete / action confirms (overlay, Esc / backdrop close).
import { useState } from 'react';
-import { Moon, Sun } from 'lucide-react';
+import { LogOut, Moon, Sun } from 'lucide-react';
+import { useApiClient } from '@dar/data';
import { Button, Modal } from '@dar/ui';
import { resolveTheme, setTheme, type Theme } from '../theme';
export function SettingsModal({ onClose }: { onClose: () => void }) {
+ const client = useApiClient();
const [theme, setThemeState] = useState(() => resolveTheme());
+ const [loggingOut, setLoggingOut] = useState(false);
const choose = (next: Theme) => {
setTheme(next);
setThemeState(next);
};
+ const handleLogout = async () => {
+ setLoggingOut(true);
+ try {
+ await client.logout();
+ } catch {
+ // Logout is idempotent server-side; even on a transient error we
+ // still bounce the user out so the UI can't pretend they're signed
+ // in. The reload re-runs the auth gate (login page / Django login).
+ }
+ // Full reload clears all in-memory + cached state and re-enters the
+ // shell, which routes an anonymous session to the login screen.
+ window.location.reload();
+ };
+
const optionClass = (active: boolean): string =>
[
'flex flex-1 items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium',
@@ -56,6 +73,12 @@ export function SettingsModal({ onClose }: { onClose: () => void }) {
Saved on this device.
+
+
Session
+
+
);
}
diff --git a/frontend/packages/api/src/client.ts b/frontend/packages/api/src/client.ts
index 20e189c..5a74c3e 100644
--- a/frontend/packages/api/src/client.ts
+++ b/frontend/packages/api/src/client.ts
@@ -176,6 +176,17 @@ export class ApiClient {
return this.request('POST', 'login/', { username, password });
}
+ /**
+ * End the current session via the package's logout endpoint
+ * (`POST /api/v1/logout/`, `api/views/auth.py`) — a thin JSON shell over
+ * Django's own `logout` that flushes the session server-side. Idempotent
+ * (logging out while already anonymous is a harmless 200). CSRF is
+ * enforced by the middleware, same as `login()`.
+ */
+ logout(): Promise<{ detail: string }> {
+ return this.request<{ detail: string }>('POST', 'logout/', {});
+ }
+
/** The create-form schema for a NEW object (GET //add/). */
addForm(appLabel: string, modelName: string): Promise {
return this.request('GET', `${appLabel}/${modelName}/add/`);