Multitenant implementation with custom Domain, theme per vendor

This commit is contained in:
2025-10-26 23:49:29 +01:00
parent c88775134d
commit 1e0cbf5927
24 changed files with 3470 additions and 624 deletions

View File

@@ -6,6 +6,7 @@ This module combines all admin-related API endpoints:
- Authentication (login/logout)
- Vendor management (CRUD, bulk operations)
- Vendor domains management (custom domains, DNS verification)
- Vendor themes management (theme editor, presets)
- User management (status, roles)
- Dashboard and statistics
- Marketplace monitoring
@@ -22,6 +23,7 @@ from . import (
auth,
vendors,
vendor_domains,
vendor_themes,
users,
dashboard,
marketplace,
@@ -54,6 +56,9 @@ router.include_router(vendors.router, tags=["admin-vendors"])
# Include vendor domains management endpoints
router.include_router(vendor_domains.router, tags=["admin-vendor-domains"])
# Include vendor themes management endpoints
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
# ============================================================================
# User Management

View File

@@ -17,6 +17,7 @@ Routes:
- GET /vendors/{vendor_code} → Vendor details (auth required)
- GET /vendors/{vendor_code}/edit → Edit vendor form (auth required)
- GET /vendors/{vendor_code}/domains → Vendor domains management (auth required)
- GET /vendors/{vendor_code}/theme → Vendor theme editor (auth required)
- GET /users → User management page (auth required)
- GET /imports → Import history page (auth required)
- GET /settings → Settings page (auth required)
@@ -205,6 +206,7 @@ async def admin_vendor_theme_page(
):
"""
Render vendor theme customization page.
Allows admins to customize colors, fonts, layout, and branding.
"""
return templates.TemplateResponse(
"admin/vendor-theme.html",

View File

@@ -0,0 +1,326 @@
# app/api/v1/admin/vendor_themes.py
"""
Vendor theme management endpoints for admin.
These endpoints allow admins to:
- View vendor themes
- Apply theme presets
- Customize theme settings
- Reset themes to default
All operations use the service layer for business logic.
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Path, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user, get_db
from app.services.vendor_theme_service import vendor_theme_service
from app.exceptions.vendor import VendorNotFoundException
from app.exceptions.vendor_theme import (
VendorThemeNotFoundException,
ThemePresetNotFoundException,
ThemeValidationException,
ThemeOperationException,
InvalidColorFormatException,
InvalidFontFamilyException
)
from models.database.user import User
from models.schema.vendor_theme import (
VendorThemeResponse,
VendorThemeUpdate,
ThemePresetResponse,
ThemePresetListResponse
)
router = APIRouter(prefix="/vendor-themes")
logger = logging.getLogger(__name__)
# ============================================================================
# PRESET ENDPOINTS
# ============================================================================
@router.get("/presets", response_model=ThemePresetListResponse)
async def get_theme_presets(
current_admin: User = Depends(get_current_admin_user)
):
"""
Get all available theme presets with preview information.
Returns list of presets that can be applied to vendor themes.
Each preset includes color palette, fonts, and layout configuration.
**Permissions:** Admin only
**Returns:**
- List of available theme presets with preview data
"""
logger.info("Getting theme presets")
try:
presets = vendor_theme_service.get_available_presets()
return {"presets": presets}
except Exception as e:
logger.error(f"Failed to get theme presets: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve theme presets"
)
# ============================================================================
# THEME RETRIEVAL
# ============================================================================
@router.get("/{vendor_code}", response_model=VendorThemeResponse)
async def get_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
):
"""
Get theme configuration for a vendor.
Returns the vendor's custom theme if exists, otherwise returns default theme.
**Path Parameters:**
- `vendor_code`: Vendor code (e.g., VENDOR001)
**Permissions:** Admin only
**Returns:**
- Complete theme configuration including colors, fonts, layout, and branding
**Errors:**
- `404`: Vendor not found
- `500`: Internal server error
"""
logger.info(f"Getting theme for vendor: {vendor_code}")
try:
theme = vendor_theme_service.get_theme(db, vendor_code)
return theme
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except Exception as e:
logger.error(f"Failed to get theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve theme"
)
# ============================================================================
# THEME UPDATE
# ============================================================================
@router.put("/{vendor_code}", response_model=VendorThemeResponse)
async def update_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"),
theme_data: VendorThemeUpdate = None,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
):
"""
Update or create theme for a vendor.
Accepts partial updates - only provided fields are updated.
If vendor has no theme, a new one is created.
**Path Parameters:**
- `vendor_code`: Vendor code (e.g., VENDOR001)
**Request Body:**
- `theme_name`: Optional theme name
- `colors`: Optional color palette (primary, secondary, accent, etc.)
- `fonts`: Optional font settings (heading, body)
- `layout`: Optional layout settings (style, header, product_card)
- `branding`: Optional branding assets (logo, favicon, etc.)
- `custom_css`: Optional custom CSS rules
- `social_links`: Optional social media links
**Permissions:** Admin only
**Returns:**
- Updated theme configuration
**Errors:**
- `404`: Vendor not found
- `422`: Validation error (invalid colors, fonts, or layout values)
- `500`: Internal server error
"""
logger.info(f"Updating theme for vendor: {vendor_code}")
try:
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
return theme.to_dict()
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except (ThemeValidationException, InvalidColorFormatException, InvalidFontFamilyException) as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=e.message
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to update theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update theme"
)
# ============================================================================
# PRESET APPLICATION
# ============================================================================
@router.post("/{vendor_code}/preset/{preset_name}")
async def apply_theme_preset(
vendor_code: str = Path(..., description="Vendor code"),
preset_name: str = Path(..., description="Preset name"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
):
"""
Apply a theme preset to a vendor.
Replaces the vendor's current theme with the selected preset.
Available presets can be retrieved from the `/presets` endpoint.
**Path Parameters:**
- `vendor_code`: Vendor code (e.g., VENDOR001)
- `preset_name`: Name of preset to apply (e.g., 'modern', 'classic')
**Available Presets:**
- `default`: Clean and professional
- `modern`: Contemporary tech-inspired
- `classic`: Traditional and trustworthy
- `minimal`: Ultra-clean black and white
- `vibrant`: Bold and energetic
- `elegant`: Sophisticated gray tones
- `nature`: Fresh and eco-friendly
**Permissions:** Admin only
**Returns:**
- Success message and applied theme configuration
**Errors:**
- `404`: Vendor or preset not found
- `500`: Internal server error
"""
logger.info(f"Applying preset '{preset_name}' to vendor {vendor_code}")
try:
theme = vendor_theme_service.apply_theme_preset(db, vendor_code, preset_name)
return {
"message": f"Applied {preset_name} preset successfully",
"theme": theme.to_dict()
}
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except ThemePresetNotFoundException as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to apply preset to vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to apply preset"
)
# ============================================================================
# THEME DELETION
# ============================================================================
@router.delete("/{vendor_code}")
async def delete_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user)
):
"""
Delete custom theme for a vendor.
Removes the vendor's custom theme. After deletion, the vendor
will revert to using the default platform theme.
**Path Parameters:**
- `vendor_code`: Vendor code (e.g., VENDOR001)
**Permissions:** Admin only
**Returns:**
- Success message
**Errors:**
- `404`: Vendor not found or vendor has no custom theme
- `500`: Internal server error
"""
logger.info(f"Deleting theme for vendor: {vendor_code}")
try:
result = vendor_theme_service.delete_theme(db, vendor_code)
return result
except VendorNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' not found"
)
except VendorThemeNotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Vendor '{vendor_code}' has no custom theme"
)
except ThemeOperationException as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e.message
)
except Exception as e:
logger.error(f"Failed to delete theme for vendor {vendor_code}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete theme"
)

View File

@@ -113,7 +113,6 @@ def create_vendor_with_owner(
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
theme_config=vendor.theme_config or {},
is_active=vendor.is_active,
is_verified=vendor.is_verified,
created_at=vendor.created_at,
@@ -197,7 +196,6 @@ def get_vendor_details(
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
theme_config=vendor.theme_config or {},
is_active=vendor.is_active,
is_verified=vendor.is_verified,
created_at=vendor.created_at,
@@ -250,7 +248,6 @@ def update_vendor(
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
theme_config=vendor.theme_config or {},
is_active=vendor.is_active,
is_verified=vendor.is_verified,
created_at=vendor.created_at,
@@ -354,7 +351,6 @@ def toggle_vendor_verification(
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
theme_config=vendor.theme_config or {},
is_active=vendor.is_active,
is_verified=vendor.is_verified,
created_at=vendor.created_at,
@@ -406,7 +402,6 @@ def toggle_vendor_status(
letzshop_csv_url_fr=vendor.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor.letzshop_csv_url_en,
letzshop_csv_url_de=vendor.letzshop_csv_url_de,
theme_config=vendor.theme_config or {},
is_active=vendor.is_active,
is_verified=vendor.is_verified,
created_at=vendor.created_at,

View File

@@ -37,7 +37,6 @@ def get_vendor_settings(
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
"theme_config": vendor.theme_config,
"is_active": vendor.is_active,
"is_verified": vendor.is_verified,
}
@@ -72,24 +71,3 @@ def update_marketplace_settings(
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
}
@router.put("/theme")
def update_theme_settings(
theme_config: dict,
vendor: Vendor = Depends(require_vendor_context()),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Update vendor theme configuration."""
if not vendor_service.can_update_vendor(vendor, current_user):
raise HTTPException(status_code=403, detail="Insufficient permissions")
vendor.theme_config = theme_config
db.commit()
db.refresh(vendor)
return {
"message": "Theme settings updated successfully",
"theme_config": vendor.theme_config,
}