""" Authentication and User API routes """ from fastapi import APIRouter, HTTPException, Depends, Header, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel, EmailStr from typing import Optional, Dict, Any from models.subscription import UserCreate, UserLogin, UserResponse, PlanType, PLANS, CREDIT_PACKAGES from services.auth_service import ( create_user, authenticate_user, get_user_by_id, create_access_token, create_refresh_token, verify_token, check_usage_limits, update_user ) from services.payment_service import ( create_checkout_session, create_credits_checkout, cancel_subscription, get_billing_portal_url, handle_webhook, is_stripe_configured ) router = APIRouter(prefix="/api/auth", tags=["Authentication"]) security = HTTPBearer(auto_error=False) # Request/Response models class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" user: UserResponse class RefreshRequest(BaseModel): refresh_token: str class CheckoutRequest(BaseModel): plan: PlanType billing_period: str = "monthly" class CreditsCheckoutRequest(BaseModel): package_index: int # Dependency to get current user async def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)): if not credentials: return None payload = verify_token(credentials.credentials) if not payload: return None user = get_user_by_id(payload.get("sub")) return user async def require_user(credentials: HTTPAuthorizationCredentials = Depends(security)): if not credentials: raise HTTPException(status_code=401, detail="Not authenticated") payload = verify_token(credentials.credentials) if not payload: raise HTTPException(status_code=401, detail="Invalid or expired token") user = get_user_by_id(payload.get("sub")) if not user: raise HTTPException(status_code=401, detail="User not found") return user def user_to_response(user) -> UserResponse: """Convert User to UserResponse with plan limits""" plan_limits = PLANS[user.plan] return UserResponse( id=user.id, email=user.email, name=user.name, avatar_url=user.avatar_url, plan=user.plan, subscription_status=user.subscription_status, docs_translated_this_month=user.docs_translated_this_month, pages_translated_this_month=user.pages_translated_this_month, api_calls_this_month=user.api_calls_this_month, extra_credits=user.extra_credits, created_at=user.created_at, plan_limits={ "docs_per_month": plan_limits["docs_per_month"], "max_pages_per_doc": plan_limits["max_pages_per_doc"], "max_file_size_mb": plan_limits["max_file_size_mb"], "providers": plan_limits["providers"], "features": plan_limits["features"], "api_access": plan_limits.get("api_access", False), } ) # Auth endpoints @router.post("/register", response_model=TokenResponse) async def register(user_create: UserCreate): """Register a new user""" try: user = create_user(user_create) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) access_token = create_access_token(user.id) refresh_token = create_refresh_token(user.id) return TokenResponse( access_token=access_token, refresh_token=refresh_token, user=user_to_response(user) ) @router.post("/login", response_model=TokenResponse) async def login(credentials: UserLogin): """Login with email and password""" user = authenticate_user(credentials.email, credentials.password) if not user: raise HTTPException(status_code=401, detail="Invalid email or password") access_token = create_access_token(user.id) refresh_token = create_refresh_token(user.id) return TokenResponse( access_token=access_token, refresh_token=refresh_token, user=user_to_response(user) ) @router.post("/refresh", response_model=TokenResponse) async def refresh_tokens(request: RefreshRequest): """Refresh access token""" payload = verify_token(request.refresh_token) if not payload or payload.get("type") != "refresh": raise HTTPException(status_code=401, detail="Invalid refresh token") user = get_user_by_id(payload.get("sub")) if not user: raise HTTPException(status_code=401, detail="User not found") access_token = create_access_token(user.id) refresh_token = create_refresh_token(user.id) return TokenResponse( access_token=access_token, refresh_token=refresh_token, user=user_to_response(user) ) @router.get("/me", response_model=UserResponse) async def get_me(user=Depends(require_user)): """Get current user info""" return user_to_response(user) @router.get("/usage") async def get_usage(user=Depends(require_user)): """Get current usage and limits""" return check_usage_limits(user) @router.put("/settings") async def update_settings(settings: Dict[str, Any], user=Depends(require_user)): """Update user settings""" allowed_fields = [ "default_source_lang", "default_target_lang", "default_provider", "ollama_endpoint", "ollama_model", "name" ] updates = {k: v for k, v in settings.items() if k in allowed_fields} updated_user = update_user(user.id, updates) if not updated_user: raise HTTPException(status_code=400, detail="Failed to update settings") return user_to_response(updated_user) # Plans endpoint (public) @router.get("/plans") async def get_plans(): """Get all available plans""" plans = [] for plan_type, config in PLANS.items(): plans.append({ "id": plan_type.value, "name": config["name"], "price_monthly": config["price_monthly"], "price_yearly": config["price_yearly"], "features": config["features"], "docs_per_month": config["docs_per_month"], "max_pages_per_doc": config["max_pages_per_doc"], "max_file_size_mb": config["max_file_size_mb"], "providers": config["providers"], "api_access": config.get("api_access", False), "popular": plan_type == PlanType.PRO, }) return {"plans": plans, "credit_packages": CREDIT_PACKAGES} # Payment endpoints @router.post("/checkout/subscription") async def checkout_subscription(request: CheckoutRequest, user=Depends(require_user)): """Create Stripe checkout session for subscription""" if not is_stripe_configured(): # Demo mode - just upgrade the user update_user(user.id, {"plan": request.plan.value}) return {"demo_mode": True, "message": "Upgraded in demo mode", "plan": request.plan.value} result = await create_checkout_session( user.id, request.plan, request.billing_period ) if "error" in result: raise HTTPException(status_code=400, detail=result["error"]) return result @router.post("/checkout/credits") async def checkout_credits(request: CreditsCheckoutRequest, user=Depends(require_user)): """Create Stripe checkout session for credits""" if not is_stripe_configured(): # Demo mode - add credits directly from services.auth_service import add_credits credits = CREDIT_PACKAGES[request.package_index]["credits"] add_credits(user.id, credits) return {"demo_mode": True, "message": f"Added {credits} credits in demo mode"} result = await create_credits_checkout(user.id, request.package_index) if "error" in result: raise HTTPException(status_code=400, detail=result["error"]) return result @router.post("/subscription/cancel") async def cancel_user_subscription(user=Depends(require_user)): """Cancel subscription""" result = await cancel_subscription(user.id) if "error" in result: raise HTTPException(status_code=400, detail=result["error"]) return result @router.get("/billing-portal") async def get_billing_portal(user=Depends(require_user)): """Get Stripe billing portal URL""" url = await get_billing_portal_url(user.id) if not url: raise HTTPException(status_code=400, detail="Billing portal not available") return {"url": url} # Stripe webhook @router.post("/webhook/stripe") async def stripe_webhook(request: Request, stripe_signature: str = Header(None)): """Handle Stripe webhooks""" payload = await request.body() result = await handle_webhook(payload, stripe_signature or "") if "error" in result: raise HTTPException(status_code=400, detail=result["error"]) return result