2026-04-20 17:11:31 +08:00
|
|
|
from uuid import UUID
|
|
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
|
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from ...core.database import get_session
|
|
|
|
|
from ...core.security import verify_password
|
|
|
|
|
from ...service.user import UserService
|
|
|
|
|
from ...service.auth import get_auth_service
|
2026-04-26 10:20:32 +08:00
|
|
|
from ...model.user import UserCreate, UserRead, UserLogin, Userlogout, UserSession
|
|
|
|
|
from ...model.http_error import HTTPError, Detail
|
2026-03-06 17:57:44 +08:00
|
|
|
from ..deps import get_current_user
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
from plugins.jwt.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
|
|
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
router = APIRouter(tags=["authentication"])
|
|
|
|
|
|
2026-04-13 15:55:12 +08:00
|
|
|
access_scheme = HTTPBearer(scheme_name="AccessToken")
|
|
|
|
|
refresh_scheme = HTTPBearer(scheme_name="RefreshToken")
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.post(
|
|
|
|
|
"/register",
|
|
|
|
|
response_model=UserRead,
|
|
|
|
|
responses={400: {"model": HTTPError, "description": "邮箱已被注册"}},
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def register(
|
|
|
|
|
user_data: UserCreate,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
用户注册接口
|
|
|
|
|
|
|
|
|
|
创建新用户账户,检查邮箱是否已被注册,如果邮箱未被注册则创建新用户。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_data: 用户注册数据,包含邮箱、密码、用户名和全名等信息
|
|
|
|
|
session: 数据库会话对象,用于执行数据库操作
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
UserRead: 创建成功的用户信息,包含用户ID、邮箱、用户名等字段
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
HTTPException: 当邮箱已被注册时抛出400错误
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
existing = await UserService.get_user_by_email(user_data.email, session)
|
|
|
|
|
if existing:
|
2026-04-26 10:20:32 +08:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail=Detail(
|
|
|
|
|
message="邮箱已被注册", code=status.HTTP_400_BAD_REQUEST
|
|
|
|
|
).model_dump(),
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
user = await UserService.create_user(
|
|
|
|
|
session=session,
|
|
|
|
|
email=user_data.email,
|
|
|
|
|
password=user_data.password,
|
|
|
|
|
username=user_data.username,
|
|
|
|
|
full_name=user_data.full_name,
|
|
|
|
|
)
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.post(
|
|
|
|
|
"/login",
|
|
|
|
|
response_model=TokenResponse,
|
|
|
|
|
responses={
|
|
|
|
|
400: {"model": HTTPError, "description": "邮箱不存在"},
|
|
|
|
|
401: {"model": HTTPError, "description": "密码错误"},
|
|
|
|
|
402: {"model": HTTPError, "description": "用户被禁用"},
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def login(
|
|
|
|
|
user_data: UserLogin,
|
|
|
|
|
request: Request,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
用户登录接口
|
|
|
|
|
|
|
|
|
|
验证用户凭据并生成访问令牌和刷新令牌。依次检查邮箱是否存在、密码是否正确、用户是否被禁用。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_data: 用户登录数据,包含邮箱和密码
|
|
|
|
|
request: FastAPI请求对象,用于获取客户端信息
|
|
|
|
|
session: 数据库会话对象,用于查询用户信息
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
TokenResponse: 包含访问令牌(access_token)、刷新令牌(refresh_token)和令牌类型(token_type)的响应对象
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
HTTPException:
|
|
|
|
|
- 400错误:当邮箱不存在时
|
|
|
|
|
- 401错误:当密码错误时
|
|
|
|
|
- 402错误:当用户账户被禁用时
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
user = await UserService.get_user_by_email(user_data.email, session)
|
|
|
|
|
if not user:
|
2026-04-26 10:20:32 +08:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400, detail=Detail(message="邮箱不存在", code=400).model_dump()
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
if not verify_password(user_data.password, user.hashed_password):
|
2026-04-26 10:20:32 +08:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400, detail=Detail(message="密码错误", code=401).model_dump()
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
if not user.is_active:
|
2026-04-26 10:20:32 +08:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400, detail=Detail(message="用户被禁用", code=402).model_dump()
|
|
|
|
|
)
|
2026-04-13 15:55:12 +08:00
|
|
|
auth_service = get_auth_service()
|
|
|
|
|
return await auth_service.create_tokens(user, request, device_id=None)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.post(
|
|
|
|
|
"/refresh",
|
|
|
|
|
response_model=RefreshTokenResponse,
|
|
|
|
|
responses={
|
|
|
|
|
401: {"model": HTTPError, "description": "无效的刷新令牌"},
|
|
|
|
|
},
|
|
|
|
|
summary="刷新访问令牌",
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def refresh_token(
|
|
|
|
|
request: Request,
|
2026-04-13 15:55:12 +08:00
|
|
|
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
|
2026-03-06 17:57:44 +08:00
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
刷新访问令牌接口
|
|
|
|
|
|
|
|
|
|
使用有效的刷新令牌获取新的访问令牌。验证刷新令牌的有效性,如果有效则生成新的访问令牌。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
request: FastAPI请求对象,用于获取客户端信息
|
|
|
|
|
credentials: HTTP认证凭证,从请求头中提取的刷新令牌
|
|
|
|
|
session: 数据库会话对象,用于验证令牌状态
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
RefreshTokenResponse: 包含新的访问令牌(access_token)和令牌类型(token_type)的响应对象
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
HTTPException: 当刷新令牌无效或已过期时抛出401错误
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
auth_service = get_auth_service()
|
|
|
|
|
|
|
|
|
|
refresh_token = credentials.credentials
|
|
|
|
|
new_access_token = await auth_service.refresh_access_token(
|
|
|
|
|
refresh_token, session, request
|
|
|
|
|
)
|
|
|
|
|
if not new_access_token:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
2026-04-26 10:20:32 +08:00
|
|
|
detail=Detail(message="无效的刷新令牌", code=401).model_dump(),
|
2026-03-06 17:57:44 +08:00
|
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
|
|
|
)
|
|
|
|
|
return {"access_token": new_access_token, "token_type": "bearer"}
|
|
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.post("/logout", response_model=Userlogout)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def logout(
|
2026-04-13 15:55:12 +08:00
|
|
|
credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
|
2026-03-06 17:57:44 +08:00
|
|
|
):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
用户登出接口
|
|
|
|
|
|
|
|
|
|
使当前的访问令牌失效,实现单设备登出功能。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
credentials: HTTP认证凭证,从请求头中提取的访问令牌
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Userlogout: 登出成功响应,包含成功消息和状态码200
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
auth_service = get_auth_service()
|
|
|
|
|
token = credentials.credentials
|
|
|
|
|
await auth_service.logout(token)
|
2026-04-26 10:20:32 +08:00
|
|
|
return {"message": "登出成功", "code": 200}
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.post("/logout-all", summary="登出所有设备", response_model=Userlogout)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def logout_all(current_user: TokenData = Depends(get_current_user)):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
登出所有设备接口
|
|
|
|
|
|
|
|
|
|
使指定用户的所有活跃会话令牌失效,实现多设备同时登出功能。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
current_user: 当前登录用户的令牌数据,包含用户ID等信息
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Userlogout: 登出成功响应,包含成功消息和状态码200
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
auth_service = get_auth_service()
|
|
|
|
|
await auth_service.logout_all(current_user.user_id)
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
return {"message": "所有设备已登出", "code": 200}
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/sessions", summary="获取用户会话")
|
|
|
|
|
async def get_sessions(current_user: TokenData = Depends(get_current_user)):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
获取用户会话列表接口
|
|
|
|
|
|
|
|
|
|
查询并返回指定用户的所有活跃会话信息,包括每个会话的设备信息和创建时间等。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
current_user: 当前登录用户的令牌数据,包含用户ID等信息
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
UserSession: 用户会话信息对象,包含用户ID、会话列表和会话总数
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
auth_service = get_auth_service()
|
|
|
|
|
sessions = await auth_service.get_user_sessions(current_user.user_id)
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
return UserSession(
|
|
|
|
|
user_id=current_user.user_id, sessions=sessions, count=len(sessions)
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@router.get(
|
|
|
|
|
"/me",
|
|
|
|
|
response_model=UserRead,
|
|
|
|
|
summary="获取当前用户信息",
|
|
|
|
|
responses={404: {"model": HTTPError, "description": "用户不存在"}},
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
async def get_current_user_info(
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
current_user: TokenData = Depends(get_current_user),
|
|
|
|
|
):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
获取当前用户信息接口
|
|
|
|
|
|
|
|
|
|
根据当前登录用户的令牌信息,从数据库中查询并返回完整的用户资料。
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
session: 数据库会话对象,用于查询用户信息
|
|
|
|
|
current_user: 当前登录用户的令牌数据,包含用户ID等信息
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
UserRead: 用户详细信息,包含用户ID、邮箱、用户名、全名、激活状态等字段
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
HTTPException: 当用户不存在时抛出404错误
|
|
|
|
|
"""
|
2026-04-20 17:11:31 +08:00
|
|
|
user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
|
2026-03-06 17:57:44 +08:00
|
|
|
if not user:
|
2026-04-26 10:20:32 +08:00
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=Detail(message="用户不存在", code=404).model_dump(),
|
|
|
|
|
)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
return user
|