Remove API URL UI; add username/password change in settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,18 +1,11 @@
|
||||
const API_KEY = "eth_hedge_api_base";
|
||||
const TOKEN_KEY = "eth_hedge_token";
|
||||
const USER_KEY = "eth_hedge_user";
|
||||
|
||||
/** 始终同源(经 dc.hyf2.cc 反代),不再暴露可改 API 地址。 */
|
||||
export function getApiBase(): string {
|
||||
const saved = localStorage.getItem(API_KEY);
|
||||
if (saved && saved.trim()) return saved.trim().replace(/\/$/, "");
|
||||
// same-origin default when UI is served by backend on :5155
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
export function setApiBase(url: string) {
|
||||
localStorage.setItem(API_KEY, url.trim().replace(/\/$/, ""));
|
||||
}
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
@@ -25,6 +18,7 @@ export function setSession(token: string, username: string) {
|
||||
export function clearSession() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
localStorage.removeItem("eth_hedge_api_base");
|
||||
}
|
||||
|
||||
export function getUsername(): string | null {
|
||||
@@ -80,6 +74,17 @@ export async function login(username: string, password: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function changeCredentials(input: {
|
||||
current_password: string;
|
||||
new_username: string;
|
||||
new_password: string;
|
||||
}) {
|
||||
return apiFetch<LoginResult>("/api/auth/change-credentials", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export type MarketSnapshot = {
|
||||
connected: boolean;
|
||||
updated_at_ms: number | null;
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getApiBase, login, setApiBase, setSession } from "../api/client";
|
||||
import { login, setSession } from "../api/client";
|
||||
|
||||
export default function LoginPage() {
|
||||
const nav = useNavigate();
|
||||
const [apiBase, setApi] = useState(getApiBase());
|
||||
const [username, setUsername] = useState("admin");
|
||||
const [password, setPassword] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const hint = useMemo(
|
||||
() => "默认同域 API。测试环境反代:https://dc.hyf2.cc",
|
||||
[],
|
||||
);
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setErr("");
|
||||
setLoading(true);
|
||||
try {
|
||||
setApiBase(apiBase);
|
||||
const res = await login(username.trim(), password);
|
||||
setSession(res.token, res.username);
|
||||
nav("/plan", { replace: true });
|
||||
@@ -35,19 +28,8 @@ export default function LoginPage() {
|
||||
<div className="login-wrap">
|
||||
<form className="login-box" onSubmit={onSubmit}>
|
||||
<h1>eth_hedge_sim</h1>
|
||||
<p>模拟盘登录 · 可自定义 API 地址</p>
|
||||
<p>模拟盘登录</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
<div className="field">
|
||||
<label htmlFor="api">API 地址</label>
|
||||
<input
|
||||
id="api"
|
||||
className="mono"
|
||||
value={apiBase}
|
||||
onChange={(e) => setApi(e.target.value)}
|
||||
placeholder="https://dc.hyf2.cc"
|
||||
autoComplete="url"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="user">用户名</label>
|
||||
<input
|
||||
@@ -72,7 +54,6 @@ export default function LoginPage() {
|
||||
<button className="btn block" type="submit" disabled={loading}>
|
||||
{loading ? "登录中…" : "登录"}
|
||||
</button>
|
||||
<div className="hint">{hint}</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,37 +1,99 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { getApiBase, setApiBase } from "../api/client";
|
||||
import { changeCredentials, getUsername, setSession } from "../api/client";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [apiBase, setApi] = useState(getApiBase());
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [ok, setOk] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
function onSave(e: FormEvent) {
|
||||
async function onSave(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setApiBase(apiBase);
|
||||
setSaved(true);
|
||||
window.setTimeout(() => setSaved(false), 1500);
|
||||
setErr("");
|
||||
setOk("");
|
||||
if (newPassword !== confirmPassword) {
|
||||
setErr("两次输入的新密码不一致");
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 4) {
|
||||
setErr("新密码至少 4 位");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await changeCredentials({
|
||||
current_password: currentPassword,
|
||||
new_username: newUsername.trim(),
|
||||
new_password: newPassword,
|
||||
});
|
||||
setSession(res.token, res.username);
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setOk("用户名/密码已更新");
|
||||
} catch (ex) {
|
||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ maxWidth: 560 }}>
|
||||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||
<p style={{ color: "var(--muted)" }}>
|
||||
前端可单独指定后端 API;测试环境反代为 https://dc.hyf2.cc(同源可留空当前域名)。
|
||||
</p>
|
||||
<p style={{ color: "var(--muted)" }}>修改登录用户名与密码(写入服务器 .env,立即生效)。</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||||
<form onSubmit={onSave}>
|
||||
<div className="field">
|
||||
<label htmlFor="api">API 地址</label>
|
||||
<label htmlFor="user">新用户名</label>
|
||||
<input
|
||||
id="api"
|
||||
className="mono"
|
||||
value={apiBase}
|
||||
onChange={(e) => setApi(e.target.value)}
|
||||
id="user"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit">
|
||||
保存
|
||||
<div className="field">
|
||||
<label htmlFor="cur">当前密码</label>
|
||||
<input
|
||||
id="cur"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="np">新密码</label>
|
||||
<input
|
||||
id="np"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cp">确认新密码</label>
|
||||
<input
|
||||
id="cp"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit" disabled={loading}>
|
||||
{loading ? "保存中…" : "保存"}
|
||||
</button>
|
||||
{saved ? <span style={{ marginLeft: 10, color: "var(--up)" }}>已保存</span> : null}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user