diff --git a/services/web-ui/public/screens-admin.jsx b/services/web-ui/public/screens-admin.jsx
index 44ff897..72c10bf 100644
--- a/services/web-ui/public/screens-admin.jsx
+++ b/services/web-ui/public/screens-admin.jsx
@@ -1406,10 +1406,137 @@ function DetailRow({ k, v, mono }) {
);
}
+function AccountSection() {
+ const [current, setCurrent] = React.useState('');
+ const [next, setNext] = React.useState('');
+ const [confirm, setConfirm] = React.useState('');
+ const [msg, setMsg] = React.useState(null); // { kind: 'ok'|'err', text }
+ const [busy, setBusy] = React.useState(false);
+
+ const submit = async () => {
+ setMsg(null);
+ if (next !== confirm) { setMsg({ kind: 'err', text: 'Passwords do not match' }); return; }
+ if (next.length < 12) { setMsg({ kind: 'err', text: 'New password must be at least 12 characters' }); return; }
+ setBusy(true);
+ try {
+ const r = await fetch('/api/v1/auth/password', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
+ body: JSON.stringify({ current_password: current, new_password: next }),
+ });
+ if (r.status === 204) {
+ setMsg({ kind: 'ok', text: 'Password updated' });
+ setCurrent(''); setNext(''); setConfirm('');
+ } else {
+ const body = await r.json().catch(() => ({}));
+ setMsg({ kind: 'err', text: body.error || 'Failed (' + r.status + ')' });
+ }
+ } finally { setBusy(false); }
+ };
+
+ return (
+
+ );
+}
+
+function ApiTokensSection() {
+ const [tokens, setTokens] = React.useState([]);
+ const [name, setName] = React.useState('');
+ const [justCreated, setJustCreated] = React.useState(null); // { token, prefix, name }
+ const [busy, setBusy] = React.useState(false);
+
+ const load = React.useCallback(async () => {
+ const r = await fetch('/api/v1/auth/tokens', { credentials: 'include' });
+ if (r.status === 200) setTokens(await r.json());
+ }, []);
+
+ React.useEffect(() => { load(); }, [load]);
+
+ const create = async () => {
+ if (!name.trim()) return;
+ setBusy(true);
+ try {
+ const r = await fetch('/api/v1/auth/tokens', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
+ body: JSON.stringify({ name: name.trim() }),
+ });
+ if (r.status === 201) {
+ const created = await r.json();
+ setJustCreated(created);
+ setName('');
+ await load();
+ }
+ } finally { setBusy(false); }
+ };
+
+ const revoke = async (id) => {
+ await fetch('/api/v1/auth/tokens/' + id, {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: { 'X-Requested-With': 'dragonflight-ui' },
+ });
+ await load();
+ };
+
+ return (
+
+ API Tokens
+
+ {justCreated && (
+
+
+ Save this token now — it will not be shown again
+
+
{justCreated.token}
+
+
+
+ )}
+
+
+ setName(e.target.value)} style={{ flex: 1 }} />
+
+
+
+
+ {tokens.length === 0 &&
No tokens yet.
}
+ {tokens.map(t => (
+
+
{t.name}
+
{t.prefix}…
+
{t.last_used_at ? new Date(t.last_used_at).toLocaleString() : 'never used'}
+
+
+ ))}
+
+
+ );
+}
+
function Settings() {
- const [section, setSection] = React.useState('storage');
+ const [section, setSection] = React.useState('account');
const SECTIONS = [
+ { id: 'account', label: 'Account', icon: 'user' },
{ id: 'storage', label: 'Storage', icon: 'hdd' },
{ id: 'proxy', label: 'Proxy encoding', icon: 'gpu' },
{ id: 'sdk', label: 'Capture SDKs', icon: 'video' },
@@ -1435,6 +1562,12 @@ function Settings() {
))}
+ {section === 'account' && (
+ <>
+
+
+ >
+ )}
{section === 'storage' &&
}
{section === 'proxy' &&
}
{section === 'sdk' &&
}