| """ |
| API authentication decorators. |
| """ |
|
|
| import logging |
| from functools import wraps |
|
|
| from django.conf import settings |
| from django.core.cache import cache |
| from django.http import JsonResponse |
| from django.utils import timezone |
|
|
| from core.models import DataStoreAPIToken |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| API_RATE_LIMIT = getattr(settings, "API_RATE_LIMIT", 60) |
| API_RATE_WINDOW = 60 |
|
|
|
|
| def api_token_required(view_func): |
| """ |
| Decorator to require API token authentication. |
| |
| Validates the token, checks expiration, enforces rate limiting, |
| and attaches the token to the request for view access. |
| |
| Token can be provided via: |
| - Authorization: Bearer <token> |
| - X-API-Key: <token> |
| """ |
| @wraps(view_func) |
| def wrapper(request, *args, **kwargs): |
| |
| token = _extract_token(request) |
| if not token: |
| return JsonResponse( |
| {"error": {"code": "UNAUTHORIZED", "message": "API token required"}}, |
| status=401, |
| ) |
|
|
| |
| try: |
| api_token = DataStoreAPIToken.objects.select_related("datastore").get( |
| token=token, |
| is_active=True, |
| ) |
| except DataStoreAPIToken.DoesNotExist: |
| logger.warning(f"API request with invalid token: {token[:8]}...") |
| return JsonResponse( |
| {"error": {"code": "UNAUTHORIZED", "message": "Invalid API token"}}, |
| status=401, |
| ) |
|
|
| |
| if api_token.expires_at and api_token.expires_at < timezone.now(): |
| logger.info(f"API request with expired token: {api_token.name}") |
| return JsonResponse( |
| {"error": {"code": "UNAUTHORIZED", "message": "API token has expired"}}, |
| status=401, |
| ) |
|
|
| |
| rate_key = f"api_rate_{api_token.id}" |
| requests_count = cache.get(rate_key, 0) |
|
|
| if requests_count >= API_RATE_LIMIT: |
| logger.warning(f"API rate limit exceeded for token: {api_token.name}") |
| return JsonResponse( |
| {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}, |
| status=429, |
| ) |
|
|
| cache.set(rate_key, requests_count + 1, API_RATE_WINDOW) |
|
|
| |
| DataStoreAPIToken.objects.filter(id=api_token.id).update( |
| last_used_at=timezone.now() |
| ) |
|
|
| |
| request.api_token = api_token |
|
|
| return view_func(request, *args, **kwargs) |
|
|
| return wrapper |
|
|
|
|
| def _extract_token(request): |
| """ |
| Extract API token from request headers. |
| |
| Supports: |
| - Authorization: Bearer <token> |
| - X-API-Key: <token> |
| """ |
| |
| auth_header = request.headers.get("Authorization", "") |
| if auth_header.startswith("Bearer "): |
| return auth_header[7:] |
|
|
| |
| return request.headers.get("X-API-Key") |
|
|
|
|
| def add_cors_headers(response): |
| """ |
| Add CORS headers to API response. |
| |
| For self-hosted deployments, we allow all origins by default. |
| This can be configured via API_CORS_ORIGINS setting. |
| """ |
| cors_origins = getattr(settings, "API_CORS_ORIGINS", "*") |
|
|
| response["Access-Control-Allow-Origin"] = cors_origins |
| response["Access-Control-Allow-Methods"] = "GET, OPTIONS" |
| response["Access-Control-Allow-Headers"] = "Authorization, X-API-Key, Content-Type" |
| response["Access-Control-Max-Age"] = "86400" |
|
|
| return response |
|
|