Files
orion/app/api/v1/admin/module_config.py
Samir Boulahtit 4e28d91a78 refactor: migrate templates and static files to self-contained modules
Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 14:34:16 +01:00

419 lines
13 KiB
Python

# app/api/v1/admin/module_config.py
"""
Admin API endpoints for Module Configuration Management.
Provides per-module configuration for platforms:
- GET /module-config/platforms/{platform_id}/modules/{module_code}/config - Get module config
- PUT /module-config/platforms/{platform_id}/modules/{module_code}/config - Update module config
- GET /module-config/defaults/{module_code} - Get config defaults for a module
All endpoints require super admin access.
"""
import logging
from typing import Any
from fastapi import APIRouter, Depends, Path
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_super_admin, get_db
from app.exceptions import ValidationException
from app.modules.registry import MODULES
from app.modules.service import module_service
from app.modules.tenancy.services.platform_service import platform_service
from models.schema.auth import UserContext
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/module-config")
# =============================================================================
# Config Defaults per Module
# =============================================================================
# Default configuration options per module
MODULE_CONFIG_DEFAULTS: dict[str, dict[str, Any]] = {
"billing": {
"stripe_mode": "test",
"default_trial_days": 14,
"allow_free_tier": True,
},
"inventory": {
"low_stock_threshold": 10,
"enable_locations": False,
},
"orders": {
"require_payment": True,
"auto_archive_days": 90,
},
"marketplace": {
"sync_frequency_hours": 24,
"auto_import_products": False,
},
"customers": {
"enable_segmentation": True,
"marketing_consent_default": False,
},
"cms": {
"max_pages": 50,
"enable_seo": True,
},
"analytics": {
"data_retention_days": 365,
"enable_export": True,
},
"messaging": {
"enable_attachments": True,
"max_attachment_size_mb": 10,
},
"monitoring": {
"log_retention_days": 30,
"alert_email": "",
},
}
# Config option metadata for UI rendering
MODULE_CONFIG_SCHEMA: dict[str, list[dict[str, Any]]] = {
"billing": [
{
"key": "stripe_mode",
"label": "Stripe Mode",
"type": "select",
"options": ["test", "live"],
"description": "Use test or live Stripe API keys",
},
{
"key": "default_trial_days",
"label": "Default Trial Days",
"type": "number",
"min": 0,
"max": 90,
"description": "Number of trial days for new subscriptions",
},
{
"key": "allow_free_tier",
"label": "Allow Free Tier",
"type": "boolean",
"description": "Allow vendors to use free tier indefinitely",
},
],
"inventory": [
{
"key": "low_stock_threshold",
"label": "Low Stock Threshold",
"type": "number",
"min": 0,
"max": 1000,
"description": "Stock level below which low stock alerts trigger",
},
{
"key": "enable_locations",
"label": "Enable Locations",
"type": "boolean",
"description": "Enable multiple inventory locations",
},
],
"orders": [
{
"key": "require_payment",
"label": "Require Payment",
"type": "boolean",
"description": "Require payment before order confirmation",
},
{
"key": "auto_archive_days",
"label": "Auto Archive Days",
"type": "number",
"min": 30,
"max": 365,
"description": "Days after which completed orders are archived",
},
],
"marketplace": [
{
"key": "sync_frequency_hours",
"label": "Sync Frequency (hours)",
"type": "number",
"min": 1,
"max": 168,
"description": "How often to sync with external marketplaces",
},
{
"key": "auto_import_products",
"label": "Auto Import Products",
"type": "boolean",
"description": "Automatically import new products from marketplace",
},
],
"customers": [
{
"key": "enable_segmentation",
"label": "Enable Segmentation",
"type": "boolean",
"description": "Enable customer segmentation and tagging",
},
{
"key": "marketing_consent_default",
"label": "Marketing Consent Default",
"type": "boolean",
"description": "Default value for marketing consent checkbox",
},
],
"cms": [
{
"key": "max_pages",
"label": "Max Pages",
"type": "number",
"min": 1,
"max": 500,
"description": "Maximum number of content pages allowed",
},
{
"key": "enable_seo",
"label": "Enable SEO",
"type": "boolean",
"description": "Enable SEO fields for content pages",
},
],
"analytics": [
{
"key": "data_retention_days",
"label": "Data Retention (days)",
"type": "number",
"min": 30,
"max": 730,
"description": "How long to keep analytics data",
},
{
"key": "enable_export",
"label": "Enable Export",
"type": "boolean",
"description": "Allow exporting analytics data",
},
],
"messaging": [
{
"key": "enable_attachments",
"label": "Enable Attachments",
"type": "boolean",
"description": "Allow file attachments in messages",
},
{
"key": "max_attachment_size_mb",
"label": "Max Attachment Size (MB)",
"type": "number",
"min": 1,
"max": 50,
"description": "Maximum attachment file size in megabytes",
},
],
"monitoring": [
{
"key": "log_retention_days",
"label": "Log Retention (days)",
"type": "number",
"min": 7,
"max": 365,
"description": "How long to keep log files",
},
{
"key": "alert_email",
"label": "Alert Email",
"type": "string",
"description": "Email address for system alerts (blank to disable)",
},
],
}
# =============================================================================
# Pydantic Schemas
# =============================================================================
class ModuleConfigResponse(BaseModel):
"""Module configuration response."""
module_code: str
module_name: str
config: dict[str, Any]
schema_info: list[dict[str, Any]] = Field(default_factory=list)
defaults: dict[str, Any] = Field(default_factory=dict)
class UpdateConfigRequest(BaseModel):
"""Request to update module configuration."""
config: dict[str, Any] = Field(..., description="Configuration key-value pairs")
class ConfigDefaultsResponse(BaseModel):
"""Response for module config defaults."""
module_code: str
module_name: str
defaults: dict[str, Any]
schema_info: list[dict[str, Any]]
# =============================================================================
# API Endpoints
# =============================================================================
@router.get("/platforms/{platform_id}/modules/{module_code}/config", response_model=ModuleConfigResponse)
async def get_module_config(
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Get configuration for a specific module on a platform.
Returns current config values merged with defaults.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise ValidationException(f"Unknown module: {module_code}")
module = MODULES[module_code]
# Get current config
current_config = module_service.get_module_config(db, platform_id, module_code)
# Merge with defaults
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
merged_config = {**defaults, **current_config}
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} fetched config "
f"for module '{module_code}' on platform {platform.code}"
)
return ModuleConfigResponse(
module_code=module_code,
module_name=module.name,
config=merged_config,
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
defaults=defaults,
)
@router.put("/platforms/{platform_id}/modules/{module_code}/config", response_model=ModuleConfigResponse)
async def update_module_config(
update_data: UpdateConfigRequest,
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Update configuration for a specific module on a platform.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise ValidationException(f"Unknown module: {module_code}")
module = MODULES[module_code]
# Update config
success = module_service.set_module_config(db, platform_id, module_code, update_data.config)
if success:
db.commit()
# Get updated config
current_config = module_service.get_module_config(db, platform_id, module_code)
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
merged_config = {**defaults, **current_config}
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} updated config "
f"for module '{module_code}' on platform {platform.code}: {update_data.config}"
)
return ModuleConfigResponse(
module_code=module_code,
module_name=module.name,
config=merged_config,
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
defaults=defaults,
)
@router.get("/defaults/{module_code}", response_model=ConfigDefaultsResponse)
async def get_config_defaults(
module_code: str = Path(..., description="Module code"),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Get default configuration for a module.
Returns the default config values and schema for a module.
Super admin only.
"""
# Validate module code
if module_code not in MODULES:
raise ValidationException(f"Unknown module: {module_code}")
module = MODULES[module_code]
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} fetched defaults "
f"for module '{module_code}'"
)
return ConfigDefaultsResponse(
module_code=module_code,
module_name=module.name,
defaults=MODULE_CONFIG_DEFAULTS.get(module_code, {}),
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
)
@router.post("/platforms/{platform_id}/modules/{module_code}/reset")
async def reset_module_config(
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_super_admin),
):
"""
Reset module configuration to defaults.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise ValidationException(f"Unknown module: {module_code}")
# Reset to defaults
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
success = module_service.set_module_config(db, platform_id, module_code, defaults)
if success:
db.commit()
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} reset config "
f"for module '{module_code}' on platform {platform.code} to defaults"
)
return {
"success": success,
"message": f"Module '{module_code}' config reset to defaults",
"config": defaults,
}