PMFlow/backend/app/api/auth.py

124 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""认证 API"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import or_
from sqlalchemy.future import select
from ..core.database import get_db
from ..models import User, UserStatus
from ..schemas import PhoneCodeSendRequest, PhoneCodeVerifyRequest, UserLoginResponse, UserRegisterRequest
from ..services.auth import AuthService
from ..services.phone_auth_service import send_phone_verification_code, verify_phone_and_login
from ..services.platform_access_service import get_platform_context
from ..services.workspace_service import (
ensure_active_workspace_membership,
ensure_personal_workspace,
list_user_workspaces,
register_user_with_personal_workspace,
)
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login", response_model=UserLoginResponse)
async def login(
credentials: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
):
"""用户登录,返回 JWT"""
result = await db.execute(
select(User).where(or_(User.username == credentials.username, User.phone_e164 == credentials.username))
)
user = result.scalar_one_or_none()
if not user or not AuthService.verify_password(
credentials.password, user.password_hash
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not getattr(user, "is_active", True) or user.status != UserStatus.ACTIVE.value:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User is disabled",
)
await ensure_personal_workspace(db, user=user, commit=False)
await ensure_active_workspace_membership(db, user=user, commit=False)
await db.commit()
access_token = AuthService.create_access_token(data={"sub": user.username})
workspaces = await list_user_workspaces(db, user=user)
platform_context = await get_platform_context(db, user=user)
return UserLoginResponse(
access_token=access_token,
token_type="bearer",
user={**user.to_dict(), "workspace_context": workspaces, "platform_context": platform_context},
)
@router.post("/phone/send-code")
async def send_phone_code(
body: PhoneCodeSendRequest,
request: Request,
db: AsyncSession = Depends(get_db),
):
"""发送手机号验证码。开发环境会返回 dev_code,生产环境接短信服务商后不返回验证码。"""
return await send_phone_verification_code(
db,
country_code=body.phone_country_code,
phone_number=body.phone_number,
purpose=body.purpose,
request_ip=request.client.host if request.client else None,
)
@router.post("/phone/verify", response_model=UserLoginResponse)
async def verify_phone_code_login(
body: PhoneCodeVerifyRequest,
db: AsyncSession = Depends(get_db),
):
"""手机号验证码登录/自注册,并可绑定邀请。"""
result = await verify_phone_and_login(
db,
country_code=body.phone_country_code,
phone_number=body.phone_number,
code=body.code,
purpose=body.purpose,
invite_token=body.invite_token,
name=body.name,
)
return UserLoginResponse(
access_token=result["access_token"],
token_type=result["token_type"],
user=result["user"],
)
@router.post("/register", response_model=UserLoginResponse)
async def register(
body: UserRegisterRequest,
db: AsyncSession = Depends(get_db),
):
"""注册账号并自动创建个人空间。"""
user = await register_user_with_personal_workspace(
db,
username=body.username,
password=body.password,
name=body.name,
email=body.email or f"{body.username}@placeholder.local",
)
access_token = AuthService.create_access_token(data={"sub": user.username})
workspaces = await list_user_workspaces(db, user=user)
platform_context = await get_platform_context(db, user=user)
return UserLoginResponse(
access_token=access_token,
token_type="bearer",
user={**user.to_dict(), "workspace_context": workspaces, "platform_context": platform_context},
)