feat: add system settings page for admin username and password

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 08:56:54 +08:00
parent 4743f3efdd
commit b5cba83df4
20 changed files with 647 additions and 117 deletions
+2 -15
View File
@@ -1,13 +1,12 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
fetchAuthStatus,
fetchHealth,
fetchLatest,
Health,
Latest,
logout,
} from "../api/client";
import AppNav from "../components/AppNav";
import LoginGate from "../components/LoginGate";
function fmt(n?: number | null, d = 1) {
@@ -94,19 +93,7 @@ export default function Dashboard() {
<div className="layout">
<div className="brand"></div>
<div className="sub"> · = ÷ · Asia/Shanghai</div>
<div className="nav">
<Link to="/"></Link>
<Link to="/ops-map"></Link>
<a
href="#logout"
onClick={(e) => {
e.preventDefault();
logout().finally(() => setNeedLogin(true));
}}
>
退
</a>
</div>
<AppNav />
{err && <p style={{ color: "#e85d5d" }}>{err}</p>}
<div className="row">
<div className="tile">
+12 -30
View File
@@ -1,13 +1,12 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
fetchAuthStatus,
fetchAccountSettings,
fetchOpsMap,
LeverageStats,
logout,
MovePointsStats,
OpsMap,
} from "../api/client";
import AppNav from "../components/AppNav";
import LeverageChart from "../components/LeverageChart";
import LoginGate from "../components/LoginGate";
import MovePointsChart from "../components/MovePointsChart";
@@ -35,22 +34,17 @@ export default function OpsMapPage() {
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchAuthStatus()
.then(async (st) => {
if (!st.auth_required) {
setNeedLogin(false);
setReady(true);
return;
}
try {
await fetchOpsMap({ range: "day", bucket_minutes: 60 });
setNeedLogin(false);
} catch {
setNeedLogin(true);
}
fetchAccountSettings()
.then(() => {
setNeedLogin(false);
setReady(true);
})
.catch((e) => setErr(String(e)));
.catch((e) => {
const msg = String(e);
if (msg.includes("unauthorized")) setNeedLogin(true);
else setErr(msg);
setReady(true);
});
}, []);
useEffect(() => {
@@ -96,19 +90,7 @@ export default function OpsMapPage() {
<div className="layout">
<div className="brand"></div>
<div className="sub"> · × </div>
<div className="nav">
<Link to="/"></Link>
<Link to="/ops-map"></Link>
<a
href="#logout"
onClick={(e) => {
e.preventDefault();
logout().finally(() => setNeedLogin(true));
}}
>
退
</a>
</div>
<AppNav />
<div className="toolbar">
<div className="seg">
+155
View File
@@ -0,0 +1,155 @@
import { FormEvent, useEffect, useState } from "react";
import {
fetchAccountSettings,
logout,
updateAccountSettings,
} from "../api/client";
import AppNav from "../components/AppNav";
import LoginGate from "../components/LoginGate";
export default function SettingsPage() {
const [needLogin, setNeedLogin] = useState(false);
const [ready, setReady] = useState(false);
const [username, setUsername] = useState("admin");
const [newUsername, setNewUsername] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [authRequired, setAuthRequired] = useState(true);
const [msg, setMsg] = useState<string | null>(null);
const [err, setErr] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
fetchAccountSettings()
.then((d) => {
setUsername(d.username);
setNewUsername(d.username);
setAuthRequired(d.auth_required);
setNeedLogin(false);
setReady(true);
})
.catch((e) => {
const m = String(e);
if (m.includes("unauthorized")) setNeedLogin(true);
else setErr(m);
setReady(true);
});
}, []);
const submit = async (e: FormEvent) => {
e.preventDefault();
setBusy(true);
setErr(null);
setMsg(null);
if (newPassword && newPassword !== confirmPassword) {
setErr("两次输入的新密码不一致");
setBusy(false);
return;
}
try {
const res = await updateAccountSettings({
current_password: currentPassword,
new_username: newUsername !== username ? newUsername : undefined,
new_password: newPassword || undefined,
});
if (res.relogin_required) {
await logout();
setMsg(res.message || "已保存,请重新登录");
setNeedLogin(true);
return;
}
setMsg(res.message || "已保存");
setUsername(res.username);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (ex) {
setErr(String(ex));
} finally {
setBusy(false);
}
};
if (!ready) {
return (
<div className="layout">
<div className="sub"></div>
</div>
);
}
if (needLogin) {
return <LoginGate onOk={() => window.location.reload()} />;
}
return (
<div className="layout">
<div className="brand"></div>
<div className="sub"> · </div>
<AppNav />
<div className="center-page">
<form className="tile settings-card" onSubmit={submit}>
<div className="settings-title"></div>
{!authRequired && (
<p className="settings-hint">AUTH_SECRET=disabled .env </p>
)}
<label className="field">
<span className="label"></span>
<input
className="field-input"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
autoComplete="username"
disabled={!authRequired}
/>
</label>
<label className="field">
<span className="label"></span>
<input
className="field-input"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="current-password"
disabled={!authRequired}
/>
</label>
<label className="field">
<span className="label"></span>
<input
className="field-input"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
disabled={!authRequired}
/>
</label>
<label className="field">
<span className="label"></span>
<input
className="field-input"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="new-password"
disabled={!authRequired}
/>
</label>
{err && <p className="form-err">{err}</p>}
{msg && <p className="form-ok">{msg}</p>}
<button className="btn-primary" type="submit" disabled={busy || !authRequired || !currentPassword}>
{busy ? "保存中…" : "保存"}
</button>
</form>
</div>
</div>
);
}