from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import Annotated from app.database import get_db from app.models.user import User from app.core.security import hash_password, verify_password, create_access_token, create_refresh_token, decode_token from pydantic import BaseModel, EmailStr from datetime import datetime router = APIRouter() class RegisterRequest(BaseModel): phone: str password: str username: str = "" class LoginResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" user: dict class RefreshRequest(BaseModel): refresh_token: str @router.post("/register") async def register(data: RegisterRequest, db: Annotated[AsyncSession, Depends(get_db)]): existing = await db.execute(select(User).where(User.phone == data.phone)) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Phone already registered") user = User( phone=data.phone, username=data.username or data.phone, password_hash=hash_password(data.password), tier="free", ) db.add(user) await db.flush() return { "id": str(user.id), "phone": user.phone, "username": user.username, "tier": user.tier, } @router.post("/login", response_model=LoginResponse) async def login( form: Annotated[OAuth2PasswordRequestForm, Depends()], db: Annotated[AsyncSession, Depends(get_db)], ): result = await db.execute(select(User).where(User.phone == form.username)) user = result.scalar_one_or_none() if not user or not verify_password(form.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", ) return LoginResponse( access_token=create_access_token({"sub": str(user.id), "tier": user.tier}), refresh_token=create_refresh_token({"sub": str(user.id)}), user={ "id": str(user.id), "phone": user.phone, "username": user.username, "tier": user.tier, }, ) @router.post("/refresh") async def refresh(data: RefreshRequest): payload = decode_token(data.refresh_token) if not payload or payload.get("type") != "refresh": raise HTTPException(status_code=401, detail="Invalid refresh token") return { "access_token": create_access_token({"sub": payload["sub"]}), "token_type": "bearer", } @router.get("/me") async def get_me( authorization: str = None, db: Annotated[AsyncSession, Depends(get_db)] = None, ): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing token") payload = decode_token(authorization[7:]) if not payload: raise HTTPException(status_code=401, detail="Invalid token") result = await db.execute(select(User).where(User.id == payload["sub"])) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") return { "id": str(user.id), "phone": user.phone, "username": user.username, "tier": user.tier, "settings": user.settings, "created_at": user.created_at.isoformat() if user.created_at else None, } class SettingsUpdate(BaseModel): preferred_translate_provider: str = None reply_tone: str = None timezone: str = None languages: list = None @router.patch("/settings") async def update_settings( data: SettingsUpdate, authorization: str = None, db: Annotated[AsyncSession, Depends(get_db)] = None, ): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing token") payload = decode_token(authorization[7:]) if not payload: raise HTTPException(status_code=401, detail="Invalid token") result = await db.execute(select(User).where(User.id == payload["sub"])) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") settings = user.settings or {} for key, value in data.dict(exclude_unset=True).items(): if value is not None: settings[key] = value user.settings = settings await db.flush() return {"settings": user.settings}