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

View File

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

View File

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

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_submissions') }}">投稿审核</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 %}

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