feat: add Admin UI for platform module management (Phase 5)
Added a complete Admin UI for managing platform modules:
API endpoints (app/api/v1/admin/modules.py):
- GET /modules - List all available modules
- GET /modules/platforms/{id} - Get platform modules
- PUT /modules/platforms/{id} - Update enabled modules
- POST /modules/platforms/{id}/enable - Enable a module
- POST /modules/platforms/{id}/disable - Disable a module
- POST /modules/platforms/{id}/enable-all - Enable all
- POST /modules/platforms/{id}/disable-optional - Core only
Admin UI:
- New page route: /admin/platforms/{code}/modules
- Template: platform-modules.html
- JavaScript: platform-modules.js
- Link added to platform-detail.html Super Admin section
Features:
- Toggle modules on/off with dependency resolution
- Enable all / Core only bulk actions
- Visual dependency indicators
- Separate sections for core vs optional modules
- Feature list preview per module
Also includes require_menu_access updates to page routes from Phase 2.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@ from . import (
|
||||
media,
|
||||
menu_config,
|
||||
messages,
|
||||
modules,
|
||||
monitoring,
|
||||
notifications,
|
||||
order_item_exceptions,
|
||||
@@ -125,6 +126,9 @@ router.include_router(platforms.router, tags=["admin-platforms"])
|
||||
# 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"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# User Management
|
||||
|
||||
399
app/api/v1/admin/modules.py
Normal file
399
app/api/v1/admin/modules.py
Normal file
@@ -0,0 +1,399 @@
|
||||
# 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.services.platform_service import platform_service
|
||||
from models.database.user import User
|
||||
|
||||
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
|
||||
|
||||
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: User = 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: User = 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: User = 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: User = 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.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: User = 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.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.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: User = 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: User = 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