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")), ): 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": "密码已更新,请重新登录"}