126 lines
5.3 KiB
Python
126 lines
5.3 KiB
Python
"""
|
||
管理后台登录页:/admin-login(表单登录,Cookie 保持会话)
|
||
"""
|
||
from flask import request, redirect, url_for, make_response, Response, current_app
|
||
from flask_jwt_extended import create_access_token
|
||
|
||
from app.models.user import User, Role
|
||
from app.utils.auth import check_password
|
||
|
||
ADMIN_COOKIE_NAME = "admin_token"
|
||
ADMIN_COOKIE_MAX_AGE = 86400 * 7 # 7 天
|
||
|
||
|
||
LOGIN_HTML = """
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>管理后台登录 - Chat Platform Admin</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f5f5f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||
.card { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.08); width: 100%; max-width: 360px; }
|
||
h1 { margin: 0 0 1.5rem 0; font-size: 1.25rem; color: #333; }
|
||
.field { margin-bottom: 1rem; }
|
||
label { display: block; margin-bottom: 0.35rem; font-size: 0.875rem; color: #555; }
|
||
input[type="text"], input[type="password"] { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; }
|
||
input:focus { outline: none; border-color: #333; }
|
||
.error { color: #c00; font-size: 0.875rem; margin-top: 0.5rem; }
|
||
button { width: 100%; padding: 0.6rem 1rem; margin-top: 0.5rem; background: #333; color: #fff; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
|
||
button:hover { background: #555; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<h1>Chat Platform Admin</h1>
|
||
<form method="post" action="/admin-login">
|
||
<div class="field">
|
||
<label for="username">用户名</label>
|
||
<input id="username" name="username" type="text" autocomplete="username" required placeholder="admin">
|
||
</div>
|
||
<div class="field">
|
||
<label for="password">密码</label>
|
||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||
</div>
|
||
<div class="field">
|
||
<button type="submit">登录</button>
|
||
</div>
|
||
<!-- error will be injected by server -->
|
||
</form>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
def register_admin_login_routes(flask_app):
|
||
"""注册 /admin-login 与 /admin-logout(独立路径,避免与 Flask-Admin /admin/* 冲突)。"""
|
||
|
||
@flask_app.route("/admin-login", methods=["GET", "POST"])
|
||
def admin_login():
|
||
if request.method == "GET":
|
||
return Response(LOGIN_HTML, mimetype="text/html; charset=utf-8")
|
||
try:
|
||
username = (request.form.get("username") or "").strip()
|
||
password = request.form.get("password") or ""
|
||
if not username or not password:
|
||
html = LOGIN_HTML.replace(
|
||
"</form>",
|
||
'<p class="error">请填写用户名和密码</p></form>',
|
||
)
|
||
return Response(html, mimetype="text/html; charset=utf-8")
|
||
user = User.query.filter_by(username=username).first()
|
||
if not user or not check_password(password, user.password_hash):
|
||
html = LOGIN_HTML.replace(
|
||
"</form>",
|
||
'<p class="error">用户名或密码错误</p></form>',
|
||
)
|
||
return Response(html, mimetype="text/html; charset=utf-8")
|
||
if not user.is_active:
|
||
html = LOGIN_HTML.replace(
|
||
"</form>",
|
||
'<p class="error">账号已停用</p></form>',
|
||
)
|
||
return Response(html, mimetype="text/html; charset=utf-8")
|
||
role = Role.query.get(user.role_id) if user.role_id else None
|
||
if not role or role.name != "admin":
|
||
html = LOGIN_HTML.replace(
|
||
"</form>",
|
||
'<p class="error">无管理员权限</p></form>',
|
||
)
|
||
return Response(html, mimetype="text/html; charset=utf-8")
|
||
token = create_access_token(identity=user.id)
|
||
# 直接重定向到 /admin/,避免 url_for 与 Flask-Admin 版本差异
|
||
resp = make_response(redirect("/admin/"))
|
||
resp.set_cookie(
|
||
ADMIN_COOKIE_NAME,
|
||
token,
|
||
max_age=ADMIN_COOKIE_MAX_AGE,
|
||
path="/admin",
|
||
httponly=True,
|
||
samesite="Lax",
|
||
secure=request.is_secure,
|
||
)
|
||
return resp
|
||
except Exception as e:
|
||
current_app.logger.exception("admin_login failed")
|
||
err_msg = str(e) if current_app.debug else "登录处理失败,请稍后重试"
|
||
html = LOGIN_HTML.replace(
|
||
"</form>",
|
||
f'<p class="error">{err_msg}</p></form>',
|
||
)
|
||
return Response(html, mimetype="text/html; charset=utf-8"), 500
|
||
|
||
@flask_app.route("/admin-logout", methods=["GET"])
|
||
def admin_logout():
|
||
resp = make_response(redirect(url_for("admin_login")))
|
||
resp.delete_cookie(ADMIN_COOKIE_NAME, path="/admin")
|
||
return resp
|
||
|
||
@flask_app.route("/admin/login", methods=["GET"])
|
||
def admin_login_legacy():
|
||
"""兼容旧链接:/admin/login -> /admin-login"""
|
||
return redirect(url_for("admin_login"))
|