revamping documentation
This commit is contained in:
1970
docs/api/RBAC.md
1970
docs/api/RBAC.md
File diff suppressed because it is too large
Load Diff
285
docs/api/authentication-flow-diagrams.md
Normal file
285
docs/api/authentication-flow-diagrams.md
Normal 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
|
||||
271
docs/api/authentication-quick-reference.md
Normal file
271
docs/api/authentication-quick-reference.md
Normal 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
316
docs/api/rbac-visual-guide.md
Normal file
316
docs/api/rbac-visual-guide.md
Normal 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/*
|
||||
```
|
||||
Reference in New Issue
Block a user