From 000e4f8d3a2eb06cd19ef5483b0f8247c8dfbbf1 Mon Sep 17 00:00:00 2001 From: wangfq Date: Thu, 11 Jun 2026 09:11:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20manager=20?= =?UTF-8?q?=E8=A7=92=E8=89=B2=EF=BC=8Cadmin+manager=20=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=9D=83=E9=99=90=EF=BC=88=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=99=A4=E5=A4=96=EF=BC=89=EF=BC=8C=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=94=A8=E6=88=B7=E5=8F=AF=E8=87=AA=E8=A1=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.py: 新增 privileged_required 装饰器 (admin+manager),admin_required 仅限用户管理 - 路由权限: fixture/logs/device_logs/test_data 的 admin 检查改为 admin+manager - 前端: 导航栏/删除按钮/配置按钮扩展为 admin+manager 可见 - 用户管理: 角色下拉增加 manager 选项,仍仅 admin 可访问 - 新增 /change-password 路由+模板,所有登录用户可自行修改密码 - edc_server models.py: role COMMENT 更新 + ALTER TABLE 迁移 --- edc-web/app/auth.py | 64 +++++++++++++++++++++- edc-web/app/routes/device_logs.py | 2 +- edc-web/app/routes/fixture.py | 6 +- edc-web/app/routes/logs.py | 6 +- edc-web/app/routes/test_data.py | 2 +- edc-web/app/static/js/devices.js | 2 +- edc-web/app/templates/base.html | 5 +- edc-web/app/templates/change_password.html | 38 +++++++++++++ edc-web/app/templates/device_logs.html | 2 +- edc-web/app/templates/test_data.html | 2 +- edc-web/app/templates/users.html | 2 + edc_server | 2 +- 12 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 edc-web/app/templates/change_password.html diff --git a/edc-web/app/auth.py b/edc-web/app/auth.py index 65c310a..8d74806 100644 --- a/edc-web/app/auth.py +++ b/edc-web/app/auth.py @@ -46,7 +46,7 @@ def init_auth(app): # ─── 装饰器 ──────────────────────────────────────────────────────── def admin_required(f): - """要求 admin 角色""" + """要求 admin 角色(仅 admin,manager 不可通过)""" from functools import wraps @wraps(f) @login_required @@ -57,6 +57,18 @@ def admin_required(f): return wrapper +def privileged_required(f): + """要求 admin 或 manager 角色""" + from functools import wraps + @wraps(f) + @login_required + def wrapper(*args, **kwargs): + if current_user.role not in ("admin", "manager"): + return "权限不足", 403 + return f(*args, **kwargs) + return wrapper + + # ─── 登录 / 登出 ──────────────────────────────────────────────────── @auth_bp.route("/login", methods=["GET", "POST"]) @@ -86,3 +98,53 @@ def logout(): ip=request.remote_addr or "", result="ok") logout_user() return redirect(url_for("auth.login")) + + +@auth_bp.route("/change-password", methods=["GET", "POST"]) +@login_required +def change_password(): + """所有用户自行修改密码""" + if request.method == "POST": + old_password = request.form.get("old_password", "") + new_password = request.form.get("new_password", "").strip() + confirm_password = request.form.get("confirm_password", "") + + if not old_password or not new_password: + flash("所有字段都不能为空") + return render_template("change_password.html") + + if len(new_password) < 6: + flash("新密码至少6位") + return render_template("change_password.html") + + if new_password != confirm_password: + flash("两次输入的新密码不一致") + return render_template("change_password.html") + + # 验证旧密码 + from app.models import get_conn, get_user_by_username + from werkzeug.security import generate_password_hash + user_dict = get_user_by_username(current_user.username) + if not user_dict or not check_password_hash(user_dict["password_hash"], old_password): + flash("原密码错误") + return render_template("change_password.html") + + # 更新密码 + conn = get_conn() + try: + with conn.cursor() as cur: + cur.execute( + "UPDATE tb_user SET password_hash=%s WHERE id=%s", + (generate_password_hash(new_password), current_user.id), + ) + conn.commit() + finally: + conn.close() + + insert_log(current_user.id, current_user.username, "update", + target="self", detail="修改个人密码", + result="ok", ip=request.remote_addr or "") + flash("密码修改成功") + return redirect(url_for("devices.index")) + + return render_template("change_password.html") diff --git a/edc-web/app/routes/device_logs.py b/edc-web/app/routes/device_logs.py index 71a9c01..1acef1b 100644 --- a/edc-web/app/routes/device_logs.py +++ b/edc-web/app/routes/device_logs.py @@ -72,7 +72,7 @@ def api_export(): @login_required def api_device_logs_delete(): """删除设备日志(admin 权限)""" - if current_user.role != "admin": + if current_user.role not in ("admin", "manager"): return jsonify({"ok": False, "error": "无权限"}), 403 data = request.get_json() diff --git a/edc-web/app/routes/fixture.py b/edc-web/app/routes/fixture.py index 0333214..880cadc 100644 --- a/edc-web/app/routes/fixture.py +++ b/edc-web/app/routes/fixture.py @@ -114,7 +114,7 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int, @login_required def fixture_page(dnt_id): """工装配置页面""" - if current_user.role != "admin": + if current_user.role not in ("admin", "manager"): return "无权限:仅管理员可访问工装配置", 403 device = get_device_by_id(dnt_id) if not device: @@ -135,7 +135,7 @@ def vehicle_base_test_page(): @login_required def api_fixture_command(): """发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)""" - if current_user.role != "admin": + if current_user.role not in ("admin", "manager"): return jsonify({"ok": False, "error": "无权限:仅管理员可执行工装指令"}), 403 data = request.get_json() dnt_id = data.get("dnt_id") @@ -226,7 +226,7 @@ def api_get_fixture_param(dnt_id): @login_required def api_save_fixture_param(dnt_id): """保存工装测试参数(仅数据库,不下发设备)""" - if current_user.role != "admin": + if current_user.role not in ("admin", "manager"): return jsonify({"ok": False, "error": "无权限:仅管理员可修改工装参数"}), 403 data = request.get_json() if not data: diff --git a/edc-web/app/routes/logs.py b/edc-web/app/routes/logs.py index 405dc57..61aec41 100644 --- a/edc-web/app/routes/logs.py +++ b/edc-web/app/routes/logs.py @@ -2,20 +2,20 @@ from flask import Blueprint, jsonify, render_template, request from flask_login import login_required -from app.auth import admin_required +from app.auth import privileged_required from app.models import get_logs bp = Blueprint("logs", __name__, url_prefix="/logs") @bp.route("/") -@admin_required +@privileged_required def logs_page(): return render_template("logs.html") @bp.route("/api/logs") -@admin_required +@privileged_required def api_logs(): page = request.args.get("page", 1, type=int) per_page = request.args.get("per_page", 30, type=int) diff --git a/edc-web/app/routes/test_data.py b/edc-web/app/routes/test_data.py index 1b640f3..70cb2cb 100644 --- a/edc-web/app/routes/test_data.py +++ b/edc-web/app/routes/test_data.py @@ -84,7 +84,7 @@ def api_export(): @login_required def api_delete(): """删除测试数据(仅 admin)""" - if current_user.role != "admin": + if current_user.role not in ("admin", "manager"): return jsonify({"ok": False, "error": "无权限"}), 403 data = request.get_json() or {} diff --git a/edc-web/app/static/js/devices.js b/edc-web/app/static/js/devices.js index 2b76e80..605bd3e 100644 --- a/edc-web/app/static/js/devices.js +++ b/edc-web/app/static/js/devices.js @@ -25,7 +25,7 @@ function renderTable(devices) { ${d.last_login || '-'} - ${USER_ROLE === 'admin' ? `` : ''} + ${USER_ROLE === 'admin' || USER_ROLE === 'manager' ? `` : ''} `; }).join(""); diff --git a/edc-web/app/templates/base.html b/edc-web/app/templates/base.html index b4a3220..25fc311 100644 --- a/edc-web/app/templates/base.html +++ b/edc-web/app/templates/base.html @@ -10,14 +10,17 @@