249 lines
9.3 KiB
Python
249 lines
9.3 KiB
Python
import json
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from sqlite3 import IntegrityError
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
from app.core.security import (
|
|
AdminPrincipal,
|
|
ROLE_NAMES,
|
|
ROLE_PERMISSIONS,
|
|
admin_auth,
|
|
decode_jwt_token,
|
|
hash_password,
|
|
normalize_roles,
|
|
parse_utc_text,
|
|
require_permission,
|
|
token_digest,
|
|
token_response_for_user,
|
|
verify_password,
|
|
)
|
|
from app.repositories import admin_user_repository
|
|
from app.schemas.admin_auth import (
|
|
AdminLoginRequest,
|
|
AdminUserCreateRequest,
|
|
AdminUserResetPasswordRequest,
|
|
AdminUserStatusRequest,
|
|
AdminUserUpdateRequest,
|
|
ChangePasswordRequest,
|
|
RefreshTokenRequest,
|
|
)
|
|
|
|
|
|
router = APIRouter(tags=["admin-auth"])
|
|
|
|
VALID_ROLES = set(ROLE_PERMISSIONS.keys())
|
|
|
|
|
|
def validate_username(username: str) -> str:
|
|
value = username.strip()
|
|
if not re.fullmatch(r"[A-Za-z0-9_.@-]{3,64}", value):
|
|
raise HTTPException(status_code=400, detail="用户名只能包含字母、数字、下划线、点、@ 和短横线,长度 3-64")
|
|
return value
|
|
|
|
|
|
def validate_password(password: str) -> str:
|
|
value = password.strip()
|
|
if len(value) < 8:
|
|
raise HTTPException(status_code=400, detail="密码至少需要 8 个字符")
|
|
if len(value) > 128:
|
|
raise HTTPException(status_code=400, detail="密码不能超过 128 个字符")
|
|
return value
|
|
|
|
|
|
def validate_roles(roles: list[str]) -> list[str]:
|
|
result = []
|
|
for role in roles:
|
|
value = str(role).strip()
|
|
if value:
|
|
result.append(value)
|
|
if not result:
|
|
raise HTTPException(status_code=400, detail="至少选择一个角色")
|
|
invalid = [role for role in result if role not in VALID_ROLES]
|
|
if invalid:
|
|
raise HTTPException(status_code=400, detail=f"未知角色:{', '.join(invalid)}")
|
|
return result
|
|
|
|
|
|
def public_user(row) -> dict:
|
|
return {
|
|
"id": row["id"],
|
|
"username": row["username"],
|
|
"display_name": row["display_name"] or row["username"],
|
|
"roles": normalize_roles(row["roles"]),
|
|
"status": row["status"],
|
|
"created_at": row["created_at"],
|
|
"updated_at": row["updated_at"],
|
|
"last_login_at": row["last_login_at"],
|
|
}
|
|
|
|
|
|
def role_options() -> list[dict]:
|
|
return [
|
|
{
|
|
"value": role,
|
|
"label": ROLE_NAMES.get(role, role),
|
|
"permissions": permissions,
|
|
}
|
|
for role, permissions in ROLE_PERMISSIONS.items()
|
|
]
|
|
|
|
|
|
@router.post("/login")
|
|
def login(body: AdminLoginRequest, request: Request):
|
|
username = body.username.strip()
|
|
password = body.password
|
|
row = admin_user_repository.get_user(username)
|
|
if not row or row["status"] != "active" or not verify_password(password, row["password_hash"]):
|
|
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
|
admin_user_repository.mark_login(username)
|
|
return token_response_for_user(row, request)
|
|
|
|
|
|
@router.post("/refresh-token")
|
|
def refresh_token(body: RefreshTokenRequest, request: Request):
|
|
payload = decode_jwt_token(body.refreshToken.strip(), "refresh")
|
|
token_id = str(payload.get("jti") or "")
|
|
username = str(payload.get("sub") or "")
|
|
row = admin_user_repository.get_refresh_token(token_id)
|
|
if not row or row["username"] != username or row["revoked_at"]:
|
|
raise HTTPException(status_code=401, detail="刷新令牌无效")
|
|
if row["token_hash"] != token_digest(body.refreshToken.strip()):
|
|
raise HTTPException(status_code=401, detail="刷新令牌无效")
|
|
if parse_utc_text(row["expires_at"]) <= datetime.now(timezone.utc):
|
|
raise HTTPException(status_code=401, detail="刷新令牌已过期")
|
|
user_row = admin_user_repository.get_user(username)
|
|
if not user_row or user_row["status"] != "active":
|
|
raise HTTPException(status_code=401, detail="管理员账号不存在或已禁用")
|
|
admin_user_repository.revoke_refresh_token(token_id)
|
|
return token_response_for_user(user_row, request)
|
|
|
|
|
|
@router.get("/admin/auth/check")
|
|
def admin_auth_check(principal: AdminPrincipal = Depends(admin_auth)):
|
|
return {
|
|
"code": 0,
|
|
"msg": "管理员登录凭证有效",
|
|
"username": principal.username,
|
|
"nickname": principal.display_name,
|
|
"roles": principal.roles,
|
|
"permissions": principal.permissions,
|
|
"auth_type": principal.auth_type,
|
|
}
|
|
|
|
|
|
@router.get("/admin/user/list")
|
|
def admin_user_list(principal: AdminPrincipal = Depends(require_permission("admin:manage"))):
|
|
return {
|
|
"code": 0,
|
|
"list": [public_user(row) for row in admin_user_repository.list_users()],
|
|
"roles": role_options(),
|
|
"current_username": principal.username,
|
|
}
|
|
|
|
|
|
@router.post("/admin/user/create")
|
|
def admin_user_create(
|
|
body: AdminUserCreateRequest,
|
|
principal: AdminPrincipal = Depends(require_permission("admin:manage")),
|
|
):
|
|
username = validate_username(body.username)
|
|
password = validate_password(body.password)
|
|
roles = validate_roles(body.roles)
|
|
display_name = body.display_name.strip() or username
|
|
try:
|
|
admin_user_repository.create_user(
|
|
username,
|
|
hash_password(password),
|
|
display_name,
|
|
json.dumps(roles, ensure_ascii=False),
|
|
"active",
|
|
)
|
|
except IntegrityError:
|
|
raise HTTPException(status_code=409, detail="用户名已存在")
|
|
return {"code": 0, "msg": "管理员用户已创建"}
|
|
|
|
|
|
@router.post("/admin/user/update")
|
|
def admin_user_update(
|
|
body: AdminUserUpdateRequest,
|
|
principal: AdminPrincipal = Depends(require_permission("admin:manage")),
|
|
):
|
|
username = validate_username(body.username)
|
|
row = admin_user_repository.get_user(username)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="管理员用户不存在")
|
|
roles = validate_roles(body.roles)
|
|
old_roles = normalize_roles(row["roles"])
|
|
if "super_admin" in old_roles and "super_admin" not in roles:
|
|
active_super_admins = admin_user_repository.count_active_super_admins()
|
|
if row["status"] == "active" and active_super_admins <= 1:
|
|
raise HTTPException(status_code=400, detail="不能移除最后一个可用超级管理员")
|
|
display_name = body.display_name.strip() or username
|
|
admin_user_repository.update_user_profile(
|
|
username,
|
|
display_name,
|
|
json.dumps(roles, ensure_ascii=False),
|
|
)
|
|
return {"code": 0, "msg": "管理员用户已更新"}
|
|
|
|
|
|
@router.post("/admin/user/status")
|
|
def admin_user_status(
|
|
body: AdminUserStatusRequest,
|
|
principal: AdminPrincipal = Depends(require_permission("admin:manage")),
|
|
):
|
|
username = validate_username(body.username)
|
|
status = body.status.strip()
|
|
if status not in {"active", "disabled"}:
|
|
raise HTTPException(status_code=400, detail="状态只能是 active 或 disabled")
|
|
row = admin_user_repository.get_user(username)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="管理员用户不存在")
|
|
roles = normalize_roles(row["roles"])
|
|
if status == "disabled" and "super_admin" in roles:
|
|
active_super_admins = admin_user_repository.count_active_super_admins()
|
|
if row["status"] == "active" and active_super_admins <= 1:
|
|
raise HTTPException(status_code=400, detail="不能禁用最后一个可用超级管理员")
|
|
if username == principal.username and status == "disabled":
|
|
raise HTTPException(status_code=400, detail="不能禁用当前登录账号")
|
|
admin_user_repository.set_user_status(username, status)
|
|
if status == "disabled":
|
|
admin_user_repository.revoke_refresh_tokens_for_user(username)
|
|
return {"code": 0, "msg": "管理员用户状态已更新"}
|
|
|
|
|
|
@router.post("/admin/user/password/reset")
|
|
def admin_user_reset_password(
|
|
body: AdminUserResetPasswordRequest,
|
|
principal: AdminPrincipal = Depends(require_permission("admin:manage")),
|
|
):
|
|
username = validate_username(body.username)
|
|
new_password = validate_password(body.new_password)
|
|
row = admin_user_repository.get_user(username)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="管理员用户不存在")
|
|
admin_user_repository.update_password(username, hash_password(new_password))
|
|
admin_user_repository.revoke_refresh_tokens_for_user(username)
|
|
return {"code": 0, "msg": "管理员密码已重置"}
|
|
|
|
|
|
@router.post("/admin/user/password/change")
|
|
def change_password(
|
|
body: ChangePasswordRequest,
|
|
principal: AdminPrincipal = Depends(require_permission("admin:access")),
|
|
):
|
|
if principal.auth_type == "legacy_token":
|
|
raise HTTPException(status_code=400, detail="兼容令牌登录不能修改用户密码,请使用用户名密码登录")
|
|
new_password = body.new_password.strip()
|
|
if len(new_password) < 8:
|
|
raise HTTPException(status_code=400, detail="新密码至少需要 8 个字符")
|
|
if len(new_password) > 128:
|
|
raise HTTPException(status_code=400, detail="新密码不能超过 128 个字符")
|
|
row = admin_user_repository.get_user(principal.username)
|
|
if not row or not verify_password(body.current_password, row["password_hash"]):
|
|
raise HTTPException(status_code=403, detail="当前密码错误")
|
|
admin_user_repository.update_password(principal.username, hash_password(new_password))
|
|
return {"code": 0, "msg": "密码已更新,请重新登录"}
|