fix: 确保默认Admin账户存在且登录用户名不区分大小写
This commit is contained in:
57
.trae/documents/管理员登录与角色管理改造计划.md
Normal file
57
.trae/documents/管理员登录与角色管理改造计划.md
Normal file
@@ -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/<id>/approve|reject`
|
||||||
|
- `GET /admin/reviews/submissions`、`POST /admin/reviews/submissions/<id>/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` 登录默认账户 → 强制改密 → 进入后台 → 创建副管理员/审核员 → 使用审核员账户访问审核页
|
||||||
|
|
||||||
|
---
|
||||||
|
确认后我将开始实施上述改造并进行验证。
|
||||||
@@ -17,6 +17,26 @@ def create_app():
|
|||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
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
|
@app.context_processor
|
||||||
def inject_csrf():
|
def inject_csrf():
|
||||||
return dict(csrf_token=generate_csrf())
|
return dict(csrf_token=generate_csrf())
|
||||||
@@ -42,15 +62,4 @@ def create_app():
|
|||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
from .cli import register_cli
|
from .cli import register_cli
|
||||||
register_cli(app)
|
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
|
return app
|
||||||
|
|||||||
@@ -1,29 +1,117 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, flash
|
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 ..extensions import db
|
||||||
from ..models import User, UserStatus, Post, ReviewStatus, Activity, ActivityStatus, ActivitySubmission, ReviewLog
|
from ..models import User, UserStatus, Post, ReviewStatus, Activity, ActivityStatus, ActivitySubmission, ReviewLog
|
||||||
|
from sqlalchemy import func
|
||||||
from ..services.notify import notify
|
from ..services.notify import notify
|
||||||
|
|
||||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||||
|
|
||||||
def is_admin():
|
def role():
|
||||||
return current_user.is_authenticated and current_user.role == "admin"
|
return current_user.role if current_user.is_authenticated else None
|
||||||
|
|
||||||
@bp.before_request
|
@bp.before_request
|
||||||
def guard():
|
def guard():
|
||||||
if request.endpoint and request.endpoint.startswith("admin."):
|
if request.endpoint and request.endpoint.startswith("admin."):
|
||||||
if not is_admin():
|
r = role()
|
||||||
return redirect(url_for("auth.login"))
|
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("/")
|
@bp.route("/", methods=["GET","POST"])
|
||||||
@login_required
|
|
||||||
def dashboard():
|
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_users = User.query.filter_by(status=UserStatus.pending).count()
|
||||||
pending_posts = Post.query.filter_by(status=ReviewStatus.pending).count()
|
pending_posts = Post.query.filter_by(status=ReviewStatus.pending).count()
|
||||||
pending_subs = ActivitySubmission.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)
|
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")
|
@bp.route("/reviews/users")
|
||||||
@login_required
|
@login_required
|
||||||
def review_users():
|
def review_users():
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class User(db.Model, UserMixin):
|
|||||||
password_hash = db.Column(db.String(255), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
role = db.Column(db.String(32), default="user")
|
role = db.Column(db.String(32), default="user")
|
||||||
status = db.Column(db.Enum(UserStatus), default=UserStatus.pending, nullable=False)
|
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))
|
identity_photo_path = db.Column(db.String(512))
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
profile = db.relationship("Profile", backref="user", uselist=False)
|
profile = db.relationship("Profile", backref="user", uselist=False)
|
||||||
|
|||||||
11
app/templates/admin/change_password.html
Normal file
11
app/templates/admin/change_password.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}修改密码{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h3 class="mb-3">首次登录请修改管理员密码</h3>
|
||||||
|
<form class="card p-3" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<div class="mb-3"><label class="form-label">新密码</label><input class="form-control" name="password" type="password" required></div>
|
||||||
|
<div class="mb-3"><label class="form-label">确认密码</label><input class="form-control" name="confirm" type="password" required></div>
|
||||||
|
<button class="btn btn-brand" type="submit">保存</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
20
app/templates/admin/checker.html
Normal file
20
app/templates/admin/checker.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}审核员{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h3 class="mb-3">审核员管理</h3>
|
||||||
|
<form class="card p-3 mb-3" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<div class="mb-2"><input class="form-control" name="email" placeholder="邮箱" required></div>
|
||||||
|
<div class="mb-2"><input class="form-control" name="username" placeholder="用户名" required></div>
|
||||||
|
<div class="mb-2"><input class="form-control" name="password" type="password" placeholder="密码" required></div>
|
||||||
|
<button class="btn btn-brand" type="submit">创建审核员</button>
|
||||||
|
</form>
|
||||||
|
<div class="card p-3">
|
||||||
|
<h5>现有审核员</h5>
|
||||||
|
<ul>
|
||||||
|
{% for u in users %}
|
||||||
|
<li>{{ u.username }}({{ u.email }})</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,4 +12,7 @@
|
|||||||
<a class="btn btn-link" href="{{ url_for('admin.review_posts') }}">作品审核</a>
|
<a class="btn btn-link" href="{{ url_for('admin.review_posts') }}">作品审核</a>
|
||||||
<a class="btn btn-link" href="{{ url_for('admin.review_submissions') }}">投稿审核</a>
|
<a class="btn btn-link" href="{{ url_for('admin.review_submissions') }}">投稿审核</a>
|
||||||
<a class="btn btn-link" href="{{ url_for('admin.manage_activities') }}">活动管理</a>
|
<a class="btn btn-link" href="{{ url_for('admin.manage_activities') }}">活动管理</a>
|
||||||
|
<a class="btn btn-link" href="{{ url_for('admin.change_password') }}">修改管理员密码</a>
|
||||||
|
<a class="btn btn-link" href="{{ url_for('admin.sub_admin_page') }}">副管理员</a>
|
||||||
|
<a class="btn btn-link" href="{{ url_for('admin.checker_page') }}">审核员</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
11
app/templates/admin/login.html
Normal file
11
app/templates/admin/login.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}后台登录{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h3 class="mb-3">管理员登录</h3>
|
||||||
|
<form class="card p-3" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<div class="mb-3"><label class="form-label">用户名</label><input class="form-control" name="username" required></div>
|
||||||
|
<div class="mb-3"><label class="form-label">密码</label><input class="form-control" name="password" type="password" required></div>
|
||||||
|
<button class="btn btn-brand" type="submit">登录</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
20
app/templates/admin/sub_admin.html
Normal file
20
app/templates/admin/sub_admin.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}副管理员{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h3 class="mb-3">副管理员管理</h3>
|
||||||
|
<form class="card p-3 mb-3" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<div class="mb-2"><input class="form-control" name="email" placeholder="邮箱" required></div>
|
||||||
|
<div class="mb-2"><input class="form-control" name="username" placeholder="用户名" required></div>
|
||||||
|
<div class="mb-2"><input class="form-control" name="password" type="password" placeholder="密码" required></div>
|
||||||
|
<button class="btn btn-brand" type="submit">创建副管理员</button>
|
||||||
|
</form>
|
||||||
|
<div class="card p-3">
|
||||||
|
<h5>现有副管理员</h5>
|
||||||
|
<ul>
|
||||||
|
{% for u in users %}
|
||||||
|
<li>{{ u.username }}({{ u.email }})</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user