File size: 13,167 Bytes
98a8586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
"""
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

    @staticmethod
    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

    @staticmethod
    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)}

    @staticmethod
    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