rem
This commit is contained in:
@@ -1,493 +0,0 @@
|
|||||||
# Authentication Guide
|
|
||||||
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
|
|
||||||
Complete guide for authenticating with the IGNY8 API v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The IGNY8 API uses **JWT (JSON Web Token) Bearer Token** authentication. All endpoints require authentication except:
|
|
||||||
- `POST /api/v1/auth/login/` - User login
|
|
||||||
- `POST /api/v1/auth/register/` - User registration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentication Flow
|
|
||||||
|
|
||||||
### 1. Register or Login
|
|
||||||
|
|
||||||
**Register** (if new user):
|
|
||||||
```http
|
|
||||||
POST /api/v1/auth/register/
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"username": "user",
|
|
||||||
"password": "secure_password123",
|
|
||||||
"first_name": "John",
|
|
||||||
"last_name": "Doe"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Login** (existing user):
|
|
||||||
```http
|
|
||||||
POST /api/v1/auth/login/
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"password": "secure_password123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Receive Tokens
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"user": {
|
|
||||||
"id": 1,
|
|
||||||
"email": "user@example.com",
|
|
||||||
"username": "user",
|
|
||||||
"role": "owner",
|
|
||||||
"account": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "My Account",
|
|
||||||
"slug": "my-account"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAxMjM0NTZ9...",
|
|
||||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAxODk0NTZ9..."
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Use Access Token
|
|
||||||
|
|
||||||
Include the `access` token in all subsequent requests:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/planner/keywords/
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Refresh Token (when expired)
|
|
||||||
|
|
||||||
When the access token expires (15 minutes), use the refresh token:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/auth/refresh/
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
||||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Token Expiration
|
|
||||||
|
|
||||||
- **Access Token**: 15 minutes
|
|
||||||
- **Refresh Token**: 7 days
|
|
||||||
|
|
||||||
### Handling Token Expiration
|
|
||||||
|
|
||||||
**Option 1: Automatic Refresh**
|
|
||||||
```python
|
|
||||||
def get_access_token():
|
|
||||||
# Check if token is expired
|
|
||||||
if is_token_expired(current_token):
|
|
||||||
# Refresh token
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/auth/refresh/",
|
|
||||||
json={"refresh": refresh_token}
|
|
||||||
)
|
|
||||||
data = response.json()
|
|
||||||
if data['success']:
|
|
||||||
return data['data']['access']
|
|
||||||
return current_token
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Re-login**
|
|
||||||
```python
|
|
||||||
def login():
|
|
||||||
response = requests.post(
|
|
||||||
f"{BASE_URL}/auth/login/",
|
|
||||||
json={"email": email, "password": password}
|
|
||||||
)
|
|
||||||
data = response.json()
|
|
||||||
if data['success']:
|
|
||||||
return data['data']['access']
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Examples
|
|
||||||
|
|
||||||
### Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
class Igny8API:
|
|
||||||
def __init__(self, base_url="https://api.igny8.com/api/v1"):
|
|
||||||
self.base_url = base_url
|
|
||||||
self.access_token = None
|
|
||||||
self.refresh_token = None
|
|
||||||
self.token_expires_at = None
|
|
||||||
|
|
||||||
def login(self, email, password):
|
|
||||||
"""Login and store tokens"""
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.base_url}/auth/login/",
|
|
||||||
json={"email": email, "password": password}
|
|
||||||
)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data['success']:
|
|
||||||
self.access_token = data['data']['access']
|
|
||||||
self.refresh_token = data['data']['refresh']
|
|
||||||
# Token expires in 15 minutes
|
|
||||||
self.token_expires_at = datetime.now() + timedelta(minutes=14)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"Login failed: {data['error']}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def refresh_access_token(self):
|
|
||||||
"""Refresh access token using refresh token"""
|
|
||||||
if not self.refresh_token:
|
|
||||||
return False
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.base_url}/auth/refresh/",
|
|
||||||
json={"refresh": self.refresh_token}
|
|
||||||
)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data['success']:
|
|
||||||
self.access_token = data['data']['access']
|
|
||||||
self.refresh_token = data['data']['refresh']
|
|
||||||
self.token_expires_at = datetime.now() + timedelta(minutes=14)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"Token refresh failed: {data['error']}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_headers(self):
|
|
||||||
"""Get headers with valid access token"""
|
|
||||||
# Check if token is expired or about to expire
|
|
||||||
if not self.token_expires_at or datetime.now() >= self.token_expires_at:
|
|
||||||
if not self.refresh_access_token():
|
|
||||||
raise Exception("Token expired and refresh failed")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'Authorization': f'Bearer {self.access_token}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
def get(self, endpoint):
|
|
||||||
"""Make authenticated GET request"""
|
|
||||||
response = requests.get(
|
|
||||||
f"{self.base_url}{endpoint}",
|
|
||||||
headers=self.get_headers()
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def post(self, endpoint, data):
|
|
||||||
"""Make authenticated POST request"""
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.base_url}{endpoint}",
|
|
||||||
headers=self.get_headers(),
|
|
||||||
json=data
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
api = Igny8API()
|
|
||||||
api.login("user@example.com", "password")
|
|
||||||
|
|
||||||
# Make authenticated requests
|
|
||||||
keywords = api.get("/planner/keywords/")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class Igny8API {
|
|
||||||
constructor(baseUrl = 'https://api.igny8.com/api/v1') {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.accessToken = null;
|
|
||||||
this.refreshToken = null;
|
|
||||||
this.tokenExpiresAt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(email, password) {
|
|
||||||
const response = await fetch(`${this.baseUrl}/auth/login/`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password })
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.accessToken = data.data.access;
|
|
||||||
this.refreshToken = data.data.refresh;
|
|
||||||
// Token expires in 15 minutes
|
|
||||||
this.tokenExpiresAt = new Date(Date.now() + 14 * 60 * 1000);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.error('Login failed:', data.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshAccessToken() {
|
|
||||||
if (!this.refreshToken) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/auth/refresh/`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ refresh: this.refreshToken })
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.accessToken = data.data.access;
|
|
||||||
this.refreshToken = data.data.refresh;
|
|
||||||
this.tokenExpiresAt = new Date(Date.now() + 14 * 60 * 1000);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.error('Token refresh failed:', data.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHeaders() {
|
|
||||||
// Check if token is expired or about to expire
|
|
||||||
if (!this.tokenExpiresAt || new Date() >= this.tokenExpiresAt) {
|
|
||||||
if (!await this.refreshAccessToken()) {
|
|
||||||
throw new Error('Token expired and refresh failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'Authorization': `Bearer ${this.accessToken}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(endpoint) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${this.baseUrl}${endpoint}`,
|
|
||||||
{ headers: await this.getHeaders() }
|
|
||||||
);
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async post(endpoint, data) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${this.baseUrl}${endpoint}`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: await this.getHeaders(),
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
const api = new Igny8API();
|
|
||||||
await api.login('user@example.com', 'password');
|
|
||||||
|
|
||||||
// Make authenticated requests
|
|
||||||
const keywords = await api.get('/planner/keywords/');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Best Practices
|
|
||||||
|
|
||||||
### 1. Store Tokens Securely
|
|
||||||
|
|
||||||
**❌ Don't:**
|
|
||||||
- Store tokens in localStorage (XSS risk)
|
|
||||||
- Commit tokens to version control
|
|
||||||
- Log tokens in console/logs
|
|
||||||
- Send tokens in URL parameters
|
|
||||||
|
|
||||||
**✅ Do:**
|
|
||||||
- Store tokens in httpOnly cookies (server-side)
|
|
||||||
- Use secure storage (encrypted) for client-side
|
|
||||||
- Rotate tokens regularly
|
|
||||||
- Implement token revocation
|
|
||||||
|
|
||||||
### 2. Handle Token Expiration
|
|
||||||
|
|
||||||
Always check token expiration and refresh before making requests:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def is_token_valid(token_expires_at):
|
|
||||||
# Refresh 1 minute before expiration
|
|
||||||
return datetime.now() < (token_expires_at - timedelta(minutes=1))
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Implement Retry Logic
|
|
||||||
|
|
||||||
```python
|
|
||||||
def make_request_with_retry(url, headers, max_retries=3):
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
# Token expired, refresh and retry
|
|
||||||
refresh_token()
|
|
||||||
headers = get_headers()
|
|
||||||
continue
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
raise Exception("Max retries exceeded")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Validate Token Before Use
|
|
||||||
|
|
||||||
```python
|
|
||||||
def validate_token(token):
|
|
||||||
try:
|
|
||||||
# Decode token (without verification for structure check)
|
|
||||||
import jwt
|
|
||||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
|
||||||
exp = decoded.get('exp')
|
|
||||||
|
|
||||||
if exp and datetime.fromtimestamp(exp) < datetime.now():
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Authentication Errors
|
|
||||||
|
|
||||||
**401 Unauthorized**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Authentication required",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Include valid `Authorization: Bearer <token>` header.
|
|
||||||
|
|
||||||
**403 Forbidden**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Permission denied",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: User lacks required permissions. Check user role and resource access.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Authentication
|
|
||||||
|
|
||||||
### Using Swagger UI
|
|
||||||
|
|
||||||
1. Navigate to `https://api.igny8.com/api/docs/`
|
|
||||||
2. Click "Authorize" button
|
|
||||||
3. Enter: `Bearer <your_token>`
|
|
||||||
4. Click "Authorize"
|
|
||||||
5. All requests will include the token
|
|
||||||
|
|
||||||
### Using cURL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Login
|
|
||||||
curl -X POST https://api.igny8.com/api/v1/auth/login/ \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"user@example.com","password":"password"}'
|
|
||||||
|
|
||||||
# Use token
|
|
||||||
curl -X GET https://api.igny8.com/api/v1/planner/keywords/ \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
||||||
-H "Content-Type: application/json"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Issue: "Authentication required" (401)
|
|
||||||
|
|
||||||
**Causes**:
|
|
||||||
- Missing Authorization header
|
|
||||||
- Invalid token format
|
|
||||||
- Expired token
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Verify `Authorization: Bearer <token>` header is included
|
|
||||||
2. Check token is not expired
|
|
||||||
3. Refresh token or re-login
|
|
||||||
|
|
||||||
### Issue: "Permission denied" (403)
|
|
||||||
|
|
||||||
**Causes**:
|
|
||||||
- User lacks required role
|
|
||||||
- Resource belongs to different account
|
|
||||||
- Site/sector access denied
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Check user role has required permissions
|
|
||||||
2. Verify resource belongs to user's account
|
|
||||||
3. Check site/sector access permissions
|
|
||||||
|
|
||||||
### Issue: Token expires frequently
|
|
||||||
|
|
||||||
**Solution**: Implement automatic token refresh before expiration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
**API Version**: 1.0.0
|
|
||||||
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
# API Error Codes Reference
|
|
||||||
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
|
|
||||||
This document provides a comprehensive reference for all error codes and error scenarios in the IGNY8 API v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Response Format
|
|
||||||
|
|
||||||
All errors follow this unified format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Error message",
|
|
||||||
"errors": {
|
|
||||||
"field_name": ["Field-specific errors"]
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HTTP Status Codes
|
|
||||||
|
|
||||||
### 200 OK
|
|
||||||
**Meaning**: Request successful
|
|
||||||
**Response**: Success response with data
|
|
||||||
|
|
||||||
### 201 Created
|
|
||||||
**Meaning**: Resource created successfully
|
|
||||||
**Response**: Success response with created resource data
|
|
||||||
|
|
||||||
### 204 No Content
|
|
||||||
**Meaning**: Resource deleted successfully
|
|
||||||
**Response**: Empty response body
|
|
||||||
|
|
||||||
### 400 Bad Request
|
|
||||||
**Meaning**: Validation error or invalid request
|
|
||||||
**Common Causes**:
|
|
||||||
- Missing required fields
|
|
||||||
- Invalid field values
|
|
||||||
- Invalid data format
|
|
||||||
- Business logic validation failures
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Validation failed",
|
|
||||||
"errors": {
|
|
||||||
"email": ["This field is required"],
|
|
||||||
"password": ["Password must be at least 8 characters"]
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 401 Unauthorized
|
|
||||||
**Meaning**: Authentication required
|
|
||||||
**Common Causes**:
|
|
||||||
- Missing Authorization header
|
|
||||||
- Invalid or expired token
|
|
||||||
- Token not provided
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Authentication required",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 403 Forbidden
|
|
||||||
**Meaning**: Permission denied
|
|
||||||
**Common Causes**:
|
|
||||||
- User lacks required role
|
|
||||||
- User doesn't have access to resource
|
|
||||||
- Account/site/sector access denied
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Permission denied",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 404 Not Found
|
|
||||||
**Meaning**: Resource not found
|
|
||||||
**Common Causes**:
|
|
||||||
- Invalid resource ID
|
|
||||||
- Resource doesn't exist
|
|
||||||
- Resource belongs to different account
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Resource not found",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 409 Conflict
|
|
||||||
**Meaning**: Resource conflict
|
|
||||||
**Common Causes**:
|
|
||||||
- Duplicate resource (e.g., email already exists)
|
|
||||||
- Resource state conflict
|
|
||||||
- Concurrent modification
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Conflict",
|
|
||||||
"errors": {
|
|
||||||
"email": ["User with this email already exists"]
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 422 Unprocessable Entity
|
|
||||||
**Meaning**: Validation failed
|
|
||||||
**Common Causes**:
|
|
||||||
- Complex validation rules failed
|
|
||||||
- Business logic validation failed
|
|
||||||
- Data integrity constraints violated
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Validation failed",
|
|
||||||
"errors": {
|
|
||||||
"site": ["Site must belong to your account"],
|
|
||||||
"sector": ["Sector must belong to the selected site"]
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 429 Too Many Requests
|
|
||||||
**Meaning**: Rate limit exceeded
|
|
||||||
**Common Causes**:
|
|
||||||
- Too many requests in time window
|
|
||||||
- AI function rate limit exceeded
|
|
||||||
- Authentication rate limit exceeded
|
|
||||||
|
|
||||||
**Response Headers**:
|
|
||||||
- `X-Throttle-Limit`: Maximum requests allowed
|
|
||||||
- `X-Throttle-Remaining`: Remaining requests (0)
|
|
||||||
- `X-Throttle-Reset`: Unix timestamp when limit resets
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Rate limit exceeded",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Wait until `X-Throttle-Reset` timestamp before retrying.
|
|
||||||
|
|
||||||
### 500 Internal Server Error
|
|
||||||
**Meaning**: Server error
|
|
||||||
**Common Causes**:
|
|
||||||
- Unexpected server error
|
|
||||||
- Database error
|
|
||||||
- External service failure
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Internal server error",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Retry request. If persistent, contact support with `request_id`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Field-Specific Error Messages
|
|
||||||
|
|
||||||
### Authentication Errors
|
|
||||||
|
|
||||||
| Field | Error Message | Description |
|
|
||||||
|-------|---------------|-------------|
|
|
||||||
| `email` | "This field is required" | Email not provided |
|
|
||||||
| `email` | "Invalid email format" | Email format invalid |
|
|
||||||
| `email` | "User with this email already exists" | Email already registered |
|
|
||||||
| `password` | "This field is required" | Password not provided |
|
|
||||||
| `password` | "Password must be at least 8 characters" | Password too short |
|
|
||||||
| `password` | "Invalid credentials" | Wrong password |
|
|
||||||
|
|
||||||
### Planner Module Errors
|
|
||||||
|
|
||||||
| Field | Error Message | Description |
|
|
||||||
|-------|---------------|-------------|
|
|
||||||
| `seed_keyword_id` | "This field is required" | Seed keyword not provided |
|
|
||||||
| `seed_keyword_id` | "Invalid seed keyword" | Seed keyword doesn't exist |
|
|
||||||
| `site_id` | "This field is required" | Site not provided |
|
|
||||||
| `site_id` | "Site must belong to your account" | Site access denied |
|
|
||||||
| `sector_id` | "This field is required" | Sector not provided |
|
|
||||||
| `sector_id` | "Sector must belong to the selected site" | Sector-site mismatch |
|
|
||||||
| `status` | "Invalid status value" | Status value not allowed |
|
|
||||||
|
|
||||||
### Writer Module Errors
|
|
||||||
|
|
||||||
| Field | Error Message | Description |
|
|
||||||
|-------|---------------|-------------|
|
|
||||||
| `title` | "This field is required" | Title not provided |
|
|
||||||
| `site_id` | "This field is required" | Site not provided |
|
|
||||||
| `sector_id` | "This field is required" | Sector not provided |
|
|
||||||
| `image_type` | "Invalid image type" | Image type not allowed |
|
|
||||||
|
|
||||||
### System Module Errors
|
|
||||||
|
|
||||||
| Field | Error Message | Description |
|
|
||||||
|-------|---------------|-------------|
|
|
||||||
| `api_key` | "This field is required" | API key not provided |
|
|
||||||
| `api_key` | "Invalid API key format" | API key format invalid |
|
|
||||||
| `integration_type` | "Invalid integration type" | Integration type not allowed |
|
|
||||||
|
|
||||||
### Billing Module Errors
|
|
||||||
|
|
||||||
| Field | Error Message | Description |
|
|
||||||
|-------|---------------|-------------|
|
|
||||||
| `amount` | "This field is required" | Amount not provided |
|
|
||||||
| `amount` | "Amount must be positive" | Invalid amount value |
|
|
||||||
| `credits` | "Insufficient credits" | Not enough credits available |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling Best Practices
|
|
||||||
|
|
||||||
### 1. Always Check `success` Field
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data['success']:
|
|
||||||
# Handle success
|
|
||||||
result = data['data'] or data['results']
|
|
||||||
else:
|
|
||||||
# Handle error
|
|
||||||
error_message = data['error']
|
|
||||||
field_errors = data.get('errors', {})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Handle Field-Specific Errors
|
|
||||||
|
|
||||||
```python
|
|
||||||
if not data['success']:
|
|
||||||
if 'errors' in data:
|
|
||||||
for field, errors in data['errors'].items():
|
|
||||||
print(f"{field}: {', '.join(errors)}")
|
|
||||||
else:
|
|
||||||
print(f"Error: {data['error']}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Use Request ID for Support
|
|
||||||
|
|
||||||
```python
|
|
||||||
if not data['success']:
|
|
||||||
request_id = data.get('request_id')
|
|
||||||
print(f"Error occurred. Request ID: {request_id}")
|
|
||||||
# Include request_id when contacting support
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Handle Rate Limiting
|
|
||||||
|
|
||||||
```python
|
|
||||||
if response.status_code == 429:
|
|
||||||
reset_time = response.headers.get('X-Throttle-Reset')
|
|
||||||
wait_seconds = int(reset_time) - int(time.time())
|
|
||||||
print(f"Rate limited. Wait {wait_seconds} seconds.")
|
|
||||||
time.sleep(wait_seconds)
|
|
||||||
# Retry request
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Retry on Server Errors
|
|
||||||
|
|
||||||
```python
|
|
||||||
if response.status_code >= 500:
|
|
||||||
# Retry with exponential backoff
|
|
||||||
time.sleep(2 ** retry_count)
|
|
||||||
# Retry request
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Error Scenarios
|
|
||||||
|
|
||||||
### Scenario 1: Missing Authentication
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```http
|
|
||||||
GET /api/v1/planner/keywords/
|
|
||||||
(No Authorization header)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response** (401):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Authentication required",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Include `Authorization: Bearer <token>` header.
|
|
||||||
|
|
||||||
### Scenario 2: Invalid Resource ID
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```http
|
|
||||||
GET /api/v1/planner/keywords/99999/
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response** (404):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Resource not found",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Verify resource ID exists and belongs to your account.
|
|
||||||
|
|
||||||
### Scenario 3: Validation Error
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```http
|
|
||||||
POST /api/v1/planner/keywords/
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"seed_keyword_id": null,
|
|
||||||
"site_id": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response** (400):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Validation failed",
|
|
||||||
"errors": {
|
|
||||||
"seed_keyword_id": ["This field is required"],
|
|
||||||
"sector_id": ["This field is required"]
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Provide all required fields with valid values.
|
|
||||||
|
|
||||||
### Scenario 4: Rate Limit Exceeded
|
|
||||||
|
|
||||||
**Request**: Multiple rapid requests
|
|
||||||
|
|
||||||
**Response** (429):
|
|
||||||
```http
|
|
||||||
HTTP/1.1 429 Too Many Requests
|
|
||||||
X-Throttle-Limit: 60
|
|
||||||
X-Throttle-Remaining: 0
|
|
||||||
X-Throttle-Reset: 1700123456
|
|
||||||
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Rate limit exceeded",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution**: Wait until `X-Throttle-Reset` timestamp, then retry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debugging Tips
|
|
||||||
|
|
||||||
1. **Always include `request_id`** when reporting errors
|
|
||||||
2. **Check response headers** for rate limit information
|
|
||||||
3. **Verify authentication token** is valid and not expired
|
|
||||||
4. **Check field-specific errors** in `errors` object
|
|
||||||
5. **Review request payload** matches API specification
|
|
||||||
6. **Use Swagger UI** to test endpoints interactively
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
**API Version**: 1.0.0
|
|
||||||
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
# API Migration Guide
|
|
||||||
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
|
|
||||||
Guide for migrating existing API consumers to IGNY8 API Standard v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The IGNY8 API v1.0 introduces a unified response format that standardizes all API responses. This guide helps you migrate existing code to work with the new format.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
### Before (Legacy Format)
|
|
||||||
|
|
||||||
**Success Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Keyword",
|
|
||||||
"status": "active"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"detail": "Not found."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (Unified Format v1.0)
|
|
||||||
|
|
||||||
**Success Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "Keyword",
|
|
||||||
"status": "active"
|
|
||||||
},
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Resource not found",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
### Step 1: Update Response Parsing
|
|
||||||
|
|
||||||
#### Before
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
# Direct access
|
|
||||||
keyword_id = data['id']
|
|
||||||
keyword_name = data['name']
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
# Check success first
|
|
||||||
if data['success']:
|
|
||||||
# Extract data from unified format
|
|
||||||
keyword_data = data['data'] # or data['results'] for lists
|
|
||||||
keyword_id = keyword_data['id']
|
|
||||||
keyword_name = keyword_data['name']
|
|
||||||
else:
|
|
||||||
# Handle error
|
|
||||||
error_message = data['error']
|
|
||||||
raise Exception(error_message)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Update Error Handling
|
|
||||||
|
|
||||||
#### Before
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
if e.response.status_code == 404:
|
|
||||||
print("Not found")
|
|
||||||
elif e.response.status_code == 400:
|
|
||||||
print("Bad request")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if not data['success']:
|
|
||||||
# Unified error format
|
|
||||||
error_message = data['error']
|
|
||||||
field_errors = data.get('errors', {})
|
|
||||||
|
|
||||||
if response.status_code == 404:
|
|
||||||
print(f"Not found: {error_message}")
|
|
||||||
elif response.status_code == 400:
|
|
||||||
print(f"Validation error: {error_message}")
|
|
||||||
for field, errors in field_errors.items():
|
|
||||||
print(f" {field}: {', '.join(errors)}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Update Pagination Handling
|
|
||||||
|
|
||||||
#### Before
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
results = data['results']
|
|
||||||
next_page = data['next']
|
|
||||||
count = data['count']
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After
|
|
||||||
|
|
||||||
```python
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data['success']:
|
|
||||||
# Paginated response format
|
|
||||||
results = data['results'] # Same field name
|
|
||||||
next_page = data['next'] # Same field name
|
|
||||||
count = data['count'] # Same field name
|
|
||||||
else:
|
|
||||||
# Handle error
|
|
||||||
raise Exception(data['error'])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Update Frontend Code
|
|
||||||
|
|
||||||
#### Before (JavaScript)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const response = await fetch(url, { headers });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Direct access
|
|
||||||
const keywordId = data.id;
|
|
||||||
const keywordName = data.name;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After (JavaScript)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const response = await fetch(url, { headers });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Check success first
|
|
||||||
if (data.success) {
|
|
||||||
// Extract data from unified format
|
|
||||||
const keywordData = data.data || data.results;
|
|
||||||
const keywordId = keywordData.id;
|
|
||||||
const keywordName = keywordData.name;
|
|
||||||
} else {
|
|
||||||
// Handle error
|
|
||||||
console.error('Error:', data.error);
|
|
||||||
if (data.errors) {
|
|
||||||
// Handle field-specific errors
|
|
||||||
Object.entries(data.errors).forEach(([field, errors]) => {
|
|
||||||
console.error(`${field}: ${errors.join(', ')}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Helper Functions
|
|
||||||
|
|
||||||
### Python Helper
|
|
||||||
|
|
||||||
```python
|
|
||||||
def parse_api_response(response):
|
|
||||||
"""Parse unified API response format"""
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data.get('success'):
|
|
||||||
# Return data or results
|
|
||||||
return data.get('data') or data.get('results')
|
|
||||||
else:
|
|
||||||
# Raise exception with error details
|
|
||||||
error_msg = data.get('error', 'Unknown error')
|
|
||||||
errors = data.get('errors', {})
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
error_msg += f": {errors}"
|
|
||||||
|
|
||||||
raise Exception(error_msg)
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
keyword_data = parse_api_response(response)
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Helper
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function parseApiResponse(data) {
|
|
||||||
if (data.success) {
|
|
||||||
return data.data || data.results;
|
|
||||||
} else {
|
|
||||||
const error = new Error(data.error);
|
|
||||||
error.errors = data.errors || {};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
const response = await fetch(url, { headers });
|
|
||||||
const data = await response.json();
|
|
||||||
try {
|
|
||||||
const keywordData = parseApiResponse(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API Error:', error.message);
|
|
||||||
if (error.errors) {
|
|
||||||
// Handle field-specific errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
### 1. Response Structure
|
|
||||||
|
|
||||||
**Breaking**: All responses now include `success` field and wrap data in `data` or `results`.
|
|
||||||
|
|
||||||
**Migration**: Update all response parsing code to check `success` and extract `data`/`results`.
|
|
||||||
|
|
||||||
### 2. Error Format
|
|
||||||
|
|
||||||
**Breaking**: Error responses now use unified format with `error` and `errors` fields.
|
|
||||||
|
|
||||||
**Migration**: Update error handling to use new format.
|
|
||||||
|
|
||||||
### 3. Request ID
|
|
||||||
|
|
||||||
**New**: All responses include `request_id` for debugging.
|
|
||||||
|
|
||||||
**Migration**: Optional - can be used for support requests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Non-Breaking Changes
|
|
||||||
|
|
||||||
### 1. Pagination
|
|
||||||
|
|
||||||
**Status**: Compatible - same field names (`count`, `next`, `previous`, `results`)
|
|
||||||
|
|
||||||
**Migration**: No changes needed, but wrap in success check.
|
|
||||||
|
|
||||||
### 2. Authentication
|
|
||||||
|
|
||||||
**Status**: Compatible - same JWT Bearer token format
|
|
||||||
|
|
||||||
**Migration**: No changes needed.
|
|
||||||
|
|
||||||
### 3. Endpoint URLs
|
|
||||||
|
|
||||||
**Status**: Compatible - same endpoint paths
|
|
||||||
|
|
||||||
**Migration**: No changes needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Migration
|
|
||||||
|
|
||||||
### 1. Update Test Code
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Before
|
|
||||||
def test_get_keyword():
|
|
||||||
response = client.get('/api/v1/planner/keywords/1/')
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()['id'] == 1
|
|
||||||
|
|
||||||
# After
|
|
||||||
def test_get_keyword():
|
|
||||||
response = client.get('/api/v1/planner/keywords/1/')
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
assert data['success'] == True
|
|
||||||
assert data['data']['id'] == 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test Error Handling
|
|
||||||
|
|
||||||
```python
|
|
||||||
def test_not_found():
|
|
||||||
response = client.get('/api/v1/planner/keywords/99999/')
|
|
||||||
assert response.status_code == 404
|
|
||||||
data = response.json()
|
|
||||||
assert data['success'] == False
|
|
||||||
assert data['error'] == "Resource not found"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Checklist
|
|
||||||
|
|
||||||
- [ ] Update response parsing to check `success` field
|
|
||||||
- [ ] Extract data from `data` or `results` field
|
|
||||||
- [ ] Update error handling to use unified format
|
|
||||||
- [ ] Update pagination handling (wrap in success check)
|
|
||||||
- [ ] Update frontend code (if applicable)
|
|
||||||
- [ ] Update test code
|
|
||||||
- [ ] Test all endpoints
|
|
||||||
- [ ] Update documentation
|
|
||||||
- [ ] Deploy and monitor
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If issues arise during migration:
|
|
||||||
|
|
||||||
1. **Temporary Compatibility Layer**: Add wrapper to convert unified format back to legacy format
|
|
||||||
2. **Feature Flag**: Use feature flag to toggle between formats
|
|
||||||
3. **Gradual Migration**: Migrate endpoints one module at a time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For migration support:
|
|
||||||
- Review [API Documentation](API-DOCUMENTATION.md)
|
|
||||||
- Check [Error Codes Reference](ERROR-CODES.md)
|
|
||||||
- Contact support with `request_id` from failed requests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
**API Version**: 1.0.0
|
|
||||||
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
# Rate Limiting Guide
|
|
||||||
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
|
|
||||||
Complete guide for understanding and handling rate limits in the IGNY8 API v1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Rate limiting protects the API from abuse and ensures fair resource usage. Different operation types have different rate limits based on their resource intensity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rate Limit Headers
|
|
||||||
|
|
||||||
Every API response includes rate limit information in headers:
|
|
||||||
|
|
||||||
- `X-Throttle-Limit`: Maximum requests allowed in the time window
|
|
||||||
- `X-Throttle-Remaining`: Remaining requests in current window
|
|
||||||
- `X-Throttle-Reset`: Unix timestamp when the limit resets
|
|
||||||
|
|
||||||
### Example Response Headers
|
|
||||||
|
|
||||||
```http
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
X-Throttle-Limit: 60
|
|
||||||
X-Throttle-Remaining: 45
|
|
||||||
X-Throttle-Reset: 1700123456
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rate Limit Scopes
|
|
||||||
|
|
||||||
Rate limits are scoped by operation type:
|
|
||||||
|
|
||||||
### AI Functions (Expensive Operations)
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `ai_function` | 10/min | Auto-cluster, content generation |
|
|
||||||
| `image_gen` | 15/min | Image generation (DALL-E, Runware) |
|
|
||||||
| `planner_ai` | 10/min | AI-powered planner operations |
|
|
||||||
| `writer_ai` | 10/min | AI-powered writer operations |
|
|
||||||
|
|
||||||
### Content Operations
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `content_write` | 30/min | Content creation, updates |
|
|
||||||
| `content_read` | 100/min | Content listing, retrieval |
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `auth` | 20/min | Login, register, password reset |
|
|
||||||
| `auth_strict` | 5/min | Sensitive auth operations |
|
|
||||||
|
|
||||||
### Planner Operations
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `planner` | 60/min | Keywords, clusters, ideas CRUD |
|
|
||||||
|
|
||||||
### Writer Operations
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `writer` | 60/min | Tasks, content, images CRUD |
|
|
||||||
|
|
||||||
### System Operations
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `system` | 100/min | Settings, prompts, profiles |
|
|
||||||
| `system_admin` | 30/min | Admin-only system operations |
|
|
||||||
|
|
||||||
### Billing Operations
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `billing` | 30/min | Credit queries, usage logs |
|
|
||||||
| `billing_admin` | 10/min | Credit management (admin) |
|
|
||||||
|
|
||||||
### Default
|
|
||||||
|
|
||||||
| Scope | Limit | Endpoints |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| `default` | 100/min | Endpoints without explicit scope |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rate Limit Exceeded (429)
|
|
||||||
|
|
||||||
When rate limit is exceeded, you receive:
|
|
||||||
|
|
||||||
**Status Code**: `429 Too Many Requests`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Rate limit exceeded",
|
|
||||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Headers**:
|
|
||||||
```http
|
|
||||||
X-Throttle-Limit: 60
|
|
||||||
X-Throttle-Remaining: 0
|
|
||||||
X-Throttle-Reset: 1700123456
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling Rate Limits
|
|
||||||
|
|
||||||
**1. Check Headers Before Request**
|
|
||||||
|
|
||||||
```python
|
|
||||||
def make_request(url, headers):
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
# Check remaining requests
|
|
||||||
remaining = int(response.headers.get('X-Throttle-Remaining', 0))
|
|
||||||
|
|
||||||
if remaining < 5:
|
|
||||||
# Approaching limit, slow down
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Handle 429 Response**
|
|
||||||
|
|
||||||
```python
|
|
||||||
def make_request_with_backoff(url, headers, max_retries=3):
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == 429:
|
|
||||||
# Get reset time
|
|
||||||
reset_time = int(response.headers.get('X-Throttle-Reset', 0))
|
|
||||||
current_time = int(time.time())
|
|
||||||
wait_seconds = max(1, reset_time - current_time)
|
|
||||||
|
|
||||||
print(f"Rate limited. Waiting {wait_seconds} seconds...")
|
|
||||||
time.sleep(wait_seconds)
|
|
||||||
continue
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
raise Exception("Max retries exceeded")
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Implement Exponential Backoff**
|
|
||||||
|
|
||||||
```python
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
|
|
||||||
def make_request_with_exponential_backoff(url, headers):
|
|
||||||
max_wait = 60 # Maximum wait time in seconds
|
|
||||||
base_wait = 1 # Base wait time in seconds
|
|
||||||
|
|
||||||
for attempt in range(5):
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code != 429:
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
# Exponential backoff with jitter
|
|
||||||
wait_time = min(
|
|
||||||
base_wait * (2 ** attempt) + random.uniform(0, 1),
|
|
||||||
max_wait
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Rate limited. Waiting {wait_time:.2f} seconds...")
|
|
||||||
time.sleep(wait_time)
|
|
||||||
|
|
||||||
raise Exception("Rate limit exceeded after retries")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Monitor Rate Limit Headers
|
|
||||||
|
|
||||||
Always check `X-Throttle-Remaining` to avoid hitting limits:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def check_rate_limit(response):
|
|
||||||
remaining = int(response.headers.get('X-Throttle-Remaining', 0))
|
|
||||||
|
|
||||||
if remaining < 10:
|
|
||||||
print(f"Warning: Only {remaining} requests remaining")
|
|
||||||
|
|
||||||
return remaining
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Implement Request Queuing
|
|
||||||
|
|
||||||
For bulk operations, queue requests to stay within limits:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
|
|
||||||
class RateLimitedAPI:
|
|
||||||
def __init__(self, requests_per_minute=60):
|
|
||||||
self.queue = queue.Queue()
|
|
||||||
self.requests_per_minute = requests_per_minute
|
|
||||||
self.min_interval = 60 / requests_per_minute
|
|
||||||
self.last_request_time = 0
|
|
||||||
|
|
||||||
def make_request(self, url, headers):
|
|
||||||
# Ensure minimum interval between requests
|
|
||||||
elapsed = time.time() - self.last_request_time
|
|
||||||
if elapsed < self.min_interval:
|
|
||||||
time.sleep(self.min_interval - elapsed)
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
self.last_request_time = time.time()
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Cache Responses
|
|
||||||
|
|
||||||
Cache frequently accessed data to reduce API calls:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from functools import lru_cache
|
|
||||||
import time
|
|
||||||
|
|
||||||
class CachedAPI:
|
|
||||||
def __init__(self, cache_ttl=300): # 5 minutes
|
|
||||||
self.cache = {}
|
|
||||||
self.cache_ttl = cache_ttl
|
|
||||||
|
|
||||||
def get_cached(self, url, headers, cache_key):
|
|
||||||
# Check cache
|
|
||||||
if cache_key in self.cache:
|
|
||||||
data, timestamp = self.cache[cache_key]
|
|
||||||
if time.time() - timestamp < self.cache_ttl:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Fetch from API
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
# Store in cache
|
|
||||||
self.cache[cache_key] = (data, time.time())
|
|
||||||
|
|
||||||
return data
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Batch Requests When Possible
|
|
||||||
|
|
||||||
Use bulk endpoints instead of multiple individual requests:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ❌ Don't: Multiple individual requests
|
|
||||||
for keyword_id in keyword_ids:
|
|
||||||
response = requests.get(f"/api/v1/planner/keywords/{keyword_id}/", headers=headers)
|
|
||||||
|
|
||||||
# ✅ Do: Use bulk endpoint if available
|
|
||||||
response = requests.post(
|
|
||||||
"/api/v1/planner/keywords/bulk/",
|
|
||||||
json={"ids": keyword_ids},
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rate Limit Bypass
|
|
||||||
|
|
||||||
### Development/Debug Mode
|
|
||||||
|
|
||||||
Rate limiting is automatically bypassed when:
|
|
||||||
- `DEBUG=True` in Django settings
|
|
||||||
- `IGNY8_DEBUG_THROTTLE=True` environment variable
|
|
||||||
- User belongs to `aws-admin` account
|
|
||||||
- User has `admin` or `developer` role
|
|
||||||
|
|
||||||
**Note**: Headers are still set for debugging, but requests are not blocked.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monitoring Rate Limits
|
|
||||||
|
|
||||||
### Track Usage
|
|
||||||
|
|
||||||
```python
|
|
||||||
class RateLimitMonitor:
|
|
||||||
def __init__(self):
|
|
||||||
self.usage_by_scope = {}
|
|
||||||
|
|
||||||
def track_request(self, response, scope):
|
|
||||||
if scope not in self.usage_by_scope:
|
|
||||||
self.usage_by_scope[scope] = {
|
|
||||||
'total': 0,
|
|
||||||
'limited': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
self.usage_by_scope[scope]['total'] += 1
|
|
||||||
|
|
||||||
if response.status_code == 429:
|
|
||||||
self.usage_by_scope[scope]['limited'] += 1
|
|
||||||
|
|
||||||
remaining = int(response.headers.get('X-Throttle-Remaining', 0))
|
|
||||||
limit = int(response.headers.get('X-Throttle-Limit', 0))
|
|
||||||
|
|
||||||
usage_percent = ((limit - remaining) / limit) * 100
|
|
||||||
|
|
||||||
if usage_percent > 80:
|
|
||||||
print(f"Warning: {scope} at {usage_percent:.1f}% capacity")
|
|
||||||
|
|
||||||
def get_report(self):
|
|
||||||
return self.usage_by_scope
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Issue: Frequent 429 Errors
|
|
||||||
|
|
||||||
**Causes**:
|
|
||||||
- Too many requests in short time
|
|
||||||
- Not checking rate limit headers
|
|
||||||
- No request throttling implemented
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Implement request throttling
|
|
||||||
2. Monitor `X-Throttle-Remaining` header
|
|
||||||
3. Add delays between requests
|
|
||||||
4. Use bulk endpoints when available
|
|
||||||
|
|
||||||
### Issue: Rate Limits Too Restrictive
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Contact support for higher limits (if justified)
|
|
||||||
2. Optimize requests (cache, batch, reduce frequency)
|
|
||||||
3. Use development account for testing (bypass enabled)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Examples
|
|
||||||
|
|
||||||
### Python - Complete Rate Limit Handler
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class RateLimitedClient:
|
|
||||||
def __init__(self, base_url, token):
|
|
||||||
self.base_url = base_url
|
|
||||||
self.headers = {
|
|
||||||
'Authorization': f'Bearer {token}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
self.rate_limits = {}
|
|
||||||
|
|
||||||
def _wait_for_rate_limit(self, scope='default'):
|
|
||||||
"""Wait if approaching rate limit"""
|
|
||||||
if scope in self.rate_limits:
|
|
||||||
limit_info = self.rate_limits[scope]
|
|
||||||
remaining = limit_info.get('remaining', 0)
|
|
||||||
reset_time = limit_info.get('reset_time', 0)
|
|
||||||
|
|
||||||
if remaining < 5:
|
|
||||||
wait_time = max(0, reset_time - time.time())
|
|
||||||
if wait_time > 0:
|
|
||||||
print(f"Rate limit low. Waiting {wait_time:.1f}s...")
|
|
||||||
time.sleep(wait_time)
|
|
||||||
|
|
||||||
def _update_rate_limit_info(self, response, scope='default'):
|
|
||||||
"""Update rate limit information from response headers"""
|
|
||||||
limit = response.headers.get('X-Throttle-Limit')
|
|
||||||
remaining = response.headers.get('X-Throttle-Remaining')
|
|
||||||
reset = response.headers.get('X-Throttle-Reset')
|
|
||||||
|
|
||||||
if limit and remaining and reset:
|
|
||||||
self.rate_limits[scope] = {
|
|
||||||
'limit': int(limit),
|
|
||||||
'remaining': int(remaining),
|
|
||||||
'reset_time': int(reset)
|
|
||||||
}
|
|
||||||
|
|
||||||
def request(self, method, endpoint, scope='default', **kwargs):
|
|
||||||
"""Make rate-limited request"""
|
|
||||||
# Wait if approaching limit
|
|
||||||
self._wait_for_rate_limit(scope)
|
|
||||||
|
|
||||||
# Make request
|
|
||||||
url = f"{self.base_url}{endpoint}"
|
|
||||||
response = requests.request(method, url, headers=self.headers, **kwargs)
|
|
||||||
|
|
||||||
# Update rate limit info
|
|
||||||
self._update_rate_limit_info(response, scope)
|
|
||||||
|
|
||||||
# Handle rate limit error
|
|
||||||
if response.status_code == 429:
|
|
||||||
reset_time = int(response.headers.get('X-Throttle-Reset', 0))
|
|
||||||
wait_time = max(1, reset_time - time.time())
|
|
||||||
print(f"Rate limited. Waiting {wait_time:.1f}s...")
|
|
||||||
time.sleep(wait_time)
|
|
||||||
# Retry once
|
|
||||||
response = requests.request(method, url, headers=self.headers, **kwargs)
|
|
||||||
self._update_rate_limit_info(response, scope)
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def get(self, endpoint, scope='default'):
|
|
||||||
return self.request('GET', endpoint, scope)
|
|
||||||
|
|
||||||
def post(self, endpoint, data, scope='default'):
|
|
||||||
return self.request('POST', endpoint, scope, json=data)
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
client = RateLimitedClient("https://api.igny8.com/api/v1", "your_token")
|
|
||||||
|
|
||||||
# Make requests with automatic rate limit handling
|
|
||||||
keywords = client.get("/planner/keywords/", scope="planner")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
**API Version**: 1.0.0
|
|
||||||
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
# Section 1 & 2 Implementation Summary
|
|
||||||
|
|
||||||
**API Standard v1.0 Implementation**
|
|
||||||
**Sections Completed**: Section 1 (Testing) & Section 2 (Documentation)
|
|
||||||
**Date**: 2025-11-16
|
|
||||||
**Status**: ✅ Complete
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document summarizes the implementation of **Section 1: Testing** and **Section 2: Documentation** from the Unified API Standard v1.0 implementation plan.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section 1: Testing ✅
|
|
||||||
|
|
||||||
### Implementation Summary
|
|
||||||
|
|
||||||
Comprehensive test suite created to verify the Unified API Standard v1.0 implementation across all modules and components.
|
|
||||||
|
|
||||||
### Test Suite Structure
|
|
||||||
|
|
||||||
#### Unit Tests (4 files, ~61 test methods)
|
|
||||||
|
|
||||||
1. **test_response.py** (153 lines)
|
|
||||||
- Tests for `success_response()`, `error_response()`, `paginated_response()`
|
|
||||||
- Tests for `get_request_id()`
|
|
||||||
- Verifies unified response format with `success`, `data`/`results`, `message`, `error`, `errors`, `request_id`
|
|
||||||
- **18 test methods**
|
|
||||||
|
|
||||||
2. **test_exception_handler.py** (177 lines)
|
|
||||||
- Tests for `custom_exception_handler()`
|
|
||||||
- Tests all exception types:
|
|
||||||
- `ValidationError` (400)
|
|
||||||
- `AuthenticationFailed` (401)
|
|
||||||
- `PermissionDenied` (403)
|
|
||||||
- `NotFound` (404)
|
|
||||||
- `Throttled` (429)
|
|
||||||
- Generic exceptions (500)
|
|
||||||
- Tests debug mode behavior (traceback, view, path, method)
|
|
||||||
- **12 test methods**
|
|
||||||
|
|
||||||
3. **test_permissions.py** (245 lines)
|
|
||||||
- Tests for all permission classes:
|
|
||||||
- `IsAuthenticatedAndActive`
|
|
||||||
- `HasTenantAccess`
|
|
||||||
- `IsViewerOrAbove`
|
|
||||||
- `IsEditorOrAbove`
|
|
||||||
- `IsAdminOrOwner`
|
|
||||||
- Tests role-based access control (viewer, editor, admin, owner, developer)
|
|
||||||
- Tests tenant isolation
|
|
||||||
- Tests admin/system account bypass logic
|
|
||||||
- **20 test methods**
|
|
||||||
|
|
||||||
4. **test_throttles.py** (145 lines)
|
|
||||||
- Tests for `DebugScopedRateThrottle`
|
|
||||||
- Tests bypass logic:
|
|
||||||
- DEBUG mode bypass
|
|
||||||
- Environment flag bypass (`IGNY8_DEBUG_THROTTLE`)
|
|
||||||
- Admin/developer/system account bypass
|
|
||||||
- Tests rate parsing and throttle headers
|
|
||||||
- **11 test methods**
|
|
||||||
|
|
||||||
#### Integration Tests (9 files, ~54 test methods)
|
|
||||||
|
|
||||||
1. **test_integration_base.py** (107 lines)
|
|
||||||
- Base test class with common fixtures
|
|
||||||
- Helper methods:
|
|
||||||
- `assert_unified_response_format()` - Verifies unified response structure
|
|
||||||
- `assert_paginated_response()` - Verifies pagination format
|
|
||||||
- Sets up: User, Account, Plan, Site, Sector, Industry, SeedKeyword
|
|
||||||
|
|
||||||
2. **test_integration_planner.py** (120 lines)
|
|
||||||
- Tests Planner module endpoints:
|
|
||||||
- `KeywordViewSet` (CRUD operations)
|
|
||||||
- `ClusterViewSet` (CRUD operations)
|
|
||||||
- `ContentIdeasViewSet` (CRUD operations)
|
|
||||||
- Tests AI actions:
|
|
||||||
- `auto_cluster` - Automatic keyword clustering
|
|
||||||
- `auto_generate_ideas` - AI content idea generation
|
|
||||||
- `bulk_queue_to_writer` - Bulk task creation
|
|
||||||
- Tests unified response format and permissions
|
|
||||||
- **12 test methods**
|
|
||||||
|
|
||||||
3. **test_integration_writer.py** (65 lines)
|
|
||||||
- Tests Writer module endpoints:
|
|
||||||
- `TasksViewSet` (CRUD operations)
|
|
||||||
- `ContentViewSet` (CRUD operations)
|
|
||||||
- `ImagesViewSet` (CRUD operations)
|
|
||||||
- Tests AI actions:
|
|
||||||
- `auto_generate_content` - AI content generation
|
|
||||||
- `generate_image_prompts` - Image prompt generation
|
|
||||||
- `generate_images` - AI image generation
|
|
||||||
- Tests unified response format and permissions
|
|
||||||
- **6 test methods**
|
|
||||||
|
|
||||||
4. **test_integration_system.py** (50 lines)
|
|
||||||
- Tests System module endpoints:
|
|
||||||
- `AIPromptViewSet` (CRUD operations)
|
|
||||||
- `SystemSettingsViewSet` (CRUD operations)
|
|
||||||
- `IntegrationSettingsViewSet` (CRUD operations)
|
|
||||||
- Tests actions:
|
|
||||||
- `save_prompt` - Save AI prompt
|
|
||||||
- `test` - Test integration connection
|
|
||||||
- `task_progress` - Get task progress
|
|
||||||
- **5 test methods**
|
|
||||||
|
|
||||||
5. **test_integration_billing.py** (50 lines)
|
|
||||||
- Tests Billing module endpoints:
|
|
||||||
- `CreditBalanceViewSet` (balance, summary, limits actions)
|
|
||||||
- `CreditUsageViewSet` (usage summary)
|
|
||||||
- `CreditTransactionViewSet` (CRUD operations)
|
|
||||||
- Tests unified response format and permissions
|
|
||||||
- **5 test methods**
|
|
||||||
|
|
||||||
6. **test_integration_auth.py** (100 lines)
|
|
||||||
- Tests Auth module endpoints:
|
|
||||||
- `AuthViewSet` (register, login, me, change_password, refresh_token, reset_password)
|
|
||||||
- `UsersViewSet` (CRUD operations)
|
|
||||||
- `GroupsViewSet` (CRUD operations)
|
|
||||||
- `AccountsViewSet` (CRUD operations)
|
|
||||||
- `SiteViewSet` (CRUD operations)
|
|
||||||
- `SectorViewSet` (CRUD operations)
|
|
||||||
- `IndustryViewSet` (CRUD operations)
|
|
||||||
- `SeedKeywordViewSet` (CRUD operations)
|
|
||||||
- Tests authentication flows and unified response format
|
|
||||||
- **8 test methods**
|
|
||||||
|
|
||||||
7. **test_integration_errors.py** (95 lines)
|
|
||||||
- Tests error scenarios:
|
|
||||||
- 400 Bad Request (validation errors)
|
|
||||||
- 401 Unauthorized (authentication errors)
|
|
||||||
- 403 Forbidden (permission errors)
|
|
||||||
- 404 Not Found (resource not found)
|
|
||||||
- 429 Too Many Requests (rate limiting)
|
|
||||||
- 500 Internal Server Error (generic errors)
|
|
||||||
- Tests unified error format for all scenarios
|
|
||||||
- **6 test methods**
|
|
||||||
|
|
||||||
8. **test_integration_pagination.py** (100 lines)
|
|
||||||
- Tests pagination across all modules:
|
|
||||||
- Default pagination (page size 10)
|
|
||||||
- Custom page size (1-100)
|
|
||||||
- Page parameter
|
|
||||||
- Empty results
|
|
||||||
- Count, next, previous fields
|
|
||||||
- Tests pagination on: Keywords, Clusters, Tasks, Content, Users, Accounts
|
|
||||||
- **10 test methods**
|
|
||||||
|
|
||||||
9. **test_integration_rate_limiting.py** (120 lines)
|
|
||||||
- Tests rate limiting:
|
|
||||||
- Throttle headers (`X-Throttle-Limit`, `X-Throttle-Remaining`, `X-Throttle-Reset`)
|
|
||||||
- Bypass logic (admin/system accounts, DEBUG mode)
|
|
||||||
- Different throttle scopes (read, write, ai)
|
|
||||||
- 429 response handling
|
|
||||||
- **7 test methods**
|
|
||||||
|
|
||||||
### Test Statistics
|
|
||||||
|
|
||||||
- **Total Test Files**: 13
|
|
||||||
- **Total Test Methods**: ~115
|
|
||||||
- **Total Lines of Code**: ~1,500
|
|
||||||
- **Coverage**: 100% of API Standard components
|
|
||||||
|
|
||||||
### What Tests Verify
|
|
||||||
|
|
||||||
1. **Unified Response Format**
|
|
||||||
- All responses include `success` field (true/false)
|
|
||||||
- Success responses include `data` (single object) or `results` (list)
|
|
||||||
- Error responses include `error` (message) and `errors` (field-specific)
|
|
||||||
- All responses include `request_id` (UUID)
|
|
||||||
|
|
||||||
2. **Status Codes**
|
|
||||||
- Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500)
|
|
||||||
- Proper error messages for each status code
|
|
||||||
- Field-specific errors for validation failures
|
|
||||||
|
|
||||||
3. **Pagination**
|
|
||||||
- Paginated responses include `count`, `next`, `previous`, `results`
|
|
||||||
- Page size limits enforced (max 100)
|
|
||||||
- Empty results handled correctly
|
|
||||||
- Default page size (10) works correctly
|
|
||||||
|
|
||||||
4. **Error Handling**
|
|
||||||
- All exceptions wrapped in unified format
|
|
||||||
- Field-specific errors included in `errors` object
|
|
||||||
- Debug info (traceback, view, path, method) in DEBUG mode
|
|
||||||
- Request ID included in all error responses
|
|
||||||
|
|
||||||
5. **Permissions**
|
|
||||||
- Role-based access control (viewer, editor, admin, owner, developer)
|
|
||||||
- Tenant isolation (users can only access their account's data)
|
|
||||||
- Site/sector scoping (users can only access their assigned sites/sectors)
|
|
||||||
- Admin/system account bypass (full access)
|
|
||||||
|
|
||||||
6. **Rate Limiting**
|
|
||||||
- Throttle headers present in all responses
|
|
||||||
- Bypass logic for admin/developer/system account users
|
|
||||||
- Bypass in DEBUG mode (for development)
|
|
||||||
- Different throttle scopes (read, write, ai)
|
|
||||||
|
|
||||||
### Test Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
python manage.py test igny8_core.api.tests --verbosity=2
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
python manage.py test igny8_core.api.tests.test_response
|
|
||||||
|
|
||||||
# Run specific test class
|
|
||||||
python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
coverage run --source='igny8_core.api' manage.py test igny8_core.api.tests
|
|
||||||
coverage report
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Results
|
|
||||||
|
|
||||||
All tests pass successfully:
|
|
||||||
- ✅ Unit tests: 61/61 passing
|
|
||||||
- ✅ Integration tests: 54/54 passing
|
|
||||||
- ✅ Total: 115/115 passing
|
|
||||||
|
|
||||||
### Files Created
|
|
||||||
|
|
||||||
- `backend/igny8_core/api/tests/__init__.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_response.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_exception_handler.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_permissions.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_throttles.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_base.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_planner.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_writer.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_system.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_billing.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_auth.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_errors.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_pagination.py`
|
|
||||||
- `backend/igny8_core/api/tests/test_integration_rate_limiting.py`
|
|
||||||
- `backend/igny8_core/api/tests/README.md`
|
|
||||||
- `backend/igny8_core/api/tests/TEST_SUMMARY.md`
|
|
||||||
- `backend/igny8_core/api/tests/run_tests.py`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section 2: Documentation ✅
|
|
||||||
|
|
||||||
### Implementation Summary
|
|
||||||
|
|
||||||
Complete documentation system for IGNY8 API v1.0 including OpenAPI 3.0 schema generation, interactive Swagger UI, and comprehensive documentation files.
|
|
||||||
|
|
||||||
### OpenAPI/Swagger Integration
|
|
||||||
|
|
||||||
#### Package Installation
|
|
||||||
- ✅ Installed `drf-spectacular>=0.27.0`
|
|
||||||
- ✅ Added to `INSTALLED_APPS` in `settings.py`
|
|
||||||
- ✅ Configured `REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS']`
|
|
||||||
|
|
||||||
#### Configuration (`backend/igny8_core/settings.py`)
|
|
||||||
|
|
||||||
```python
|
|
||||||
SPECTACULAR_SETTINGS = {
|
|
||||||
'TITLE': 'IGNY8 API v1.0',
|
|
||||||
'DESCRIPTION': 'Comprehensive REST API for content planning, creation, and management...',
|
|
||||||
'VERSION': '1.0.0',
|
|
||||||
'SCHEMA_PATH_PREFIX': '/api/v1',
|
|
||||||
'COMPONENT_SPLIT_REQUEST': True,
|
|
||||||
'TAGS': [
|
|
||||||
{'name': 'Authentication', 'description': 'User authentication and registration'},
|
|
||||||
{'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'},
|
|
||||||
{'name': 'Writer', 'description': 'Tasks, content, and images'},
|
|
||||||
{'name': 'System', 'description': 'Settings, prompts, and integrations'},
|
|
||||||
{'name': 'Billing', 'description': 'Credits, usage, and transactions'},
|
|
||||||
],
|
|
||||||
'EXTENSIONS_INFO': {
|
|
||||||
'x-code-samples': [
|
|
||||||
{'lang': 'Python', 'source': '...'},
|
|
||||||
{'lang': 'JavaScript', 'source': '...'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Endpoints Created
|
|
||||||
|
|
||||||
- ✅ `/api/schema/` - OpenAPI 3.0 schema (JSON/YAML)
|
|
||||||
- ✅ `/api/docs/` - Swagger UI (interactive documentation)
|
|
||||||
- ✅ `/api/redoc/` - ReDoc (alternative documentation UI)
|
|
||||||
|
|
||||||
#### Schema Extensions
|
|
||||||
|
|
||||||
Created `backend/igny8_core/api/schema_extensions.py`:
|
|
||||||
- ✅ `JWTAuthenticationExtension` - JWT Bearer token authentication
|
|
||||||
- ✅ `CSRFExemptSessionAuthenticationExtension` - Session authentication
|
|
||||||
- ✅ Proper OpenAPI security scheme definitions
|
|
||||||
|
|
||||||
#### URL Configuration (`backend/igny8_core/urls.py`)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from drf_spectacular.views import (
|
|
||||||
SpectacularAPIView,
|
|
||||||
SpectacularRedocView,
|
|
||||||
SpectacularSwaggerView,
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# ... other URLs ...
|
|
||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
|
||||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
|
||||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentation Files Created
|
|
||||||
|
|
||||||
#### 1. API-DOCUMENTATION.md
|
|
||||||
**Purpose**: Complete API reference
|
|
||||||
**Contents**:
|
|
||||||
- Quick start guide
|
|
||||||
- Authentication guide
|
|
||||||
- Response format details
|
|
||||||
- Error handling
|
|
||||||
- Rate limiting
|
|
||||||
- Pagination
|
|
||||||
- Endpoint reference
|
|
||||||
- Code examples (Python, JavaScript, cURL)
|
|
||||||
|
|
||||||
#### 2. AUTHENTICATION-GUIDE.md
|
|
||||||
**Purpose**: Authentication and authorization
|
|
||||||
**Contents**:
|
|
||||||
- JWT Bearer token authentication
|
|
||||||
- Token management and refresh
|
|
||||||
- Code examples (Python, JavaScript)
|
|
||||||
- Security best practices
|
|
||||||
- Token expiration handling
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
#### 3. ERROR-CODES.md
|
|
||||||
**Purpose**: Complete error code reference
|
|
||||||
**Contents**:
|
|
||||||
- HTTP status codes (200, 201, 400, 401, 403, 404, 409, 422, 429, 500)
|
|
||||||
- Field-specific error messages
|
|
||||||
- Error handling best practices
|
|
||||||
- Common error scenarios
|
|
||||||
- Debugging tips
|
|
||||||
|
|
||||||
#### 4. RATE-LIMITING.md
|
|
||||||
**Purpose**: Rate limiting and throttling
|
|
||||||
**Contents**:
|
|
||||||
- Rate limit scopes and limits
|
|
||||||
- Handling rate limits (429 responses)
|
|
||||||
- Best practices
|
|
||||||
- Code examples with backoff strategies
|
|
||||||
- Request queuing and caching
|
|
||||||
|
|
||||||
#### 5. MIGRATION-GUIDE.md
|
|
||||||
**Purpose**: Migration guide for API consumers
|
|
||||||
**Contents**:
|
|
||||||
- What changed in v1.0
|
|
||||||
- Step-by-step migration instructions
|
|
||||||
- Code examples (before/after)
|
|
||||||
- Breaking and non-breaking changes
|
|
||||||
- Migration checklist
|
|
||||||
|
|
||||||
#### 6. WORDPRESS-PLUGIN-INTEGRATION.md
|
|
||||||
**Purpose**: WordPress plugin integration
|
|
||||||
**Contents**:
|
|
||||||
- Complete PHP API client class
|
|
||||||
- Authentication implementation
|
|
||||||
- Error handling
|
|
||||||
- WordPress admin integration
|
|
||||||
- Two-way sync (WordPress → IGNY8)
|
|
||||||
- Site data fetching (posts, taxonomies, products, attributes)
|
|
||||||
- Semantic mapping and content restructuring
|
|
||||||
- Best practices
|
|
||||||
- Testing examples
|
|
||||||
|
|
||||||
#### 7. README.md
|
|
||||||
**Purpose**: Documentation index
|
|
||||||
**Contents**:
|
|
||||||
- Documentation index
|
|
||||||
- Quick start guide
|
|
||||||
- Links to all documentation files
|
|
||||||
- Support information
|
|
||||||
|
|
||||||
### Documentation Statistics
|
|
||||||
|
|
||||||
- **Total Documentation Files**: 7
|
|
||||||
- **Total Pages**: ~100+ pages of documentation
|
|
||||||
- **Code Examples**: Python, JavaScript, PHP, cURL
|
|
||||||
- **Coverage**: 100% of API features documented
|
|
||||||
|
|
||||||
### Access Points
|
|
||||||
|
|
||||||
#### Interactive Documentation
|
|
||||||
- **Swagger UI**: `https://api.igny8.com/api/docs/`
|
|
||||||
- **ReDoc**: `https://api.igny8.com/api/redoc/`
|
|
||||||
- **OpenAPI Schema**: `https://api.igny8.com/api/schema/`
|
|
||||||
|
|
||||||
#### Documentation Files
|
|
||||||
- All files in `docs/` directory
|
|
||||||
- Index: `docs/README.md`
|
|
||||||
|
|
||||||
### Files Created/Modified
|
|
||||||
|
|
||||||
#### Backend Files
|
|
||||||
- `backend/igny8_core/settings.py` - Added drf-spectacular configuration
|
|
||||||
- `backend/igny8_core/urls.py` - Added schema/documentation endpoints
|
|
||||||
- `backend/igny8_core/api/schema_extensions.py` - Custom authentication extensions
|
|
||||||
- `backend/requirements.txt` - Added drf-spectacular>=0.27.0
|
|
||||||
|
|
||||||
#### Documentation Files
|
|
||||||
- `docs/API-DOCUMENTATION.md`
|
|
||||||
- `docs/AUTHENTICATION-GUIDE.md`
|
|
||||||
- `docs/ERROR-CODES.md`
|
|
||||||
- `docs/RATE-LIMITING.md`
|
|
||||||
- `docs/MIGRATION-GUIDE.md`
|
|
||||||
- `docs/WORDPRESS-PLUGIN-INTEGRATION.md`
|
|
||||||
- `docs/README.md`
|
|
||||||
- `docs/DOCUMENTATION-SUMMARY.md`
|
|
||||||
- `docs/SECTION-2-COMPLETE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification & Status
|
|
||||||
|
|
||||||
### Section 1: Testing ✅
|
|
||||||
- ✅ All test files created
|
|
||||||
- ✅ All tests passing (115/115)
|
|
||||||
- ✅ 100% coverage of API Standard components
|
|
||||||
- ✅ Unit tests: 61/61 passing
|
|
||||||
- ✅ Integration tests: 54/54 passing
|
|
||||||
- ✅ Test documentation created
|
|
||||||
|
|
||||||
### Section 2: Documentation ✅
|
|
||||||
- ✅ drf-spectacular installed and configured
|
|
||||||
- ✅ Schema generation working (OpenAPI 3.0)
|
|
||||||
- ✅ Schema endpoint accessible (`/api/schema/`)
|
|
||||||
- ✅ Swagger UI accessible (`/api/docs/`)
|
|
||||||
- ✅ ReDoc accessible (`/api/redoc/`)
|
|
||||||
- ✅ 7 comprehensive documentation files created
|
|
||||||
- ✅ Code examples included (Python, JavaScript, PHP, cURL)
|
|
||||||
- ✅ Changelog updated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deliverables
|
|
||||||
|
|
||||||
### Section 1 Deliverables
|
|
||||||
1. ✅ Complete test suite (13 test files, 115 test methods)
|
|
||||||
2. ✅ Test documentation (README.md, TEST_SUMMARY.md)
|
|
||||||
3. ✅ Test runner script (run_tests.py)
|
|
||||||
4. ✅ All tests passing
|
|
||||||
|
|
||||||
### Section 2 Deliverables
|
|
||||||
1. ✅ OpenAPI 3.0 schema generation
|
|
||||||
2. ✅ Interactive Swagger UI
|
|
||||||
3. ✅ ReDoc documentation
|
|
||||||
4. ✅ 7 comprehensive documentation files
|
|
||||||
5. ✅ Code examples in multiple languages
|
|
||||||
6. ✅ Integration guides
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### Completed ✅
|
|
||||||
- ✅ Section 1: Testing - Complete
|
|
||||||
- ✅ Section 2: Documentation - Complete
|
|
||||||
|
|
||||||
### Remaining
|
|
||||||
- Section 3: Frontend Refactoring (if applicable)
|
|
||||||
- Section 4: Additional Features (if applicable)
|
|
||||||
- Section 5: Performance Optimization (if applicable)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Both **Section 1: Testing** and **Section 2: Documentation** have been successfully implemented and verified:
|
|
||||||
|
|
||||||
- **Testing**: Comprehensive test suite with 115 test methods covering all API Standard components
|
|
||||||
- **Documentation**: Complete documentation system with OpenAPI schema, Swagger UI, and 7 comprehensive guides
|
|
||||||
|
|
||||||
All deliverables are complete, tested, and ready for use.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2025-11-16
|
|
||||||
**API Version**: 1.0.0
|
|
||||||
**Status**: ✅ Complete
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Section 2: Documentation - COMPLETE ✅
|
|
||||||
|
|
||||||
**Date Completed**: 2025-11-16
|
|
||||||
**Status**: All Documentation Implemented, Verified, and Fully Functional
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Section 2: Documentation has been successfully implemented with:
|
|
||||||
- ✅ OpenAPI 3.0 schema generation (drf-spectacular v0.29.0)
|
|
||||||
- ✅ Interactive Swagger UI and ReDoc
|
|
||||||
- ✅ 7 comprehensive documentation files
|
|
||||||
- ✅ Code examples in multiple languages
|
|
||||||
- ✅ Integration guides for all platforms
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deliverables
|
|
||||||
|
|
||||||
### 1. OpenAPI/Swagger Integration ✅
|
|
||||||
- **Package**: drf-spectacular v0.29.0 installed
|
|
||||||
- **Endpoints**:
|
|
||||||
- `/api/schema/` - OpenAPI 3.0 schema
|
|
||||||
- `/api/docs/` - Swagger UI
|
|
||||||
- `/api/redoc/` - ReDoc
|
|
||||||
- **Configuration**: Comprehensive settings with API description, tags, code samples
|
|
||||||
|
|
||||||
### 2. Documentation Files ✅
|
|
||||||
- **API-DOCUMENTATION.md** - Complete API reference
|
|
||||||
- **AUTHENTICATION-GUIDE.md** - Auth guide with examples
|
|
||||||
- **ERROR-CODES.md** - Error code reference
|
|
||||||
- **RATE-LIMITING.md** - Rate limiting guide
|
|
||||||
- **MIGRATION-GUIDE.md** - Migration instructions
|
|
||||||
- **WORDPRESS-PLUGIN-INTEGRATION.md** - WordPress integration
|
|
||||||
- **README.md** - Documentation index
|
|
||||||
|
|
||||||
### 3. Schema Extensions ✅
|
|
||||||
- Custom JWT authentication extension
|
|
||||||
- Session authentication extension
|
|
||||||
- Proper OpenAPI security schemes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
✅ **drf-spectacular**: Installed and configured
|
|
||||||
✅ **Schema Generation**: Working (database created and migrations applied)
|
|
||||||
✅ **Schema Endpoint**: `/api/schema/` returns 200 OK with OpenAPI 3.0 schema
|
|
||||||
✅ **Swagger UI**: `/api/docs/` displays full API documentation
|
|
||||||
✅ **ReDoc**: `/api/redoc/` displays full API documentation
|
|
||||||
✅ **Documentation Files**: 7 files created
|
|
||||||
✅ **Changelog**: Updated with documentation section
|
|
||||||
✅ **Code Examples**: Python, JavaScript, PHP, cURL included
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Access
|
|
||||||
|
|
||||||
- **Swagger UI**: `https://api.igny8.com/api/docs/`
|
|
||||||
- **ReDoc**: `https://api.igny8.com/api/redoc/`
|
|
||||||
- **OpenAPI Schema**: `https://api.igny8.com/api/schema/`
|
|
||||||
- **Documentation Files**: `docs/` directory
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
**Section 2: Documentation - COMPLETE** ✅
|
|
||||||
|
|
||||||
All documentation is implemented, verified, and fully functional:
|
|
||||||
- Database created and migrations applied
|
|
||||||
- Schema generation working (OpenAPI 3.0)
|
|
||||||
- Swagger UI displaying full API documentation
|
|
||||||
- ReDoc displaying full API documentation
|
|
||||||
- All endpoints accessible and working
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Completed**: 2025-11-16
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user