From 2aec4636cba789851454a1da7fbeaf41d5ff16eb Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 27 May 2026 15:20:57 -0400 Subject: [PATCH] =?UTF-8?q?feat(web-ui):=20Settings=20=E2=86=92=20Account?= =?UTF-8?q?=20(change=20password)=20+=20API=20Tokens=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web-ui/public/screens-admin.jsx | 135 ++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) 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 ( +
+

Account

+
+ + setCurrent(e.target.value)} /> + + setNext(e.target.value)} /> + + setConfirm(e.target.value)} /> +
+ {msg && ( +
{msg.text}
+ )} + +
+ ); +} + +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' && }