1193 lines
34 KiB
Markdown
1193 lines
34 KiB
Markdown
# API Implementation Plan - Section 4: Apply Rate Limiting and Throttling Rules
|
|
|
|
**Date:** 2025-01-XX
|
|
**Status:** Planning
|
|
**Priority:** High
|
|
**Related Document:** `API-ENDPOINTS-ANALYSIS.md`
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This document outlines the implementation plan for **Section 4: Apply Rate Limiting and Throttling Rules** across the IGNY8 API layer. The goal is to prevent abuse, ensure fair resource usage, and enforce plan limits by introducing consistent per-user and per-function rate limits using DRF throttling and internal usage tracking.
|
|
|
|
**Key Objectives:**
|
|
- Enable DRF throttling system with scoped rate limits
|
|
- Assign throttle scopes to all major functions (AI, auth, content, images)
|
|
- Implement plan-based throttling (optional, future phase)
|
|
- Log and expose rate limit information
|
|
- Integrate with existing credit system for quota enforcement
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Current State Analysis](#current-state-analysis)
|
|
2. [Implementation Tasks](#implementation-tasks)
|
|
3. [Task 1: Enable DRF Throttling System](#task-1-enable-drf-throttling-system)
|
|
4. [Task 2: Assign Throttle Scopes in ViewSets](#task-2-assign-throttle-scopes-in-viewsets)
|
|
5. [Task 3: Override User/Account-Level Throttles (Optional)](#task-3-override-useraccount-level-throttles-optional)
|
|
6. [Task 4: Log + Expose Rate Limit Info](#task-4-log--expose-rate-limit-info)
|
|
7. [Task 5: Advanced Daily/Monthly Quotas Per Plan](#task-5-advanced-dailymonthly-quotas-per-plan)
|
|
8. [Task 6: Testing Matrix](#task-6-testing-matrix)
|
|
9. [Task 7: Changelog Entry](#task-7-changelog-entry)
|
|
10. [Testing Strategy](#testing-strategy)
|
|
11. [Rollout Plan](#rollout-plan)
|
|
12. [Success Criteria](#success-criteria)
|
|
|
|
---
|
|
|
|
## Current State Analysis
|
|
|
|
### Current Rate Limiting Status
|
|
|
|
Based on `API-ENDPOINTS-ANALYSIS.md`:
|
|
|
|
1. **Rate Limiting:** ❌ **Not Implemented**
|
|
- All endpoints are currently unlimited
|
|
- No throttling configuration in settings
|
|
- No rate limit enforcement
|
|
|
|
2. **Credit System:**
|
|
- `CreditUsageLog` model exists for tracking usage
|
|
- `CreditBalance` model tracks account credits
|
|
- Credit deduction happens but no rate limiting based on credits
|
|
|
|
3. **Plan System:**
|
|
- `Plan` model exists with different tiers
|
|
- Plans have credit limits but no API rate limits defined
|
|
- No plan-based throttling implemented
|
|
|
|
4. **AI Functions:**
|
|
- High-cost operations (AI generation, image generation)
|
|
- Currently no rate limiting on these expensive endpoints
|
|
- Risk of abuse and cost overruns
|
|
|
|
---
|
|
|
|
## Implementation Tasks
|
|
|
|
### Overview
|
|
|
|
| Task ID | Task Name | Priority | Estimated Effort | Dependencies |
|
|
|---------|-----------|----------|------------------|--------------|
|
|
| 1.1 | Configure DRF throttling in settings | High | 2 hours | None |
|
|
| 1.2 | Add debug throttle bypass | High | 1 hour | 1.1 |
|
|
| 2.1 | Audit endpoints for throttle scopes | Medium | 3 hours | None |
|
|
| 2.2 | Assign throttle scopes to ViewSets | High | 6 hours | 1.1 |
|
|
| 3.1 | Create plan-based throttle class (optional) | Low | 8 hours | 1.1 |
|
|
| 4.1 | Verify throttle headers | High | 2 hours | 2.2 |
|
|
| 4.2 | Add frontend rate limit handling | Medium | 4 hours | 4.1 |
|
|
| 4.3 | Implement abuse detection logging | Medium | 3 hours | 2.2 |
|
|
| 5.1 | Integrate with credit system | Low | 6 hours | 3.1 |
|
|
| 6.1 | Create test scenarios | High | 4 hours | 2.2 |
|
|
| 7.1 | Create changelog entry | Low | 1 hour | All tasks |
|
|
|
|
**Total Estimated Effort:** ~40 hours
|
|
|
|
---
|
|
|
|
## Task 1: Enable DRF Throttling System
|
|
|
|
### Goal
|
|
Configure Django REST Framework to use scoped rate throttling with appropriate rate limits for different endpoint types.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 1.1: Configure DRF Throttling in Settings
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
**Update REST_FRAMEWORK configuration:**
|
|
```python
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'igny8_core.api.pagination.CustomPageNumberPagination',
|
|
'PAGE_SIZE': 10,
|
|
'DEFAULT_FILTER_BACKENDS': [
|
|
'django_filters.rest_framework.DjangoFilterBackend',
|
|
'rest_framework.filters.SearchFilter',
|
|
'rest_framework.filters.OrderingFilter',
|
|
],
|
|
'DEFAULT_PERMISSION_CLASSES': [
|
|
'rest_framework.permissions.AllowAny', # Will be changed in Section 2
|
|
],
|
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
'igny8_core.api.authentication.JWTAuthentication',
|
|
'igny8_core.api.authentication.CSRFExemptSessionAuthentication',
|
|
'rest_framework.authentication.BasicAuthentication',
|
|
],
|
|
'EXCEPTION_HANDLER': 'igny8_core.api.exception_handlers.custom_exception_handler',
|
|
# Throttling Configuration
|
|
'DEFAULT_THROTTLE_CLASSES': [
|
|
'rest_framework.throttling.ScopedRateThrottle',
|
|
],
|
|
'DEFAULT_THROTTLE_RATES': {
|
|
# AI Functions - Expensive operations
|
|
'ai_function': '10/min', # AI content generation, clustering
|
|
'image_gen': '15/min', # Image generation
|
|
|
|
# Content Operations
|
|
'content_write': '30/min', # Content creation, updates
|
|
'content_read': '100/min', # Content listing, retrieval
|
|
|
|
# Authentication
|
|
'auth': '20/min', # Login, register, password reset
|
|
'auth_strict': '5/min', # Sensitive auth operations
|
|
|
|
# Planner Operations
|
|
'planner': '60/min', # Keyword, cluster, idea operations
|
|
'planner_ai': '10/min', # AI-powered planner operations
|
|
|
|
# Writer Operations
|
|
'writer': '60/min', # Task, content management
|
|
'writer_ai': '10/min', # AI-powered writer operations
|
|
|
|
# System Operations
|
|
'system': '100/min', # Settings, prompts, profiles
|
|
'system_admin': '30/min', # Admin-only system operations
|
|
|
|
# Billing Operations
|
|
'billing': '30/min', # Credit queries, usage logs
|
|
'billing_admin': '10/min', # Credit management (admin)
|
|
|
|
# Default fallback
|
|
'default': '100/min', # Default for endpoints without scope
|
|
},
|
|
}
|
|
```
|
|
|
|
#### Step 1.2: Add Debug Throttle Bypass
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
**Add environment variable:**
|
|
```python
|
|
import os
|
|
|
|
# Throttling Configuration
|
|
IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', 'False').lower() == 'true'
|
|
|
|
# Only apply throttling if not in debug mode or explicitly enabled
|
|
if not DEBUG and not IGNY8_DEBUG_THROTTLE:
|
|
REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [
|
|
'rest_framework.throttling.ScopedRateThrottle',
|
|
]
|
|
else:
|
|
# In debug mode, use a no-op throttle class
|
|
REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = []
|
|
```
|
|
|
|
**Create Custom Throttle Class with Debug Bypass:**
|
|
|
|
**File:** `backend/igny8_core/api/throttles.py`
|
|
|
|
**Implementation:**
|
|
```python
|
|
"""
|
|
Custom Throttle Classes for IGNY8 API
|
|
"""
|
|
|
|
from django.conf import settings
|
|
from rest_framework.throttling import ScopedRateThrottle
|
|
|
|
|
|
class DebugScopedRateThrottle(ScopedRateThrottle):
|
|
"""
|
|
Scoped rate throttle that can be bypassed in debug mode.
|
|
|
|
Usage:
|
|
throttle_scope = 'ai_function'
|
|
"""
|
|
|
|
def allow_request(self, request, view):
|
|
"""
|
|
Check if request should be throttled.
|
|
Bypass throttling if DEBUG=True or IGNY8_DEBUG_THROTTLE=True.
|
|
"""
|
|
# Bypass throttling in debug mode
|
|
if settings.DEBUG or getattr(settings, 'IGNY8_DEBUG_THROTTLE', False):
|
|
return True
|
|
|
|
# Use parent class throttling logic
|
|
return super().allow_request(request, view)
|
|
```
|
|
|
|
**Update settings to use custom throttle:**
|
|
```python
|
|
'DEFAULT_THROTTLE_CLASSES': [
|
|
'igny8_core.api.throttles.DebugScopedRateThrottle',
|
|
],
|
|
```
|
|
|
|
**Estimated Time:** 2 hours (settings) + 1 hour (debug bypass) = 3 hours
|
|
|
|
---
|
|
|
|
## Task 2: Assign Throttle Scopes in ViewSets
|
|
|
|
### Goal
|
|
Assign appropriate throttle scopes to all ViewSets and custom actions based on their function and resource cost.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 2.1: Audit Endpoints for Throttle Scopes
|
|
|
|
**Action Items:**
|
|
1. List all ViewSets and custom actions
|
|
2. Categorize by function type (AI, content, auth, etc.)
|
|
3. Identify high-cost operations (AI, image generation)
|
|
4. Create mapping of endpoints to throttle scopes
|
|
|
|
**Endpoint Categories:**
|
|
|
|
**AI Functions (10/min):**
|
|
- `POST /api/v1/planner/keywords/auto_cluster/` - AI clustering
|
|
- `POST /api/v1/planner/clusters/auto_generate_ideas/` - AI idea generation
|
|
- `POST /api/v1/writer/tasks/auto_generate_content/` - AI content generation
|
|
- `POST /api/v1/writer/content/generate_image_prompts/` - AI prompt generation
|
|
|
|
**Image Generation (15/min):**
|
|
- `POST /api/v1/writer/images/generate_images/` - Image generation
|
|
- `POST /api/v1/writer/images/auto_generate/` - Legacy image generation
|
|
|
|
**Content Write (30/min):**
|
|
- `POST /api/v1/writer/tasks/` - Create task
|
|
- `POST /api/v1/writer/content/` - Create content
|
|
- `PUT /api/v1/writer/tasks/{id}/` - Update task
|
|
- `PUT /api/v1/writer/content/{id}/` - Update content
|
|
|
|
**Planner AI (10/min):**
|
|
- `POST /api/v1/planner/keywords/auto_cluster/`
|
|
- `POST /api/v1/planner/clusters/auto_generate_ideas/`
|
|
|
|
**Planner Standard (60/min):**
|
|
- `GET /api/v1/planner/keywords/` - List keywords
|
|
- `POST /api/v1/planner/keywords/` - Create keyword
|
|
- `GET /api/v1/planner/clusters/` - List clusters
|
|
- `POST /api/v1/planner/ideas/` - Create idea
|
|
|
|
**Auth (20/min):**
|
|
- `POST /api/v1/auth/login/`
|
|
- `POST /api/v1/auth/register/`
|
|
- `POST /api/v1/auth/change-password/`
|
|
|
|
**Auth Strict (5/min):**
|
|
- `POST /api/v1/auth/password-reset/` - If exists
|
|
- `POST /api/v1/auth/users/invite/` - User invitation
|
|
|
|
**System Admin (30/min):**
|
|
- `POST /api/v1/system/settings/integrations/{pk}/save/`
|
|
- `POST /api/v1/system/settings/integrations/{pk}/test/`
|
|
|
|
**Billing Admin (10/min):**
|
|
- `POST /api/v1/billing/credits/transactions/` - Create transaction
|
|
|
|
**Estimated Time:** 3 hours
|
|
|
|
#### Step 2.2: Assign Throttle Scopes to ViewSets
|
|
|
|
**Example Implementation:**
|
|
|
|
**Planner Module:**
|
|
|
|
**File:** `backend/planner/api/viewsets.py`
|
|
|
|
```python
|
|
from igny8_core.api.viewsets import BaseTenantViewSet
|
|
|
|
class KeywordViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'planner' # Default for standard operations
|
|
queryset = Keyword.objects.all()
|
|
serializer_class = KeywordSerializer
|
|
|
|
@action(
|
|
detail=False,
|
|
methods=['post'],
|
|
throttle_scope='ai_function' # Override for AI operations
|
|
)
|
|
def auto_cluster(self, request):
|
|
# AI clustering logic
|
|
pass
|
|
|
|
|
|
class ClusterViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'planner'
|
|
queryset = Cluster.objects.all()
|
|
serializer_class = ClusterSerializer
|
|
|
|
@action(
|
|
detail=False,
|
|
methods=['post'],
|
|
throttle_scope='planner_ai' # AI-powered operations
|
|
)
|
|
def auto_generate_ideas(self, request):
|
|
# AI idea generation logic
|
|
pass
|
|
```
|
|
|
|
**Writer Module:**
|
|
|
|
**File:** `backend/writer/api/viewsets.py`
|
|
|
|
```python
|
|
class TasksViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'writer' # Default for standard operations
|
|
queryset = Task.objects.all()
|
|
serializer_class = TaskSerializer
|
|
|
|
@action(
|
|
detail=False,
|
|
methods=['post'],
|
|
throttle_scope='ai_function' # AI content generation
|
|
)
|
|
def auto_generate_content(self, request):
|
|
# AI content generation logic
|
|
pass
|
|
|
|
|
|
class ImagesViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'writer'
|
|
queryset = Image.objects.all()
|
|
serializer_class = ImageSerializer
|
|
|
|
@action(
|
|
detail=False,
|
|
methods=['post'],
|
|
throttle_scope='image_gen' # Image generation
|
|
)
|
|
def generate_images(self, request):
|
|
# Image generation logic
|
|
pass
|
|
```
|
|
|
|
**Auth Module:**
|
|
|
|
**File:** `backend/auth/api/views.py`
|
|
|
|
```python
|
|
from rest_framework.views import APIView
|
|
from rest_framework.throttling import ScopedRateThrottle
|
|
|
|
class LoginView(APIView):
|
|
throttle_scope = 'auth'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def post(self, request):
|
|
# Login logic
|
|
pass
|
|
|
|
|
|
class RegisterView(APIView):
|
|
throttle_scope = 'auth'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def post(self, request):
|
|
# Registration logic
|
|
pass
|
|
|
|
|
|
class ChangePasswordView(APIView):
|
|
throttle_scope = 'auth'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def post(self, request):
|
|
# Password change logic
|
|
pass
|
|
```
|
|
|
|
**System Module:**
|
|
|
|
**File:** `backend/system/api/viewsets.py`
|
|
|
|
```python
|
|
class IntegrationSettingsViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'system'
|
|
|
|
@action(
|
|
detail=True,
|
|
methods=['post'],
|
|
throttle_scope='system_admin' # Admin operations
|
|
)
|
|
def save(self, request, pk=None):
|
|
# Save integration settings
|
|
pass
|
|
|
|
@action(
|
|
detail=True,
|
|
methods=['post'],
|
|
throttle_scope='system_admin'
|
|
)
|
|
def test(self, request, pk=None):
|
|
# Test integration connection
|
|
pass
|
|
```
|
|
|
|
**Billing Module:**
|
|
|
|
**File:** `backend/billing/api/viewsets.py`
|
|
|
|
```python
|
|
class CreditTransactionViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'billing'
|
|
|
|
# Read-only operations use 'billing' scope
|
|
# Write operations would use 'billing_admin' if implemented
|
|
```
|
|
|
|
**Estimated Time:** 6 hours
|
|
|
|
---
|
|
|
|
## Task 3: Override User/Account-Level Throttles (Optional)
|
|
|
|
### Goal
|
|
Implement plan-based throttling that applies different rate limits based on user's subscription plan.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 3.1: Create Plan-Based Throttle Class
|
|
|
|
**File:** `backend/igny8_core/api/throttles.py`
|
|
|
|
**Implementation:**
|
|
```python
|
|
"""
|
|
Custom Throttle Classes for IGNY8 API
|
|
"""
|
|
|
|
from django.conf import settings
|
|
from rest_framework.throttling import SimpleRateThrottle
|
|
|
|
|
|
class PlanBasedRateThrottle(SimpleRateThrottle):
|
|
"""
|
|
Rate throttle that applies different limits based on user's plan.
|
|
|
|
Plan tiers:
|
|
- Free: 10/min for AI functions
|
|
- Pro: 100/min for AI functions
|
|
- Enterprise: Unlimited (or very high limit)
|
|
|
|
Usage:
|
|
throttle_scope = 'ai_function' # Base scope
|
|
# Plan limits override base scope
|
|
"""
|
|
|
|
# Plan-based rate limits
|
|
PLAN_RATES = {
|
|
'free': {
|
|
'ai_function': '10/min',
|
|
'image_gen': '15/min',
|
|
'content_write': '30/min',
|
|
},
|
|
'pro': {
|
|
'ai_function': '100/min',
|
|
'image_gen': '50/min',
|
|
'content_write': '100/min',
|
|
},
|
|
'enterprise': {
|
|
'ai_function': '1000/min', # Effectively unlimited
|
|
'image_gen': '200/min',
|
|
'content_write': '500/min',
|
|
},
|
|
}
|
|
|
|
def get_rate(self):
|
|
"""
|
|
Get rate limit based on user's plan.
|
|
"""
|
|
# Get scope from view
|
|
scope = getattr(self.view, 'throttle_scope', None)
|
|
if not scope:
|
|
return None
|
|
|
|
# Get user's plan
|
|
user = self.request.user
|
|
if not user or not user.is_authenticated:
|
|
# Anonymous users use default rate
|
|
return self.get_default_rate(scope)
|
|
|
|
# Get account and plan
|
|
account = getattr(user, 'account', None)
|
|
if not account:
|
|
return self.get_default_rate(scope)
|
|
|
|
plan = getattr(account, 'plan', None)
|
|
if not plan:
|
|
return self.get_default_rate(scope)
|
|
|
|
plan_name = plan.name.lower() if hasattr(plan, 'name') else 'free'
|
|
|
|
# Get rate for plan and scope
|
|
plan_rates = self.PLAN_RATES.get(plan_name, self.PLAN_RATES['free'])
|
|
rate = plan_rates.get(scope, self.get_default_rate(scope))
|
|
|
|
return rate
|
|
|
|
def get_default_rate(self, scope):
|
|
"""
|
|
Get default rate from settings if plan-based rate not found.
|
|
"""
|
|
throttle_rates = getattr(settings, 'REST_FRAMEWORK', {}).get('DEFAULT_THROTTLE_RATES', {})
|
|
return throttle_rates.get(scope, '100/min')
|
|
|
|
def get_cache_key(self, request, view):
|
|
"""
|
|
Generate cache key based on user ID and scope.
|
|
"""
|
|
if request.user.is_authenticated:
|
|
ident = request.user.pk
|
|
else:
|
|
ident = self.get_ident(request)
|
|
|
|
scope = getattr(view, 'throttle_scope', None)
|
|
return self.cache_format % {
|
|
'scope': scope,
|
|
'ident': ident
|
|
}
|
|
```
|
|
|
|
#### Step 3.2: Apply Plan-Based Throttling
|
|
|
|
**Update settings to use plan-based throttle for AI functions:**
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
```python
|
|
# For AI functions, use plan-based throttling
|
|
# For other functions, use scoped throttling
|
|
|
|
# This would require custom throttle class selection per ViewSet
|
|
# Or use a mixin that selects throttle class based on scope
|
|
```
|
|
|
|
**Alternative: Use in specific ViewSets:**
|
|
|
|
```python
|
|
from igny8_core.api.throttles import PlanBasedRateThrottle
|
|
|
|
class TasksViewSet(BaseTenantViewSet):
|
|
throttle_scope = 'ai_function'
|
|
throttle_classes = [PlanBasedRateThrottle] # Use plan-based for this ViewSet
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def auto_generate_content(self, request):
|
|
# AI content generation
|
|
pass
|
|
```
|
|
|
|
**Estimated Time:** 8 hours (optional, future phase)
|
|
|
|
---
|
|
|
|
## Task 4: Log + Expose Rate Limit Info
|
|
|
|
### Goal
|
|
Ensure rate limit information is properly exposed to clients and logged for abuse detection.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 4.1: Verify Throttle Headers
|
|
|
|
**DRF automatically adds these headers:**
|
|
- `X-Throttle-Limit`: Maximum number of requests allowed
|
|
- `X-Throttle-Remaining`: Number of requests remaining
|
|
- `Retry-After`: Seconds to wait before retrying (when throttled)
|
|
|
|
**Test in Postman:**
|
|
1. Make requests to throttled endpoint
|
|
2. Check response headers
|
|
3. Verify headers are present and correct
|
|
|
|
**Example Response Headers:**
|
|
```
|
|
HTTP/1.1 200 OK
|
|
X-Throttle-Limit: 10
|
|
X-Throttle-Remaining: 9
|
|
X-Throttle-Reset: 1640995200
|
|
```
|
|
|
|
**When Throttled (429):**
|
|
```
|
|
HTTP/1.1 429 Too Many Requests
|
|
X-Throttle-Limit: 10
|
|
X-Throttle-Remaining: 0
|
|
Retry-After: 60
|
|
```
|
|
|
|
**Estimated Time:** 2 hours
|
|
|
|
#### Step 4.2: Add Frontend Rate Limit Handling
|
|
|
|
**File:** `frontend/src/services/api.ts`
|
|
|
|
**Update fetchAPI to handle rate limits:**
|
|
```typescript
|
|
export async function fetchAPI(endpoint: string, options?: RequestInit) {
|
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
...options?.headers,
|
|
'Authorization': `Bearer ${getToken()}`,
|
|
},
|
|
});
|
|
|
|
// Check for rate limiting
|
|
if (response.status === 429) {
|
|
const retryAfter = response.headers.get('Retry-After');
|
|
const throttleLimit = response.headers.get('X-Throttle-Limit');
|
|
const throttleRemaining = response.headers.get('X-Throttle-Remaining');
|
|
|
|
// Show user-friendly error
|
|
showNotification(
|
|
`Rate limit exceeded. Please wait ${retryAfter} seconds before trying again.`,
|
|
'error'
|
|
);
|
|
|
|
// Store rate limit info for UI
|
|
if (throttleLimit && throttleRemaining !== null) {
|
|
storeRateLimitInfo({
|
|
limit: parseInt(throttleLimit),
|
|
remaining: parseInt(throttleRemaining),
|
|
retryAfter: retryAfter ? parseInt(retryAfter) : 60,
|
|
});
|
|
}
|
|
|
|
throw new Error('Rate limit exceeded');
|
|
}
|
|
|
|
// Check throttle headers on successful requests
|
|
const throttleLimit = response.headers.get('X-Throttle-Limit');
|
|
const throttleRemaining = response.headers.get('X-Throttle-Remaining');
|
|
|
|
if (throttleLimit && throttleRemaining !== null) {
|
|
const remaining = parseInt(throttleRemaining);
|
|
const limit = parseInt(throttleLimit);
|
|
const percentage = (remaining / limit) * 100;
|
|
|
|
// Show warning if close to limit
|
|
if (percentage < 20) {
|
|
showNotification(
|
|
`Warning: You have ${remaining} of ${limit} requests remaining.`,
|
|
'warning'
|
|
);
|
|
}
|
|
|
|
// Store for UI display
|
|
storeRateLimitInfo({
|
|
limit,
|
|
remaining,
|
|
percentage,
|
|
});
|
|
}
|
|
|
|
return response;
|
|
}
|
|
```
|
|
|
|
**Add Rate Limit Display Component:**
|
|
|
|
**File:** `frontend/src/components/RateLimitIndicator.tsx`
|
|
|
|
```typescript
|
|
import { useRateLimitStore } from '@/stores/rateLimitStore';
|
|
|
|
export function RateLimitIndicator() {
|
|
const { limit, remaining, percentage } = useRateLimitStore();
|
|
|
|
if (!limit) return null;
|
|
|
|
return (
|
|
<div className="rate-limit-indicator">
|
|
<span>API Calls: {remaining} / {limit}</span>
|
|
<div className="progress-bar">
|
|
<div
|
|
className="progress-fill"
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
</div>
|
|
{percentage < 20 && (
|
|
<span className="warning">Low on requests</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Estimated Time:** 4 hours
|
|
|
|
#### Step 4.3: Implement Abuse Detection Logging
|
|
|
|
**File:** `backend/igny8_core/api/middleware.py`
|
|
|
|
**Add rate limit logging middleware:**
|
|
```python
|
|
"""
|
|
Rate Limit Logging Middleware
|
|
"""
|
|
|
|
import logging
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RateLimitLoggingMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware that logs rate limit hits for abuse detection.
|
|
"""
|
|
|
|
def process_response(self, request, response):
|
|
"""
|
|
Log rate limit hits (429 responses).
|
|
"""
|
|
if response.status_code == 429:
|
|
# Log rate limit hit
|
|
logger.warning(
|
|
f"Rate limit exceeded: {request.method} {request.path}",
|
|
extra={
|
|
'user_id': request.user.id if request.user.is_authenticated else None,
|
|
'ip_address': self.get_client_ip(request),
|
|
'user_agent': request.META.get('HTTP_USER_AGENT'),
|
|
'throttle_scope': getattr(request, 'throttle_scope', None),
|
|
}
|
|
)
|
|
|
|
# Check for repeated hits (potential abuse)
|
|
# This could be enhanced with Redis/cache to track frequency
|
|
self.check_for_abuse(request)
|
|
|
|
return response
|
|
|
|
def get_client_ip(self, request):
|
|
"""
|
|
Get client IP address.
|
|
"""
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
ip = x_forwarded_for.split(',')[0]
|
|
else:
|
|
ip = request.META.get('REMOTE_ADDR')
|
|
return ip
|
|
|
|
def check_for_abuse(self, request):
|
|
"""
|
|
Check if user is repeatedly hitting rate limits (potential abuse).
|
|
"""
|
|
# TODO: Implement abuse detection logic
|
|
# - Track rate limit hits per user/IP
|
|
# - Alert if threshold exceeded
|
|
# - Optionally block user/IP temporarily
|
|
pass
|
|
```
|
|
|
|
**Register middleware:**
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
```python
|
|
MIDDLEWARE = [
|
|
# ... other middleware ...
|
|
'igny8_core.api.middleware.RateLimitLoggingMiddleware',
|
|
# ... other middleware ...
|
|
]
|
|
```
|
|
|
|
**Estimated Time:** 3 hours
|
|
|
|
---
|
|
|
|
## Task 5: Advanced Daily/Monthly Quotas Per Plan
|
|
|
|
### Goal
|
|
Implement quota-based usage limits that go beyond rate limiting, integrated with the existing credit system.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 5.1: Integrate with Credit System
|
|
|
|
**This task extends the existing credit system to enforce quotas:**
|
|
|
|
**File:** `backend/igny8_core/api/middleware.py`
|
|
|
|
**Add quota check middleware:**
|
|
```python
|
|
"""
|
|
Quota Enforcement Middleware
|
|
"""
|
|
|
|
from django.http import JsonResponse
|
|
from rest_framework import status
|
|
from igny8_core.api.response import error_response
|
|
|
|
|
|
class QuotaEnforcementMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware that enforces daily/monthly quotas based on plan and credits.
|
|
"""
|
|
|
|
def process_request(self, request):
|
|
"""
|
|
Check if user has exceeded quota before processing request.
|
|
"""
|
|
# Only check for authenticated users
|
|
if not request.user or not request.user.is_authenticated:
|
|
return None
|
|
|
|
# Get account
|
|
account = getattr(request.user, 'account', None)
|
|
if not account:
|
|
return None
|
|
|
|
# Check if endpoint requires quota check
|
|
if not self.requires_quota_check(request):
|
|
return None
|
|
|
|
# Check credits
|
|
from igny8_core.modules.billing.models import CreditBalance
|
|
|
|
try:
|
|
balance = CreditBalance.objects.get(account=account)
|
|
|
|
# Check if credits are exhausted
|
|
if balance.credits <= 0:
|
|
return error_response(
|
|
error="Credits exhausted. Please purchase more credits to continue.",
|
|
status_code=status.HTTP_402_PAYMENT_REQUIRED
|
|
)
|
|
|
|
# Check daily/monthly quotas (if implemented)
|
|
# This would require additional quota tracking models
|
|
|
|
except CreditBalance.DoesNotExist:
|
|
# No balance record - allow request (will be created on first use)
|
|
pass
|
|
|
|
return None
|
|
|
|
def requires_quota_check(self, request):
|
|
"""
|
|
Check if this endpoint requires quota validation.
|
|
"""
|
|
# Check if it's an AI function or expensive operation
|
|
throttle_scope = getattr(request, 'throttle_scope', None)
|
|
expensive_scopes = ['ai_function', 'image_gen', 'planner_ai', 'writer_ai']
|
|
|
|
return throttle_scope in expensive_scopes
|
|
```
|
|
|
|
**Estimated Time:** 6 hours (optional, future phase)
|
|
|
|
---
|
|
|
|
## Task 6: Testing Matrix
|
|
|
|
### Goal
|
|
Create comprehensive test scenarios to validate rate limiting works correctly.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 6.1: Create Test Scenarios
|
|
|
|
**Test Cases:**
|
|
|
|
**1. AI Function Rate Limiting (10/min)**
|
|
```python
|
|
# Test: Make 11 requests to /api/v1/writer/tasks/auto_generate_content/
|
|
# Expected:
|
|
# - Requests 1-10: 200 OK
|
|
# - Request 11: 429 Too Many Requests
|
|
# - Headers: X-Throttle-Limit: 10, X-Throttle-Remaining: 0, Retry-After: 60
|
|
```
|
|
|
|
**2. Image Generation Rate Limiting (15/min)**
|
|
```python
|
|
# Test: Make 16 requests to /api/v1/writer/images/generate_images/
|
|
# Expected:
|
|
# - Requests 1-15: 200 OK
|
|
# - Request 16: 429 Too Many Requests
|
|
```
|
|
|
|
**3. Auth Rate Limiting (20/min)**
|
|
```python
|
|
# Test: Make 21 login attempts
|
|
# Expected:
|
|
# - Requests 1-20: 200 OK or 401 Unauthorized (depending on credentials)
|
|
# - Request 21: 429 Too Many Requests
|
|
```
|
|
|
|
**4. Debug Mode Bypass**
|
|
```python
|
|
# Test: Set IGNY8_DEBUG_THROTTLE=True
|
|
# Expected: All requests allowed regardless of rate limits
|
|
```
|
|
|
|
**5. Throttle Headers Verification**
|
|
```python
|
|
# Test: Make requests and check headers
|
|
# Expected:
|
|
# - X-Throttle-Limit header present
|
|
# - X-Throttle-Remaining header present and decrements
|
|
# - Retry-After header present when throttled
|
|
```
|
|
|
|
**6. Plan-Based Throttling (if implemented)**
|
|
```python
|
|
# Test: Free plan user vs Pro plan user
|
|
# Expected:
|
|
# - Free plan: 10/min for AI functions
|
|
# - Pro plan: 100/min for AI functions
|
|
```
|
|
|
|
**Automated Test Implementation:**
|
|
|
|
**File:** `backend/igny8_core/api/tests/test_throttling.py`
|
|
|
|
```python
|
|
"""
|
|
Tests for Rate Limiting and Throttling
|
|
"""
|
|
|
|
from django.test import TestCase
|
|
from rest_framework.test import APIClient
|
|
from rest_framework import status
|
|
from django.contrib.auth import get_user_model
|
|
from igny8_core.auth.models import Account
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ThrottlingTestCase(TestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.account = Account.objects.create(name="Test Account")
|
|
self.user = User.objects.create_user(
|
|
email="test@example.com",
|
|
password="testpass123",
|
|
account=self.account
|
|
)
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
def test_ai_function_rate_limit(self):
|
|
"""Test that AI functions are rate limited to 10/min"""
|
|
endpoint = '/api/v1/writer/tasks/auto_generate_content/'
|
|
|
|
# Make 10 requests (should all succeed)
|
|
for i in range(10):
|
|
response = self.client.post(endpoint, {})
|
|
self.assertIn(response.status_code, [200, 400, 500]) # Not throttled
|
|
|
|
# 11th request should be throttled
|
|
response = self.client.post(endpoint, {})
|
|
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
|
|
self.assertIn('Retry-After', response.headers)
|
|
|
|
def test_throttle_headers_present(self):
|
|
"""Test that throttle headers are present in responses"""
|
|
endpoint = '/api/v1/writer/tasks/auto_generate_content/'
|
|
response = self.client.post(endpoint, {})
|
|
|
|
self.assertIn('X-Throttle-Limit', response.headers)
|
|
self.assertIn('X-Throttle-Remaining', response.headers)
|
|
|
|
def test_debug_throttle_bypass(self):
|
|
"""Test that throttling is bypassed in debug mode"""
|
|
from django.conf import settings
|
|
original_debug = settings.DEBUG
|
|
|
|
try:
|
|
settings.DEBUG = True
|
|
|
|
# Make 20 requests (should all succeed in debug mode)
|
|
endpoint = '/api/v1/writer/tasks/auto_generate_content/'
|
|
for i in range(20):
|
|
response = self.client.post(endpoint, {})
|
|
self.assertNotEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
|
|
finally:
|
|
settings.DEBUG = original_debug
|
|
```
|
|
|
|
**Estimated Time:** 4 hours
|
|
|
|
---
|
|
|
|
## Task 7: Changelog Entry
|
|
|
|
### Goal
|
|
Document the rate limiting implementation in the changelog.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 7.1: Update CHANGELOG.md
|
|
|
|
**File:** `CHANGELOG.md`
|
|
|
|
**Add Entry:**
|
|
```markdown
|
|
## [Unreleased] - 2025-01-XX
|
|
|
|
### Added
|
|
- **Rate Limiting**: Added scoped API rate limiting for AI, image, auth, and content actions
|
|
- Configured DRF throttling with scoped rate limits
|
|
- AI functions: 10 requests/minute
|
|
- Image generation: 15 requests/minute
|
|
- Content operations: 30 requests/minute
|
|
- Authentication: 20 requests/minute
|
|
- Added `DebugScopedRateThrottle` with debug mode bypass
|
|
- Rate limit information exposed via `X-Throttle-Limit` and `X-Throttle-Remaining` headers
|
|
- Added rate limit logging for abuse detection
|
|
- Frontend rate limit handling and UI indicators
|
|
|
|
### Changed
|
|
- All ViewSets now have explicit `throttle_scope` assignments
|
|
- Rate limiting can be bypassed in development mode via `IGNY8_DEBUG_THROTTLE=True`
|
|
|
|
### Security
|
|
- Rate limiting prevents API abuse and ensures fair resource usage
|
|
- Abuse detection logging identifies repeated rate limit hits
|
|
|
|
### Affected Areas
|
|
- API Layer (`igny8_core/api/throttles.py`)
|
|
- All ViewSets (throttle scope assignments)
|
|
- Frontend (`frontend/src/services/api.ts`)
|
|
- Settings (`backend/igny8_core/settings.py`)
|
|
|
|
### Migration Guide
|
|
1. **Development:**
|
|
- Set `IGNY8_DEBUG_THROTTLE=True` to bypass throttling during development
|
|
- Or set `DEBUG=True` in Django settings
|
|
|
|
2. **Production:**
|
|
- Rate limits are automatically enforced
|
|
- Monitor rate limit logs for abuse patterns
|
|
- Adjust rate limits in `DEFAULT_THROTTLE_RATES` if needed
|
|
|
|
3. **Frontend:**
|
|
- Handle 429 responses gracefully
|
|
- Display rate limit information to users
|
|
- Show warnings when approaching limits
|
|
```
|
|
|
|
**Estimated Time:** 1 hour
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**File:** `backend/igny8_core/api/tests/test_throttles.py`
|
|
|
|
**Test Cases:**
|
|
- Throttle class rate limit enforcement
|
|
- Debug mode bypass
|
|
- Plan-based throttling (if implemented)
|
|
- Throttle header generation
|
|
- Cache key generation
|
|
|
|
### Integration Tests
|
|
|
|
**Test Cases:**
|
|
- End-to-end rate limiting on actual endpoints
|
|
- Throttle header presence and correctness
|
|
- Rate limit reset after time window
|
|
- Multiple users with separate rate limits
|
|
- Abuse detection logging
|
|
|
|
### Manual Testing
|
|
|
|
**Checklist:**
|
|
- [ ] AI functions rate limited to 10/min
|
|
- [ ] Image generation rate limited to 15/min
|
|
- [ ] Auth endpoints rate limited to 20/min
|
|
- [ ] Throttle headers present in responses
|
|
- [ ] 429 responses include Retry-After header
|
|
- [ ] Debug mode bypasses throttling
|
|
- [ ] Frontend displays rate limit warnings
|
|
- [ ] Abuse detection logs rate limit hits
|
|
|
|
---
|
|
|
|
## Rollout Plan
|
|
|
|
### Phase 1: Foundation (Week 1)
|
|
- ✅ Task 1: Configure DRF throttling
|
|
- ✅ Task 2.1: Audit endpoints
|
|
- ✅ Unit tests for throttle classes
|
|
|
|
### Phase 2: Scope Assignment (Week 2)
|
|
- ✅ Task 2.2: Assign throttle scopes
|
|
- ✅ Task 4.1: Verify throttle headers
|
|
- ✅ Integration testing
|
|
|
|
### Phase 3: Frontend & Logging (Week 3)
|
|
- ✅ Task 4.2: Frontend rate limit handling
|
|
- ✅ Task 4.3: Abuse detection logging
|
|
- ✅ End-to-end testing
|
|
|
|
### Phase 4: Advanced Features (Week 4 - Optional)
|
|
- ✅ Task 3: Plan-based throttling (if needed)
|
|
- ✅ Task 5: Quota integration (if needed)
|
|
|
|
### Phase 5: Documentation & Release (Week 5)
|
|
- ✅ Task 6: Complete test scenarios
|
|
- ✅ Task 7: Changelog entry
|
|
- ✅ Release to staging
|
|
- ✅ Production deployment
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
### Definition of Done
|
|
|
|
1. ✅ DRF throttling configured with appropriate rate limits
|
|
2. ✅ All ViewSets have explicit throttle scopes
|
|
3. ✅ Throttle headers exposed in responses
|
|
4. ✅ Frontend handles rate limits gracefully
|
|
5. ✅ Abuse detection logging implemented
|
|
6. ✅ Debug mode bypass works correctly
|
|
7. ✅ All test scenarios pass
|
|
8. ✅ Documentation updated
|
|
9. ✅ Changelog entry created
|
|
|
|
### Metrics
|
|
|
|
- **Coverage:** 100% of expensive endpoints have throttle scopes
|
|
- **Security:** Rate limiting prevents abuse
|
|
- **User Experience:** Rate limit information clearly communicated
|
|
- **Performance:** Throttling has minimal overhead
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### Risks
|
|
|
|
1. **Too Restrictive Limits:** Rate limits may be too low for legitimate users
|
|
- **Mitigation:** Start with conservative limits, adjust based on usage patterns
|
|
|
|
2. **Performance Impact:** Throttling checks may add overhead
|
|
- **Mitigation:** Use efficient caching (Redis), minimal overhead expected
|
|
|
|
3. **Frontend Breaking:** Frontend may not handle 429 responses
|
|
- **Mitigation:** Update frontend error handling, test thoroughly
|
|
|
|
4. **Debug Mode in Production:** Accidental debug mode could bypass throttling
|
|
- **Mitigation:** Ensure DEBUG=False in production, use environment variables
|
|
|
|
### Rollback Plan
|
|
|
|
If issues arise:
|
|
1. Increase rate limits temporarily
|
|
2. Disable throttling for specific scopes if needed
|
|
3. Revert throttle scope assignments
|
|
4. Document issues for future fixes
|
|
|
|
---
|
|
|
|
**Document Status:** Implementation Plan
|
|
**Last Updated:** 2025-01-XX
|
|
**Next Review:** After Phase 1 completion
|
|
|