Your Name 4caafdb50f feat: 添加后端核心模块
用户认证:
- User 模型(支持邮箱/手机号登录)
- 双 Token JWT 认证(accessToken + refreshToken)
- 注册/登录/刷新 Token API

组织模型:
- Brand(品牌方)、Agency(代理商)、Creator(达人)
- 多对多关系:品牌方↔代理商、代理商↔达人

项目与任务:
- Project 模型(品牌方发布)
- Task 模型(完整审核流程追踪)
- Brief 模型(解析后的结构化内容)

文件上传:
- 阿里云 OSS 直传签名服务
- 支持分片上传,最大 500MB

数据库迁移:
- 003_user_org_project_task.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 13:47:36 +08:00

247 lines
6.3 KiB
Python

"""
认证 API
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.auth import (
RegisterRequest,
LoginRequest,
LoginResponse,
RefreshTokenRequest,
RefreshTokenResponse,
UserResponse,
)
from app.services.auth import (
get_user_by_email,
get_user_by_phone,
get_user_by_id,
create_user,
authenticate_user,
create_access_token,
create_refresh_token,
update_refresh_token,
decode_token,
get_user_organization_info,
)
router = APIRouter(prefix="/auth", tags=["认证"])
@router.post("/register", response_model=LoginResponse, status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequest,
db: AsyncSession = Depends(get_db),
):
"""
用户注册
- 支持邮箱或手机号注册(至少提供一个)
- 注册后自动登录,返回 Token
"""
# 验证至少提供邮箱或手机号
if not request.email and not request.phone:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请提供邮箱或手机号",
)
# 检查邮箱是否已存在
if request.email:
existing = await get_user_by_email(db, request.email)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该邮箱已被注册",
)
# 检查手机号是否已存在
if request.phone:
existing = await get_user_by_phone(db, request.phone)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该手机号已被注册",
)
# 创建用户
user = await create_user(
db=db,
email=request.email,
phone=request.phone,
password=request.password,
name=request.name,
role=request.role,
)
# 生成 Token
access_token = create_access_token(user.id)
refresh_token, refresh_expires_at = create_refresh_token(user.id)
# 保存 refresh token
await update_refresh_token(db, user, refresh_token, refresh_expires_at)
await db.commit()
# 获取组织信息
org_info = await get_user_organization_info(db, user)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
user=UserResponse(
id=user.id,
email=user.email,
phone=user.phone,
name=user.name,
avatar=user.avatar,
role=user.role,
is_verified=user.is_verified,
**org_info,
),
)
@router.post("/login", response_model=LoginResponse)
async def login(
request: LoginRequest,
db: AsyncSession = Depends(get_db),
):
"""
用户登录
- 支持邮箱+密码 或 手机号+密码 登录
- 返回 accessToken 和 refreshToken
"""
# 验证请求参数
if not request.email and not request.phone:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请提供邮箱或手机号",
)
if not request.password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请提供密码",
)
# 验证用户
user = await authenticate_user(
db=db,
email=request.email,
phone=request.phone,
password=request.password,
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="邮箱/手机号或密码错误",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="账号已被禁用",
)
# 生成 Token
access_token = create_access_token(user.id)
refresh_token, refresh_expires_at = create_refresh_token(user.id)
# 保存 refresh token
await update_refresh_token(db, user, refresh_token, refresh_expires_at)
await db.commit()
# 获取组织信息
org_info = await get_user_organization_info(db, user)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
user=UserResponse(
id=user.id,
email=user.email,
phone=user.phone,
name=user.name,
avatar=user.avatar,
role=user.role,
is_verified=user.is_verified,
**org_info,
),
)
@router.post("/refresh", response_model=RefreshTokenResponse)
async def refresh_token(
request: RefreshTokenRequest,
db: AsyncSession = Depends(get_db),
):
"""
刷新 Access Token
- 使用 refreshToken 获取新的 accessToken
- refreshToken 有效期 7 天
"""
# 解码 refresh token
payload = decode_token(request.refresh_token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的 refresh token",
)
if payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的 token 类型",
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的 token",
)
# 获取用户
user = await get_user_by_id(db, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在",
)
# 验证 refresh token 是否匹配
if user.refresh_token != request.refresh_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="refresh token 已失效",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="账号已被禁用",
)
# 生成新的 access token
access_token = create_access_token(user.id)
return RefreshTokenResponse(access_token=access_token)
@router.post("/logout")
async def logout(
db: AsyncSession = Depends(get_db),
# TODO: 添加认证依赖
):
"""
退出登录
- 清除 refresh token
"""
# TODO: 实现退出登录
return {"message": "已退出登录"}