From 60fff79936b0108132a58607eecddc5711afc7ea Mon Sep 17 00:00:00 2001 From: AnthonyDuan Date: Sun, 7 Dec 2025 11:06:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A1=AE=E4=BF=9D=E9=BB=98=E8=AE=A4Admi?= =?UTF-8?q?n=E8=B4=A6=E6=88=B7=E5=AD=98=E5=9C=A8=E4=B8=94=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=94=A8=E6=88=B7=E5=90=8D=E4=B8=8D=E5=8C=BA=E5=88=86?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../documents/管理员登录与角色管理改造计划.md | 57 ++++++++++ app/__init__.py | 31 ++++-- app/blueprints/admin.py | 102 ++++++++++++++++-- app/models.py | 1 + app/templates/admin/change_password.html | 11 ++ app/templates/admin/checker.html | 20 ++++ app/templates/admin/dashboard.html | 3 + app/templates/admin/login.html | 11 ++ app/templates/admin/sub_admin.html | 20 ++++ 9 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 .trae/documents/管理员登录与角色管理改造计划.md create mode 100644 app/templates/admin/change_password.html create mode 100644 app/templates/admin/checker.html create mode 100644 app/templates/admin/login.html create mode 100644 app/templates/admin/sub_admin.html diff --git a/.trae/documents/管理员登录与角色管理改造计划.md b/.trae/documents/管理员登录与角色管理改造计划.md new file mode 100644 index 0000000..7d6c4d5 --- /dev/null +++ b/.trae/documents/管理员登录与角色管理改造计划.md @@ -0,0 +1,57 @@ +## 目标 +- 管理员登录入口改为 `/admin`,使用默认账户:用户名 `Admin`,密码 `lzgzsystem` +- 首次进入管理员界面强制修改密码(未修改前禁止访问后台其他页面) +- 管理员可创建与管理两类角色:`sub_admin`(副管理员),`checker`(审核员) +- 角色管理页面路径:`/sub-admin` 与 `/checker` + +## 数据模型改动 +- `users` 表:新增字段 `must_change_password`(bool,默认 `False`;初始管理员为 `True`) +- `role` 字段允许值:`admin`、`sub_admin`、`checker`、`user` + +## 初始化与登录流程 +- 应用启动时检测是否存在 `role='admin'` 用户;若不存在则自动创建: + - 用户名:`Admin`,密码:`lzgzsystem`(存储为哈希),状态:`approved`,`must_change_password=True` +- `GET/POST /admin`: + - 显示管理员登录表单(用户名/密码) + - 登录成功后检查 `must_change_password`:若为 `True`,重定向 `GET /admin/change-password` +- `GET/POST /admin/change-password`: + - 强制管理员修改密码(校验长度与复杂度、确认密码一致) + - 修改成功后将 `must_change_password=False`,跳转至管理员仪表盘 + +## 权限与访问控制 +- 后台守卫逻辑: + - `admin`:访问全部后台页面(含角色创建与分配) + - `sub_admin`:访问审核与活动管理,但不能创建/变更管理员与审核员角色 + - `checker`:仅可访问图片与投稿审核相关页面(`/admin/reviews/posts`、`/admin/reviews/submissions`) +- 原 `admin` 蓝图的 `before_request` 更新为:如非登录态或无后台权限角色,重定向到 `/admin` 登录页 +- 移除之前的 `/setup/admin` 首次引导逻辑(入口统一 `/admin`) + +## 页面与路由新增 +- 角色管理: + - `GET/POST /sub-admin`:列表所有 `sub_admin`;提供表单创建新副管理员(邮箱/用户名/密码),或将现有用户提升为 `sub_admin`;支持撤销角色 + - `GET/POST /checker`:列表所有 `checker`;同上创建审核员或将现有用户赋予 `checker` 角色;支持撤销角色 +- 导航: + - 登录后后台导航新增入口到 `/sub-admin` 与 `/checker` + +## 审核权限调整 +- `checker` 可访问: + - `GET /admin/reviews/posts`、`POST /admin/reviews/posts//approve|reject` + - `GET /admin/reviews/submissions`、`POST /admin/reviews/submissions//approve|reject` +- `checker` 不允许:用户注册审核与活动创建/发布关闭 + +## 安全与校验 +- 登录密码与修改密码使用 `werkzeug.security` 哈希 +- CSRF 保护表单;错误信息与成功提示采用圆角卡片样式 +- 防止硬编码密码泄露:仅首启自动创建一次,之后必须修改密码 + +## 实施步骤 +1. 修改 `User` 模型,新增 `must_change_password` 字段 +2. 在应用工厂中:启动时自动创建默认管理员(若不存在) +3. 在 `admin` 蓝图中新增 `/admin` 登录与 `/admin/change-password` 强制修改密码流程,更新守卫权限逻辑 +4. 新增 `sub-admin` 与 `checker` 管理页面与路由(创建、提升、撤销) +5. 调整审核路由访问权限,确保 `checker` 只能审核图片/投稿 +6. 更新模板导航,增加入口,保持圆润主题风格 +7. 验证:首次启动 → `/admin` 登录默认账户 → 强制改密 → 进入后台 → 创建副管理员/审核员 → 使用审核员账户访问审核页 + +--- +确认后我将开始实施上述改造并进行验证。 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index de2bbef..0131b59 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,6 +17,26 @@ def create_app(): csrf.init_app(app) with app.app_context(): db.create_all() + from .models import User, UserStatus, Profile + from sqlalchemy import text + try: + db.session.execute(text("ALTER TABLE user ADD COLUMN must_change_password BOOLEAN DEFAULT 0")) + db.session.commit() + except Exception: + pass + from .models import User, UserStatus, Profile + from werkzeug.security import generate_password_hash + admin_any = User.query.filter_by(role="admin").first() + admin_named = User.query.filter_by(username="Admin").first() + if not admin_named: + email = "admin@example.com" + if User.query.filter_by(email=email).first(): + email = "admin2@example.com" + u = User(email=email, username="Admin", password_hash=generate_password_hash("lzgzsystem"), role="admin", status=UserStatus.approved, must_change_password=True) + db.session.add(u) + db.session.flush() + db.session.add(Profile(user_id=u.id)) + db.session.commit() @app.context_processor def inject_csrf(): return dict(csrf_token=generate_csrf()) @@ -42,15 +62,4 @@ def create_app(): app.register_blueprint(admin_bp) from .cli import register_cli register_cli(app) - @app.before_request - def ensure_admin_setup(): - from .models import User - from flask import request, redirect, url_for - try: - has_admin = User.query.filter_by(role="admin").first() is not None - except Exception: - has_admin = True - if not has_admin: - if not (request.path.startswith("/setup/admin") or request.path.startswith("/static")): - return redirect(url_for("setup.admin")) return app diff --git a/app/blueprints/admin.py b/app/blueprints/admin.py index 4b268b6..a2d0049 100644 --- a/app/blueprints/admin.py +++ b/app/blueprints/admin.py @@ -1,29 +1,117 @@ from datetime import datetime from flask import Blueprint, render_template, redirect, url_for, request, flash -from flask_login import login_required, current_user +from flask_login import login_required, current_user, login_user +from werkzeug.security import check_password_hash, generate_password_hash from ..extensions import db from ..models import User, UserStatus, Post, ReviewStatus, Activity, ActivityStatus, ActivitySubmission, ReviewLog +from sqlalchemy import func from ..services.notify import notify bp = Blueprint("admin", __name__, url_prefix="/admin") -def is_admin(): - return current_user.is_authenticated and current_user.role == "admin" +def role(): + return current_user.role if current_user.is_authenticated else None @bp.before_request def guard(): if request.endpoint and request.endpoint.startswith("admin."): - if not is_admin(): - return redirect(url_for("auth.login")) + r = role() + allowed_checker = {"admin.review_posts","admin.approve_post","admin.reject_post","admin.review_submissions","admin.approve_submission","admin.reject_submission"} + allowed_sub = {"admin.review_users","admin.approve_user","admin.reject_user","admin.manage_activities","admin.publish_activity","admin.close_activity"} + if request.endpoint in {"admin.dashboard","admin.change_password"}: + return None + if r == "admin": + return None + if r == "sub_admin" and (request.endpoint in allowed_checker or request.endpoint in allowed_sub): + return None + if r == "checker" and request.endpoint in allowed_checker: + return None + return redirect(url_for("admin.dashboard")) -@bp.route("/") -@login_required +@bp.route("/", methods=["GET","POST"]) def dashboard(): + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + user = User.query.filter(func.lower(User.username)==(username or "").lower(), User.role.in_(["admin","sub_admin","checker"])) .first() + if not user or not check_password_hash(user.password_hash, password) or user.status != UserStatus.approved: + flash("登录失败") + return render_template("admin/login.html") + login_user(user) + if user.role == "admin" and getattr(user, "must_change_password", False): + return redirect(url_for("admin.change_password")) + if not current_user.is_authenticated or role() not in {"admin","sub_admin","checker"}: + return render_template("admin/login.html") + if role()=="admin" and getattr(current_user, "must_change_password", False): + return redirect(url_for("admin.change_password")) pending_users = User.query.filter_by(status=UserStatus.pending).count() pending_posts = Post.query.filter_by(status=ReviewStatus.pending).count() pending_subs = ActivitySubmission.query.filter_by(status=ReviewStatus.pending).count() return render_template("admin/dashboard.html", pending_users=pending_users, pending_posts=pending_posts, pending_subs=pending_subs) +@bp.route("/change-password", methods=["GET","POST"]) +@login_required +def change_password(): + if role() != "admin": + return redirect(url_for("admin.dashboard")) + if request.method == "POST": + p1 = request.form.get("password") + p2 = request.form.get("confirm") + if not p1 or len(p1) < 8 or p1 != p2: + flash("密码至少8位且两次一致") + return render_template("admin/change_password.html") + current_user.password_hash = generate_password_hash(p1) + current_user.must_change_password = False + db.session.commit() + return redirect(url_for("admin.dashboard")) + return render_template("admin/change_password.html") + +@bp.route("/sub-admin", methods=["GET","POST"]) +@login_required +def sub_admin_page(): + if role() != "admin": + return redirect(url_for("admin.dashboard")) + if request.method == "POST": + email = request.form.get("email") + username = request.form.get("username") + password = request.form.get("password") + if not email or not username or not password: + flash("请完整填写信息") + else: + if User.query.filter((User.email==email) | (User.username==username)).first(): + flash("邮箱或用户名已存在") + else: + u = User(email=email, username=username, password_hash=generate_password_hash(password), role="sub_admin", status=UserStatus.approved) + db.session.add(u) + db.session.commit() + flash("副管理员已创建") + return redirect(url_for("admin.sub_admin_page")) + users = User.query.filter_by(role="sub_admin").all() + return render_template("admin/sub_admin.html", users=users) + +@bp.route("/checker", methods=["GET","POST"]) +@login_required +def checker_page(): + if role() != "admin": + return redirect(url_for("admin.dashboard")) + if request.method == "POST": + email = request.form.get("email") + username = request.form.get("username") + password = request.form.get("password") + if not email or not username or not password: + flash("请完整填写信息") + else: + if User.query.filter((User.email==email) | (User.username==username)).first(): + flash("邮箱或用户名已存在") + else: + u = User(email=email, username=username, password_hash=generate_password_hash(password), role="checker", status=UserStatus.approved) + db.session.add(u) + db.session.commit() + flash("审核员已创建") + return redirect(url_for("admin.checker_page")) + users = User.query.filter_by(role="checker").all() + return render_template("admin/checker.html", users=users) + @bp.route("/reviews/users") @login_required def review_users(): diff --git a/app/models.py b/app/models.py index a5b4e87..666efec 100644 --- a/app/models.py +++ b/app/models.py @@ -30,6 +30,7 @@ class User(db.Model, UserMixin): password_hash = db.Column(db.String(255), nullable=False) role = db.Column(db.String(32), default="user") status = db.Column(db.Enum(UserStatus), default=UserStatus.pending, nullable=False) + must_change_password = db.Column(db.Boolean, default=False) identity_photo_path = db.Column(db.String(512)) created_at = db.Column(db.DateTime, default=datetime.utcnow) profile = db.relationship("Profile", backref="user", uselist=False) diff --git a/app/templates/admin/change_password.html b/app/templates/admin/change_password.html new file mode 100644 index 0000000..b09b72f --- /dev/null +++ b/app/templates/admin/change_password.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block title %}修改密码{% endblock %} +{% block content %} +

首次登录请修改管理员密码

+
+ +
+
+ +
+{% endblock %} diff --git a/app/templates/admin/checker.html b/app/templates/admin/checker.html new file mode 100644 index 0000000..71ec3f0 --- /dev/null +++ b/app/templates/admin/checker.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block title %}审核员{% endblock %} +{% block content %} +

审核员管理

+
+ +
+
+
+ +
+
+
现有审核员
+
    + {% for u in users %} +
  • {{ u.username }}({{ u.email }})
  • + {% endfor %} +
+
+{% endblock %} diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html index 9dcad17..ef6d8dc 100644 --- a/app/templates/admin/dashboard.html +++ b/app/templates/admin/dashboard.html @@ -12,4 +12,7 @@ 作品审核 投稿审核 活动管理 +修改管理员密码 +副管理员 +审核员 {% endblock %} diff --git a/app/templates/admin/login.html b/app/templates/admin/login.html new file mode 100644 index 0000000..46b4dd5 --- /dev/null +++ b/app/templates/admin/login.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block title %}后台登录{% endblock %} +{% block content %} +

管理员登录

+
+ +
+
+ +
+{% endblock %} diff --git a/app/templates/admin/sub_admin.html b/app/templates/admin/sub_admin.html new file mode 100644 index 0000000..41d1a58 --- /dev/null +++ b/app/templates/admin/sub_admin.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block title %}副管理员{% endblock %} +{% block content %} +

副管理员管理

+
+ +
+
+
+ +
+
+
现有副管理员
+
    + {% for u in users %} +
  • {{ u.username }}({{ u.email }})
  • + {% endfor %} +
+
+{% endblock %}