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):
|
def admin_required(f):
|
||||||
"""要求 admin 角色"""
|
"""要求 admin 角色(仅 admin,manager 不可通过)"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
@login_required
|
@login_required
|
||||||
@@ -57,6 +57,18 @@ def admin_required(f):
|
|||||||
return wrapper
|
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"])
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
@@ -86,3 +98,53 @@ def logout():
|
|||||||
ip=request.remote_addr or "", result="ok")
|
ip=request.remote_addr or "", result="ok")
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("auth.login"))
|
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
|
@login_required
|
||||||
def api_device_logs_delete():
|
def api_device_logs_delete():
|
||||||
"""删除设备日志(admin 权限)"""
|
"""删除设备日志(admin 权限)"""
|
||||||
if current_user.role != "admin":
|
if current_user.role not in ("admin", "manager"):
|
||||||
return jsonify({"ok": False, "error": "无权限"}), 403
|
return jsonify({"ok": False, "error": "无权限"}), 403
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
|||||||
@login_required
|
@login_required
|
||||||
def fixture_page(dnt_id):
|
def fixture_page(dnt_id):
|
||||||
"""工装配置页面"""
|
"""工装配置页面"""
|
||||||
if current_user.role != "admin":
|
if current_user.role not in ("admin", "manager"):
|
||||||
return "无权限:仅管理员可访问工装配置", 403
|
return "无权限:仅管理员可访问工装配置", 403
|
||||||
device = get_device_by_id(dnt_id)
|
device = get_device_by_id(dnt_id)
|
||||||
if not device:
|
if not device:
|
||||||
@@ -135,7 +135,7 @@ def vehicle_base_test_page():
|
|||||||
@login_required
|
@login_required
|
||||||
def api_fixture_command():
|
def api_fixture_command():
|
||||||
"""发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)"""
|
"""发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)"""
|
||||||
if current_user.role != "admin":
|
if current_user.role not in ("admin", "manager"):
|
||||||
return jsonify({"ok": False, "error": "无权限:仅管理员可执行工装指令"}), 403
|
return jsonify({"ok": False, "error": "无权限:仅管理员可执行工装指令"}), 403
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
dnt_id = data.get("dnt_id")
|
dnt_id = data.get("dnt_id")
|
||||||
@@ -226,7 +226,7 @@ def api_get_fixture_param(dnt_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def api_save_fixture_param(dnt_id):
|
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
|
return jsonify({"ok": False, "error": "无权限:仅管理员可修改工装参数"}), 403
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
from flask import Blueprint, jsonify, render_template, request
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.auth import admin_required
|
from app.auth import privileged_required
|
||||||
from app.models import get_logs
|
from app.models import get_logs
|
||||||
|
|
||||||
bp = Blueprint("logs", __name__, url_prefix="/logs")
|
bp = Blueprint("logs", __name__, url_prefix="/logs")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_required
|
@privileged_required
|
||||||
def logs_page():
|
def logs_page():
|
||||||
return render_template("logs.html")
|
return render_template("logs.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/logs")
|
@bp.route("/api/logs")
|
||||||
@admin_required
|
@privileged_required
|
||||||
def api_logs():
|
def api_logs():
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
per_page = request.args.get("per_page", 30, type=int)
|
per_page = request.args.get("per_page", 30, type=int)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def api_export():
|
|||||||
@login_required
|
@login_required
|
||||||
def api_delete():
|
def api_delete():
|
||||||
"""删除测试数据(仅 admin)"""
|
"""删除测试数据(仅 admin)"""
|
||||||
if current_user.role != "admin":
|
if current_user.role not in ("admin", "manager"):
|
||||||
return jsonify({"ok": False, "error": "无权限"}), 403
|
return jsonify({"ok": False, "error": "无权限"}), 403
|
||||||
|
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function renderTable(devices) {
|
|||||||
<td>${d.last_login || '-'}</td>
|
<td>${d.last_login || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
<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>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|||||||
@@ -10,14 +10,17 @@
|
|||||||
<nav class="top-menu">
|
<nav class="top-menu">
|
||||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
||||||
<a href="/test-data" class="{% if request.path == '/test-data' %}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="/device-logs" class="{% if request.path == '/device-logs' %}active{% endif %}">设备日志</a>
|
||||||
<a href="/logs/" class="{% if request.path == '/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>
|
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="user-info">
|
<span class="user-info">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ current_user.username }} ({{ current_user.role }})
|
{{ current_user.username }} ({{ current_user.role }})
|
||||||
|
<a href="/change-password">修改密码</a>
|
||||||
<a href="/logout">退出</a>
|
<a href="/logout">退出</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</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>
|
</label>
|
||||||
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||||||
<button onclick="exportCSV()" class="btn-export">导出 CSV</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>
|
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
<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>
|
<button id="btn-delete" class="btn-delete" onclick="confirmDelete()">🗑 删除</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<label>角色:
|
<label>角色:
|
||||||
<select id="new-role" style="margin:0 8px;">
|
<select id="new-role" style="margin:0 8px;">
|
||||||
<option value="operator">operator</option>
|
<option value="operator">operator</option>
|
||||||
|
<option value="manager">manager</option>
|
||||||
<option value="admin">admin</option>
|
<option value="admin">admin</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -51,6 +52,7 @@ async function loadUsers() {
|
|||||||
<td>
|
<td>
|
||||||
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
||||||
<option value="operator" ${u.role==='operator'?'selected':''}>operator</option>
|
<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>
|
<option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
|
||||||
</select>
|
</select>
|
||||||
<select onchange="updateUser(${u.id}, this, 'is_active')" data-field="is_active">
|
<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