feat(dashboard): logout button + admin user-management window

Logout: new sidebar link 'Log out (username)' that POSTs /api/logout
(clears session cookie) and navigates to /login. Visible to everyone.
Replaces 'no logout functionality' state where users could only get
out by deleting cookies manually.

Admin window: new 'Admin · Users' window (only shown when current
user.is_admin) lists all users in a table with:
  - Add user (username + password + admin checkbox)
  - Reset password inline per row
  - Toggle admin per row
  - Delete user per row (blocked for self)
Wraps the existing /api-admin/users CRUD endpoints in main.py.

Plumbing: useCurrentUser hook fetches /me on mount; apiPatch+apiDelete
helpers added to api/client.ts; new endpoint wrappers exported from
api/endpoints.ts; AdminUsersWindow.tsx registered in WindowRenderer
under id prefix 'adminusers'; CSS for admin table/form/buttons and
the muted-red logout link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-15 20:10:10 +02:00
parent 88e9e88f46
commit 1c1c43d28b
23 changed files with 521 additions and 50 deletions

View file

@ -1,8 +1,17 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useWindowManager } from '../../contexts/WindowManagerContext';
import { useCurrentUser } from '../../hooks/useCurrentUser';
import { logout } from '../../api/endpoints';
export const SidebarWindowButtons: React.FC = () => {
const { openWindow } = useWindowManager();
const { user } = useCurrentUser();
const isAdmin = !!user?.is_admin;
const onLogout = useCallback(async () => {
if (!confirm('Log out?')) return;
try { await logout(); } catch { window.location.href = '/login'; }
}, []);
return (
<div className="ml-tool-links">
@ -18,6 +27,18 @@ export const SidebarWindowButtons: React.FC = () => {
onClick={() => openWindow('vitalsharing', 'Vital Sharing')}>🤝 Vitals</span>
<span className="ml-tool-link" style={{ cursor: 'pointer' }}
onClick={() => openWindow('combatpicker', 'Combat Stats')}> Combat</span>
{isAdmin && (
<span className="ml-tool-link" style={{ cursor: 'pointer' }}
onClick={() => openWindow('adminusers', 'Admin · Users')}>🛡 Admin</span>
)}
<span
className="ml-tool-link ml-tool-link-logout"
style={{ cursor: 'pointer' }}
onClick={onLogout}
title={user ? `Logged in as ${user.username}` : 'Log out'}
>
🚪 Log out{user ? ` (${user.username})` : ''}
</span>
</div>
);
};