Allow changing username and password in settings; admin can delete duplicate accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-17 19:05:41 +08:00
parent 972d5a1e8c
commit d25a4c6862
6 changed files with 137 additions and 20 deletions
+58 -1
View File
@@ -387,21 +387,78 @@ def verify_login(username: str, password: str) -> Optional[dict]:
def change_user_password(user_id: int, old_password: str, new_password: str) -> None: def change_user_password(user_id: int, old_password: str, new_password: str) -> None:
change_user_credentials(user_id, old_password, new_username=None, new_password=new_password)
def change_user_credentials(
user_id: int,
old_password: str,
new_username: Optional[str] = None,
new_password: Optional[str] = None,
) -> dict:
"""校验当前密码后,可改用户名和/或密码。返回更新后的用户信息。"""
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
if len(new_password) < 6: new_username = (new_username or "").strip() or None
new_password = new_password if new_password else None
if new_password is not None and len(new_password) < 6:
raise ValueError("新密码至少 6 位") raise ValueError("新密码至少 6 位")
if new_username is not None and len(new_username) < 2:
raise ValueError("用户名至少 2 位")
with get_db() as conn: with get_db() as conn:
user = get_user(conn, user_id) user = get_user(conn, user_id)
if not user: if not user:
raise ValueError("用户不存在") raise ValueError("用户不存在")
if not check_password_hash(user["password_hash"], old_password): if not check_password_hash(user["password_hash"], old_password):
raise ValueError("当前密码不正确") raise ValueError("当前密码不正确")
username_changed = bool(new_username and new_username != user["username"])
password_changed = new_password is not None
if not username_changed and not password_changed:
raise ValueError("请修改用户名或填写新密码")
if username_changed:
taken = conn.execute(
"SELECT id FROM users WHERE username=? AND id!=?",
(new_username, user_id),
).fetchone()
if taken:
raise ValueError("用户名已被占用")
conn.execute("UPDATE users SET username=? WHERE id=?", (new_username, user_id))
if password_changed:
conn.execute( conn.execute(
"UPDATE users SET password_hash=? WHERE id=?", "UPDATE users SET password_hash=? WHERE id=?",
(generate_password_hash(new_password), user_id), (generate_password_hash(new_password), user_id),
) )
# 同步旧 settings 键(兼容)
updated = get_user(conn, user_id)
if updated and updated.get("role") == "admin":
set_setting(conn, "admin_user", updated["username"])
if password_changed:
set_setting(conn, "admin_password_hash", updated["password_hash"])
return updated or user
def delete_account(conn: sqlite3.Connection, account_id: int) -> None:
"""删除交易账户;若仍被用户绑定则拒绝。"""
bound = conn.execute(
"SELECT id, username FROM users WHERE account_id=?", (account_id,)
).fetchone()
if bound:
raise ValueError(f"账户已绑定用户 {bound['username']},请先调整用户绑定")
pending = conn.execute(
"SELECT COUNT(*) AS c FROM activations WHERE account_id=? AND status='pending'",
(account_id,),
).fetchone()["c"]
if pending:
raise ValueError("该账户仍有未使用的激活码,请先作废或用完后再删")
if not conn.execute("SELECT id FROM accounts WHERE id=?", (account_id,)).fetchone():
raise ValueError("账户不存在")
conn.execute("DELETE FROM accounts WHERE id=?", (account_id,))
def create_user( def create_user(
conn: sqlite3.Connection, conn: sqlite3.Connection,
+45
View File
@@ -95,9 +95,43 @@ def register_routes(app: Flask) -> None:
def settings_page(): def settings_page():
return render_template("settings.html") return render_template("settings.html")
@app.post("/settings/profile")
@login_required
def settings_profile():
old_pwd = request.form.get("old_password") or ""
new_username = (request.form.get("username") or "").strip()
new_pwd = request.form.get("new_password") or ""
new_pwd2 = request.form.get("new_password2") or ""
if new_pwd or new_pwd2:
if new_pwd != new_pwd2:
flash("两次输入的新密码不一致", "error")
return redirect(url_for("settings_page"))
else:
new_pwd = None
try:
prev = session.get("username") or ""
updated = database.change_user_credentials(
int(session["user_id"]),
old_pwd,
new_username=new_username,
new_password=new_pwd,
)
session["username"] = updated["username"]
session["user"] = updated["username"]
parts = []
if updated["username"] != prev:
parts.append("用户名")
if new_pwd:
parts.append("密码")
flash(("、".join(parts) or "资料") + "已更新", "ok")
except ValueError as exc:
flash(str(exc), "error")
return redirect(url_for("settings_page"))
@app.post("/settings/password") @app.post("/settings/password")
@login_required @login_required
def settings_password(): def settings_password():
"""兼容旧表单:仅改密码。"""
old_pwd = request.form.get("old_password") or "" old_pwd = request.form.get("old_password") or ""
new_pwd = request.form.get("new_password") or "" new_pwd = request.form.get("new_password") or ""
new_pwd2 = request.form.get("new_password2") or "" new_pwd2 = request.form.get("new_password2") or ""
@@ -216,6 +250,17 @@ def register_routes(app: Flask) -> None:
flash("标注已保存", "ok") flash("标注已保存", "ok")
return redirect(url_for("accounts_page")) return redirect(url_for("accounts_page"))
@app.post("/accounts/<int:aid>/delete")
@admin_required
def accounts_delete(aid: int):
try:
with database.get_db() as conn:
database.delete_account(conn, aid)
flash("账户已删除", "ok")
except ValueError as exc:
flash(str(exc), "error")
return redirect(url_for("accounts_page"))
@app.get("/machines") @app.get("/machines")
@login_required @login_required
def machines_page(): def machines_page():
+9 -1
View File
@@ -39,7 +39,15 @@
<button class="btn secondary" style="margin-top:6px" type="submit">保存</button> <button class="btn secondary" style="margin-top:6px" type="submit">保存</button>
</form> </form>
</td> </td>
<td>#{{ a.id }}</td> <td>
#{{ a.id }}
{% if can_create %}
<form method="post" action="{{ url_for('accounts_delete', aid=a.id) }}" style="margin-top:8px"
onsubmit="return confirm('确认删除账户 #{{ a.id }} {{ a.name }}?');">
<button class="btn secondary" type="submit">删除</button>
</form>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
+1 -1
View File
@@ -27,7 +27,7 @@
<form method="post"> <form method="post">
<div class="form-row"> <div class="form-row">
<label>用户名</label> <label>用户名</label>
<input name="username" autocomplete="username" value="admin" required> <input name="username" autocomplete="username" placeholder="用户名" required>
</div> </div>
<div class="form-row"> <div class="form-row">
<label>密码</label> <label>密码</label>
+14 -9
View File
@@ -4,29 +4,34 @@
<h2 style="margin:0 0 14px">系统设置</h2> <h2 style="margin:0 0 14px">系统设置</h2>
<div class="grid cols-2"> <div class="grid cols-2">
<div class="card"> <div class="card">
<h3>修改登录密码</h3> <h3>修改登录账号</h3>
<p class="muted">修改当前登录用户密码,立即生效。管理员请在「用户」页创建普通用户。</p> <p class="muted">可改用户名和/或密码;须验证当前密码。立即生效。</p>
<form method="post" action="{{ url_for('settings_password') }}" style="margin-top:12px"> <form method="post" action="{{ url_for('settings_profile') }}" style="margin-top:12px">
<input type="hidden" name="_prev_username" value="{{ current_user.username }}">
<div class="form-row"> <div class="form-row">
<label>当前密码</label> <label>用户名</label>
<input name="username" value="{{ current_user.username }}" required minlength="2" autocomplete="username">
</div>
<div class="form-row">
<label>当前密码(必填)</label>
<input name="old_password" type="password" autocomplete="current-password" required> <input name="old_password" type="password" autocomplete="current-password" required>
</div> </div>
<div class="form-row"> <div class="form-row">
<label>新密码(至少 6 位)</label> <label>新密码(不改请留空,至少 6 位)</label>
<input name="new_password" type="password" autocomplete="new-password" required minlength="6"> <input name="new_password" type="password" autocomplete="new-password" minlength="6">
</div> </div>
<div class="form-row"> <div class="form-row">
<label>确认新密码</label> <label>确认新密码</label>
<input name="new_password2" type="password" autocomplete="new-password" required minlength="6"> <input name="new_password2" type="password" autocomplete="new-password" minlength="6">
</div> </div>
<button class="btn" type="submit">保存密码</button> <button class="btn" type="submit">保存</button>
</form> </form>
</div> </div>
<div class="card"> <div class="card">
<h3>服务信息</h3> <h3>服务信息</h3>
<p class="muted">对外地址:{{ public_base }}</p> <p class="muted">对外地址:{{ public_base }}</p>
<p class="muted">端口:5800</p> <p class="muted">端口:5800</p>
<p class="muted">当前登录用户:{{ session.get('user') }}</p> <p class="muted">当前登录用户:{{ current_user.username }}({{ current_user.role }})</p>
<p class="muted" style="margin-top:12px">通用 exe 放置目录:<code>releases/黄金市价辅助.exe</code></p> <p class="muted" style="margin-top:12px">通用 exe 放置目录:<code>releases/黄金市价辅助.exe</code></p>
</div> </div>
</div> </div>
+6 -4
View File
@@ -22,13 +22,15 @@
- **账户 / 用户 / 机器**:管理员可见;普通用户仅「我的账户」 - **账户 / 用户 / 机器**:管理员可见;普通用户仅「我的账户」
- **系统设置**:修改当前登录用户密码 - **系统设置**:修改当前登录用户密码
### 修改密码 ### 修改用户名 / 密码
1. 登录后打开 **系统设置** 1. 登录后打开 **系统设置**
2. 输入当前密码与新密码(至少 6 位)→ 保存 2. 可改 **用户名**;改密码则填写新密码(至少 6 位,不改留空)
3. 下次登录使用新密码 3. 须填写 **当前密码** 验证 → 保存
4. 下次用新用户名/密码登录
> 初始管理员密码来自部署时的 `.env`(默认 admin123);修改后以系统设置为准。 > 初始管理员来自部署 `.env`(默认 `admin` / `admin123`);改过后以系统设置为准。
> 早期版本会种子创建 1 个示例交易账户「模拟盘-主」;现已不再自动创建。重复项可在账户页删除。
## 2. 管理员:建账户与用户 ## 2. 管理员:建账户与用户