124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""认证 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},
|
||
)
|