office_translator/routes/auth_routes.py
Sepehr fcabe882cd feat: Add complete monetization system
Backend:
- User authentication with JWT tokens (auth_service.py)
- Subscription plans: Free, Starter (), Pro (), Business (), Enterprise
- Stripe integration for payments (payment_service.py)
- Usage tracking and quotas
- Credit packages for pay-per-use
- Plan-based provider restrictions

Frontend:
- Landing page with hero, features, pricing preview (landing-sections.tsx)
- Pricing page with all plans and credit packages (/pricing)
- User dashboard with usage stats (/dashboard)
- Login/Register pages with validation (/auth/login, /auth/register)
- Ollama self-hosting setup guide (/ollama-setup)
- Updated sidebar with user section and plan badge

Monetization strategy:
- Freemium: 3 docs/day, Ollama only
- Starter: 50 docs/month, Google Translate
- Pro: 200 docs/month, all providers, API access
- Business: 1000 docs/month, team management
- Enterprise: Custom pricing, SLA

Self-hosted option:
- Free unlimited usage with own Ollama server
- Complete privacy (data never leaves machine)
- Step-by-step setup guide included
2025-11-30 21:11:51 +01:00

282 lines
8.8 KiB
Python

"""
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