Add roles/ACL so admin and users can issue activation codes; exe opens review login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+167
-44
@@ -4,11 +4,11 @@ from __future__ import annotations
|
||||
import json
|
||||
import secrets
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
flash,
|
||||
g,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
@@ -25,24 +25,48 @@ from app import db as database
|
||||
def login_required(view):
|
||||
@wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not session.get("user"):
|
||||
if not session.get("user_id"):
|
||||
return redirect(url_for("login", next=request.path))
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def admin_required(view):
|
||||
@wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not session.get("user_id"):
|
||||
return redirect(url_for("login", next=request.path))
|
||||
if session.get("role") != "admin":
|
||||
flash("需要管理员权限", "error")
|
||||
return redirect(url_for("dashboard"))
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def current_user() -> dict:
|
||||
return {
|
||||
"id": session.get("user_id"),
|
||||
"username": session.get("username"),
|
||||
"role": session.get("role"),
|
||||
"account_id": session.get("account_id"),
|
||||
}
|
||||
|
||||
|
||||
def register_routes(app: Flask) -> None:
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
return {
|
||||
"app_name": "Gold Monitor",
|
||||
"public_base": Config.PUBLIC_BASE_URL,
|
||||
"current_user": current_user(),
|
||||
"is_admin": session.get("role") == "admin",
|
||||
}
|
||||
|
||||
@app.get("/login")
|
||||
def login():
|
||||
if session.get("user"):
|
||||
if session.get("user_id"):
|
||||
return redirect(url_for("dashboard"))
|
||||
return render_template("login.html")
|
||||
|
||||
@@ -50,11 +74,16 @@ def register_routes(app: Flask) -> None:
|
||||
def login_post():
|
||||
user = (request.form.get("username") or "").strip()
|
||||
pwd = request.form.get("password") or ""
|
||||
if database.verify_admin(user, pwd):
|
||||
session["user"] = user
|
||||
return redirect(request.args.get("next") or url_for("dashboard"))
|
||||
flash("用户名或密码错误", "error")
|
||||
return render_template("login.html"), 401
|
||||
u = database.verify_login(user, pwd)
|
||||
if not u:
|
||||
flash("用户名或密码错误", "error")
|
||||
return render_template("login.html"), 401
|
||||
session["user_id"] = u["id"]
|
||||
session["username"] = u["username"]
|
||||
session["role"] = u["role"]
|
||||
session["account_id"] = u.get("account_id")
|
||||
session["user"] = u["username"] # 兼容旧模板
|
||||
return redirect(request.args.get("next") or url_for("dashboard"))
|
||||
|
||||
@app.get("/logout")
|
||||
def logout():
|
||||
@@ -76,19 +105,62 @@ def register_routes(app: Flask) -> None:
|
||||
flash("两次输入的新密码不一致", "error")
|
||||
return redirect(url_for("settings_page"))
|
||||
try:
|
||||
database.change_admin_password(old_pwd, new_pwd)
|
||||
database.change_user_password(int(session["user_id"]), old_pwd, new_pwd)
|
||||
flash("密码已更新,请牢记新密码", "ok")
|
||||
except ValueError as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(url_for("settings_page"))
|
||||
|
||||
# ---------- 用户管理(仅管理员)----------
|
||||
@app.get("/users")
|
||||
@admin_required
|
||||
def users_page():
|
||||
with database.get_db() as conn:
|
||||
users = database.list_users(conn)
|
||||
accounts = database.list_accounts(conn)
|
||||
# 未被占用的账户(用于新建普通用户)
|
||||
used = {u["account_id"] for u in users if u.get("account_id")}
|
||||
free_accounts = [a for a in accounts if a["id"] not in used]
|
||||
return render_template(
|
||||
"users.html",
|
||||
users=users,
|
||||
accounts=accounts,
|
||||
free_accounts=free_accounts,
|
||||
)
|
||||
|
||||
@app.post("/users")
|
||||
@admin_required
|
||||
def users_create():
|
||||
username = (request.form.get("username") or "").strip()
|
||||
password = request.form.get("password") or ""
|
||||
role = (request.form.get("role") or "user").strip()
|
||||
account_raw = request.form.get("account_id") or ""
|
||||
account_id = int(account_raw) if account_raw.strip().isdigit() else None
|
||||
try:
|
||||
with database.get_db() as conn:
|
||||
database.create_user(conn, username, password, role, account_id)
|
||||
flash("用户已创建", "ok")
|
||||
except ValueError as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(url_for("users_page"))
|
||||
|
||||
@app.get("/")
|
||||
@login_required
|
||||
def dashboard():
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
accounts = database.list_accounts(conn)
|
||||
machines = database.list_machines(conn)
|
||||
trades = database.list_trades(conn, 20)
|
||||
if u["role"] == "admin":
|
||||
accounts = database.list_accounts(conn)
|
||||
machines = database.list_machines(conn)
|
||||
trades = database.list_trades(conn, 20)
|
||||
else:
|
||||
accounts = [
|
||||
a
|
||||
for a in database.list_accounts(conn)
|
||||
if a["id"] == u.get("account_id")
|
||||
]
|
||||
machines = database.list_machines(conn)
|
||||
trades = database.list_trades(conn, 20, account_id=u.get("account_id"))
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
accounts=accounts,
|
||||
@@ -96,16 +168,19 @@ def register_routes(app: Flask) -> None:
|
||||
trades=trades,
|
||||
)
|
||||
|
||||
# ---------- 账户 / 机器 ----------
|
||||
# ---------- 账户(仅管理员创建)----------
|
||||
@app.get("/accounts")
|
||||
@login_required
|
||||
def accounts_page():
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
items = database.list_accounts(conn)
|
||||
return render_template("accounts.html", items=items)
|
||||
if u["role"] != "admin":
|
||||
items = [a for a in items if a["id"] == u.get("account_id")]
|
||||
return render_template("accounts.html", items=items, can_create=(u["role"] == "admin"))
|
||||
|
||||
@app.post("/accounts")
|
||||
@login_required
|
||||
@admin_required
|
||||
def accounts_create():
|
||||
name = (request.form.get("name") or "").strip()
|
||||
if not name:
|
||||
@@ -125,12 +200,16 @@ def register_routes(app: Flask) -> None:
|
||||
database.now(),
|
||||
),
|
||||
)
|
||||
flash("账户已添加", "ok")
|
||||
flash("账户已创建(仅管理员可创建)", "ok")
|
||||
return redirect(url_for("accounts_page"))
|
||||
|
||||
@app.post("/accounts/<int:aid>/note")
|
||||
@login_required
|
||||
def accounts_note(aid: int):
|
||||
u = current_user()
|
||||
if not database.user_can_access_account(u, aid):
|
||||
flash("无权修改该账户", "error")
|
||||
return redirect(url_for("accounts_page"))
|
||||
note = (request.form.get("note") or "").strip()
|
||||
with database.get_db() as conn:
|
||||
conn.execute("UPDATE accounts SET note=? WHERE id=?", (note, aid))
|
||||
@@ -140,12 +219,26 @@ def register_routes(app: Flask) -> None:
|
||||
@app.get("/machines")
|
||||
@login_required
|
||||
def machines_page():
|
||||
# 机器列表:管理员全看;普通用户只看与自己账户激活相关的机器
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
items = database.list_machines(conn)
|
||||
return render_template("machines.html", items=items)
|
||||
if u["role"] == "admin":
|
||||
items = database.list_machines(conn)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT m.* FROM machines m
|
||||
JOIN activations act ON act.machine_id = m.id
|
||||
WHERE act.account_id=?
|
||||
ORDER BY m.id DESC
|
||||
""",
|
||||
(u.get("account_id"),),
|
||||
)
|
||||
items = [database.row_to_dict(r) for r in cur.fetchall()]
|
||||
return render_template("machines.html", items=items, is_admin=(u["role"] == "admin"))
|
||||
|
||||
@app.post("/machines")
|
||||
@login_required
|
||||
@admin_required
|
||||
def machines_create():
|
||||
name = (request.form.get("name") or "").strip() or "新机器"
|
||||
with database.get_db() as conn:
|
||||
@@ -170,22 +263,40 @@ def register_routes(app: Flask) -> None:
|
||||
flash("机器标注已保存", "ok")
|
||||
return redirect(url_for("machines_page"))
|
||||
|
||||
# ---------- 下载 / 激活 ----------
|
||||
# ---------- 下载 / 激活(管理员与用户都可发码;用户仅自己账户)----------
|
||||
@app.get("/download")
|
||||
@login_required
|
||||
def download_page():
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
accounts = database.list_accounts(conn)
|
||||
all_accounts = database.list_accounts(conn)
|
||||
if u["role"] == "admin":
|
||||
accounts = all_accounts
|
||||
else:
|
||||
accounts = [a for a in all_accounts if a["id"] == u.get("account_id")]
|
||||
machines = database.list_machines(conn)
|
||||
acts = conn.execute(
|
||||
"""
|
||||
SELECT act.*, a.name AS account_name, m.name AS machine_name
|
||||
FROM activations act
|
||||
LEFT JOIN accounts a ON a.id = act.account_id
|
||||
LEFT JOIN machines m ON m.id = act.machine_id
|
||||
ORDER BY act.id DESC LIMIT 50
|
||||
"""
|
||||
).fetchall()
|
||||
if u["role"] == "admin":
|
||||
acts = conn.execute(
|
||||
"""
|
||||
SELECT act.*, a.name AS account_name, m.name AS machine_name
|
||||
FROM activations act
|
||||
LEFT JOIN accounts a ON a.id = act.account_id
|
||||
LEFT JOIN machines m ON m.id = act.machine_id
|
||||
ORDER BY act.id DESC LIMIT 50
|
||||
"""
|
||||
).fetchall()
|
||||
else:
|
||||
acts = conn.execute(
|
||||
"""
|
||||
SELECT act.*, a.name AS account_name, m.name AS machine_name
|
||||
FROM activations act
|
||||
LEFT JOIN accounts a ON a.id = act.account_id
|
||||
LEFT JOIN machines m ON m.id = act.machine_id
|
||||
WHERE act.account_id=?
|
||||
ORDER BY act.id DESC LIMIT 50
|
||||
""",
|
||||
(u.get("account_id"),),
|
||||
).fetchall()
|
||||
activations = [database.row_to_dict(r) for r in acts]
|
||||
exe_name = "黄金市价辅助.exe"
|
||||
exe_path = RELEASES_DIR / exe_name
|
||||
@@ -201,15 +312,19 @@ def register_routes(app: Flask) -> None:
|
||||
@app.post("/download/activation")
|
||||
@login_required
|
||||
def download_create_activation():
|
||||
u = current_user()
|
||||
account_id = int(request.form.get("account_id") or 0)
|
||||
machine_raw = request.form.get("machine_id") or ""
|
||||
machine_id = int(machine_raw) if machine_raw.strip().isdigit() else None
|
||||
with database.get_db() as conn:
|
||||
if not conn.execute("SELECT id FROM accounts WHERE id=?", (account_id,)).fetchone():
|
||||
flash("账户不存在", "error")
|
||||
return redirect(url_for("download_page"))
|
||||
code = database.create_activation(conn, account_id, machine_id)
|
||||
flash(f"已生成激活码:{code}", "ok")
|
||||
if not database.user_can_access_account(u, account_id):
|
||||
flash("只能为自己的账户发放激活码", "error")
|
||||
return redirect(url_for("download_page"))
|
||||
try:
|
||||
with database.get_db() as conn:
|
||||
code = database.create_activation(conn, account_id, machine_id, u.get("id"))
|
||||
flash(f"已生成激活码:{code}", "ok")
|
||||
except ValueError as exc:
|
||||
flash(str(exc), "error")
|
||||
return redirect(url_for("download_page"))
|
||||
|
||||
@app.get("/download/file")
|
||||
@@ -218,12 +333,10 @@ def register_routes(app: Flask) -> None:
|
||||
name = "黄金市价辅助.exe"
|
||||
path = RELEASES_DIR / name
|
||||
if not path.is_file():
|
||||
# 提供占位说明文件,避免空目录无下载
|
||||
readme = RELEASES_DIR / "README_PLACE_EXE_HERE.txt"
|
||||
if not readme.is_file():
|
||||
readme.write_text(
|
||||
"请将打包好的通用辅助 exe 放到本目录,命名为:黄金市价辅助.exe\n"
|
||||
"客户端首次启动使用网页生成的激活码绑定机器与账户。\n",
|
||||
"请将打包好的通用辅助 exe 放到本目录,命名为:黄金市价辅助.exe\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return send_from_directory(RELEASES_DIR, readme.name, as_attachment=True)
|
||||
@@ -258,7 +371,6 @@ def register_routes(app: Flask) -> None:
|
||||
)
|
||||
return jsonify({"ok": True, "server_time": database.now()})
|
||||
|
||||
# ---------- 成交上报 / 复盘 ----------
|
||||
@app.post("/api/v1/trades")
|
||||
def api_trades_upsert():
|
||||
token = request.headers.get("X-Device-Token") or ""
|
||||
@@ -267,6 +379,8 @@ def register_routes(app: Flask) -> None:
|
||||
act = database.auth_token(conn, token)
|
||||
if not act:
|
||||
return jsonify({"ok": False, "error": "未激活"}), 401
|
||||
if not act.get("account_id"):
|
||||
return jsonify({"ok": False, "error": "激活码未绑定账户"}), 400
|
||||
snapshot = data.get("snapshot") or {}
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -326,19 +440,23 @@ def register_routes(app: Flask) -> None:
|
||||
@app.get("/records")
|
||||
@login_required
|
||||
def records_page():
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
trades = database.list_trades(conn, 300)
|
||||
if u["role"] == "admin":
|
||||
trades = database.list_trades(conn, 300)
|
||||
else:
|
||||
trades = database.list_trades(conn, 300, account_id=u.get("account_id"))
|
||||
return render_template("records.html", trades=trades)
|
||||
|
||||
@app.get("/records/<int:tid>")
|
||||
@login_required
|
||||
def record_detail(tid: int):
|
||||
u = current_user()
|
||||
with database.get_db() as conn:
|
||||
trade = database.get_trade(conn, tid)
|
||||
if not trade:
|
||||
flash("记录不存在", "error")
|
||||
if not trade or not database.user_can_access_account(u, trade.get("account_id")):
|
||||
flash("记录不存在或无权查看", "error")
|
||||
return redirect(url_for("records_page"))
|
||||
# 四周期:以开仓时间为中心取前后窗口
|
||||
center = trade.get("open_time") or trade.get("created_at") or database.now()
|
||||
window = 3600 * 6
|
||||
charts = {}
|
||||
@@ -351,9 +469,14 @@ def register_routes(app: Flask) -> None:
|
||||
@app.post("/records/<int:tid>/annotate")
|
||||
@login_required
|
||||
def record_annotate(tid: int):
|
||||
u = current_user()
|
||||
entry_style = (request.form.get("entry_style") or "").strip()
|
||||
note = (request.form.get("review_note") or "").strip()
|
||||
with database.get_db() as conn:
|
||||
trade = database.get_trade(conn, tid)
|
||||
if not trade or not database.user_can_access_account(u, trade.get("account_id")):
|
||||
flash("无权标注该记录", "error")
|
||||
return redirect(url_for("records_page"))
|
||||
conn.execute(
|
||||
"UPDATE trades SET entry_style=?, review_note=? WHERE id=?",
|
||||
(entry_style, note, tid),
|
||||
|
||||
Reference in New Issue
Block a user