revamping documentation
This commit is contained in:
1799
docs/backend/admin-feature-integration.md
Normal file
1799
docs/backend/admin-feature-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
649
docs/backend/admin-integration-guide.md
Normal file
649
docs/backend/admin-integration-guide.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# Admin Models Integration Guide
|
||||
|
||||
## What We've Added
|
||||
|
||||
You now have:
|
||||
|
||||
1. **Database Models** (`models/database/admin.py`):
|
||||
- `AdminAuditLog` - Track all admin actions
|
||||
- `AdminNotification` - System alerts for admins
|
||||
- `AdminSetting` - Platform-wide settings
|
||||
- `PlatformAlert` - System health alerts
|
||||
- `AdminSession` - Track admin login sessions
|
||||
|
||||
2. **Pydantic Schemas** (`models/schemas/admin.py`):
|
||||
- Request/response models for all admin operations
|
||||
- Validation for bulk operations
|
||||
- System health check schemas
|
||||
|
||||
3. **Services**:
|
||||
- `AdminAuditService` - Audit logging operations
|
||||
- `AdminSettingsService` - Platform settings management
|
||||
|
||||
4. **API Endpoints**:
|
||||
- `/api/v1/admin/audit` - Audit log endpoints
|
||||
- `/api/v1/admin/settings` - Settings management
|
||||
- `/api/v1/admin/notifications` - Notifications & alerts (stubs)
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Integration
|
||||
|
||||
### Step 1: Update Database
|
||||
|
||||
Add the new models to your database imports:
|
||||
|
||||
```python
|
||||
# models/database/__init__.py
|
||||
from .admin import (
|
||||
AdminAuditLog,
|
||||
AdminNotification,
|
||||
AdminSetting,
|
||||
PlatformAlert,
|
||||
AdminSession
|
||||
)
|
||||
```
|
||||
|
||||
Run database migration:
|
||||
```bash
|
||||
# Create migration
|
||||
alembic revision --autogenerate -m "Add admin models"
|
||||
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### Step 2: Update Admin API Router
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/__init__.py
|
||||
from fastapi import APIRouter
|
||||
from . import auth, vendors, users, dashboard, marketplace, audit, settings, notifications
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
# Include all admin routers
|
||||
router.include_router(auth.router)
|
||||
router.include_router(vendors.router)
|
||||
router.include_router(users.router)
|
||||
router.include_router(dashboard.router)
|
||||
router.include_router(marketplace.router)
|
||||
router.include_router(audit.router) # NEW
|
||||
router.include_router(settings.router) # NEW
|
||||
router.include_router(notifications.router) # NEW
|
||||
```
|
||||
|
||||
### Step 3: Add Audit Logging to Existing Admin Operations
|
||||
|
||||
Update your `admin_service.py` to log actions:
|
||||
|
||||
```python
|
||||
# app/services/admin_service.py
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
|
||||
class AdminService:
|
||||
|
||||
def create_vendor_with_owner(
|
||||
self, db: Session, vendor_data: VendorCreate
|
||||
) -> Tuple[Vendor, User, str]:
|
||||
"""Create vendor with owner user account."""
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
vendor, owner_user, temp_password = # ... your creation logic
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin_id, # You'll need to pass this
|
||||
action="create_vendor",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor.id),
|
||||
details={
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"owner_email": owner_user.email
|
||||
}
|
||||
)
|
||||
|
||||
return vendor, owner_user, temp_password
|
||||
|
||||
def toggle_vendor_status(
|
||||
self, db: Session, vendor_id: int, admin_user_id: int
|
||||
) -> Tuple[Vendor, str]:
|
||||
"""Toggle vendor status with audit logging."""
|
||||
|
||||
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
|
||||
old_status = vendor.is_active
|
||||
|
||||
# ... toggle logic ...
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=admin_user_id,
|
||||
action="toggle_vendor_status",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id),
|
||||
details={
|
||||
"old_status": "active" if old_status else "inactive",
|
||||
"new_status": "active" if vendor.is_active else "inactive"
|
||||
}
|
||||
)
|
||||
|
||||
return vendor, message
|
||||
```
|
||||
|
||||
### Step 4: Update API Endpoints to Pass Admin User ID
|
||||
|
||||
Your API endpoints need to pass the current admin's ID to service methods:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendors.py
|
||||
|
||||
@router.post("", response_model=VendorResponse)
|
||||
def create_vendor_with_owner(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Create vendor with audit logging."""
|
||||
|
||||
vendor, owner_user, temp_password = admin_service.create_vendor_with_owner(
|
||||
db=db,
|
||||
vendor_data=vendor_data,
|
||||
admin_user_id=current_admin.id # Pass admin ID for audit logging
|
||||
)
|
||||
|
||||
# Audit log is automatically created inside the service
|
||||
|
||||
return {
|
||||
**VendorResponse.model_validate(vendor).model_dump(),
|
||||
"owner_email": owner_user.email,
|
||||
"owner_username": owner_user.username,
|
||||
"temporary_password": temp_password,
|
||||
"login_url": f"{vendor.subdomain}.platform.com/vendor/login"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/status")
|
||||
def toggle_vendor_status(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle vendor status with audit logging."""
|
||||
vendor, message = admin_service.toggle_vendor_status(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
admin_user_id=current_admin.id # Pass for audit
|
||||
)
|
||||
return {"message": message, "vendor": VendorResponse.model_validate(vendor)}
|
||||
```
|
||||
|
||||
### Step 5: Add Request Context to Audit Logs
|
||||
|
||||
To capture IP address and user agent, use FastAPI's Request object:
|
||||
|
||||
```python
|
||||
# app/api/v1/admin/vendors.py
|
||||
from fastapi import Request
|
||||
|
||||
@router.delete("/{vendor_id}")
|
||||
def delete_vendor(
|
||||
vendor_id: int,
|
||||
request: Request, # Add request parameter
|
||||
confirm: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Delete vendor with full audit trail."""
|
||||
|
||||
if not confirm:
|
||||
raise HTTPException(status_code=400, detail="Confirmation required")
|
||||
|
||||
# Get request metadata
|
||||
ip_address = request.client.host if request.client else None
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
message = admin_service.delete_vendor(db, vendor_id)
|
||||
|
||||
# Log with full context
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="delete_vendor",
|
||||
target_type="vendor",
|
||||
target_id=str(vendor_id),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
details={"confirm": True}
|
||||
)
|
||||
|
||||
return {"message": message}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: Platform Settings Usage
|
||||
|
||||
### Creating Default Settings
|
||||
|
||||
```python
|
||||
# scripts/init_platform_settings.py
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from models.schemas.admin import AdminSettingCreate
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Create default platform settings
|
||||
settings = [
|
||||
AdminSettingCreate(
|
||||
key="max_vendors_allowed",
|
||||
value="1000",
|
||||
value_type="integer",
|
||||
category="system",
|
||||
description="Maximum number of vendors allowed on the platform",
|
||||
is_public=False
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="maintenance_mode",
|
||||
value="false",
|
||||
value_type="boolean",
|
||||
category="system",
|
||||
description="Enable maintenance mode (blocks all non-admin access)",
|
||||
is_public=True
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="vendor_trial_days",
|
||||
value="30",
|
||||
value_type="integer",
|
||||
category="system",
|
||||
description="Default trial period for new vendors (days)",
|
||||
is_public=False
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="stripe_publishable_key",
|
||||
value="pk_test_...",
|
||||
value_type="string",
|
||||
category="payments",
|
||||
description="Stripe publishable key",
|
||||
is_public=True
|
||||
),
|
||||
AdminSettingCreate(
|
||||
key="stripe_secret_key",
|
||||
value="sk_test_...",
|
||||
value_type="string",
|
||||
category="payments",
|
||||
description="Stripe secret key",
|
||||
is_encrypted=True,
|
||||
is_public=False
|
||||
)
|
||||
]
|
||||
|
||||
for setting_data in settings:
|
||||
try:
|
||||
admin_settings_service.upsert_setting(db, setting_data, admin_user_id=1)
|
||||
print(f"✓ Created setting: {setting_data.key}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to create {setting_data.key}: {e}")
|
||||
|
||||
db.close()
|
||||
```
|
||||
|
||||
### Using Settings in Your Code
|
||||
|
||||
```python
|
||||
# app/services/vendor_service.py
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
|
||||
def can_create_vendor(db: Session) -> bool:
|
||||
"""Check if platform allows creating more vendors."""
|
||||
|
||||
max_vendors = admin_settings_service.get_setting_value(
|
||||
db=db,
|
||||
key="max_vendors_allowed",
|
||||
default=1000
|
||||
)
|
||||
|
||||
current_count = db.query(Vendor).count()
|
||||
|
||||
return current_count < max_vendors
|
||||
|
||||
|
||||
def is_maintenance_mode(db: Session) -> bool:
|
||||
"""Check if platform is in maintenance mode."""
|
||||
return admin_settings_service.get_setting_value(
|
||||
db=db,
|
||||
key="maintenance_mode",
|
||||
default=False
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Admin Dashboard with Audit Logs
|
||||
|
||||
```html
|
||||
<!-- templates/admin/audit_logs.html -->
|
||||
<div x-data="auditLogs()" x-init="loadLogs()">
|
||||
<h1>Audit Logs</h1>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<select x-model="filters.action" @change="loadLogs()">
|
||||
<option value="">All Actions</option>
|
||||
<option value="create_vendor">Create Vendor</option>
|
||||
<option value="delete_vendor">Delete Vendor</option>
|
||||
<option value="toggle_vendor_status">Toggle Status</option>
|
||||
<option value="update_setting">Update Setting</option>
|
||||
</select>
|
||||
|
||||
<select x-model="filters.target_type" @change="loadLogs()">
|
||||
<option value="">All Targets</option>
|
||||
<option value="vendor">Vendors</option>
|
||||
<option value="user">Users</option>
|
||||
<option value="setting">Settings</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Logs Table -->
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Admin</th>
|
||||
<th>Action</th>
|
||||
<th>Target</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="log in logs" :key="log.id">
|
||||
<tr>
|
||||
<td x-text="formatDate(log.created_at)"></td>
|
||||
<td x-text="log.admin_username"></td>
|
||||
<td>
|
||||
<span class="badge" x-text="log.action"></span>
|
||||
</td>
|
||||
<td x-text="`${log.target_type}:${log.target_id}`"></td>
|
||||
<td>
|
||||
<button @click="showDetails(log)">View</button>
|
||||
</td>
|
||||
<td x-text="log.ip_address"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination">
|
||||
<button @click="previousPage()" :disabled="skip === 0">Previous</button>
|
||||
<span x-text="`Page ${currentPage} of ${totalPages}`"></span>
|
||||
<button @click="nextPage()" :disabled="!hasMore">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function auditLogs() {
|
||||
return {
|
||||
logs: [],
|
||||
filters: {
|
||||
action: '',
|
||||
target_type: '',
|
||||
admin_user_id: null
|
||||
},
|
||||
skip: 0,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
|
||||
async loadLogs() {
|
||||
const params = new URLSearchParams({
|
||||
skip: this.skip,
|
||||
limit: this.limit,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
const response = await apiClient.get(`/api/v1/admin/audit/logs?${params}`);
|
||||
this.logs = response.logs;
|
||||
this.total = response.total;
|
||||
},
|
||||
|
||||
showDetails(log) {
|
||||
// Show modal with full details
|
||||
console.log('Details:', log.details);
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
|
||||
get currentPage() {
|
||||
return Math.floor(this.skip / this.limit) + 1;
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.total / this.limit);
|
||||
},
|
||||
|
||||
get hasMore() {
|
||||
return this.skip + this.limit < this.total;
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
this.skip += this.limit;
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
previousPage() {
|
||||
this.skip = Math.max(0, this.skip - this.limit);
|
||||
this.loadLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Platform Settings Management
|
||||
|
||||
```html
|
||||
<!-- templates/admin/settings.html -->
|
||||
<div x-data="platformSettings()" x-init="loadSettings()">
|
||||
<h1>Platform Settings</h1>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
@click="selectedCategory = 'system'"
|
||||
:class="{'active': selectedCategory === 'system'}"
|
||||
>System</button>
|
||||
<button
|
||||
@click="selectedCategory = 'security'"
|
||||
:class="{'active': selectedCategory === 'security'}"
|
||||
>Security</button>
|
||||
<button
|
||||
@click="selectedCategory = 'payments'"
|
||||
:class="{'active': selectedCategory === 'payments'}"
|
||||
>Payments</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings List -->
|
||||
<div class="settings-list">
|
||||
<template x-for="setting in filteredSettings" :key="setting.id">
|
||||
<div class="setting-item">
|
||||
<div class="setting-header">
|
||||
<h3 x-text="setting.key"></h3>
|
||||
<span class="badge" x-text="setting.value_type"></span>
|
||||
</div>
|
||||
<p class="setting-description" x-text="setting.description"></p>
|
||||
|
||||
<div class="setting-value">
|
||||
<input
|
||||
type="text"
|
||||
:value="setting.value"
|
||||
@change="updateSetting(setting.key, $event.target.value)"
|
||||
>
|
||||
<span class="updated-at" x-text="`Updated: ${formatDate(setting.updated_at)}`"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add New Setting -->
|
||||
<button @click="showAddModal = true" class="btn-primary">
|
||||
Add New Setting
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function platformSettings() {
|
||||
return {
|
||||
settings: [],
|
||||
selectedCategory: 'system',
|
||||
showAddModal: false,
|
||||
|
||||
async loadSettings() {
|
||||
const response = await apiClient.get('/api/v1/admin/settings');
|
||||
this.settings = response.settings;
|
||||
},
|
||||
|
||||
get filteredSettings() {
|
||||
if (!this.selectedCategory) return this.settings;
|
||||
return this.settings.filter(s => s.category === this.selectedCategory);
|
||||
},
|
||||
|
||||
async updateSetting(key, newValue) {
|
||||
try {
|
||||
await apiClient.put(`/api/v1/admin/settings/${key}`, {
|
||||
value: newValue
|
||||
});
|
||||
showNotification('Setting updated successfully', 'success');
|
||||
this.loadSettings();
|
||||
} catch (error) {
|
||||
showNotification('Failed to update setting', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the New Features
|
||||
|
||||
### Test Audit Logging
|
||||
|
||||
```python
|
||||
# tests/test_admin_audit.py
|
||||
import pytest
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
|
||||
def test_log_admin_action(db_session, test_admin_user):
|
||||
"""Test logging admin actions."""
|
||||
log = admin_audit_service.log_action(
|
||||
db=db_session,
|
||||
admin_user_id=test_admin_user.id,
|
||||
action="create_vendor",
|
||||
target_type="vendor",
|
||||
target_id="123",
|
||||
details={"vendor_code": "TEST"}
|
||||
)
|
||||
|
||||
assert log is not None
|
||||
assert log.action == "create_vendor"
|
||||
assert log.target_type == "vendor"
|
||||
assert log.details["vendor_code"] == "TEST"
|
||||
|
||||
def test_query_audit_logs(db_session, test_admin_user):
|
||||
"""Test querying audit logs with filters."""
|
||||
# Create test logs
|
||||
for i in range(5):
|
||||
admin_audit_service.log_action(
|
||||
db=db_session,
|
||||
admin_user_id=test_admin_user.id,
|
||||
action=f"test_action_{i}",
|
||||
target_type="test",
|
||||
target_id=str(i)
|
||||
)
|
||||
|
||||
# Query logs
|
||||
from models.schemas.admin import AdminAuditLogFilters
|
||||
filters = AdminAuditLogFilters(limit=10)
|
||||
logs = admin_audit_service.get_audit_logs(db_session, filters)
|
||||
|
||||
assert len(logs) == 5
|
||||
```
|
||||
|
||||
### Test Platform Settings
|
||||
|
||||
```python
|
||||
# tests/test_admin_settings.py
|
||||
def test_create_setting(db_session, test_admin_user):
|
||||
"""Test creating platform setting."""
|
||||
from models.schemas.admin import AdminSettingCreate
|
||||
|
||||
setting_data = AdminSettingCreate(
|
||||
key="test_setting",
|
||||
value="test_value",
|
||||
value_type="string",
|
||||
category="test"
|
||||
)
|
||||
|
||||
result = admin_settings_service.create_setting(
|
||||
db=db_session,
|
||||
setting_data=setting_data,
|
||||
admin_user_id=test_admin_user.id
|
||||
)
|
||||
|
||||
assert result.key == "test_setting"
|
||||
assert result.value == "test_value"
|
||||
|
||||
def test_get_setting_value_with_type_conversion(db_session):
|
||||
"""Test getting setting values with proper type conversion."""
|
||||
# Create integer setting
|
||||
setting_data = AdminSettingCreate(
|
||||
key="max_vendors",
|
||||
value="100",
|
||||
value_type="integer",
|
||||
category="system"
|
||||
)
|
||||
admin_settings_service.create_setting(db_session, setting_data, 1)
|
||||
|
||||
# Get value (should be converted to int)
|
||||
value = admin_settings_service.get_setting_value(db_session, "max_vendors")
|
||||
assert isinstance(value, int)
|
||||
assert value == 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
You now have a complete admin infrastructure with:
|
||||
|
||||
✅ **Audit Logging**: Track all admin actions for compliance
|
||||
✅ **Platform Settings**: Manage global configuration
|
||||
✅ **Notifications**: System alerts for admins (structure ready)
|
||||
✅ **Platform Alerts**: Health monitoring (structure ready)
|
||||
✅ **Session Tracking**: Monitor admin logins (structure ready)
|
||||
|
||||
### Next Steps
|
||||
|
||||
1. **Apply database migrations** to create new tables
|
||||
2. **Update admin API router** to include new endpoints
|
||||
3. **Add audit logging** to existing admin operations
|
||||
4. **Create default platform settings** using the script
|
||||
5. **Build frontend pages** for audit logs and settings
|
||||
6. **Implement notification service** (notifications.py stubs)
|
||||
7. **Add monitoring** for platform alerts
|
||||
|
||||
These additions make your platform production-ready with full compliance and monitoring capabilities!
|
||||
305
docs/backend/middleware-reference.md
Normal file
305
docs/backend/middleware-reference.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# API Reference
|
||||
|
||||
Complete technical reference for all middleware components, utilities, and core classes.
|
||||
|
||||
## Overview
|
||||
|
||||
This reference provides detailed API documentation for all internal modules and classes. All documentation is auto-generated from source code docstrings.
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### AuthManager
|
||||
|
||||
The core authentication manager handling JWT tokens, password hashing, and role-based access control.
|
||||
|
||||
::: middleware.auth.AuthManager
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
show_root_toc_entry: false
|
||||
members:
|
||||
- __init__
|
||||
- hash_password
|
||||
- verify_password
|
||||
- authenticate_user
|
||||
- create_access_token
|
||||
- verify_token
|
||||
- get_current_user
|
||||
- require_role
|
||||
- require_admin
|
||||
- require_vendor
|
||||
- require_customer
|
||||
- create_default_admin_user
|
||||
|
||||
---
|
||||
|
||||
## Multi-Tenant Context Management
|
||||
|
||||
### VendorContextManager
|
||||
|
||||
Detects and manages vendor context from custom domains, subdomains, or path-based routing. This is the foundation of the multi-tenant system.
|
||||
|
||||
**Key Features:**
|
||||
- Custom domain routing (customdomain.com → Vendor)
|
||||
- Subdomain routing (vendor1.platform.com → Vendor)
|
||||
- Path-based routing (/vendor/vendor1/ → Vendor)
|
||||
- Clean path extraction for nested routing
|
||||
|
||||
::: middleware.vendor_context.VendorContextManager
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
### VendorContextMiddleware
|
||||
|
||||
ASGI middleware that wraps VendorContextManager for FastAPI integration.
|
||||
|
||||
::: middleware.vendor_context.VendorContextMiddleware
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
---
|
||||
|
||||
## Request Context Detection
|
||||
|
||||
### RequestContext
|
||||
|
||||
Enum defining all possible request context types in the application.
|
||||
|
||||
::: middleware.context_middleware.RequestContext
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
members:
|
||||
- API
|
||||
- ADMIN
|
||||
- VENDOR_DASHBOARD
|
||||
- SHOP
|
||||
- FALLBACK
|
||||
|
||||
### ContextManager
|
||||
|
||||
Detects the type of request (API, Admin, Vendor Dashboard, Shop) based on URL patterns.
|
||||
|
||||
**Context Detection Rules:**
|
||||
- `/api/` → API context
|
||||
- `/admin/` → Admin context
|
||||
- `/vendor/` → Vendor Dashboard context
|
||||
- `/shop/` → Shop context
|
||||
- Default → Fallback context
|
||||
|
||||
::: middleware.context_middleware.ContextManager
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
### ContextMiddleware
|
||||
|
||||
ASGI middleware for context detection. Must run AFTER VendorContextMiddleware.
|
||||
|
||||
::: middleware.context_middleware.ContextMiddleware
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
---
|
||||
|
||||
## Theme Management
|
||||
|
||||
### ThemeContextManager
|
||||
|
||||
Manages vendor-specific theme configuration and injection into request context.
|
||||
|
||||
::: middleware.theme_context.ThemeContextManager
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
### ThemeContextMiddleware
|
||||
|
||||
ASGI middleware for theme injection. Must run AFTER ContextDetectionMiddleware.
|
||||
|
||||
::: middleware.theme_context.ThemeContextMiddleware
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### RateLimiter
|
||||
|
||||
In-memory rate limiter using a sliding window algorithm for request throttling.
|
||||
|
||||
**Features:**
|
||||
- Sliding window algorithm for accurate rate limiting
|
||||
- Per-client tracking
|
||||
- Automatic cleanup of old entries
|
||||
- Configurable limits and time windows
|
||||
|
||||
::: middleware.rate_limiter.RateLimiter
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
### Rate Limiting Decorator
|
||||
|
||||
Decorator for applying rate limits to FastAPI endpoints.
|
||||
|
||||
::: middleware.decorators.rate_limit
|
||||
options:
|
||||
show_source: true
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
**Usage Example:**
|
||||
```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"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### LoggingMiddleware
|
||||
|
||||
Middleware for request/response logging and performance monitoring.
|
||||
|
||||
**Logged Information:**
|
||||
- Request method, path, and client IP
|
||||
- Response status code
|
||||
- Request processing time
|
||||
- Errors and exceptions
|
||||
|
||||
**Added Headers:**
|
||||
- `X-Process-Time`: Request processing duration in seconds
|
||||
|
||||
::: middleware.logging_middleware.LoggingMiddleware
|
||||
options:
|
||||
show_source: false
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
---
|
||||
|
||||
## Path Rewriting
|
||||
|
||||
### path_rewrite_middleware
|
||||
|
||||
Middleware function that rewrites request paths for path-based vendor routing.
|
||||
|
||||
**Purpose:**
|
||||
Allows `/vendor/VENDORCODE/shop/products` to be internally routed as `/shop/products` for proper FastAPI route matching.
|
||||
|
||||
**Execution Order:**
|
||||
Must run AFTER VendorContextMiddleware and BEFORE ContextDetectionMiddleware.
|
||||
|
||||
::: middleware.path_rewrite_middleware.path_rewrite_middleware
|
||||
options:
|
||||
show_source: true
|
||||
heading_level: 4
|
||||
show_root_heading: false
|
||||
|
||||
---
|
||||
|
||||
## Middleware Execution Order
|
||||
|
||||
The middleware stack must be configured in the correct order for proper functionality:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Request] --> B[LoggingMiddleware]
|
||||
B --> C[VendorContextMiddleware]
|
||||
C --> D[PathRewriteMiddleware]
|
||||
D --> E[ContextMiddleware]
|
||||
E --> F[ThemeContextMiddleware]
|
||||
F --> G[Application Routes]
|
||||
G --> H[Response]
|
||||
```
|
||||
|
||||
**Critical Dependencies:**
|
||||
1. **VendorContextMiddleware** must run first to detect vendor
|
||||
2. **PathRewriteMiddleware** needs `clean_path` from VendorContext
|
||||
3. **ContextMiddleware** needs rewritten path
|
||||
4. **ThemeContextMiddleware** needs context type
|
||||
|
||||
---
|
||||
|
||||
## Request State Variables
|
||||
|
||||
Middleware components inject the following variables into `request.state`:
|
||||
|
||||
| Variable | Set By | Type | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `vendor` | VendorContextMiddleware | Vendor | Current vendor object |
|
||||
| `vendor_id` | VendorContextMiddleware | int | Current vendor ID |
|
||||
| `clean_path` | VendorContextMiddleware | str | Path without vendor prefix |
|
||||
| `context_type` | ContextMiddleware | RequestContext | Request context (API/Admin/Vendor/Shop) |
|
||||
| `theme` | ThemeContextMiddleware | dict | Vendor theme configuration |
|
||||
|
||||
**Usage in Routes:**
|
||||
```python
|
||||
from fastapi import Request
|
||||
|
||||
@app.get("/shop/products")
|
||||
async def get_products(request: Request):
|
||||
vendor = request.state.vendor
|
||||
context = request.state.context_type
|
||||
theme = request.state.theme
|
||||
return {"vendor": vendor.name, "context": context}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Error Handling
|
||||
All middleware should properly handle exceptions and log errors for debugging:
|
||||
|
||||
```python
|
||||
try:
|
||||
# Middleware logic
|
||||
except Exception as e:
|
||||
logger.error(f"Middleware error: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
### Performance
|
||||
- Keep middleware logic minimal and fast
|
||||
- Use async/await properly for non-blocking operations
|
||||
- Log performance metrics for monitoring
|
||||
|
||||
### Testing
|
||||
- Test middleware in isolation
|
||||
- Mock request.state for unit tests
|
||||
- Test middleware execution order
|
||||
- Verify error handling paths
|
||||
|
||||
For testing examples, see the [Testing Guide](../testing/testing-guide.md).
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Authentication Guide](../api/authentication.md) - User authentication and JWT tokens
|
||||
- [RBAC Documentation](../api/RBAC.md) - Role-based access control
|
||||
- [Error Handling](../api/error-handling.md) - Exception handling patterns
|
||||
- [Rate Limiting](../api/rate-limiting.md) - API rate limiting strategies
|
||||
484
docs/backend/overview.md
Normal file
484
docs/backend/overview.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Backend Development
|
||||
|
||||
Guide for developing backend features, services, and API endpoints.
|
||||
|
||||
## Overview
|
||||
|
||||
The Wizamart backend is built with FastAPI and follows a service-oriented architecture pattern. This guide covers backend development practices, patterns, and technical references.
|
||||
|
||||
## Backend Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/ # API routes (REST endpoints)
|
||||
│ ├── v1/ # Version 1 API
|
||||
│ │ ├── admin/ # Admin API endpoints
|
||||
│ │ ├── vendor/ # Vendor API endpoints
|
||||
│ │ └── shop/ # Shop API endpoints
|
||||
│ └── main.py # API router configuration
|
||||
│
|
||||
├── routes/ # Page routes (HTML)
|
||||
│ ├── admin_pages.py # Admin page routes
|
||||
│ ├── vendor_pages.py # Vendor page routes
|
||||
│ └── shop_pages.py # Shop page routes
|
||||
│
|
||||
├── services/ # Business logic layer
|
||||
│ ├── admin_service.py
|
||||
│ ├── product_service.py
|
||||
│ ├── order_service.py
|
||||
│ └── ...
|
||||
│
|
||||
├── core/ # Core functionality
|
||||
│ ├── database.py # Database connection
|
||||
│ ├── config.py # Configuration
|
||||
│ └── lifespan.py # Application lifecycle
|
||||
│
|
||||
└── exceptions/ # Custom exceptions
|
||||
├── base.py
|
||||
└── handler.py
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Creating a New API Endpoint
|
||||
|
||||
**Steps**:
|
||||
1. Define Pydantic schema in `models/schema/`
|
||||
2. Create service method in `app/services/`
|
||||
3. Add API route in `app/api/v1/`
|
||||
4. Write tests in `tests/`
|
||||
|
||||
**Example** - Adding a new product endpoint:
|
||||
|
||||
```python
|
||||
# Step 1: Schema (models/schema/product.py)
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
price: float
|
||||
vendor_id: int
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
price: float
|
||||
vendor_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Step 2: Service (app/services/product_service.py)
|
||||
from sqlalchemy.orm import Session
|
||||
from models.database.product import Product
|
||||
|
||||
class ProductService:
|
||||
def create_product(self, db: Session, data: ProductCreate) -> Product:
|
||||
product = Product(**data.dict())
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
return product
|
||||
|
||||
product_service = ProductService()
|
||||
|
||||
# Step 3: API Route (app/api/v1/shop/products.py)
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.services.product_service import product_service
|
||||
from models.schema.product import ProductCreate, ProductResponse
|
||||
|
||||
router = APIRouter(prefix="/products")
|
||||
|
||||
@router.post("/", response_model=ProductResponse)
|
||||
async def create_product(
|
||||
data: ProductCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return product_service.create_product(db, data)
|
||||
```
|
||||
|
||||
### 2. Creating a Service
|
||||
|
||||
Services contain business logic and should be reusable across routes.
|
||||
|
||||
**Pattern**:
|
||||
```python
|
||||
# app/services/example_service.py
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ExampleService:
|
||||
"""Service for handling example operations."""
|
||||
|
||||
def get_items(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[Item]:
|
||||
"""Get items for a vendor with pagination."""
|
||||
try:
|
||||
items = db.query(Item).filter(
|
||||
Item.vendor_id == vendor_id
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
logger.info(f"Retrieved {len(items)} items for vendor {vendor_id}")
|
||||
return items
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving items: {e}")
|
||||
raise
|
||||
|
||||
def create_item(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
data: ItemCreate
|
||||
) -> Item:
|
||||
"""Create a new item."""
|
||||
try:
|
||||
item = Item(
|
||||
vendor_id=vendor_id,
|
||||
**data.dict()
|
||||
)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
|
||||
logger.info(f"Created item {item.id} for vendor {vendor_id}")
|
||||
return item
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating item: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
# Singleton instance
|
||||
example_service = ExampleService()
|
||||
```
|
||||
|
||||
### 3. Database Operations
|
||||
|
||||
**Best Practices**:
|
||||
|
||||
```python
|
||||
# ✅ Good - Use service layer
|
||||
@router.get("/products")
|
||||
async def get_products(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
products = product_service.get_products(db, vendor_id)
|
||||
return {"products": products}
|
||||
|
||||
# ❌ Bad - Database queries in route handler
|
||||
@router.get("/products")
|
||||
async def get_products(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id
|
||||
).all()
|
||||
return {"products": products}
|
||||
```
|
||||
|
||||
**Always Scope to Vendor**:
|
||||
```python
|
||||
# ✅ Good - Scoped to vendor
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == request.state.vendor_id
|
||||
).all()
|
||||
|
||||
# ❌ Bad - Not scoped, security risk!
|
||||
products = db.query(Product).all()
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
FastAPI's dependency system provides clean code organization:
|
||||
|
||||
```python
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from middleware.auth import auth_manager
|
||||
from models.database.user import User
|
||||
|
||||
@router.get("/protected")
|
||||
async def protected_endpoint(
|
||||
current_user: User = Depends(auth_manager.get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# current_user is automatically injected
|
||||
# db session is automatically injected
|
||||
return {"user": current_user.username}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Custom Exceptions**:
|
||||
```python
|
||||
from app.exceptions import (
|
||||
ResourceNotFoundException,
|
||||
ValidationException,
|
||||
InsufficientPermissionsException
|
||||
)
|
||||
|
||||
@router.get("/products/{product_id}")
|
||||
async def get_product(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
|
||||
if not product:
|
||||
raise ResourceNotFoundException(f"Product {product_id} not found")
|
||||
|
||||
return product
|
||||
```
|
||||
|
||||
**Exception Handlers** are configured globally in `app/exceptions/handler.py`
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```python
|
||||
# tests/unit/services/test_product_service.py
|
||||
import pytest
|
||||
from app.services.product_service import ProductService
|
||||
|
||||
def test_create_product(db_session):
|
||||
service = ProductService()
|
||||
data = ProductCreate(
|
||||
name="Test Product",
|
||||
price=29.99,
|
||||
vendor_id=1
|
||||
)
|
||||
|
||||
product = service.create_product(db_session, data)
|
||||
|
||||
assert product.id is not None
|
||||
assert product.name == "Test Product"
|
||||
assert product.vendor_id == 1
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
# tests/integration/test_product_api.py
|
||||
def test_create_product_endpoint(client, auth_headers):
|
||||
response = client.post(
|
||||
"/api/v1/products",
|
||||
json={
|
||||
"name": "Test Product",
|
||||
"price": 29.99,
|
||||
"vendor_id": 1
|
||||
},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Product"
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Run linters
|
||||
make lint
|
||||
|
||||
# Auto-fix formatting
|
||||
make format
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
# Run mypy
|
||||
make lint
|
||||
```
|
||||
|
||||
**Use Type Hints**:
|
||||
```python
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def get_items(
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
limit: Optional[int] = None
|
||||
) -> List[Item]:
|
||||
query = db.query(Item).filter(Item.vendor_id == vendor_id)
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query.all()
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
**Creating a Migration**:
|
||||
```bash
|
||||
make migrate-create message="add_product_table"
|
||||
```
|
||||
|
||||
**Applying Migrations**:
|
||||
```bash
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
**See**: [Database Migrations](../development/database-migrations.md)
|
||||
|
||||
## API Documentation
|
||||
|
||||
FastAPI automatically generates API documentation:
|
||||
|
||||
- **Swagger UI**: http://localhost:8000/docs
|
||||
- **ReDoc**: http://localhost:8000/redoc
|
||||
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
||||
|
||||
**Document your endpoints**:
|
||||
```python
|
||||
@router.post(
|
||||
"/products",
|
||||
response_model=ProductResponse,
|
||||
summary="Create a new product",
|
||||
description="Creates a new product for the authenticated vendor",
|
||||
responses={
|
||||
201: {"description": "Product created successfully"},
|
||||
400: {"description": "Invalid product data"},
|
||||
401: {"description": "Not authenticated"},
|
||||
403: {"description": "Not authorized to create products"}
|
||||
}
|
||||
)
|
||||
async def create_product(
|
||||
data: ProductCreate,
|
||||
current_user: User = Depends(auth_manager.require_vendor),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new product.
|
||||
|
||||
- **name**: Product name (required)
|
||||
- **description**: Product description
|
||||
- **price**: Product price (required)
|
||||
- **vendor_id**: Vendor ID (required)
|
||||
"""
|
||||
return product_service.create_product(db, current_user.id, data)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment Variables** (`.env`):
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY=your-secret-key
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
|
||||
# Application
|
||||
ENVIRONMENT=development
|
||||
DEBUG=true
|
||||
LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
**Accessing Config**:
|
||||
```python
|
||||
from app.core.config import settings
|
||||
|
||||
database_url = settings.database_url
|
||||
secret_key = settings.jwt_secret_key
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
**Use Structured Logging**:
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def process_order(order_id: int):
|
||||
logger.info(f"Processing order", extra={"order_id": order_id})
|
||||
|
||||
try:
|
||||
# Process order
|
||||
logger.info(f"Order processed successfully", extra={"order_id": order_id})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Order processing failed",
|
||||
extra={"order_id": order_id, "error": str(e)},
|
||||
exc_info=True
|
||||
)
|
||||
raise
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Database Query Optimization
|
||||
|
||||
```python
|
||||
# ✅ Good - Use joins to avoid N+1 queries
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
products = db.query(Product).options(
|
||||
joinedload(Product.category),
|
||||
joinedload(Product.vendor)
|
||||
).filter(Product.vendor_id == vendor_id).all()
|
||||
|
||||
# ❌ Bad - N+1 query problem
|
||||
products = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id
|
||||
).all()
|
||||
|
||||
for product in products:
|
||||
category = product.category # Additional query!
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
```python
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=100)
|
||||
def get_vendor_settings(vendor_id: int) -> dict:
|
||||
# Expensive operation cached in memory
|
||||
return db.query(VendorSettings).filter(
|
||||
VendorSettings.vendor_id == vendor_id
|
||||
).first()
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Always validate input** with Pydantic schemas
|
||||
2. **Always scope queries** to vendor/user
|
||||
3. **Use parameterized queries** (SQLAlchemy ORM does this)
|
||||
4. **Never log sensitive data** (passwords, tokens, credit cards)
|
||||
5. **Use HTTPS** in production
|
||||
6. **Implement rate limiting** on sensitive endpoints
|
||||
7. **Validate file uploads** (type, size, content)
|
||||
8. **Sanitize user input** before rendering in templates
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Middleware Reference](middleware-reference.md) - Technical API documentation
|
||||
- [Architecture Overview](../architecture/overview.md) - System architecture
|
||||
- [Database Migrations](../development/database-migrations.md) - Migration guide
|
||||
- [Testing Guide](../testing/testing-guide.md) - Testing practices
|
||||
513
docs/backend/rbac-quick-reference.md
Normal file
513
docs/backend/rbac-quick-reference.md
Normal file
@@ -0,0 +1,513 @@
|
||||
# RBAC Quick Reference Card
|
||||
|
||||
**For Daily Development** | Keep this handy while coding
|
||||
|
||||
---
|
||||
|
||||
## Common Imports
|
||||
|
||||
```python
|
||||
# Authentication dependencies
|
||||
from app.api.deps import (
|
||||
get_current_admin_from_cookie_or_header,
|
||||
get_current_vendor_from_cookie_or_header,
|
||||
require_vendor_permission,
|
||||
require_vendor_owner,
|
||||
get_user_permissions
|
||||
)
|
||||
|
||||
# Permission constants
|
||||
from app.core.permissions import VendorPermissions
|
||||
|
||||
# Exceptions
|
||||
from app.exceptions import (
|
||||
InsufficientVendorPermissionsException,
|
||||
VendorOwnerOnlyException
|
||||
)
|
||||
|
||||
# Services
|
||||
from app.services.vendor_team_service import vendor_team_service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Patterns
|
||||
|
||||
### Admin Route (Cookie OR Header)
|
||||
```python
|
||||
@router.get("/admin/vendors")
|
||||
def list_vendors(
|
||||
user: User = Depends(get_current_admin_from_cookie_or_header)
|
||||
):
|
||||
# user is authenticated admin
|
||||
...
|
||||
```
|
||||
|
||||
### Admin API (Header Only)
|
||||
```python
|
||||
@router.post("/api/v1/admin/vendors")
|
||||
def create_vendor(
|
||||
user: User = Depends(get_current_admin_api)
|
||||
):
|
||||
# user is authenticated admin (header required)
|
||||
...
|
||||
```
|
||||
|
||||
### Vendor Route with Permission
|
||||
```python
|
||||
@router.post("/vendor/{code}/products")
|
||||
def create_product(
|
||||
user: User = Depends(require_vendor_permission(
|
||||
VendorPermissions.PRODUCTS_CREATE.value
|
||||
))
|
||||
):
|
||||
# user has products.create permission
|
||||
vendor = request.state.vendor
|
||||
...
|
||||
```
|
||||
|
||||
### Owner-Only Route
|
||||
```python
|
||||
@router.post("/vendor/{code}/team/invite")
|
||||
def invite_member(
|
||||
user: User = Depends(require_vendor_owner)
|
||||
):
|
||||
# user is vendor owner
|
||||
vendor = request.state.vendor
|
||||
...
|
||||
```
|
||||
|
||||
### Multi-Permission Route
|
||||
```python
|
||||
@router.post("/vendor/{code}/products/bulk")
|
||||
def bulk_operation(
|
||||
user: User = Depends(require_all_vendor_permissions(
|
||||
VendorPermissions.PRODUCTS_VIEW.value,
|
||||
VendorPermissions.PRODUCTS_EDIT.value
|
||||
))
|
||||
):
|
||||
# user has ALL specified permissions
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Constants
|
||||
|
||||
### Quick Lookup
|
||||
|
||||
```python
|
||||
# Dashboard
|
||||
VendorPermissions.DASHBOARD_VIEW
|
||||
|
||||
# Products
|
||||
VendorPermissions.PRODUCTS_VIEW
|
||||
VendorPermissions.PRODUCTS_CREATE
|
||||
VendorPermissions.PRODUCTS_EDIT
|
||||
VendorPermissions.PRODUCTS_DELETE
|
||||
VendorPermissions.PRODUCTS_IMPORT
|
||||
VendorPermissions.PRODUCTS_EXPORT
|
||||
|
||||
# Stock
|
||||
VendorPermissions.STOCK_VIEW
|
||||
VendorPermissions.STOCK_EDIT
|
||||
VendorPermissions.STOCK_TRANSFER
|
||||
|
||||
# Orders
|
||||
VendorPermissions.ORDERS_VIEW
|
||||
VendorPermissions.ORDERS_EDIT
|
||||
VendorPermissions.ORDERS_CANCEL
|
||||
VendorPermissions.ORDERS_REFUND
|
||||
|
||||
# Customers
|
||||
VendorPermissions.CUSTOMERS_VIEW
|
||||
VendorPermissions.CUSTOMERS_EDIT
|
||||
VendorPermissions.CUSTOMERS_DELETE
|
||||
VendorPermissions.CUSTOMERS_EXPORT
|
||||
|
||||
# Marketing
|
||||
VendorPermissions.MARKETING_VIEW
|
||||
VendorPermissions.MARKETING_CREATE
|
||||
VendorPermissions.MARKETING_SEND
|
||||
|
||||
# Reports
|
||||
VendorPermissions.REPORTS_VIEW
|
||||
VendorPermissions.REPORTS_FINANCIAL
|
||||
VendorPermissions.REPORTS_EXPORT
|
||||
|
||||
# Settings
|
||||
VendorPermissions.SETTINGS_VIEW
|
||||
VendorPermissions.SETTINGS_EDIT
|
||||
VendorPermissions.SETTINGS_THEME
|
||||
VendorPermissions.SETTINGS_DOMAINS
|
||||
|
||||
# Team
|
||||
VendorPermissions.TEAM_VIEW
|
||||
VendorPermissions.TEAM_INVITE
|
||||
VendorPermissions.TEAM_EDIT
|
||||
VendorPermissions.TEAM_REMOVE
|
||||
|
||||
# Imports
|
||||
VendorPermissions.IMPORTS_VIEW
|
||||
VendorPermissions.IMPORTS_CREATE
|
||||
VendorPermissions.IMPORTS_CANCEL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Helper Methods
|
||||
|
||||
```python
|
||||
# Check if admin
|
||||
user.is_admin # bool
|
||||
|
||||
# Check if vendor
|
||||
user.is_vendor # bool
|
||||
|
||||
# Check vendor ownership
|
||||
user.is_owner_of(vendor_id) # bool
|
||||
|
||||
# Check vendor membership
|
||||
user.is_member_of(vendor_id) # bool
|
||||
|
||||
# Get role in vendor
|
||||
user.get_vendor_role(vendor_id) # str: "owner" | "member" | None
|
||||
|
||||
# Check specific permission
|
||||
user.has_vendor_permission(vendor_id, "products.create") # bool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VendorUser Helper Methods
|
||||
|
||||
```python
|
||||
# Check if owner
|
||||
vendor_user.is_owner # bool
|
||||
|
||||
# Check if team member
|
||||
vendor_user.is_team_member # bool
|
||||
|
||||
# Check invitation status
|
||||
vendor_user.is_invitation_pending # bool
|
||||
|
||||
# Check permission
|
||||
vendor_user.has_permission("products.create") # bool
|
||||
|
||||
# Get all permissions
|
||||
vendor_user.get_all_permissions() # list[str]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Methods
|
||||
|
||||
### Team Management
|
||||
|
||||
```python
|
||||
# Invite team member
|
||||
vendor_team_service.invite_team_member(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
inviter=current_user,
|
||||
email="member@example.com",
|
||||
role_name="Staff",
|
||||
custom_permissions=None # Optional
|
||||
)
|
||||
|
||||
# Accept invitation
|
||||
vendor_team_service.accept_invitation(
|
||||
db=db,
|
||||
invitation_token=token,
|
||||
password="password123",
|
||||
first_name="John",
|
||||
last_name="Doe"
|
||||
)
|
||||
|
||||
# Remove team member
|
||||
vendor_team_service.remove_team_member(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
user_id=member_id
|
||||
)
|
||||
|
||||
# Update member role
|
||||
vendor_team_service.update_member_role(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
user_id=member_id,
|
||||
new_role_name="Manager",
|
||||
custom_permissions=None
|
||||
)
|
||||
|
||||
# Get team members
|
||||
members = vendor_team_service.get_team_members(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
include_inactive=False
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exception Handling
|
||||
|
||||
```python
|
||||
from app.exceptions import (
|
||||
InsufficientVendorPermissionsException,
|
||||
VendorOwnerOnlyException,
|
||||
VendorAccessDeniedException,
|
||||
InvalidInvitationTokenException,
|
||||
CannotRemoveVendorOwnerException,
|
||||
TeamMemberAlreadyExistsException
|
||||
)
|
||||
|
||||
# Raise permission error
|
||||
raise InsufficientVendorPermissionsException(
|
||||
required_permission="products.create",
|
||||
vendor_code=vendor.vendor_code
|
||||
)
|
||||
|
||||
# Raise owner-only error
|
||||
raise VendorOwnerOnlyException(
|
||||
operation="team management",
|
||||
vendor_code=vendor.vendor_code
|
||||
)
|
||||
|
||||
# Raise access denied
|
||||
raise VendorAccessDeniedException(
|
||||
vendor_code=vendor.vendor_code,
|
||||
user_id=user.id
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Permission Checks
|
||||
|
||||
### JavaScript/Alpine.js
|
||||
|
||||
```javascript
|
||||
// Check permission
|
||||
function hasPermission(permission) {
|
||||
const permissions = JSON.parse(
|
||||
localStorage.getItem('permissions') || '[]'
|
||||
);
|
||||
return permissions.includes(permission);
|
||||
}
|
||||
|
||||
// Conditional rendering
|
||||
{hasPermission('products.create') && (
|
||||
<CreateButton />
|
||||
)}
|
||||
|
||||
// Disable button
|
||||
<button disabled={!hasPermission('products.edit')}>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
// Get permissions on login
|
||||
async function getPermissions() {
|
||||
const response = await fetch(
|
||||
'/api/v1/vendor/team/me/permissions',
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
localStorage.setItem(
|
||||
'permissions',
|
||||
JSON.stringify(data.permissions)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Test
|
||||
```python
|
||||
def test_owner_has_all_permissions():
|
||||
vendor_user = create_vendor_user(user_type="owner")
|
||||
assert vendor_user.has_permission("products.create")
|
||||
assert vendor_user.has_permission("team.invite")
|
||||
```
|
||||
|
||||
### Integration Test
|
||||
```python
|
||||
def test_create_product_with_permission(client):
|
||||
user = create_user_with_permission("products.create")
|
||||
token = create_token(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/vendor/ACME/products",
|
||||
json={"name": "Test"},
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### ❌ DON'T: Check permissions in service layer
|
||||
```python
|
||||
# BAD
|
||||
def create_product(user, data):
|
||||
if not user.has_permission("products.create"):
|
||||
raise Exception()
|
||||
```
|
||||
|
||||
### ✅ DO: Check permissions at route level
|
||||
```python
|
||||
# GOOD
|
||||
@router.post("/products")
|
||||
def create_product(
|
||||
user: User = Depends(require_vendor_permission("products.create"))
|
||||
):
|
||||
return service.create_product(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ DON'T: Use magic strings
|
||||
```python
|
||||
# BAD
|
||||
require_vendor_permission("products.creat") # Typo!
|
||||
```
|
||||
|
||||
### ✅ DO: Use constants
|
||||
```python
|
||||
# GOOD
|
||||
require_vendor_permission(VendorPermissions.PRODUCTS_CREATE.value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ DON'T: Mix contexts
|
||||
```python
|
||||
# BAD - Admin trying to access vendor route
|
||||
# This will be blocked automatically
|
||||
```
|
||||
|
||||
### ✅ DO: Use correct portal
|
||||
```python
|
||||
# GOOD - Admins use /admin/*, vendors use /vendor/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Commands
|
||||
|
||||
### Check User Access
|
||||
```python
|
||||
user = db.query(User).get(user_id)
|
||||
vendor = db.query(Vendor).get(vendor_id)
|
||||
|
||||
print(f"Is owner: {user.is_owner_of(vendor.id)}")
|
||||
print(f"Is member: {user.is_member_of(vendor.id)}")
|
||||
print(f"Role: {user.get_vendor_role(vendor.id)}")
|
||||
print(f"Has products.create: {user.has_vendor_permission(vendor.id, 'products.create')}")
|
||||
```
|
||||
|
||||
### Decode JWT Token
|
||||
```python
|
||||
import jwt
|
||||
|
||||
token = "eyJ0eXAi..."
|
||||
decoded = jwt.decode(token, verify=False)
|
||||
print(f"User ID: {decoded['sub']}")
|
||||
print(f"Username: {decoded['username']}")
|
||||
print(f"Role: {decoded['role']}")
|
||||
print(f"Expires: {decoded['exp']}")
|
||||
```
|
||||
|
||||
### Check Cookie
|
||||
```javascript
|
||||
// In browser console
|
||||
document.cookie.split(';').forEach(c => console.log(c.trim()));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role Presets
|
||||
|
||||
| Role | Typical Permissions |
|
||||
|------|---------------------|
|
||||
| **Owner** | ALL (automatic) |
|
||||
| **Manager** | Most operations, no team management |
|
||||
| **Staff** | Products, orders, customers (CRUD) |
|
||||
| **Support** | Orders, customers (support focus) |
|
||||
| **Viewer** | Read-only access |
|
||||
| **Marketing** | Customers, marketing, reports |
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/
|
||||
│ ├── deps.py ← All auth dependencies
|
||||
│ └── v1/
|
||||
│ ├── admin/
|
||||
│ │ └── auth.py ← Admin login
|
||||
│ ├── vendor/
|
||||
│ │ ├── auth.py ← Vendor login
|
||||
│ │ └── team.py ← Team management
|
||||
│ └── public/
|
||||
│ └── vendors/auth.py ← Customer login
|
||||
│
|
||||
├── core/
|
||||
│ └── permissions.py ← Permission constants
|
||||
│
|
||||
├── exceptions/
|
||||
│ ├── admin.py
|
||||
│ ├── vendor.py
|
||||
│ └── auth.py
|
||||
│
|
||||
├── services/
|
||||
│ ├── auth_service.py
|
||||
│ └── vendor_team_service.py ← Team management
|
||||
│
|
||||
└── models/
|
||||
└── database/
|
||||
├── user.py ← User model
|
||||
├── vendor.py ← Vendor, VendorUser, Role
|
||||
└── customer.py ← Customer model
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status Codes
|
||||
|
||||
| Code | Meaning | Common Cause |
|
||||
|------|---------|--------------|
|
||||
| 200 | OK | Success |
|
||||
| 201 | Created | Resource created |
|
||||
| 401 | Unauthorized | No/invalid token |
|
||||
| 403 | Forbidden | No permission |
|
||||
| 404 | Not Found | Resource not found |
|
||||
| 422 | Validation Error | Invalid input |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
JWT_SECRET_KEY=your-secret-key
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION=3600 # seconds (1 hour)
|
||||
ENVIRONMENT=development|staging|production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Print and keep at your desk!**
|
||||
|
||||
For full documentation: See [RBAC Developer Guide](../api/RBAC.md)
|
||||
Reference in New Issue
Block a user