feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
认证API路由示例代码
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.core.exceptions import UnauthorizedError, ConflictError
|
||||
from app.schemas.auth import UserRegister, Token, PasswordReset
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=ResponseModel[Token], summary="用户登录")
|
||||
async def login(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
用户登录接口
|
||||
|
||||
- **username**: 用户名或邮箱
|
||||
- **password**: 密码
|
||||
|
||||
返回访问令牌和刷新令牌
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
|
||||
# 验证用户
|
||||
user = await auth_service.authenticate_user(
|
||||
username=form_data.username,
|
||||
password=form_data.password
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise UnauthorizedError("用户名或密码错误")
|
||||
|
||||
# 创建Token
|
||||
token = await auth_service.create_tokens(user)
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="登录成功",
|
||||
data=token
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register", response_model=ResponseModel[Token], status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
user_data: UserRegister,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
用户注册接口
|
||||
|
||||
注册成功后自动登录,返回Token
|
||||
"""
|
||||
# 验证密码一致性
|
||||
if user_data.password != user_data.confirm_password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="两次输入的密码不一致"
|
||||
)
|
||||
|
||||
auth_service = AuthService(db)
|
||||
|
||||
try:
|
||||
# 创建用户
|
||||
user = await auth_service.create_user(user_data)
|
||||
|
||||
# 自动登录
|
||||
token = await auth_service.create_tokens(user)
|
||||
|
||||
return ResponseModel(
|
||||
code=201,
|
||||
message="注册成功",
|
||||
data=token
|
||||
)
|
||||
except ConflictError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout", response_model=ResponseModel)
|
||||
async def logout(
|
||||
token: str = Depends(get_current_token),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
用户登出接口
|
||||
|
||||
将当前Token加入黑名单
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
await auth_service.logout(token)
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="登出成功"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=ResponseModel[Token])
|
||||
async def refresh_token(
|
||||
refresh_token: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
刷新访问令牌
|
||||
|
||||
使用刷新令牌获取新的访问令牌
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
|
||||
try:
|
||||
token = await auth_service.refresh_access_token(refresh_token)
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="刷新成功",
|
||||
data=token
|
||||
)
|
||||
except UnauthorizedError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="刷新令牌无效或已过期"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/reset-password/request", response_model=ResponseModel)
|
||||
async def request_password_reset(
|
||||
email: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
请求重置密码
|
||||
|
||||
向用户邮箱发送重置链接
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
|
||||
# 查找用户
|
||||
user = await auth_service.get_user_by_email(email)
|
||||
if not user:
|
||||
# 为了安全,即使用户不存在也返回成功
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="如果邮箱存在,重置链接已发送"
|
||||
)
|
||||
|
||||
# 生成重置令牌
|
||||
reset_token = await auth_service.create_password_reset_token(user)
|
||||
|
||||
# TODO: 发送邮件
|
||||
# await send_reset_email(email, reset_token)
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="如果邮箱存在,重置链接已发送"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/reset-password/confirm", response_model=ResponseModel)
|
||||
async def reset_password(
|
||||
reset_data: PasswordReset,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
确认重置密码
|
||||
|
||||
使用重置令牌设置新密码
|
||||
"""
|
||||
auth_service = AuthService(db)
|
||||
|
||||
try:
|
||||
await auth_service.reset_password(
|
||||
token=reset_data.token,
|
||||
new_password=reset_data.new_password
|
||||
)
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="密码重置成功"
|
||||
)
|
||||
except UnauthorizedError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="重置令牌无效或已过期"
|
||||
)
|
||||
|
||||
|
||||
# 辅助函数
|
||||
async def get_current_token(
|
||||
authorization: str = Depends(oauth2_scheme)
|
||||
) -> str:
|
||||
"""获取当前请求的Token"""
|
||||
return authorization
|
||||
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
认证服务示例代码
|
||||
"""
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token, create_refresh_token
|
||||
from app.core.exceptions import UnauthorizedError, ConflictError, ForbiddenError
|
||||
from app.core.logger import logger
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import UserRegister, Token
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务类"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def authenticate_user(self, username: str, password: str) -> Optional[User]:
|
||||
"""
|
||||
验证用户身份
|
||||
|
||||
Args:
|
||||
username: 用户名或邮箱
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
验证成功返回User对象,失败返回None
|
||||
"""
|
||||
# 查询用户(支持用户名或邮箱登录)
|
||||
query = select(User).where(
|
||||
(User.username == username) | (User.email == username)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
logger.warning("登录失败:用户不存在", username=username)
|
||||
return None
|
||||
|
||||
# 验证密码
|
||||
if not verify_password(password, user.password_hash):
|
||||
logger.warning("登录失败:密码错误", user_id=user.id, username=username)
|
||||
# TODO: 记录失败次数,实现账号锁定
|
||||
return None
|
||||
|
||||
# 检查账号状态
|
||||
if not user.is_active:
|
||||
logger.warning("登录失败:账号已禁用", user_id=user.id, username=username)
|
||||
raise ForbiddenError("账号已被禁用")
|
||||
|
||||
logger.info("用户登录成功", user_id=user.id, username=user.username)
|
||||
return user
|
||||
|
||||
async def create_user(self, user_data: UserRegister) -> User:
|
||||
"""
|
||||
创建新用户
|
||||
|
||||
Args:
|
||||
user_data: 用户注册数据
|
||||
|
||||
Returns:
|
||||
创建的用户对象
|
||||
|
||||
Raises:
|
||||
ConflictError: 用户名或邮箱已存在
|
||||
"""
|
||||
# 检查用户名是否存在
|
||||
query = select(User).where(User.username == user_data.username)
|
||||
result = await self.db.execute(query)
|
||||
if result.scalar_one_or_none():
|
||||
raise ConflictError("用户名已存在")
|
||||
|
||||
# 检查邮箱是否存在
|
||||
query = select(User).where(User.email == user_data.email)
|
||||
result = await self.db.execute(query)
|
||||
if result.scalar_one_or_none():
|
||||
raise ConflictError("邮箱已存在")
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
password_hash=get_password_hash(user_data.password),
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
role="trainee" # 默认角色为学员
|
||||
)
|
||||
|
||||
self.db.add(user)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(user)
|
||||
|
||||
logger.info("用户注册成功", user_id=user.id, username=user.username)
|
||||
return user
|
||||
|
||||
async def create_tokens(self, user: User) -> Token:
|
||||
"""
|
||||
为用户创建访问令牌和刷新令牌
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
|
||||
Returns:
|
||||
Token对象
|
||||
"""
|
||||
# 创建访问令牌
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
role=user.role,
|
||||
username=user.username
|
||||
)
|
||||
|
||||
# 创建刷新令牌
|
||||
refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer",
|
||||
expires_in=1800, # 30分钟
|
||||
user={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role
|
||||
}
|
||||
)
|
||||
|
||||
async def logout(self, token: str) -> None:
|
||||
"""
|
||||
用户登出,将token加入黑名单
|
||||
|
||||
Args:
|
||||
token: 要失效的token
|
||||
"""
|
||||
# TODO: 将token加入Redis黑名单
|
||||
# redis_key = f"blacklist:{token}"
|
||||
# await redis.setex(redis_key, 3600, "1") # 设置1小时过期
|
||||
|
||||
logger.info("用户登出成功")
|
||||
|
||||
async def refresh_access_token(self, refresh_token: str) -> Token:
|
||||
"""
|
||||
使用刷新令牌获取新的访问令牌
|
||||
|
||||
Args:
|
||||
refresh_token: 刷新令牌
|
||||
|
||||
Returns:
|
||||
新的Token对象
|
||||
"""
|
||||
# TODO: 验证刷新令牌
|
||||
# TODO: 检查是否在黑名单
|
||||
# TODO: 生成新的访问令牌
|
||||
# TODO: 可选 - 轮换刷新令牌
|
||||
|
||||
pass
|
||||
Reference in New Issue
Block a user