Multitenant implementation with custom Domain, theme per vendor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
326
app/api/v1/admin/vendor_themes.py
Normal file
326
app/api/v1/admin/vendor_themes.py
Normal 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"
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
22
app/api/v1/vendor/settings.py
vendored
22
app/api/v1/vendor/settings.py
vendored
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user