refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,8 @@ from app.modules.tenancy.exceptions import (
|
||||
from app.modules.tenancy.services.vendor_service import vendor_service
|
||||
from middleware.auth import AuthManager
|
||||
from middleware.rate_limiter import RateLimiter
|
||||
from models.database.user import User as UserModel
|
||||
from models.database.vendor import Vendor
|
||||
from app.modules.tenancy.models import User as UserModel
|
||||
from app.modules.tenancy.models import Vendor
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
# Initialize dependencies
|
||||
@@ -381,7 +381,7 @@ def get_admin_with_platform_context(
|
||||
InvalidTokenException: If platform admin token missing platform info
|
||||
InsufficientPermissionsException: If platform access revoked
|
||||
"""
|
||||
from models.database.platform import Platform
|
||||
from app.modules.tenancy.models import Platform
|
||||
|
||||
# Get raw token for platform_id extraction
|
||||
token, source = _get_token_from_request(
|
||||
@@ -553,7 +553,7 @@ def require_menu_access(menu_item_id: str, frontend_type: "FrontendType"):
|
||||
from app.modules.registry import get_menu_item_module
|
||||
from app.modules.service import module_service
|
||||
from app.modules.core.services.menu_service import menu_service
|
||||
from models.database.admin_menu_config import FrontendType as FT
|
||||
from app.modules.enums import FrontendType as FT
|
||||
|
||||
def _check_menu_access(
|
||||
request: Request,
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
"""
|
||||
Admin API router aggregation.
|
||||
|
||||
This module combines legacy admin routes with auto-discovered module routes.
|
||||
This module combines auto-discovered module routes for the admin API.
|
||||
|
||||
LEGACY ROUTES (defined in app/api/v1/admin/):
|
||||
- /menu-config/* - Navigation configuration (super admin)
|
||||
- /modules/* - Module management (super admin)
|
||||
- /module-config/* - Module settings (super admin)
|
||||
|
||||
AUTO-DISCOVERED MODULE ROUTES:
|
||||
- tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains
|
||||
- core: dashboard, settings
|
||||
All admin routes are now auto-discovered from modules:
|
||||
- tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains, modules, module_config
|
||||
- core: dashboard, settings, menu_config
|
||||
- messaging: messages, notifications, email-templates
|
||||
- monitoring: logs, tasks, tests, code_quality, audit, platform-health
|
||||
- billing: subscriptions, invoices, payments
|
||||
@@ -30,44 +25,18 @@ IMPORTANT:
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Import all admin routers (legacy routes that haven't been migrated to modules)
|
||||
# NOTE: Migrated to modules (auto-discovered):
|
||||
# - tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains
|
||||
# - core: dashboard, settings
|
||||
# - messaging: messages, notifications, email_templates
|
||||
# - monitoring: logs, tasks, tests, code_quality, audit, platform_health
|
||||
# - cms: content_pages, images, media, vendor_themes
|
||||
from . import (
|
||||
menu_config,
|
||||
module_config,
|
||||
modules,
|
||||
)
|
||||
|
||||
# Create admin router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Framework Config (remain in legacy - super admin only)
|
||||
# ============================================================================
|
||||
|
||||
# Include menu configuration endpoints (super admin only)
|
||||
router.include_router(menu_config.router, tags=["admin-menu-config"])
|
||||
|
||||
# Include module management endpoints (super admin only)
|
||||
router.include_router(modules.router, tags=["admin-modules"])
|
||||
|
||||
# Include module configuration endpoints (super admin only)
|
||||
router.include_router(module_config.router, tags=["admin-module-config"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Auto-discovered Module Routes
|
||||
# ============================================================================
|
||||
# Routes from self-contained modules are auto-discovered and registered.
|
||||
# Modules include: billing, inventory, orders, marketplace, cms, customers,
|
||||
# monitoring (logs, tasks, tests, code_quality, audit, platform_health),
|
||||
# messaging (messages, notifications, email_templates)
|
||||
# All routes from self-contained modules are auto-discovered and registered.
|
||||
# Legacy routes have been migrated to their respective modules:
|
||||
# - menu_config -> core module
|
||||
# - modules, module_config -> tenancy module
|
||||
|
||||
from app.modules.routes import get_admin_api_routes
|
||||
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
# app/api/v1/admin/menu_config.py
|
||||
"""
|
||||
Admin API endpoints for Platform Menu Configuration.
|
||||
|
||||
Provides menu visibility configuration for admin and vendor frontends:
|
||||
- GET /menu-config/platforms/{platform_id} - Get menu config for a platform
|
||||
- PUT /menu-config/platforms/{platform_id} - Update menu visibility for a platform
|
||||
- POST /menu-config/platforms/{platform_id}/reset - Reset to defaults
|
||||
- GET /menu-config/user - Get current user's menu config (super admins)
|
||||
- PUT /menu-config/user - Update current user's menu config (super admins)
|
||||
- GET /menu/admin - Get rendered admin menu for current user
|
||||
- GET /menu/vendor - Get rendered vendor menu for current platform
|
||||
|
||||
All configuration endpoints require super admin access.
|
||||
Menu rendering endpoints require authenticated admin/vendor access.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import (
|
||||
get_current_admin_from_cookie_or_header,
|
||||
get_current_super_admin,
|
||||
get_db,
|
||||
)
|
||||
from app.modules.core.services.menu_service import MenuItemConfig, menu_service
|
||||
from app.modules.tenancy.services.platform_service import platform_service
|
||||
from models.database.admin_menu_config import FrontendType # noqa: API-007 - Enum for type safety
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/menu-config")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Schemas
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class MenuItemResponse(BaseModel):
|
||||
"""Menu item configuration response."""
|
||||
|
||||
id: str
|
||||
label: str
|
||||
icon: str
|
||||
url: str
|
||||
section_id: str
|
||||
section_label: str | None = None
|
||||
is_visible: bool = True
|
||||
is_mandatory: bool = False
|
||||
is_super_admin_only: bool = False
|
||||
|
||||
|
||||
class MenuConfigResponse(BaseModel):
|
||||
"""Menu configuration response for a platform or user."""
|
||||
|
||||
frontend_type: str
|
||||
platform_id: int | None = None
|
||||
user_id: int | None = None
|
||||
items: list[MenuItemResponse]
|
||||
total_items: int
|
||||
visible_items: int
|
||||
hidden_items: int
|
||||
|
||||
|
||||
class MenuVisibilityUpdateRequest(BaseModel):
|
||||
"""Request to update menu item visibility."""
|
||||
|
||||
menu_item_id: str = Field(..., description="Menu item ID to update")
|
||||
is_visible: bool = Field(..., description="Whether the item should be visible")
|
||||
|
||||
|
||||
class BulkMenuVisibilityUpdateRequest(BaseModel):
|
||||
"""Request to update multiple menu items at once."""
|
||||
|
||||
visibility: dict[str, bool] = Field(
|
||||
...,
|
||||
description="Map of menu_item_id to is_visible",
|
||||
examples=[{"inventory": False, "orders": True}],
|
||||
)
|
||||
|
||||
|
||||
class MenuSectionResponse(BaseModel):
|
||||
"""Menu section for rendering."""
|
||||
|
||||
id: str
|
||||
label: str | None = None
|
||||
items: list[dict[str, Any]]
|
||||
|
||||
|
||||
class RenderedMenuResponse(BaseModel):
|
||||
"""Rendered menu for frontend."""
|
||||
|
||||
frontend_type: str
|
||||
sections: list[MenuSectionResponse]
|
||||
|
||||
|
||||
class MenuActionResponse(BaseModel):
|
||||
"""Response for menu action operations (reset, show-all, etc.)."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _build_menu_item_response(item: MenuItemConfig) -> MenuItemResponse:
|
||||
"""Convert MenuItemConfig to API response."""
|
||||
return MenuItemResponse(
|
||||
id=item.id,
|
||||
label=item.label,
|
||||
icon=item.icon,
|
||||
url=item.url,
|
||||
section_id=item.section_id,
|
||||
section_label=item.section_label,
|
||||
is_visible=item.is_visible,
|
||||
is_mandatory=item.is_mandatory,
|
||||
is_super_admin_only=item.is_super_admin_only,
|
||||
)
|
||||
|
||||
|
||||
def _build_menu_config_response(
|
||||
items: list[MenuItemConfig],
|
||||
frontend_type: FrontendType,
|
||||
platform_id: int | None = None,
|
||||
user_id: int | None = None,
|
||||
) -> MenuConfigResponse:
|
||||
"""Build menu configuration response."""
|
||||
item_responses = [_build_menu_item_response(item) for item in items]
|
||||
visible_count = sum(1 for item in items if item.is_visible)
|
||||
|
||||
return MenuConfigResponse(
|
||||
frontend_type=frontend_type.value,
|
||||
platform_id=platform_id,
|
||||
user_id=user_id,
|
||||
items=item_responses,
|
||||
total_items=len(items),
|
||||
visible_items=visible_count,
|
||||
hidden_items=len(items) - visible_count,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Platform Menu Configuration (Super Admin Only)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/platforms/{platform_id}", response_model=MenuConfigResponse)
|
||||
async def get_platform_menu_config(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
frontend_type: FrontendType = Query(
|
||||
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Get menu configuration for a platform.
|
||||
|
||||
Returns all menu items with their visibility status for the specified
|
||||
platform and frontend type. Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
items = menu_service.get_platform_menu_config(db, frontend_type, platform_id)
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} fetched menu config "
|
||||
f"for platform {platform.code} ({frontend_type.value})"
|
||||
)
|
||||
|
||||
return _build_menu_config_response(items, frontend_type, platform_id=platform_id)
|
||||
|
||||
|
||||
@router.put("/platforms/{platform_id}")
|
||||
async def update_platform_menu_visibility(
|
||||
update_data: MenuVisibilityUpdateRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
frontend_type: FrontendType = Query(
|
||||
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Update visibility for a single menu item for a platform.
|
||||
|
||||
Super admin only. Cannot hide mandatory items.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
menu_service.update_menu_visibility(
|
||||
db=db,
|
||||
frontend_type=frontend_type,
|
||||
menu_item_id=update_data.menu_item_id,
|
||||
is_visible=update_data.is_visible,
|
||||
platform_id=platform_id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} updated menu visibility: "
|
||||
f"{update_data.menu_item_id}={update_data.is_visible} "
|
||||
f"for platform {platform.code} ({frontend_type.value})"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Menu visibility updated"}
|
||||
|
||||
|
||||
@router.put("/platforms/{platform_id}/bulk")
|
||||
async def bulk_update_platform_menu_visibility(
|
||||
update_data: BulkMenuVisibilityUpdateRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
frontend_type: FrontendType = Query(
|
||||
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Update visibility for multiple menu items at once.
|
||||
|
||||
Super admin only. Skips mandatory items silently.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
menu_service.bulk_update_menu_visibility(
|
||||
db=db,
|
||||
frontend_type=frontend_type,
|
||||
visibility_map=update_data.visibility,
|
||||
platform_id=platform_id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} bulk updated menu visibility: "
|
||||
f"{len(update_data.visibility)} items for platform {platform.code} ({frontend_type.value})"
|
||||
)
|
||||
|
||||
return {"success": True, "message": f"Updated {len(update_data.visibility)} menu items"}
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/reset")
|
||||
async def reset_platform_menu_config(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
frontend_type: FrontendType = Query(
|
||||
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Reset menu configuration for a platform to defaults.
|
||||
|
||||
Removes all visibility overrides, making all items visible.
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
menu_service.reset_platform_menu_config(db, frontend_type, platform_id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} reset menu config "
|
||||
f"for platform {platform.code} ({frontend_type.value})"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Menu configuration reset to defaults"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# User Menu Configuration (Super Admin Only)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/user", response_model=MenuConfigResponse)
|
||||
async def get_user_menu_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Get the current super admin's personal menu configuration.
|
||||
|
||||
Only super admins can configure their own admin menu.
|
||||
"""
|
||||
items = menu_service.get_user_menu_config(db, current_user.id)
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} fetched their personal menu config"
|
||||
)
|
||||
|
||||
return _build_menu_config_response(
|
||||
items, FrontendType.ADMIN, user_id=current_user.id
|
||||
)
|
||||
|
||||
|
||||
@router.put("/user")
|
||||
async def update_user_menu_visibility(
|
||||
update_data: MenuVisibilityUpdateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Update visibility for a single menu item for the current super admin.
|
||||
|
||||
Super admin only. Cannot hide mandatory items.
|
||||
"""
|
||||
menu_service.update_menu_visibility(
|
||||
db=db,
|
||||
frontend_type=FrontendType.ADMIN,
|
||||
menu_item_id=update_data.menu_item_id,
|
||||
is_visible=update_data.is_visible,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} updated personal menu: "
|
||||
f"{update_data.menu_item_id}={update_data.is_visible}"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Menu visibility updated"}
|
||||
|
||||
|
||||
@router.post("/user/reset", response_model=MenuActionResponse)
|
||||
async def reset_user_menu_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Reset the current super admin's menu configuration (hide all except mandatory).
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
menu_service.reset_user_menu_config(db, current_user.id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} reset their personal menu config (hide all)"
|
||||
)
|
||||
|
||||
return MenuActionResponse(success=True, message="Menu configuration reset - all items hidden")
|
||||
|
||||
|
||||
@router.post("/user/show-all", response_model=MenuActionResponse)
|
||||
async def show_all_user_menu_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Show all menu items for the current super admin.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
menu_service.show_all_user_menu_config(db, current_user.id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items"
|
||||
)
|
||||
|
||||
return MenuActionResponse(success=True, message="All menu items are now visible")
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/show-all")
|
||||
async def show_all_platform_menu_config(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
frontend_type: FrontendType = Query(
|
||||
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Show all menu items for a platform.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
menu_service.show_all_platform_menu_config(db, frontend_type, platform_id)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items "
|
||||
f"for platform {platform.code} ({frontend_type.value})"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "All menu items are now visible"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Menu Rendering (For Sidebar)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/render/admin", response_model=RenderedMenuResponse)
|
||||
async def get_rendered_admin_menu(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_admin_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Get the rendered admin menu for the current user.
|
||||
|
||||
Returns the filtered menu structure based on:
|
||||
- Super admins: user-level config
|
||||
- Platform admins: platform-level config
|
||||
|
||||
Used by the frontend to render the sidebar.
|
||||
"""
|
||||
if current_user.is_super_admin:
|
||||
# Super admin: use user-level config
|
||||
menu = menu_service.get_menu_for_rendering(
|
||||
db=db,
|
||||
frontend_type=FrontendType.ADMIN,
|
||||
user_id=current_user.id,
|
||||
is_super_admin=True,
|
||||
)
|
||||
else:
|
||||
# Platform admin: use platform-level config
|
||||
# Get the selected platform from the JWT token
|
||||
platform_id = getattr(current_user, "token_platform_id", None)
|
||||
|
||||
# Fallback to first platform if no platform in token (shouldn't happen)
|
||||
if platform_id is None and current_user.admin_platforms:
|
||||
platform_id = current_user.admin_platforms[0].id
|
||||
logger.warning(
|
||||
f"[MENU_CONFIG] No platform_id in token for {current_user.email}, "
|
||||
f"falling back to first platform: {platform_id}"
|
||||
)
|
||||
|
||||
menu = menu_service.get_menu_for_rendering(
|
||||
db=db,
|
||||
frontend_type=FrontendType.ADMIN,
|
||||
platform_id=platform_id,
|
||||
is_super_admin=False,
|
||||
)
|
||||
|
||||
sections = [
|
||||
MenuSectionResponse(
|
||||
id=section["id"],
|
||||
label=section.get("label"),
|
||||
items=section["items"],
|
||||
)
|
||||
for section in menu.get("sections", [])
|
||||
]
|
||||
|
||||
return RenderedMenuResponse(
|
||||
frontend_type=FrontendType.ADMIN.value,
|
||||
sections=sections,
|
||||
)
|
||||
@@ -1,418 +0,0 @@
|
||||
# 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,
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
# app/api/v1/admin/modules.py
|
||||
"""
|
||||
Admin API endpoints for Platform Module Management.
|
||||
|
||||
Provides module enablement/disablement for platforms:
|
||||
- GET /modules - List all available modules
|
||||
- GET /modules/platforms/{platform_id} - Get modules for a platform
|
||||
- PUT /modules/platforms/{platform_id} - Update enabled modules
|
||||
- POST /modules/platforms/{platform_id}/enable - Enable a module
|
||||
- POST /modules/platforms/{platform_id}/disable - Disable a module
|
||||
|
||||
All endpoints require super admin access.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
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.modules.registry import MODULES, get_core_module_codes
|
||||
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="/modules")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Schemas
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ModuleResponse(BaseModel):
|
||||
"""Module definition response."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
description: str
|
||||
is_core: bool
|
||||
is_enabled: bool
|
||||
requires: list[str] = Field(default_factory=list)
|
||||
features: list[str] = Field(default_factory=list)
|
||||
menu_items_admin: list[str] = Field(default_factory=list)
|
||||
menu_items_vendor: list[str] = Field(default_factory=list)
|
||||
dependent_modules: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ModuleListResponse(BaseModel):
|
||||
"""Response for module list."""
|
||||
|
||||
modules: list[ModuleResponse]
|
||||
total: int
|
||||
enabled: int
|
||||
disabled: int
|
||||
|
||||
|
||||
class PlatformModulesResponse(BaseModel):
|
||||
"""Response for platform module configuration."""
|
||||
|
||||
platform_id: int
|
||||
platform_code: str
|
||||
platform_name: str
|
||||
modules: list[ModuleResponse]
|
||||
total: int
|
||||
enabled: int
|
||||
disabled: int
|
||||
|
||||
|
||||
class EnableModulesRequest(BaseModel):
|
||||
"""Request to set enabled modules."""
|
||||
|
||||
module_codes: list[str] = Field(..., description="List of module codes to enable")
|
||||
|
||||
|
||||
class ToggleModuleRequest(BaseModel):
|
||||
"""Request to enable/disable a single module."""
|
||||
|
||||
module_code: str = Field(..., description="Module code to toggle")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _get_dependent_modules(module_code: str) -> list[str]:
|
||||
"""Get modules that depend on a given module."""
|
||||
dependents = []
|
||||
for code, module in MODULES.items():
|
||||
if module_code in module.requires:
|
||||
dependents.append(code)
|
||||
return dependents
|
||||
|
||||
|
||||
def _build_module_response(
|
||||
code: str,
|
||||
is_enabled: bool,
|
||||
) -> ModuleResponse:
|
||||
"""Build ModuleResponse from module code."""
|
||||
from models.database.admin_menu_config import FrontendType # noqa: API-007 - Enum for type safety
|
||||
|
||||
module = MODULES.get(code)
|
||||
if not module:
|
||||
raise ValueError(f"Unknown module: {code}")
|
||||
|
||||
return ModuleResponse(
|
||||
code=module.code,
|
||||
name=module.name,
|
||||
description=module.description,
|
||||
is_core=module.is_core,
|
||||
is_enabled=is_enabled,
|
||||
requires=module.requires,
|
||||
features=module.features,
|
||||
menu_items_admin=module.get_menu_items(FrontendType.ADMIN),
|
||||
menu_items_vendor=module.get_menu_items(FrontendType.VENDOR),
|
||||
dependent_modules=_get_dependent_modules(module.code),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=ModuleListResponse)
|
||||
async def list_all_modules(
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
List all available modules.
|
||||
|
||||
Returns all module definitions with their metadata.
|
||||
Super admin only.
|
||||
"""
|
||||
modules = []
|
||||
for code in MODULES.keys():
|
||||
# All modules shown as enabled in the global list
|
||||
modules.append(_build_module_response(code, is_enabled=True))
|
||||
|
||||
# Sort: core first, then alphabetically
|
||||
modules.sort(key=lambda m: (not m.is_core, m.name))
|
||||
|
||||
logger.info(f"[MODULES] Super admin {current_user.email} listed all modules")
|
||||
|
||||
return ModuleListResponse(
|
||||
modules=modules,
|
||||
total=len(modules),
|
||||
enabled=len(modules),
|
||||
disabled=0,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/platforms/{platform_id}", response_model=PlatformModulesResponse)
|
||||
async def get_platform_modules(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Get module configuration for a platform.
|
||||
|
||||
Returns all modules with their enablement status for the platform.
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Get enabled module codes for this platform
|
||||
enabled_codes = module_service.get_enabled_module_codes(db, platform_id)
|
||||
|
||||
modules = []
|
||||
for code in MODULES.keys():
|
||||
is_enabled = code in enabled_codes
|
||||
modules.append(_build_module_response(code, is_enabled))
|
||||
|
||||
# Sort: core first, then alphabetically
|
||||
modules.sort(key=lambda m: (not m.is_core, m.name))
|
||||
|
||||
enabled_count = sum(1 for m in modules if m.is_enabled)
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} fetched modules "
|
||||
f"for platform {platform.code} ({enabled_count}/{len(modules)} enabled)"
|
||||
)
|
||||
|
||||
return PlatformModulesResponse(
|
||||
platform_id=platform.id,
|
||||
platform_code=platform.code,
|
||||
platform_name=platform.name,
|
||||
modules=modules,
|
||||
total=len(modules),
|
||||
enabled=enabled_count,
|
||||
disabled=len(modules) - enabled_count,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/platforms/{platform_id}", response_model=PlatformModulesResponse)
|
||||
async def update_platform_modules(
|
||||
update_data: EnableModulesRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Update enabled modules for a platform.
|
||||
|
||||
Sets the list of enabled modules. Core modules are automatically included.
|
||||
Dependencies are automatically resolved.
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Update enabled modules
|
||||
module_service.set_enabled_modules(db, platform_id, update_data.module_codes)
|
||||
db.commit()
|
||||
|
||||
# Get updated module list
|
||||
enabled_codes = module_service.get_enabled_module_codes(db, platform_id)
|
||||
|
||||
modules = []
|
||||
for code in MODULES.keys():
|
||||
is_enabled = code in enabled_codes
|
||||
modules.append(_build_module_response(code, is_enabled))
|
||||
|
||||
modules.sort(key=lambda m: (not m.is_core, m.name))
|
||||
enabled_count = sum(1 for m in modules if m.is_enabled)
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} updated modules "
|
||||
f"for platform {platform.code}: {sorted(update_data.module_codes)}"
|
||||
)
|
||||
|
||||
return PlatformModulesResponse(
|
||||
platform_id=platform.id,
|
||||
platform_code=platform.code,
|
||||
platform_name=platform.name,
|
||||
modules=modules,
|
||||
total=len(modules),
|
||||
enabled=enabled_count,
|
||||
disabled=len(modules) - enabled_count,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/enable")
|
||||
async def enable_module(
|
||||
request: ToggleModuleRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Enable a single module for a platform.
|
||||
|
||||
Also enables required dependencies.
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Validate module code
|
||||
if request.module_code not in MODULES:
|
||||
from app.modules.tenancy.exceptions import BadRequestException
|
||||
|
||||
raise BadRequestException(f"Unknown module: {request.module_code}")
|
||||
|
||||
# Enable module
|
||||
success = module_service.enable_module(db, platform_id, request.module_code)
|
||||
if success:
|
||||
db.commit()
|
||||
|
||||
# Check what dependencies were also enabled
|
||||
module = MODULES[request.module_code]
|
||||
enabled_deps = module.requires if module.requires else []
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} enabled module "
|
||||
f"'{request.module_code}' for platform {platform.code}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": f"Module '{request.module_code}' enabled",
|
||||
"also_enabled": enabled_deps,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/disable")
|
||||
async def disable_module(
|
||||
request: ToggleModuleRequest,
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Disable a single module for a platform.
|
||||
|
||||
Core modules cannot be disabled.
|
||||
Also disables modules that depend on this one.
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Validate module code
|
||||
if request.module_code not in MODULES:
|
||||
from app.modules.tenancy.exceptions import BadRequestException
|
||||
|
||||
raise BadRequestException(f"Unknown module: {request.module_code}")
|
||||
|
||||
# Check if core module
|
||||
if request.module_code in get_core_module_codes():
|
||||
from app.modules.tenancy.exceptions import BadRequestException
|
||||
|
||||
raise BadRequestException(f"Cannot disable core module: {request.module_code}")
|
||||
|
||||
# Get dependent modules before disabling
|
||||
dependents = _get_dependent_modules(request.module_code)
|
||||
|
||||
# Disable module
|
||||
success = module_service.disable_module(db, platform_id, request.module_code)
|
||||
if success:
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} disabled module "
|
||||
f"'{request.module_code}' for platform {platform.code}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": f"Module '{request.module_code}' disabled",
|
||||
"also_disabled": dependents if dependents else [],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/enable-all")
|
||||
async def enable_all_modules(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Enable all modules for a platform.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Enable all modules
|
||||
all_codes = list(MODULES.keys())
|
||||
module_service.set_enabled_modules(db, platform_id, all_codes)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} enabled all modules "
|
||||
f"for platform {platform.code}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "All modules enabled",
|
||||
"enabled_count": len(all_codes),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/platforms/{platform_id}/disable-optional")
|
||||
async def disable_optional_modules(
|
||||
platform_id: int = Path(..., description="Platform ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
"""
|
||||
Disable all optional modules for a platform, keeping only core modules.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
# Verify platform exists
|
||||
platform = platform_service.get_platform_by_id(db, platform_id)
|
||||
|
||||
# Enable only core modules
|
||||
core_codes = list(get_core_module_codes())
|
||||
module_service.set_enabled_modules(db, platform_id, core_codes)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"[MODULES] Super admin {current_user.email} disabled optional modules "
|
||||
f"for platform {platform.code} (kept {len(core_codes)} core modules)"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Optional modules disabled, core modules kept",
|
||||
"core_modules": core_codes,
|
||||
}
|
||||
Reference in New Issue
Block a user