revamping documentation

This commit is contained in:
2025-11-17 22:59:42 +01:00
parent bbd64a6f21
commit 807033be16
107 changed files with 11973 additions and 28413 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,285 @@
# Authentication Flow Diagrams
## Cookie Isolation Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Admin Area │ │ Vendor Area │ │
│ │ /admin/* │ │ /vendor/* │ │
│ │ │ │ │ │
│ │ 🍪 admin_token │ │ 🍪 vendor_token │ │
│ │ Path: /admin │ │ Path: /vendor │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ ├───────────────────────────┤ │
│ │ ❌ No Cookie Mixing │ │
│ │ │ │
└───────────┼───────────────────────────┼──────────────────────┘
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ Admin Backend │ │ Vendor Backend │
│ /admin/* │ │ /vendor/* │
│ │ │ │
│ ✅ admin_token │ │ ✅ vendor_token │
│ ❌ vendor_token │ │ ❌ admin_token │
└───────────────────────┘ └───────────────────────┘
```
## Login Flow - Admin
```
┌──────────┐
│ Browser │
└──────────┘
│ POST /api/v1/admin/auth/login
│ { username, password }
┌─────────────────────────┐
│ Admin Auth Endpoint │
│ │
│ 1. Validate credentials│
│ 2. Check role == admin │
│ 3. Generate JWT │
└─────────────────────────┘
│ Set-Cookie: admin_token=<JWT>; Path=/admin; HttpOnly; SameSite=Lax
│ Response: { access_token, user }
┌──────────┐
│ Browser │──────────────────────────────────────┐
│ │ │
│ 🍪 admin_token (Path=/admin) │
│ 💾 localStorage.access_token │
└──────────┘ │
│ │
├── Navigate to /admin/dashboard ────────────┤
│ (Cookie sent automatically) │
│ │
└── API call to /api/v1/admin/vendors ───────┤
(Authorization: Bearer <token>) │
┌──────────────▼──────────────┐
│ get_current_admin_user() │
│ │
│ 1. Check Auth header │
│ 2. Check admin_token cookie │
│ 3. Validate JWT │
│ 4. Verify role == admin │
│ ✅ Return User │
└──────────────────────────────┘
```
## Login Flow - Vendor
```
┌──────────┐
│ Browser │
└──────────┘
│ POST /api/v1/vendor/auth/login
│ { username, password }
┌─────────────────────────┐
│ Vendor Auth Endpoint │
│ │
│ 1. Validate credentials│
│ 2. Block if admin │──────> ❌ "Admins cannot access vendor portal"
│ 3. Check vendor access │
│ 4. Generate JWT │
└─────────────────────────┘
│ Set-Cookie: vendor_token=<JWT>; Path=/vendor; HttpOnly; SameSite=Lax
│ Response: { access_token, user, vendor }
┌──────────┐
│ Browser │──────────────────────────────────────┐
│ │ │
│ 🍪 vendor_token (Path=/vendor) │
│ 💾 localStorage.access_token │
└──────────┘ │
│ │
├── Navigate to /vendor/ACME/dashboard ──────┤
│ (Cookie sent automatically) │
│ │
└── API call to /api/v1/vendor/ACME/products ┤
(Authorization: Bearer <token>) │
┌──────────────▼──────────────┐
│ get_current_vendor_user() │
│ │
│ 1. Check Auth header │
│ 2. Check vendor_token cookie│
│ 3. Validate JWT │
│ 4. Block if admin │──> ❌ Error
│ 5. Verify vendor access │
│ ✅ Return User │
└──────────────────────────────┘
```
## Security Boundary Enforcement
```
┌─────────────────────┐
│ Request Comes In │
└──────────┬──────────┘
┌──────────▼──────────┐
│ What's the path? │
└──────────┬──────────┘
┌───────────────┼───────────────┐
│ │ │
Starts with Starts with Starts with
/admin/* /vendor/* /api/*
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Check for: │ │ Check for: │ │ Check for: │
│ - admin_token │ │ - vendor_token │ │ - Authorization │
│ cookie │ │ cookie │ │ header │
│ - OR Auth header │ │ - OR Auth header │ │ (required) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Validate: │ │ Validate: │ │ Validate: │
│ - JWT valid │ │ - JWT valid │ │ - JWT valid │
│ - User active │ │ - User active │ │ - User active │
│ - Role = admin │ │ - Role != admin │ │ - Any role │
│ │ │ - Has vendor │ │ (depends on │
│ │ │ access │ │ endpoint) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
✅ Allowed ✅ Allowed ✅ Allowed
```
## Cross-Context Prevention
### ❌ What's Blocked
```
Admin trying to access vendor route:
┌──────────────────────────────────────────┐
│ User: admin@example.com (role: admin) │
│ Token: Valid JWT with admin role │
│ Request: GET /vendor/ACME/dashboard │
└──────────────────────────────────────────┘
┌───────────────────────┐
│ get_current_vendor_ │
│ from_cookie_or_header │
└───────────┬───────────┘
Check: role == "admin"?
▼ Yes
❌ InsufficientPermissionsException
"Vendor access only - admins cannot use vendor portal"
```
```
Vendor trying to access admin route:
┌──────────────────────────────────────────┐
│ User: vendor@acme.com (role: vendor) │
│ Token: Valid JWT with vendor role │
│ Request: GET /admin/dashboard │
└──────────────────────────────────────────┘
┌───────────────────────┐
│ get_current_admin_ │
│ from_cookie_or_header │
└───────────┬───────────┘
Check: role == "admin"?
▼ No
❌ AdminRequiredException
"Admin privileges required"
```
```
Admin cookie sent to vendor route:
┌──────────────────────────────────────────┐
│ Cookie: admin_token=<JWT> (Path=/admin) │
│ Request: GET /vendor/ACME/dashboard │
└──────────────────────────────────────────┘
Browser checks cookie path
Path /vendor does NOT match /admin
❌ Cookie NOT sent
Request has no authentication
❌ InvalidTokenException
"Vendor authentication required"
```
## Cookie Lifecycle
```
LOGIN
├── Server generates JWT
├── Server sets cookie:
│ • Name: admin_token or vendor_token
│ • Value: JWT
│ • Path: /admin or /vendor
│ • HttpOnly: true
│ • Secure: true (production)
│ • SameSite: Lax
│ • Max-Age: matches JWT expiry
└── Server returns JWT in response body
└── Client stores in localStorage (optional)
PAGE NAVIGATION
├── Browser automatically includes cookie
│ if path matches
├── Server reads cookie
├── Server validates JWT
└── Server returns page or 401
API CALL
├── Client reads token from localStorage
├── Client adds Authorization header
│ Authorization: Bearer <JWT>
├── Server reads header
├── Server validates JWT
└── Server returns data or 401
LOGOUT
├── Client calls logout endpoint
├── Server clears cookie:
│ response.delete_cookie(name, path)
└── Client clears localStorage
localStorage.removeItem('access_token')
```
## Key Takeaways
1. **Cookie Path Isolation** = No cross-context cookies
2. **Role Checking** = Admins blocked from vendor routes
3. **Dual Auth Support** = Cookies for pages, headers for API
4. **Security First** = HttpOnly, Secure, SameSite protection
5. **Clear Boundaries** = Each context is completely isolated

View File

@@ -0,0 +1,271 @@
# Authentication Quick Reference
**Version 1.0** | One-page reference for developers
---
## Function Cheat Sheet
### For HTML Pages (accept cookie OR header)
```python
from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_vendor_from_cookie_or_header,
get_current_customer_from_cookie_or_header
)
# Admin page
@router.get("/admin/dashboard")
def admin_page(user: User = Depends(get_current_admin_from_cookie_or_header)):
pass
# Vendor page
@router.get("/vendor/{code}/dashboard")
def vendor_page(user: User = Depends(get_current_vendor_from_cookie_or_header)):
pass
# Customer page
@router.get("/shop/account/dashboard")
def customer_page(user: User = Depends(get_current_customer_from_cookie_or_header)):
pass
```
### For API Endpoints (header only - better security)
```python
from app.api.deps import (
get_current_admin_api,
get_current_vendor_api,
get_current_customer_api
)
# Admin API
@router.post("/api/v1/admin/vendors")
def admin_api(user: User = Depends(get_current_admin_api)):
pass
# Vendor API
@router.post("/api/v1/vendor/{code}/products")
def vendor_api(user: User = Depends(get_current_vendor_api)):
pass
# Customer API
@router.post("/api/v1/shop/orders")
def customer_api(user: User = Depends(get_current_customer_api)):
pass
```
---
## Three Authentication Contexts
| Context | Cookie | Path | Role | Routes |
|---------|--------|------|------|--------|
| **Admin** | `admin_token` | `/admin` | `admin` | `/admin/*` |
| **Vendor** | `vendor_token` | `/vendor` | `vendor` | `/vendor/*` |
| **Customer** | `customer_token` | `/shop` | `customer` | `/shop/account/*` |
---
## Access Control Matrix
| User | Admin Portal | Vendor Portal | Shop Catalog | Customer Account |
|------|--------------|---------------|--------------|------------------|
| Admin | ✅ | ❌ | ✅ (view) | ❌ |
| Vendor | ❌ | ✅ | ✅ (view) | ❌ |
| Customer | ❌ | ❌ | ✅ (view) | ✅ |
| Anonymous | ❌ | ❌ | ✅ (view) | ❌ |
---
## Login Endpoints
```bash
# Admin
POST /api/v1/admin/auth/login
Body: {"username": "...", "password": "..."}
# Vendor
POST /api/v1/vendor/auth/login
Body: {"username": "...", "password": "..."}
# Customer
POST /api/v1/public/vendors/{vendor_id}/customers/login
Body: {"username": "...", "password": "..."}
```
**Response:**
```json
{
"access_token": "eyJ0eXAi...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {...}
}
```
Plus HTTP-only cookie is set automatically.
---
## Frontend Patterns
### Login (Store Token)
```javascript
const response = await fetch('/api/v1/admin/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
// Cookie set automatically
// Optionally store for API calls
localStorage.setItem('token', data.access_token);
// Navigate (cookie automatic)
window.location.href = '/admin/dashboard';
```
### API Call (Use Token)
```javascript
const token = localStorage.getItem('token');
const response = await fetch('/api/v1/admin/vendors', {
headers: {
'Authorization': `Bearer ${token}`
}
});
```
### Logout
```javascript
await fetch('/api/v1/admin/auth/logout', { method: 'POST' });
localStorage.removeItem('token');
window.location.href = '/admin/login';
```
---
## Testing Commands
### curl Examples
```bash
# Login
TOKEN=$(curl -X POST http://localhost:8000/api/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
| jq -r '.access_token')
# Authenticated request
curl http://localhost:8000/api/v1/admin/vendors \
-H "Authorization: Bearer $TOKEN"
```
### Check Cookie in Browser
```javascript
// In DevTools console
document.cookie.split(';').forEach(c => console.log(c.trim()));
```
### Decode JWT
```javascript
function parseJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
console.log(parseJwt(localStorage.getItem('token')));
```
---
## Common Errors
| Error | Meaning | Solution |
|-------|---------|----------|
| `INVALID_TOKEN` | No token or invalid | Re-login |
| `TOKEN_EXPIRED` | Token expired | Re-login |
| `ADMIN_REQUIRED` | Need admin role | Use correct account |
| `INSUFFICIENT_PERMISSIONS` | Wrong role for route | Use correct portal |
| `USER_NOT_ACTIVE` | Account disabled | Contact admin |
---
## Security Rules
1.**HTML pages** use `*_from_cookie_or_header` functions
2.**API endpoints** use `*_api` functions
3.**Admins** cannot access vendor/customer portals
4.**Vendors** cannot access admin/customer portals
5.**Customers** cannot access admin/vendor portals
6.**Public shop** (`/shop/products`) needs no auth
7.**Customer accounts** (`/shop/account/*`) need auth
---
## Cookie Security
All cookies have:
-`HttpOnly=true` - JavaScript cannot read (XSS protection)
-`Secure=true` - HTTPS only (production)
-`SameSite=Lax` - CSRF protection
- ✅ Path restriction - Context isolation
---
## Quick Debug
1. **Auth not working?**
- Check DevTools → Application → Cookies
- Verify cookie name and path match route
- Check token not expired
2. **Cross-context access denied?**
- This is intentional security
- Use correct portal for your role
3. **API call fails but page loads?**
- API needs `Authorization` header
- Page uses cookie (automatic)
- Add header to API calls
---
## File Locations
```
app/api/
├── deps.py # All auth functions here
├── v1/
├── admin/auth.py # Admin login
├── vendor/auth.py # Vendor login
└── public/vendors/auth.py # Customer login
```
---
## Environment Variables
```bash
JWT_SECRET_KEY=your-secret-key
JWT_ALGORITHM=HS256
JWT_EXPIRATION=3600 # 1 hour
ENVIRONMENT=production
```
---
**Full Documentation:** See [Authentication System Documentation](authentication.md)
**Questions?** Contact backend team
---
**Print this page for quick reference!**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
# Error Handling
Comprehensive error handling system for the FastAPI multi-tenant e-commerce platform.
## Overview
The application uses a structured exception hierarchy with custom exception classes and centralized error handlers. All exceptions are logged, formatted consistently, and return appropriate HTTP status codes.
## Exception Hierarchy
### Base Exceptions
All custom exceptions inherit from base exception classes defined in `app.exceptions`:
```python
from app.exceptions import (
InvalidTokenException,
TokenExpiredException,
InvalidCredentialsException,
UserNotActiveException,
AdminRequiredException,
InsufficientPermissionsException,
RateLimitException
)
```
### Authentication Exceptions
| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `InvalidTokenException` | 401 | JWT token is invalid, malformed, or missing required claims |
| `TokenExpiredException` | 401 | JWT token has expired |
| `InvalidCredentialsException` | 401 | Username/password authentication failed |
| `UserNotActiveException` | 403 | User account is inactive or disabled |
### Authorization Exceptions
| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `AdminRequiredException` | 403 | Endpoint requires admin role |
| `InsufficientPermissionsException` | 403 | User lacks required permissions |
### Rate Limiting Exceptions
| Exception | Status Code | Description |
|-----------|-------------|-------------|
| `RateLimitException` | 429 | Too many requests, rate limit exceeded |
## Error Response Format
All errors return a consistent JSON format:
```json
{
"detail": "Error message describing what went wrong",
"status_code": 401,
"timestamp": "2024-11-16T13:00:00Z",
"path": "/api/v1/auth/login"
}
```
### Rate Limit Error Response
Rate limit errors include additional information:
```json
{
"detail": "Rate limit exceeded",
"status_code": 429,
"retry_after": 3600,
"timestamp": "2024-11-16T13:00:00Z",
"path": "/api/v1/resource"
}
```
## Usage Examples
### Raising Exceptions in Code
```python
from app.exceptions import InvalidCredentialsException, AdminRequiredException
# Authentication failure
if not user:
raise InvalidCredentialsException("User not found")
# Authorization check
if user.role != "admin":
raise AdminRequiredException()
```
### Catching Exceptions in Routes
```python
from fastapi import HTTPException
from app.exceptions import InvalidTokenException
@app.post("/api/v1/auth/protected")
async def protected_endpoint(token: str):
try:
user_data = auth_manager.verify_token(token)
return {"user": user_data}
except InvalidTokenException as e:
# Exception will be caught by global handler
raise
except Exception as e:
# Unexpected errors
logger.error(f"Unexpected error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
```
## Context-Aware Error Handling
The error handling system is context-aware and provides different error formats based on the request context:
### API Requests (`/api/*`)
Returns JSON error responses suitable for API clients.
### Admin/Vendor Dashboard (`/admin/*`, `/vendor/*`)
Returns JSON errors or redirects to error pages based on accept headers.
### Shop Requests (`/shop/*`)
Returns themed error pages matching the vendor's shop design.
## Logging
All errors are automatically logged with the following information:
- Error type and message
- Request path and method
- User information (if authenticated)
- Stack trace (for unexpected errors)
- Timestamp
Example log output:
```
2024-11-16 13:00:00 [ERROR] middleware.auth: Token verification error: Token missing user identifier
2024-11-16 13:00:00 [ERROR] app.main: Request failed: POST /api/v1/auth/login - 401
```
## Best Practices
### 1. Use Specific Exceptions
Always use the most specific exception class available:
```python
# Good
raise InvalidCredentialsException("Invalid email or password")
# Avoid
raise HTTPException(status_code=401, detail="Invalid credentials")
```
### 2. Provide Meaningful Messages
Include context in error messages:
```python
# Good
raise InvalidTokenException("Token missing user identifier")
# Avoid
raise InvalidTokenException("Invalid token")
```
### 3. Don't Expose Sensitive Information
Never include sensitive data in error messages:
```python
# Good
raise InvalidCredentialsException("Invalid email or password")
# Avoid - reveals which field is wrong
raise InvalidCredentialsException(f"User {email} not found")
```
### 4. Log Before Raising
Log errors before raising them for debugging:
```python
try:
result = risky_operation()
except OperationFailed as e:
logger.error(f"Operation failed: {e}", exc_info=True)
raise InternalServerException("Operation failed")
```
## Testing Error Handling
### Unit Tests
```python
import pytest
from app.exceptions import InvalidTokenException
def test_invalid_token():
auth_manager = AuthManager()
with pytest.raises(InvalidTokenException) as exc_info:
auth_manager.verify_token("invalid-token")
assert "Could not validate credentials" in str(exc_info.value.message)
```
### Integration Tests
```python
def test_authentication_error_response(client):
response = client.post(
"/api/v1/auth/login",
json={"username": "wrong", "password": "wrong"}
)
assert response.status_code == 401
assert "detail" in response.json()
```
## Global Exception Handlers
The application registers global exception handlers in `main.py`:
```python
from fastapi import FastAPI
from app.exceptions import InvalidTokenException, RateLimitException
app = FastAPI()
@app.exception_handler(InvalidTokenException)
async def invalid_token_handler(request, exc):
return JSONResponse(
status_code=401,
content={
"detail": exc.message,
"status_code": 401,
"timestamp": datetime.now(timezone.utc).isoformat(),
"path": str(request.url.path)
}
)
```
## Related Documentation
- [Authentication](authentication.md) - Authentication-related exceptions
- [RBAC](RBAC.md) - Authorization and permission exceptions
- [Rate Limiting](rate-limiting.md) - Rate limit error handling
- [Testing Guide](../testing/testing-guide.md) - Testing error scenarios

View File

@@ -0,0 +1,401 @@
# Rate Limiting
API rate limiting implementation using sliding window algorithm for request throttling and abuse prevention.
## Overview
The platform uses an in-memory rate limiter with a sliding window algorithm to protect endpoints from abuse and ensure fair resource usage across all clients.
## Features
- **Sliding Window Algorithm**: Accurate rate limiting based on request timestamps
- **Per-Client Tracking**: Individual limits for each client
- **Automatic Cleanup**: Removes old entries to prevent memory leaks
- **Configurable Limits**: Set custom limits per endpoint
- **Decorator-Based**: Easy integration with FastAPI routes
## How It Works
### Sliding Window Algorithm
The rate limiter uses a sliding window approach:
1. Records timestamp of each request for a client
2. When new request comes in, removes expired timestamps
3. Counts remaining requests in the current window
4. Allows or denies based on the limit
```mermaid
graph LR
A[New Request] --> B{Check Window}
B --> C[Remove Old Timestamps]
C --> D{Count < Limit?}
D -->|Yes| E[Allow Request]
D -->|No| F[Reject - 429]
E --> G[Record Timestamp]
```
### Example Timeline
For a limit of 10 requests per 60 seconds:
```
Time: 0s 10s 20s 30s 40s 50s 60s 70s
|-----|-----|-----|-----|-----|-----|-----|
Req: 1 2 3 4 5 6 7 8 9 10 <-- Window (60s) --> 11
^
ALLOWED
(req 1-3 expired)
```
## Configuration
### Global Rate Limiter
A global rate limiter instance is available:
```python
from middleware.rate_limiter import RateLimiter
rate_limiter = RateLimiter()
```
### Rate Limiter Options
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cleanup_interval` | int | 3600 | Seconds between automatic cleanups |
## Usage
### Using the Decorator
The easiest way to add rate limiting to an endpoint:
```python
from middleware.decorators import rate_limit
@app.post("/api/v1/resource")
@rate_limit(max_requests=10, window_seconds=60)
async def create_resource():
return {"status": "created"}
```
### Decorator Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_requests` | int | 100 | Maximum requests allowed |
| `window_seconds` | int | 3600 | Time window in seconds (1 hour) |
### Manual Usage
For more control, use the rate limiter directly:
```python
from middleware.rate_limiter import RateLimiter
from app.exceptions import RateLimitException
rate_limiter = RateLimiter()
@app.post("/api/v1/custom")
async def custom_endpoint(request: Request):
client_id = request.client.host
if not rate_limiter.allow_request(client_id, max_requests=5, window_seconds=60):
raise RateLimitException(
message="Too many requests",
retry_after=60
)
return {"status": "success"}
```
## Client Identification
### Current Implementation
By default, the rate limiter uses a simple client ID:
```python
client_id = "anonymous" # Basic implementation
```
### Production Recommendations
For production, implement proper client identification:
#### Option 1: By IP Address
```python
client_id = request.client.host
```
#### Option 2: By API Key
```python
api_key = request.headers.get("X-API-Key", "anonymous")
client_id = f"apikey:{api_key}"
```
#### Option 3: By Authenticated User
```python
if hasattr(request.state, 'user'):
client_id = f"user:{request.state.user.id}"
else:
client_id = f"ip:{request.client.host}"
```
#### Option 4: Combined Approach
```python
def get_client_id(request: Request) -> str:
# Prefer authenticated user
if hasattr(request.state, 'user'):
return f"user:{request.state.user.id}"
# Fall back to API key
api_key = request.headers.get("X-API-Key")
if api_key:
return f"key:{api_key}"
# Last resort: IP address
return f"ip:{request.client.host}"
```
## Common Rate Limit Configurations
### Conservative Limits
For expensive operations or authenticated endpoints:
```python
@rate_limit(max_requests=10, window_seconds=3600) # 10 per hour
async def expensive_operation():
pass
```
### Moderate Limits
For standard API operations:
```python
@rate_limit(max_requests=100, window_seconds=3600) # 100 per hour
async def standard_operation():
pass
```
### Generous Limits
For read-heavy operations:
```python
@rate_limit(max_requests=1000, window_seconds=3600) # 1000 per hour
async def read_operation():
pass
```
### Per-Minute Limits
For real-time operations:
```python
@rate_limit(max_requests=60, window_seconds=60) # 60 per minute
async def realtime_operation():
pass
```
## Error Response
When rate limit is exceeded, clients receive a 429 status code:
```json
{
"detail": "Rate limit exceeded",
"status_code": 429,
"retry_after": 3600,
"timestamp": "2024-11-16T13:00:00Z",
"path": "/api/v1/resource"
}
```
### Response Headers
Consider adding rate limit headers (future enhancement):
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1700145600
```
## Memory Management
### Automatic Cleanup
The rate limiter automatically cleans up old entries:
```python
# Runs every hour by default
cleanup_interval = 3600 # seconds
```
### Manual Cleanup
Force cleanup if needed:
```python
rate_limiter.cleanup_old_entries()
```
### Memory Considerations
For high-traffic applications:
- Each client uses approximately 8 bytes per request timestamp
- Example: 1000 clients x 100 requests = approximately 800 KB
- Consider Redis for distributed rate limiting
## Advanced Patterns
### Different Limits by Role
```python
from fastapi import Depends
from models.database.user import User
def get_rate_limit_for_user(user: User) -> tuple[int, int]:
limits = {
"admin": (10000, 3600), # 10k per hour
"vendor": (1000, 3600), # 1k per hour
"customer": (100, 3600), # 100 per hour
}
return limits.get(user.role, (100, 3600))
@app.post("/api/v1/resource")
async def resource_endpoint(
current_user: User = Depends(get_current_user)
):
max_req, window = get_rate_limit_for_user(current_user)
client_id = f"user:{current_user.id}"
if not rate_limiter.allow_request(client_id, max_req, window):
raise RateLimitException(retry_after=window)
return {"status": "success"}
```
### Endpoint-Specific Limits
```python
RATE_LIMITS = {
"/api/v1/auth/login": (5, 300), # 5 per 5 minutes
"/api/v1/products": (100, 3600), # 100 per hour
"/api/v1/orders": (50, 3600), # 50 per hour
}
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
if request.url.path in RATE_LIMITS:
max_req, window = RATE_LIMITS[request.url.path]
client_id = request.client.host
if not rate_limiter.allow_request(client_id, max_req, window):
raise RateLimitException(retry_after=window)
return await call_next(request)
```
## Testing Rate Limits
### Unit Tests
```python
import pytest
from middleware.rate_limiter import RateLimiter
def test_rate_limiter_allows_requests_within_limit():
limiter = RateLimiter()
client = "test_client"
# Should allow first 5 requests
for i in range(5):
assert limiter.allow_request(client, max_requests=5, window_seconds=60)
# Should deny 6th request
assert not limiter.allow_request(client, max_requests=5, window_seconds=60)
```
### Integration Tests
```python
def test_rate_limit_endpoint(client):
# Make requests up to limit
for i in range(10):
response = client.post("/api/v1/resource")
assert response.status_code == 200
# Next request should be rate limited
response = client.post("/api/v1/resource")
assert response.status_code == 429
assert "retry_after" in response.json()
```
## Production Considerations
### Distributed Rate Limiting
For multi-server deployments, use Redis:
```python
import redis
from datetime import datetime, timezone
class RedisRateLimiter:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
def allow_request(self, client_id: str, max_requests: int, window: int) -> bool:
key = f"ratelimit:{client_id}"
now = datetime.now(timezone.utc).timestamp()
# Remove old entries
self.redis.zremrangebyscore(key, 0, now - window)
# Count requests in window
count = self.redis.zcard(key)
if count < max_requests:
# Add new request
self.redis.zadd(key, {now: now})
self.redis.expire(key, window)
return True
return False
```
### Monitoring
Log rate limit violations for monitoring:
```python
@app.middleware("http")
async def rate_limit_monitoring(request: Request, call_next):
try:
response = await call_next(request)
return response
except RateLimitException as e:
logger.warning(
f"Rate limit exceeded",
extra={
"client": request.client.host,
"path": request.url.path,
"user_agent": request.headers.get("user-agent")
}
)
raise
```
## API Reference
For detailed implementation, see the RateLimiter class in `middleware/rate_limiter.py`.
## Related Documentation
- [Error Handling](error-handling.md) - HTTP error responses
- [Authentication](authentication.md) - API authentication
- [Error Handling](error-handling.md) - RateLimitException details
- [Authentication](authentication.md) - User-based rate limiting

View File

@@ -0,0 +1,316 @@
# RBAC Architecture Visual Guide
## System Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ PLATFORM LEVEL │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Admin Users │ │ Vendor Users │ │
│ │ role="admin" │ │ role="vendor" │ │
│ │ │ │ │ │
│ │ • Full platform │ │ • Can own/join │ │
│ │ access │ │ vendors │ │
│ │ • Cannot access │ │ • Cannot access │ │
│ │ vendor portal │ │ admin portal │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │
└──────────────────────────────────────────────┼──────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ VENDOR LEVEL │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Vendor: ACME │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ Owner │ │ Team Members │ │ │
│ │ │ user_type= │ │ user_type= │ │ │
│ │ │ "owner" │ │ "member" │ │ │
│ │ │ │ │ │ │ │
│ │ │ • All perms │ │ • Role-based perms │ │ │
│ │ │ • Can invite │ │ • Manager/Staff/etc │ │ │
│ │ │ • Can remove │ │ • Can be invited │ │ │
│ │ │ • Cannot be │ │ • Can be removed │ │ │
│ │ │ removed │ │ │ │ │
│ │ └──────────────┘ └──────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Roles │ │ │
│ │ │ │ │ │
│ │ │ • Manager (many perms) │ │ │
│ │ │ • Staff (moderate perms) │ │ │
│ │ │ • Support (limited perms) │ │ │
│ │ │ • Viewer (read-only) │ │ │
│ │ │ • Custom (owner-defined) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CUSTOMER LEVEL │
│ (Separate from Users) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Customers (per vendor) │ │
│ │ │ │
│ │ • Vendor-scoped authentication │ │
│ │ • Can self-register │ │
│ │ • Access own account + shop catalog │ │
│ │ • Cannot access admin/vendor portals │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Team Invitation Flow
```
┌──────────┐
│ Owner │
│ (ACME) │
└────┬─────┘
│ 1. Click "Invite Team Member"
│ Email: jane@example.com
│ Role: Manager
┌─────────────────────┐
│ System Creates: │
│ • User account │
│ • VendorUser │
│ • Invitation token │
└────┬────────────────┘
│ 2. Email sent to jane@example.com
┌──────────────────────┐
│ Jane clicks link │
│ /invitation/accept? │
│ token=abc123... │
└────┬─────────────────┘
│ 3. Jane sets password
│ Enters name
┌─────────────────────┐
│ Account Activated: │
│ • User.is_active │
│ • VendorUser. │
│ is_active │
└────┬────────────────┘
│ 4. Jane can now login
┌──────────────────────┐
│ Jane logs in to │
│ ACME vendor portal │
│ with Manager perms │
└──────────────────────┘
```
## Permission Check Flow
```
┌────────────────┐
│ User makes │
│ request to: │
│ POST /products │
└───────┬────────┘
┌────────────────────────────────┐
│ FastAPI Dependency: │
│ require_vendor_permission( │
│ "products.create" │
│ ) │
└───────┬────────────────────────┘
│ 1. Get vendor from request.state
│ 2. Get user from JWT
┌────────────────────────────────┐
│ Is user a member of vendor? │
└───────┬────────────────────────┘
├─ No ──> ❌ VendorAccessDeniedException
▼ Yes
┌────────────────────────────────┐
│ Is user the owner? │
└───────┬────────────────────────┘
├─ Yes ──> ✅ Allow (owners have all perms)
▼ No
┌────────────────────────────────┐
│ Get user's role and │
│ permissions from VendorUser │
└───────┬────────────────────────┘
┌────────────────────────────────┐
│ Does role contain │
│ "products.create"? │
└───────┬────────────────────────┘
├─ No ──> ❌ InsufficientVendorPermissionsException
▼ Yes
┌────────────────────────────────┐
│ ✅ Allow request │
│ Handler executes │
└────────────────────────────────┘
```
## Database Relationships
```
┌──────────────────┐
│ users │
│ │
│ id (PK) │◄────┐
│ email │ │
│ role │ │
│ ('admin' or │ │
│ 'vendor') │ │
└──────────────────┘ │
│ │
│ owner_user_id │
│ │
▼ │
┌──────────────────┐ │
│ vendors │ │
│ │ │
│ id (PK) │ │
│ vendor_code │ │
│ owner_user_id ──┼─────┘
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ vendor_users │ │ roles │
│ │ │ │
│ id (PK) │ │ id (PK) │
│ vendor_id (FK) │ │ vendor_id (FK) │
│ user_id (FK) │ │ name │
│ role_id (FK) ───┼────►│ permissions │
│ user_type │ │ (JSON) │
│ ('owner' or │ └──────────────────┘
│ 'member') │
│ invitation_* │
│ is_active │
└──────────────────┘
Separate hierarchy:
┌──────────────────┐
│ customers │
│ │
│ id (PK) │
│ vendor_id (FK) │
│ email │
│ hashed_password │
│ (vendor-scoped) │
└──────────────────┘
```
## Permission Naming Convention
```
Resource.Action
Examples:
✓ dashboard.view
✓ products.view
✓ products.create
✓ products.edit
✓ products.delete
✓ products.import
✓ products.export
✓ orders.view
✓ orders.edit
✓ orders.cancel
✓ orders.refund
✓ customers.view
✓ customers.edit
✓ reports.financial
✓ team.invite
✓ team.remove
✓ settings.edit
```
## Role Presets
```
┌──────────────────────────────────────────────────────────────┐
│ OWNER │
│ ALL PERMISSIONS (automatic, not stored in role) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ MANAGER │
│ Most permissions except: │
│ • team.invite/remove (owner only) │
│ • Critical settings (owner only) │
│ │
│ Has: products.*, orders.*, customers.*, reports.* │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ STAFF │
│ Day-to-day operations: │
│ • products.view/create/edit │
│ • stock.view/edit │
│ • orders.view/edit │
│ • customers.view │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ SUPPORT │
│ Customer service focus: │
│ • orders.view/edit │
│ • customers.view/edit │
│ • products.view (read-only) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ VIEWER │
│ Read-only access: │
│ • *.view permissions only │
│ • No edit/create/delete │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ MARKETING │
│ Marketing & analytics focus: │
│ • customers.view/export │
│ • marketing.* (all marketing actions) │
│ • reports.view │
└──────────────────────────────────────────────────────────────┘
```
## Security Boundaries
```
❌ BLOCKED ✅ ALLOWED
Admin → Vendor Portal Admin → Admin Portal
Vendor → Admin Portal Vendor → Vendor Portal
Customer → Admin Portal Customer → Shop Catalog
Customer → Vendor Portal Customer → Own Account
Cookie Isolation:
admin_token (path=/admin) ← Only sent to /admin/*
vendor_token (path=/vendor) ← Only sent to /vendor/*
customer_token (path=/shop) ← Only sent to /shop/*
```