import hashlib import hmac import time import datetime import uuid from fastapi import Depends, HTTPException, Header, Security, Request from fastapi.security import APIKeyHeader from sqlalchemy.orm import Session from sqlalchemy import func from app.database import get_db from app.models.user import User from app.models.apikey import APIKey from app.models.usage import UsageRecord from app.config import get_settings settings = get_settings() api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) RAPIDAPI_PLAN_MAP = { "BASIC": "free", "FREE": "free", "MAKER": "maker", "PRO": "maker", "GROWTH": "growth", "SCALE": "scale", "ULTRA": "scale", "MEGA": "scale", } def hash_api_key(key: str) -> str: return hashlib.sha256(key.encode()).hexdigest() def get_current_user( request: Request, x_api_key: str = Security(api_key_header), db: Session = Depends(get_db), ) -> User: # ── RapidAPI proxy auth ── proxy_secret = request.headers.get("X-RapidAPI-Proxy-Secret", "") rapidapi_user_id = request.headers.get("X-RapidAPI-User", "") rapidapi_plan = request.headers.get("X-RapidAPI-Subscription", "FREE").upper() if rapidapi_user_id: # Verify the request actually came through the RapidAPI gateway. # When a proxy secret is configured, the gateway-injected # X-RapidAPI-Proxy-Secret must match — this blocks forged # X-RapidAPI-User headers from callers hitting the origin directly # (which would otherwise bypass RapidAPI billing). # If no secret is configured we skip the check so the deployment is # never locked out before the env var is set. expected_secret = settings.rapidapi_proxy_secret if expected_secret and not hmac.compare_digest(proxy_secret, expected_secret): raise HTTPException(status_code=401, detail="Invalid RapidAPI proxy secret") # RapidAPI already authenticated the user — trust their proxy user = db.query(User).filter(User.email == f"rapidapi:{rapidapi_user_id}").first() if not user: tier = RAPIDAPI_PLAN_MAP.get(rapidapi_plan, "free") user = User( email=f"rapidapi:{rapidapi_user_id}", hashed_password="rapidapi-proxy-auth", name=f"RapidAPI User {rapidapi_user_id[:12]}", tier=tier, ) db.add(user) db.commit() db.refresh(user) else: new_tier = RAPIDAPI_PLAN_MAP.get(rapidapi_plan, "free") if user.tier != new_tier: user.tier = new_tier db.commit() return user # ── Direct API key auth ── if not x_api_key: raise HTTPException(status_code=401, detail="Missing X-API-Key header") key_hash = hash_api_key(x_api_key) api_key = db.query(APIKey).filter( APIKey.key_hash == key_hash, APIKey.is_active == True ).first() if not api_key: raise HTTPException(status_code=401, detail="Invalid or inactive API key") api_key.last_used_at = datetime.datetime.utcnow() db.commit() user = db.query(User).filter(User.id == api_key.user_id).first() if not user or not user.is_active: raise HTTPException(status_code=403, detail="Account is inactive") return user def check_rate_limit(user: User, db: Session, endpoint: str, request_count: int = 1): """Check if user has exceeded rate limits for their tier.""" tier_limits = { "free": settings.free_rpm, "maker": settings.maker_rpm, "growth": settings.growth_rpm, "scale": settings.scale_rpm, "enterprise": 10000 } per_minute_limit = tier_limits.get(user.tier, 10) # Check recent requests in last 60 seconds cutoff = datetime.datetime.utcnow() - datetime.timedelta(seconds=60) recent = db.query(func.sum(UsageRecord.count)).filter( UsageRecord.user_id == user.id, UsageRecord.created_at >= cutoff ).scalar() or 0 if recent + request_count > per_minute_limit: raise HTTPException( status_code=429, detail=f"Rate limit exceeded — quota reached. Tier '{user.tier}' allows {per_minute_limit} requests/minute." ) def check_prediction_quota(user: User, db: Session, count: int = 1): """Check if user has remaining predictions this month.""" tier_limits = { "free": settings.free_predictions, "maker": settings.maker_predictions, "growth": settings.growth_predictions, "scale": settings.scale_predictions, "enterprise": 10_000_000, } limit = tier_limits.get(user.tier, 100) month_str = datetime.datetime.utcnow().strftime("%Y-%m") used = db.query(func.sum(UsageRecord.count)).filter( UsageRecord.user_id == user.id, UsageRecord.month == month_str, UsageRecord.endpoint.in_(["predict/churn", "predict/lead", "analyze/call"]) ).scalar() or 0 if used + count > limit: raise HTTPException( status_code=429, detail=f"Monthly prediction quota exceeded. Tier '{user.tier}': {used}/{limit} predictions used this month." ) return used, limit def check_model_quota(user: User, db: Session): """Check if user can create more custom models.""" tier_limits = { "free": settings.free_models, "maker": settings.maker_models, "growth": settings.growth_models, "scale": settings.scale_models, "enterprise": 10000, } limit = tier_limits.get(user.tier, 0) current = db.query(MLModel).filter(MLModel.user_id == user.id).count() if current >= limit: raise HTTPException( status_code=429, detail=f"Model quota exceeded. Tier '{user.tier}': {current}/{limit} models used." ) return current, limit def track_usage(user: User, db: Session, endpoint: str, count: int = 1): month_str = datetime.datetime.utcnow().strftime("%Y-%m") record = UsageRecord( user_id=user.id, endpoint=endpoint, count=count, month=month_str, ) db.add(record) db.commit() def get_usage_summary(user: User, db: Session) -> dict: month_str = datetime.datetime.utcnow().strftime("%Y-%m") tier_limits = { "free": settings.free_predictions, "maker": settings.maker_predictions, "growth": settings.growth_predictions, "scale": settings.scale_predictions, "enterprise": 10_000_000, } model_limits = { "free": settings.free_models, "maker": settings.maker_models, "growth": settings.growth_models, "scale": settings.scale_models, "enterprise": 10000, } predictions_used = db.query(func.sum(UsageRecord.count)).filter( UsageRecord.user_id == user.id, UsageRecord.month == month_str, UsageRecord.endpoint.in_(["predict/churn", "predict/lead", "analyze/call"]) ).scalar() or 0 models_used = db.query(MLModel).filter(MLModel.user_id == user.id).count() usage_by_endpoint = {} records = db.query(UsageRecord).filter( UsageRecord.user_id == user.id, UsageRecord.month == month_str ).all() for r in records: usage_by_endpoint[r.endpoint] = usage_by_endpoint.get(r.endpoint, 0) + r.count pred_limit = tier_limits.get(user.tier, 100) model_limit = model_limits.get(user.tier, 0) return { "tier": user.tier, "predictions_used": predictions_used, "predictions_limit": pred_limit, "remaining_predictions": max(0, pred_limit - predictions_used), "models_used": models_used, "models_limit": model_limit, "remaining_models": max(0, model_limit - models_used), "usage_by_endpoint": usage_by_endpoint, } # Import MLModel at the bottom to avoid circular imports from app.models.mlmodel import MLModel