# app/modules/tenancy/routes/api/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.schemas.auth import UserContext from app.modules.tenancy.services.platform_service import platform_service 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_store: 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 app.modules.enums import FrontendType # 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_store=module.get_menu_items(FrontendType.STORE), 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: # 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: 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: 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, }