feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
277
backend/app/api/v1/admin_portal/auth.py
Normal file
277
backend/app/api/v1/admin_portal/auth.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
管理员认证 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="退出成功")
|
||||
|
||||
Reference in New Issue
Block a user