Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions frontend/apps/web/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,33 +193,34 @@ export function Layout({ children }: PropsWithChildren) {
)}
<span>{BRAND_TITLE}</span>
</Link>
{data?.user && (
<div className="mt-1 text-xs text-gray-400">
{data.user.display_name}
{data.user.is_superuser ? ' · superuser' : ''}
</div>
)}
<div className="mt-3 flex items-center gap-2">
<div className="mt-1 flex items-center gap-2">
{data?.user && (
<span
className="min-w-0 flex-1 truncate text-xs text-gray-400"
title={data.user.display_name}
>
{data.user.display_name}
</span>
)}
<button
type="button"
onClick={() => setSettingsOpen(true)}
aria-label="Settings"
className="inline-flex items-center gap-1.5 rounded border border-gray-700 px-2 py-1 text-xs text-gray-200 hover:bg-gray-800"
className="ml-auto inline-flex shrink-0 items-center justify-center rounded border border-gray-700 p-1.5 text-gray-200 hover:bg-gray-800"
>
<Settings className="h-3.5 w-3.5" aria-hidden />
Settings
<Settings className="h-4 w-4" aria-hidden />
</button>
{canInstall && (
<button
type="button"
onClick={promptInstall}
className="inline-flex items-center gap-1.5 rounded border border-gray-700 px-2 py-1 text-xs text-gray-200 hover:bg-gray-800"
>
<Download className="h-3.5 w-3.5" aria-hidden />
Install app
</button>
)}
</div>
{canInstall && (
<button
type="button"
onClick={promptInstall}
className="mt-2 inline-flex items-center gap-1.5 rounded border border-gray-700 px-2 py-1 text-xs text-gray-200 hover:bg-gray-800"
>
<Download className="h-3.5 w-3.5" aria-hidden />
Install app
</button>
)}
</div>

{showFilter && (
Expand Down
25 changes: 24 additions & 1 deletion frontend/apps/web/src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Theme>(() => 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',
Expand Down Expand Up @@ -56,6 +73,12 @@ export function SettingsModal({ onClose }: { onClose: () => void }) {
</div>
<p className="text-xs text-gray-500">Saved on this device.</p>
</div>
<div className="mt-4 space-y-2 border-t border-gray-200 pt-4">
<div className="text-sm font-medium text-gray-700">Session</div>
<Button variant="secondary" onClick={handleLogout} loading={loggingOut}>
<LogOut className="h-4 w-4" aria-hidden /> Log out
</Button>
</div>
</Modal>
);
}
11 changes: 11 additions & 0 deletions frontend/packages/api/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ export class ApiClient {
return this.request<LoginResponse>('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 <app>/<model>/add/). */
addForm(appLabel: string, modelName: string): Promise<AddFormResponse> {
return this.request<AddFormResponse>('GET', `${appLabel}/${modelName}/add/`);
Expand Down
Loading