Complete Phase 1 implementation: Backend (FastAPI + MongoDB + Celery): - Core: config, DB with indexes, JWT security, API key auth middleware - Models: org hierarchy (workspace/team/project), user mirror, pricing, usage events/rollups, budgets, alert log, audit log - Services: pricing engine (LiteLLM/YAML/override priority), budget check with preflight, email alerts at 50/80/100%, analytics aggregations, audit logger - API routes: public (preflight/record/upsert), admin CRUD, pricing management, budget management, analytics (summary/timeseries/breakdown/pivot), Microsoft SSO auth - Celery tasks: daily LiteLLM price sync with change notifications, daily rollup aggregation, 5-minute alert evaluator - Pricing catalogue: ElevenLabs + Google Cloud TTS in models.yaml SDK (oliver-cost-tracker Python package): - CostTracker client with httpx + exponential backoff (3 retries) - SQLite outbox with 30s background flusher (never blocks AI pipeline) - Estimators: token/char estimation per provider - BudgetExceeded / CostTrackerUnavailable exceptions Frontend (React 18 + Vite + TypeScript): - Dashboard with KPI cards, daily cost timeseries, top-model/top-user charts - Pivot Explorer with multi-dim row/col selection + stacked bar chart + table - Admin pages: Workspaces, Pricing (with LiteLLM sync + override), Budgets (with live spend bar), API Keys (show-once), Users (mirror), Audit Log - Microsoft SSO login flow Infra: docker-compose.yml (mongo + redis + api + celery worker + beat + frontend) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
41 lines
1.4 KiB
Python
41 lines
1.4 KiB
Python
from fastapi import Depends, HTTPException, Security, status
|
|
from fastapi.security import APIKeyHeader
|
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
|
|
from ..core.database import get_database
|
|
from ..core.security import verify_api_key
|
|
|
|
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
|
|
|
|
async def get_api_key_doc(
|
|
api_key: str = Security(api_key_header),
|
|
db: AsyncIOMotorDatabase = Depends(get_database),
|
|
) -> dict:
|
|
"""Validate X-API-Key and return the api_key document."""
|
|
if not api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="API key required (X-API-Key header)",
|
|
)
|
|
|
|
# Load all active keys for this source (avoid full collection scan via index on is_active)
|
|
# We use a sha256 hash lookup directly.
|
|
import hashlib
|
|
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
|
|
|
doc = await db.api_keys.find_one({"key_hash": key_hash, "is_active": True})
|
|
if not doc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid or revoked API key",
|
|
)
|
|
|
|
# Bump last_used_at in background (non-blocking)
|
|
from datetime import datetime, timezone
|
|
await db.api_keys.update_one(
|
|
{"_id": doc["_id"]},
|
|
{"$set": {"last_used_at": datetime.now(timezone.utc)}},
|
|
)
|
|
|
|
return doc
|