from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db from app.core.rate_limit import RateLimiter from app.core.security import ( create_access_token, create_refresh_token, decode_token, get_password_hash, verify_password, ) from app.models.user import User from app.schemas.user import ( RefreshTokenRequest, TokenResponse, UserRegister, ) router = APIRouter() @router.post( "/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(RateLimiter(times=5, seconds=60))], ) async def register(payload: UserRegister, db: AsyncSession = Depends(get_db)): if not payload.phone and not payload.email: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Phone or email is required", ) filters = [] if payload.phone: filters.append(User.phone == payload.phone) if payload.email: filters.append(User.email == payload.email) result = await db.execute(select(User).where(or_(*filters))) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Phone or email already registered", ) user = User( phone=payload.phone, email=payload.email, password_hash=get_password_hash(payload.password), nickname=payload.nickname, city=payload.city, identity=payload.identity or "both", ) db.add(user) await db.commit() await db.refresh(user) return TokenResponse( access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id), ) @router.post( "/login", response_model=TokenResponse, dependencies=[Depends(RateLimiter(times=10, seconds=60))], ) async def login( form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(User).where( or_(User.phone == form_data.username, User.email == form_data.username) ) ) user = result.scalar_one_or_none() if not user or not verify_password(form_data.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect account or password", headers={"WWW-Authenticate": "Bearer"}, ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled", ) return TokenResponse( access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id), ) @router.post("/refresh", response_model=TokenResponse) async def refresh_token( payload: RefreshTokenRequest, db: AsyncSession = Depends(get_db), ): try: data = decode_token(payload.refresh_token) except ValueError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token", ) if data.get("type") != "refresh": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type", ) user_id = data.get("sub") result = await db.execute(select(User).where(User.id == int(user_id))) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found", ) return TokenResponse( access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id), )