fix: 确保默认Admin账户存在且登录用户名不区分大小写

This commit is contained in:
2025-12-07 11:06:00 +08:00
parent 63db6a0815
commit 60fff79936
9 changed files with 238 additions and 18 deletions

View 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` 登录默认账户 → 强制改密 → 进入后台 → 创建副管理员/审核员 → 使用审核员账户访问审核页
---
确认后我将开始实施上述改造并进行验证。

View File

@@ -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

View File

@@ -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():

View File

@@ -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)

View 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 %}

View 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 %}

View File

@@ -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 %}

View 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 %}

View 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 %}