Files
orion/docs/backend/admin-integration-guide.md
Samir Boulahtit 35d1559162
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 47m30s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat(monitoring): add Redis exporter + Sentry docs to deployment guide
- Add redis-exporter container to docker-compose (oliver006/redis_exporter, 32MB)
- Add Redis scrape target to Prometheus config
- Add 4 Redis alert rules: RedisDown, HighMemory, HighConnections, RejectedConnections
- Document Step 19b (Sentry Error Tracking) in Hetzner deployment guide
- Document Step 19c (Redis Monitoring) in Hetzner deployment guide
- Update resource budget and port reference tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 23:30:18 +01:00

18 KiB

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:

# 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, stores, users, dashboard, marketplace, audit, settings, notifications

router = APIRouter(prefix="/admin", tags=["admin"])

# Include all admin routers
router.include_router(auth.router)
router.include_router(stores.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_store_with_owner(
        self, db: Session, store_data: StoreCreate
    ) -> Tuple[Store, User, str]:
        """Create store with owner user account."""

        # ... existing code ...

        store, 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_store",
            target_type="store",
            target_id=str(store.id),
            details={
                "store_code": store.store_code,
                "subdomain": store.subdomain,
                "owner_email": owner_user.email
            }
        )

        return store, owner_user, temp_password

    def toggle_store_status(
        self, db: Session, store_id: int, admin_user_id: int
    ) -> Tuple[Store, str]:
        """Toggle store status with audit logging."""

        store = self._get_store_by_id_or_raise(db, store_id)
        old_status = store.is_active

        # ... toggle logic ...

        # LOG THE ACTION
        admin_audit_service.log_action(
            db=db,
            admin_user_id=admin_user_id,
            action="toggle_store_status",
            target_type="store",
            target_id=str(store_id),
            details={
                "old_status": "active" if old_status else "inactive",
                "new_status": "active" if store.is_active else "inactive"
            }
        )

        return store, 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/stores.py

@router.post("", response_model=StoreResponse)
def create_store_with_owner(
    store_data: StoreCreate,
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """Create store with audit logging."""

    store, owner_user, temp_password = admin_service.create_store_with_owner(
        db=db,
        store_data=store_data,
        admin_user_id=current_admin.id  # Pass admin ID for audit logging
    )

    # Audit log is automatically created inside the service

    return {
        **StoreResponse.model_validate(store).model_dump(),
        "owner_email": owner_user.email,
        "owner_username": owner_user.username,
        "temporary_password": temp_password,
        "login_url": f"{store.subdomain}.platform.com/store/login"
    }


@router.put("/{store_id}/status")
def toggle_store_status(
    store_id: int,
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """Toggle store status with audit logging."""
    store, message = admin_service.toggle_store_status(
        db=db,
        store_id=store_id,
        admin_user_id=current_admin.id  # Pass for audit
    )
    return {"message": message, "store": StoreResponse.model_validate(store)}

Step 5: Add Request Context to Audit Logs

To capture IP address and user agent, use FastAPI's Request object:

# app/api/v1/admin/stores.py
from fastapi import Request

@router.delete("/{store_id}")
def delete_store(
    store_id: int,
    request: Request,  # Add request parameter
    confirm: bool = Query(False),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """Delete store 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_store(db, store_id)

    # Log with full context
    admin_audit_service.log_action(
        db=db,
        admin_user_id=current_admin.id,
        action="delete_store",
        target_type="store",
        target_id=str(store_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_stores_allowed",
        value="1000",
        value_type="integer",
        category="system",
        description="Maximum number of stores 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="store_trial_days",
        value="30",
        value_type="integer",
        category="system",
        description="Default trial period for new stores (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/store_service.py
from app.services.admin_settings_service import admin_settings_service

def can_create_store(db: Session) -> bool:
    """Check if platform allows creating more stores."""

    max_stores = admin_settings_service.get_setting_value(
        db=db,
        key="max_stores_allowed",
        default=1000
    )

    current_count = db.query(Store).count()

    return current_count < max_stores


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_store">Create Store</option>
            <option value="delete_store">Delete Store</option>
            <option value="toggle_store_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="store">Stores</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_store",
        target_type="store",
        target_id="123",
        details={"store_code": "TEST"}
    )

    assert log is not None
    assert log.action == "create_store"
    assert log.target_type == "store"
    assert log.details["store_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_stores",
        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_stores")
    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!