""" 管理员认证 API """ import os from datetime import datetime, timedelta from typing import Optional import jwt import pymysql from fastapi import APIRouter, Depends, HTTPException, status, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from passlib.context import CryptContext from .schemas import ( AdminLoginRequest, AdminLoginResponse, AdminUserInfo, AdminChangePasswordRequest, ResponseModel, ) router = APIRouter(prefix="/auth", tags=["管理员认证"]) # 密码加密 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # JWT 配置 SECRET_KEY = os.getenv("ADMIN_JWT_SECRET", "admin-secret-key-kaopeilian-2026") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_HOURS = 24 # 安全认证 security = HTTPBearer() # 管理库连接配置 ADMIN_DB_CONFIG = { "host": os.getenv("ADMIN_DB_HOST", "prod-mysql"), "port": int(os.getenv("ADMIN_DB_PORT", "3306")), "user": os.getenv("ADMIN_DB_USER", "root"), "password": os.getenv("ADMIN_DB_PASSWORD", "ProdMySQL2025!@#"), "db": os.getenv("ADMIN_DB_NAME", "kaopeilian_admin"), "charset": "utf8mb4", } def get_db_connection(): """获取数据库连接""" return pymysql.connect(**ADMIN_DB_CONFIG, cursorclass=pymysql.cursors.DictCursor) def verify_password(plain_password: str, hashed_password: str) -> bool: """验证密码""" return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """获取密码哈希""" return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: """创建访问令牌""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def decode_access_token(token: str) -> dict: """解码访问令牌""" try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload except jwt.ExpiredSignatureError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已过期", ) except jwt.InvalidTokenError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的Token", ) async def get_current_admin( credentials: HTTPAuthorizationCredentials = Depends(security) ) -> AdminUserInfo: """获取当前登录的管理员""" token = credentials.credentials payload = decode_access_token(token) admin_id = payload.get("sub") if not admin_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的Token", ) conn = get_db_connection() try: with conn.cursor() as cursor: cursor.execute( """ SELECT id, username, email, full_name, role, is_active, last_login_at FROM admin_users WHERE id = %s """, (admin_id,) ) admin = cursor.fetchone() if not admin: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员不存在", ) if not admin["is_active"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用", ) return AdminUserInfo( id=admin["id"], username=admin["username"], email=admin["email"], full_name=admin["full_name"], role=admin["role"], last_login_at=admin["last_login_at"], ) finally: conn.close() async def require_superadmin( admin: AdminUserInfo = Depends(get_current_admin) ) -> AdminUserInfo: """要求超级管理员权限""" if admin.role != "superadmin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要超级管理员权限", ) return admin @router.post("/login", response_model=AdminLoginResponse, summary="管理员登录") async def admin_login(request: Request, login_data: AdminLoginRequest): """ 管理员登录 - **username**: 用户名 - **password**: 密码 """ conn = get_db_connection() try: with conn.cursor() as cursor: # 查询管理员 cursor.execute( """ SELECT id, username, email, full_name, role, password_hash, is_active, last_login_at FROM admin_users WHERE username = %s """, (login_data.username,) ) admin = cursor.fetchone() if not admin: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", ) if not admin["is_active"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用", ) # 验证密码 if not verify_password(login_data.password, admin["password_hash"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", ) # 更新最后登录时间和IP client_ip = request.client.host if request.client else None cursor.execute( """ UPDATE admin_users SET last_login_at = NOW(), last_login_ip = %s WHERE id = %s """, (client_ip, admin["id"]) ) conn.commit() # 创建 Token access_token = create_access_token( data={"sub": str(admin["id"]), "username": admin["username"], "role": admin["role"]} ) return AdminLoginResponse( access_token=access_token, token_type="bearer", expires_in=ACCESS_TOKEN_EXPIRE_HOURS * 3600, admin_user=AdminUserInfo( id=admin["id"], username=admin["username"], email=admin["email"], full_name=admin["full_name"], role=admin["role"], last_login_at=datetime.now(), ), ) finally: conn.close() @router.get("/me", response_model=AdminUserInfo, summary="获取当前管理员信息") async def get_me(admin: AdminUserInfo = Depends(get_current_admin)): """获取当前登录管理员的信息""" return admin @router.post("/change-password", response_model=ResponseModel, summary="修改密码") async def change_password( data: AdminChangePasswordRequest, admin: AdminUserInfo = Depends(get_current_admin), ): """ 修改当前管理员密码 - **old_password**: 旧密码 - **new_password**: 新密码 """ conn = get_db_connection() try: with conn.cursor() as cursor: # 验证旧密码 cursor.execute( "SELECT password_hash FROM admin_users WHERE id = %s", (admin.id,) ) row = cursor.fetchone() if not verify_password(data.old_password, row["password_hash"]): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="旧密码错误", ) # 更新密码 new_hash = get_password_hash(data.new_password) cursor.execute( "UPDATE admin_users SET password_hash = %s WHERE id = %s", (new_hash, admin.id) ) conn.commit() return ResponseModel(message="密码修改成功") finally: conn.close() @router.post("/logout", response_model=ResponseModel, summary="退出登录") async def admin_logout(admin: AdminUserInfo = Depends(get_current_admin)): """退出登录(客户端需清除 Token)""" return ResponseModel(message="退出成功")