You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

116 lines
4.1 KiB

from __future__ import annotations
from datetime import UTC, datetime
from flask import Blueprint, flash, g, redirect, render_template, request, url_for
from app.auth import (
ALLOWED_ACCOUNT_ROLES,
ACTIVE_ACCOUNT_STATUS,
is_authenticated,
login_account,
logout_account,
resolve_post_login_redirect,
)
from app.extensions import db
from app.models import Account
from app.services import account_snapshot, write_audit_log
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if is_authenticated():
return redirect(resolve_post_login_redirect(g.current_account))
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
account = db.session.execute(
db.select(Account).where(Account.username == username),
).scalar_one_or_none()
if account is None or account.role not in ALLOWED_ACCOUNT_ROLES:
write_audit_log(
action_type="auth_login_failed",
target_type="account",
actor_username=username or None,
target_display_name=username or None,
after_data={
"reason": "invalid_credentials",
"attempted_username": username,
},
)
flash("用户名或密码错误。", "error")
return render_template("auth/login.html"), 400
if account.status != ACTIVE_ACCOUNT_STATUS:
write_audit_log(
action_type="auth_login_disabled",
target_type="account",
actor=account,
target_id=account.id,
target_display_name=account.display_name or account.username,
after_data={
"reason": "account_disabled",
"account": account_snapshot(account),
},
)
flash("账号已停用,无法登录。", "error")
return render_template("auth/login.html"), 403
if not account.check_password(password):
write_audit_log(
action_type="auth_login_failed",
target_type="account",
actor=account,
target_id=account.id,
target_display_name=account.display_name or account.username,
after_data={
"reason": "invalid_credentials",
"attempted_username": username,
},
)
flash("用户名或密码错误。", "error")
return render_template("auth/login.html"), 400
redirect_target = resolve_post_login_redirect(account)
account.last_login_at = datetime.now(UTC).replace(tzinfo=None)
login_account(account)
write_audit_log(
action_type="auth_login",
target_type="account",
actor=account,
target_id=account.id,
target_display_name=account.display_name or account.username,
after_data={
"account": account_snapshot(account),
"redirect_to": redirect_target,
},
commit=False,
)
db.session.commit()
flash(f"欢迎回来,{account.display_name or account.username}", "success")
return redirect(redirect_target)
return render_template("auth/login.html")
@auth_bp.route("/logout", methods=["GET", "POST"])
def logout():
current_account = g.current_account if is_authenticated() else None
write_audit_log(
action_type="auth_logout",
target_type="account",
actor=current_account,
target_id=current_account.id if current_account is not None else None,
target_display_name=(current_account.display_name or current_account.username) if current_account is not None else None,
after_data={
"account": account_snapshot(current_account),
},
)
logout_account()
flash("您已退出登录。", "success")
return redirect(url_for("auth.login"))