Integrate OpenAPI/Swagger documentation using drf-spectacular, enhancing API documentation with comprehensive guides and schema generation. Add multiple documentation files covering authentication, error codes, rate limiting, and migration strategies. Update settings and URLs to support new documentation endpoints and schema configurations.
This commit is contained in:
493
docs/AUTHENTICATION-GUIDE.md
Normal file
493
docs/AUTHENTICATION-GUIDE.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user