18 KiB
Admin Models Integration Guide
What We've Added
You now have:
-
Database Models (
models/database/admin.py):AdminAuditLog- Track all admin actionsAdminNotification- System alerts for adminsAdminSetting- Platform-wide settingsPlatformAlert- System health alertsAdminSession- Track admin login sessions
-
Pydantic Schemas (
models/schemas/admin.py):- Request/response models for all admin operations
- Validation for bulk operations
- System health check schemas
-
Services:
AdminAuditService- Audit logging operationsAdminSettingsService- Platform settings management
-
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:
# models/database/__init__.py
from .admin import (
AdminAuditLog,
AdminNotification,
AdminSetting,
PlatformAlert,
AdminSession
)
Run database migration:
# Create migration
alembic revision --autogenerate -m "Add admin models"
# Apply migration
alembic upgrade head
Step 2: Update Admin API Router
# 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:
# 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:
# 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:
# 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
# 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
# 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
<!-- 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
<!-- 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
# 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
# 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
- Apply database migrations to create new tables
- Update admin API router to include new endpoints
- Add audit logging to existing admin operations
- Create default platform settings using the script
- Build frontend pages for audit logs and settings
- Implement notification service (notifications.py stubs)
- Add monitoring for platform alerts
These additions make your platform production-ready with full compliance and monitoring capabilities!