Spaces:
Sleeping
Sleeping
| """ | |
| API Key Service | |
| Manages API keys for programmatic access to the platform. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import secrets | |
| import logging | |
| from datetime import datetime, timezone | |
| from typing import Dict, List, Optional, Any | |
| logger = logging.getLogger(__name__) | |
| # API pricing in credits | |
| API_COSTS = { | |
| "hazardguard": 10, | |
| "weatherwise": 10, | |
| "geovision": 15, | |
| "data_layers": 5, | |
| "chatbot": 10, | |
| "timelapse": 5, | |
| } | |
| class APIKeyService: | |
| """Service for managing API keys using Supabase.""" | |
| def __init__(self, supabase_client): | |
| """ | |
| Initialize the API Key service. | |
| Args: | |
| supabase_client: Initialized Supabase client with service role key | |
| """ | |
| self._client = supabase_client | |
| def _generate_key() -> tuple[str, str, str]: | |
| """ | |
| Generate a new API key. | |
| Returns: | |
| Tuple of (full_key, key_hash, key_prefix) | |
| """ | |
| # Generate a secure random key with prefix | |
| random_part = secrets.token_urlsafe(32) | |
| full_key = f"sk_live_{random_part}" | |
| # Hash the key for storage | |
| key_hash = hashlib.sha256(full_key.encode()).hexdigest() | |
| # Store prefix for identification (first 12 chars) | |
| key_prefix = full_key[:12] | |
| return full_key, key_hash, key_prefix | |
| def _hash_key(api_key: str) -> str: | |
| """Hash an API key for comparison.""" | |
| return hashlib.sha256(api_key.encode()).hexdigest() | |
| def create_key( | |
| self, | |
| user_id: str, | |
| name: str, | |
| permissions: Optional[List[str]] = None, | |
| expires_at: Optional[datetime] = None, | |
| ) -> Dict[str, Any]: | |
| """ | |
| Create a new API key for a user. | |
| Args: | |
| user_id: The user's UUID | |
| name: A friendly name for the key | |
| permissions: List of allowed scopes (defaults to all) | |
| expires_at: Optional expiration datetime | |
| Returns: | |
| Dict with key details (includes the full key ONLY on creation) | |
| """ | |
| try: | |
| if permissions is None: | |
| permissions = ["models", "data_layers", "chatbot", "timelapse"] | |
| full_key, key_hash, key_prefix = self._generate_key() | |
| data = { | |
| "user_id": user_id, | |
| "name": name, | |
| "key_hash": key_hash, | |
| "key_prefix": key_prefix, | |
| "permissions": permissions, | |
| "is_active": True, | |
| } | |
| if expires_at: | |
| data["expires_at"] = expires_at.isoformat() | |
| result = self._client.table("api_keys").insert(data).execute() | |
| if result.data and len(result.data) > 0: | |
| key_record = result.data[0] | |
| return { | |
| "success": True, | |
| "api_key": full_key, # Only returned on creation! | |
| "key_id": key_record["id"], | |
| "name": key_record["name"], | |
| "key_prefix": key_record["key_prefix"], | |
| "permissions": key_record["permissions"], | |
| "created_at": key_record["created_at"], | |
| "expires_at": key_record.get("expires_at"), | |
| } | |
| return {"success": False, "error": "Failed to create API key"} | |
| except Exception as e: | |
| logger.error(f"Error creating API key: {e}") | |
| return {"success": False, "error": str(e)} | |
| def list_keys(self, user_id: str) -> Dict[str, Any]: | |
| """ | |
| List all API keys for a user (without exposing the actual keys). | |
| Args: | |
| user_id: The user's UUID | |
| Returns: | |
| Dict with list of key metadata | |
| """ | |
| try: | |
| result = ( | |
| self._client.table("api_keys") | |
| .select("id, name, key_prefix, permissions, is_active, last_used_at, usage_count, credits_consumed, created_at, expires_at") | |
| .eq("user_id", user_id) | |
| .order("created_at", desc=True) | |
| .execute() | |
| ) | |
| return { | |
| "success": True, | |
| "keys": result.data or [], | |
| } | |
| except Exception as e: | |
| logger.error(f"Error listing API keys: {e}") | |
| return {"success": False, "error": str(e), "keys": []} | |
| def revoke_key(self, user_id: str, key_id: str) -> Dict[str, Any]: | |
| """ | |
| Revoke (soft delete) an API key. | |
| Args: | |
| user_id: The user's UUID | |
| key_id: The key's UUID | |
| Returns: | |
| Dict with success status | |
| """ | |
| try: | |
| result = ( | |
| self._client.table("api_keys") | |
| .update({"is_active": False}) | |
| .eq("id", key_id) | |
| .eq("user_id", user_id) | |
| .execute() | |
| ) | |
| if result.data and len(result.data) > 0: | |
| return {"success": True, "message": "API key revoked successfully"} | |
| return {"success": False, "error": "API key not found"} | |
| except Exception as e: | |
| logger.error(f"Error revoking API key: {e}") | |
| return {"success": False, "error": str(e)} | |
| def delete_key(self, user_id: str, key_id: str) -> Dict[str, Any]: | |
| """ | |
| Permanently delete an API key. | |
| Args: | |
| user_id: The user's UUID | |
| key_id: The key's UUID | |
| Returns: | |
| Dict with success status | |
| """ | |
| try: | |
| result = ( | |
| self._client.table("api_keys") | |
| .delete() | |
| .eq("id", key_id) | |
| .eq("user_id", user_id) | |
| .execute() | |
| ) | |
| return {"success": True, "message": "API key deleted successfully"} | |
| except Exception as e: | |
| logger.error(f"Error deleting API key: {e}") | |
| return {"success": False, "error": str(e)} | |
| def validate_key(self, api_key: str) -> Dict[str, Any]: | |
| """ | |
| Validate an API key and return associated user info. | |
| Args: | |
| api_key: The full API key to validate | |
| Returns: | |
| Dict with validation result and user info | |
| """ | |
| try: | |
| if not api_key or not api_key.startswith("sk_live_"): | |
| return {"valid": False, "error": "Invalid API key format"} | |
| key_hash = self._hash_key(api_key) | |
| result = ( | |
| self._client.table("api_keys") | |
| .select("id, user_id, name, permissions, is_active, expires_at") | |
| .eq("key_hash", key_hash) | |
| .eq("is_active", True) | |
| .execute() | |
| ) | |
| if not result.data or len(result.data) == 0: | |
| return {"valid": False, "error": "API key not found or inactive"} | |
| key_record = result.data[0] | |
| # Check expiration | |
| if key_record.get("expires_at"): | |
| expires = datetime.fromisoformat(key_record["expires_at"].replace("Z", "+00:00")) | |
| if expires < datetime.now(timezone.utc): | |
| return {"valid": False, "error": "API key has expired"} | |
| return { | |
| "valid": True, | |
| "key_id": key_record["id"], | |
| "user_id": key_record["user_id"], | |
| "name": key_record["name"], | |
| "permissions": key_record["permissions"], | |
| } | |
| except Exception as e: | |
| logger.error(f"Error validating API key: {e}") | |
| return {"valid": False, "error": str(e)} | |
| def record_usage( | |
| self, | |
| key_id: str, | |
| user_id: str, | |
| endpoint: str, | |
| method: str, | |
| credits_charged: int, | |
| status_code: int, | |
| response_time_ms: int, | |
| ip_address: Optional[str] = None, | |
| user_agent: Optional[str] = None, | |
| request_metadata: Optional[Dict] = None, | |
| ) -> bool: | |
| """ | |
| Record an API usage event and update key statistics. | |
| Args: | |
| key_id: The API key's UUID | |
| user_id: The user's UUID | |
| endpoint: The API endpoint called | |
| method: HTTP method | |
| credits_charged: Credits deducted | |
| status_code: HTTP response code | |
| response_time_ms: Response time in milliseconds | |
| ip_address: Client IP | |
| user_agent: Client user agent | |
| request_metadata: Additional metadata | |
| Returns: | |
| True if recorded successfully | |
| """ | |
| try: | |
| # Insert usage log | |
| log_data = { | |
| "api_key_id": key_id, | |
| "user_id": user_id, | |
| "endpoint": endpoint, | |
| "method": method, | |
| "credits_charged": credits_charged, | |
| "status_code": status_code, | |
| "response_time_ms": response_time_ms, | |
| "ip_address": ip_address, | |
| "user_agent": user_agent, | |
| "request_metadata": request_metadata or {}, | |
| } | |
| self._client.table("api_usage_logs").insert(log_data).execute() | |
| # Update key statistics | |
| self._client.rpc( | |
| "increment_api_key_usage", | |
| {"p_key_id": key_id, "p_credits": credits_charged} | |
| ).execute() | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error recording API usage: {e}") | |
| # Don't fail the request if logging fails | |
| return False | |
| def get_usage_stats(self, user_id: str, key_id: Optional[str] = None, days: int = 30) -> Dict[str, Any]: | |
| """ | |
| Get usage statistics for a user's API keys. | |
| Args: | |
| user_id: The user's UUID | |
| key_id: Optional specific key to filter by | |
| days: Number of days to look back | |
| Returns: | |
| Dict with usage statistics | |
| """ | |
| try: | |
| query = ( | |
| self._client.table("api_usage_logs") | |
| .select("endpoint, credits_charged, created_at, status_code") | |
| .eq("user_id", user_id) | |
| .gte("created_at", f"now() - interval '{days} days'") | |
| .order("created_at", desc=True) | |
| .limit(1000) | |
| ) | |
| if key_id: | |
| query = query.eq("api_key_id", key_id) | |
| result = query.execute() | |
| logs = result.data or [] | |
| # Aggregate stats | |
| total_calls = len(logs) | |
| total_credits = sum(log.get("credits_charged", 0) for log in logs) | |
| endpoint_stats = {} | |
| for log in logs: | |
| ep = log.get("endpoint", "unknown") | |
| if ep not in endpoint_stats: | |
| endpoint_stats[ep] = {"calls": 0, "credits": 0} | |
| endpoint_stats[ep]["calls"] += 1 | |
| endpoint_stats[ep]["credits"] += log.get("credits_charged", 0) | |
| return { | |
| "success": True, | |
| "total_calls": total_calls, | |
| "total_credits": total_credits, | |
| "endpoint_breakdown": endpoint_stats, | |
| "recent_logs": logs[:50], # Last 50 calls | |
| } | |
| except Exception as e: | |
| logger.error(f"Error getting usage stats: {e}") | |
| return {"success": False, "error": str(e)} | |
| def get_api_costs() -> Dict[str, int]: | |
| """Return the API cost structure.""" | |
| return API_COSTS.copy() | |
| def increment_credits_consumed(self, key_id: str, credits: int) -> bool: | |
| """ | |
| Increment the credits_consumed counter for an API key. | |
| Args: | |
| key_id: The API key's UUID | |
| credits: Number of credits to add to the counter | |
| Returns: | |
| True if updated successfully | |
| """ | |
| try: | |
| # Use direct SQL update to increment | |
| self._client.rpc( | |
| "increment_api_key_credits", | |
| {"p_key_id": key_id, "p_credits": credits} | |
| ).execute() | |
| return True | |
| except Exception as e: | |
| # Fallback: try direct update | |
| try: | |
| result = ( | |
| self._client.table("api_keys") | |
| .select("credits_consumed") | |
| .eq("id", key_id) | |
| .execute() | |
| ) | |
| if result.data and len(result.data) > 0: | |
| current = result.data[0].get("credits_consumed", 0) or 0 | |
| self._client.table("api_keys").update({ | |
| "credits_consumed": current + credits | |
| }).eq("id", key_id).execute() | |
| return True | |
| except Exception as e2: | |
| logger.error(f"Error incrementing credits_consumed: {e2}") | |
| return False | |