revai-api / app /dependencies.py
Shaankar39's picture
Restore RapidAPI proxy-secret validation (constant-time, dormant when unset)
535c840 verified
Raw
History Blame Contribute Delete
7.97 kB
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