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/`);