Spaces:
Sleeping
Sleeping
| 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 | |