feat: 增加 manager 角色,admin+manager 共享管理权限(用户管理除外),所有用户可自行修改密码
- 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 迁移
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -25,7 +25,7 @@ function renderTable(devices) {
|
||||
<td>${d.last_login || '-'}</td>
|
||||
<td>
|
||||
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
||||
${USER_ROLE === 'admin' ? `<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>` : ''}
|
||||
${USER_ROLE === 'admin' || USER_ROLE === 'manager' ? `<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
<nav class="top-menu">
|
||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
||||
<a href="/test-data" class="{% if request.path == '/test-data' %}active{% endif %}">测试信息</a>
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||
{% if current_user.is_authenticated and current_user.role in ('admin', 'manager') %}
|
||||
<a href="/device-logs" class="{% if request.path == '/device-logs' %}active{% endif %}">设备日志</a>
|
||||
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
||||
{% endif %}
|
||||
<span class="user-info">
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ current_user.username }} ({{ current_user.role }})
|
||||
<a href="/change-password">修改密码</a>
|
||||
<a href="/logout">退出</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
38
edc-web/app/templates/change_password.html
Normal file
38
edc-web/app/templates/change_password.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}修改密码 - EDC 工装管理系统{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width:400px;margin:40px auto;">
|
||||
<h2>修改密码</h2>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div style="background:#fef3e2;color:#b45309;padding:10px;border-radius:6px;margin-bottom:16px;">
|
||||
{% for msg in messages %}{{ msg }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div>
|
||||
<label>当前密码</label>
|
||||
<input type="password" name="old_password" required
|
||||
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||
</div>
|
||||
<div>
|
||||
<label>新密码(至少6位)</label>
|
||||
<input type="password" name="new_password" required minlength="6"
|
||||
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||
</div>
|
||||
<div>
|
||||
<label>确认新密码</label>
|
||||
<input type="password" name="confirm_password" required minlength="6"
|
||||
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<a href="/" style="line-height:36px;">← 返回</a>
|
||||
<button type="submit" class="btn-search" style="padding:8px 24px;">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -27,7 +27,7 @@
|
||||
</label>
|
||||
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||||
<button onclick="exportCSV()" class="btn-export">导出 CSV</button>
|
||||
{% if current_user.role == 'admin' %}
|
||||
{% if current_user.role in ('admin', 'manager') %}
|
||||
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
||||
{% if current_user.role == 'admin' %}
|
||||
{% if current_user.role in ('admin', 'manager') %}
|
||||
<button id="btn-delete" class="btn-delete" onclick="confirmDelete()">🗑 删除</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<label>角色:
|
||||
<select id="new-role" style="margin:0 8px;">
|
||||
<option value="operator">operator</option>
|
||||
<option value="manager">manager</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -51,6 +52,7 @@ async function loadUsers() {
|
||||
<td>
|
||||
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
||||
<option value="operator" ${u.role==='operator'?'selected':''}>operator</option>
|
||||
<option value="manager" ${u.role==='manager'?'selected':''}>manager</option>
|
||||
<option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
|
||||
</select>
|
||||
<select onchange="updateUser(${u.id}, this, 'is_active')" data-field="is_active">
|
||||
|
||||
Submodule edc_server updated: cdddfac609...25aafd57c8
Reference in New Issue
Block a user