Files
orion/app/api/v1/admin/modules.py
Samir Boulahtit cad862f469 refactor(api): introduce UserContext schema for API dependency injection
Replace direct User database model imports in API endpoints with UserContext
schema, following the architecture principle that API routes should not import
database models directly.

Changes:
- Create UserContext schema in models/schema/auth.py with from_user() factory
- Update app/api/deps.py to return UserContext from all auth dependencies
- Add _get_user_model() helper for functions needing User model access
- Update 58 API endpoint files to use UserContext instead of User
- Add noqa comments for 4 legitimate edge cases (enums, internal helpers)

Architecture validation: 0 errors (down from 61), 11 warnings remain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:47:33 +01:00

400 lines
12 KiB
Python

# 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.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.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.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: 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,
}