diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index 89ad2461..a487b5f2 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -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 diff --git a/app/api/v1/admin/modules.py b/app/api/v1/admin/modules.py new file mode 100644 index 00000000..cc291903 --- /dev/null +++ b/app/api/v1/admin/modules.py @@ -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, + } diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py index 1c6193e5..80be812d 100644 --- a/app/routes/admin_pages.py +++ b/app/routes/admin_pages.py @@ -48,11 +48,12 @@ from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session from app.api.deps import ( - get_current_admin_from_cookie_or_header, get_current_admin_optional, get_db, + require_menu_access, ) from app.core.config import settings +from models.database.admin_menu_config import FrontendType from models.database.user import User router = APIRouter() @@ -132,7 +133,7 @@ async def admin_select_platform_page( @router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) async def admin_dashboard_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("dashboard", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -156,7 +157,7 @@ async def admin_dashboard_page( @router.get("/companies", response_class=HTMLResponse, include_in_schema=False) async def admin_companies_list_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -175,7 +176,7 @@ async def admin_companies_list_page( @router.get("/companies/create", response_class=HTMLResponse, include_in_schema=False) async def admin_company_create_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -196,7 +197,7 @@ async def admin_company_create_page( async def admin_company_detail_page( request: Request, company_id: int = Path(..., description="Company ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -218,7 +219,7 @@ async def admin_company_detail_page( async def admin_company_edit_page( request: Request, company_id: int = Path(..., description="Company ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -242,7 +243,7 @@ async def admin_company_edit_page( @router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) async def admin_vendors_list_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -261,7 +262,7 @@ async def admin_vendors_list_page( @router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False) async def admin_vendor_create_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -282,7 +283,7 @@ async def admin_vendor_create_page( async def admin_vendor_detail_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -305,7 +306,7 @@ async def admin_vendor_detail_page( async def admin_vendor_edit_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -334,7 +335,7 @@ async def admin_vendor_edit_page( async def admin_vendor_domains_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -359,7 +360,7 @@ async def admin_vendor_domains_page( @router.get("/vendor-themes", response_class=HTMLResponse, include_in_schema=False) async def admin_vendor_themes_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -381,7 +382,7 @@ async def admin_vendor_themes_page( async def admin_vendor_theme_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -406,18 +407,14 @@ async def admin_vendor_theme_page( @router.get("/admin-users", response_class=HTMLResponse, include_in_schema=False) async def admin_users_list_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ Render admin users management page. Shows list of all admin users (super admins and platform admins). - Super admin only. + Super admin only (menu is in super_admin_only section). """ - from fastapi import HTTPException - - if not current_user.is_super_admin: - raise HTTPException(status_code=403, detail="Super admin access required") return templates.TemplateResponse( "admin/admin-users.html", @@ -431,17 +428,13 @@ async def admin_users_list_page( @router.get("/admin-users/create", response_class=HTMLResponse, include_in_schema=False) async def admin_user_create_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ Render admin user creation form. - Super admin only. + Super admin only (menu is in super_admin_only section). """ - from fastapi import HTTPException - - if not current_user.is_super_admin: - raise HTTPException(status_code=403, detail="Super admin access required") return templates.TemplateResponse( "admin/user-create.html", @@ -458,17 +451,13 @@ async def admin_user_create_page( async def admin_user_detail_page( request: Request, user_id: int = Path(..., description="User ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ Render admin user detail view. - Super admin only. + Super admin only (menu is in super_admin_only section). """ - from fastapi import HTTPException - - if not current_user.is_super_admin: - raise HTTPException(status_code=403, detail="Super admin access required") return templates.TemplateResponse( "admin/admin-user-detail.html", @@ -486,17 +475,13 @@ async def admin_user_detail_page( async def admin_user_edit_page( request: Request, user_id: int = Path(..., description="User ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ Render admin user edit form. - Super admin only. + Super admin only (menu is in super_admin_only section). """ - from fastapi import HTTPException - - if not current_user.is_super_admin: - raise HTTPException(status_code=403, detail="Super admin access required") return templates.TemplateResponse( "admin/admin-user-edit.html", @@ -557,7 +542,7 @@ async def admin_user_edit_page_redirect(user_id: int = Path(..., description="Us @router.get("/customers", response_class=HTMLResponse, include_in_schema=False) async def admin_customers_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("customers", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -581,7 +566,7 @@ async def admin_customers_page( @router.get("/notifications", response_class=HTMLResponse, include_in_schema=False) async def admin_notifications_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("notifications", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -605,7 +590,7 @@ async def admin_notifications_page( @router.get("/email-templates", response_class=HTMLResponse, include_in_schema=False) async def admin_email_templates_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("email-templates", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -629,7 +614,7 @@ async def admin_email_templates_page( @router.get("/messages", response_class=HTMLResponse, include_in_schema=False) async def admin_messages_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -653,7 +638,7 @@ async def admin_messages_page( async def admin_conversation_detail_page( request: Request, conversation_id: int = Path(..., description="Conversation ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -678,7 +663,7 @@ async def admin_conversation_detail_page( @router.get("/inventory", response_class=HTMLResponse, include_in_schema=False) async def admin_inventory_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("inventory", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -702,7 +687,7 @@ async def admin_inventory_page( @router.get("/orders", response_class=HTMLResponse, include_in_schema=False) async def admin_orders_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("orders", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -726,7 +711,7 @@ async def admin_orders_page( @router.get("/imports", response_class=HTMLResponse, include_in_schema=False) async def admin_imports_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("imports", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -745,7 +730,7 @@ async def admin_imports_page( @router.get("/background-tasks", response_class=HTMLResponse, include_in_schema=False) async def admin_background_tasks_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("background-tasks", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -765,7 +750,7 @@ async def admin_background_tasks_page( @router.get("/marketplace", response_class=HTMLResponse, include_in_schema=False) async def admin_marketplace_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -791,7 +776,7 @@ async def admin_marketplace_page( ) async def admin_marketplace_letzshop_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -814,7 +799,7 @@ async def admin_marketplace_letzshop_page( async def admin_letzshop_order_detail_page( request: Request, order_id: int = Path(..., description="Letzshop order ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -840,7 +825,7 @@ async def admin_letzshop_order_detail_page( async def admin_letzshop_product_detail_page( request: Request, product_id: int = Path(..., description="Marketplace Product ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -870,7 +855,7 @@ async def admin_letzshop_product_detail_page( ) async def admin_letzshop_vendor_directory_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -900,7 +885,7 @@ async def admin_letzshop_vendor_directory_page( ) async def admin_marketplace_products_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -924,7 +909,7 @@ async def admin_marketplace_products_page( async def admin_marketplace_product_detail_page( request: Request, product_id: int = Path(..., description="Marketplace Product ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -945,7 +930,7 @@ async def admin_marketplace_product_detail_page( @router.get("/vendor-products", response_class=HTMLResponse, include_in_schema=False) async def admin_vendor_products_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -964,7 +949,7 @@ async def admin_vendor_products_page( @router.get("/vendor-products/create", response_class=HTMLResponse, include_in_schema=False) async def admin_vendor_product_create_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -988,7 +973,7 @@ async def admin_vendor_product_create_page( async def admin_vendor_product_detail_page( request: Request, product_id: int = Path(..., description="Vendor Product ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1013,7 +998,7 @@ async def admin_vendor_product_detail_page( async def admin_vendor_product_edit_page( request: Request, product_id: int = Path(..., description="Vendor Product ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1038,7 +1023,7 @@ async def admin_vendor_product_edit_page( @router.get("/subscription-tiers", response_class=HTMLResponse, include_in_schema=False) async def admin_subscription_tiers_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1057,7 +1042,7 @@ async def admin_subscription_tiers_page( @router.get("/subscriptions", response_class=HTMLResponse, include_in_schema=False) async def admin_subscriptions_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("subscriptions", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1076,7 +1061,7 @@ async def admin_subscriptions_page( @router.get("/billing-history", response_class=HTMLResponse, include_in_schema=False) async def admin_billing_history_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("billing-history", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1100,7 +1085,7 @@ async def admin_billing_history_page( @router.get("/settings", response_class=HTMLResponse, include_in_schema=False) async def admin_settings_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("settings", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1116,10 +1101,33 @@ async def admin_settings_page( ) +@router.get("/my-menu", response_class=HTMLResponse, include_in_schema=False) +async def admin_my_menu_config( + request: Request, + current_user: User = Depends(require_menu_access("my-menu", FrontendType.ADMIN)), + db: Session = Depends(get_db), +): + """ + Render personal menu configuration page for super admins. + Allows super admins to customize their own sidebar menu. + """ + # Only super admins can configure their own menu + if not current_user.is_super_admin: + return RedirectResponse(url="/admin/settings", status_code=302) + + return templates.TemplateResponse( + "admin/my-menu-config.html", + { + "request": request, + "user": current_user, + }, + ) + + @router.get("/logs", response_class=HTMLResponse, include_in_schema=False) async def admin_logs_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("logs", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1143,7 +1151,7 @@ async def admin_logs_page( @router.get("/platforms", response_class=HTMLResponse, include_in_schema=False) async def admin_platforms_list( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1165,7 +1173,7 @@ async def admin_platforms_list( async def admin_platform_detail( request: Request, platform_code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1190,7 +1198,7 @@ async def admin_platform_detail( async def admin_platform_edit( request: Request, platform_code: str = Path(..., description="Platform code"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1207,6 +1215,66 @@ async def admin_platform_edit( ) +@router.get( + "/platforms/{platform_code}/menu-config", + response_class=HTMLResponse, + include_in_schema=False, +) +async def admin_platform_menu_config( + request: Request, + platform_code: str = Path(..., description="Platform code"), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), + db: Session = Depends(get_db), +): + """ + Render platform menu configuration page. + Super admin only - allows configuring which menu items are visible + for the platform's admin and vendor frontends. + """ + # Only super admins can access menu configuration + if not current_user.is_super_admin: + return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) + + return templates.TemplateResponse( + "admin/platform-menu-config.html", + { + "request": request, + "user": current_user, + "platform_code": platform_code, + }, + ) + + +@router.get( + "/platforms/{platform_code}/modules", + response_class=HTMLResponse, + include_in_schema=False, +) +async def admin_platform_modules( + request: Request, + platform_code: str = Path(..., description="Platform code"), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), + db: Session = Depends(get_db), +): + """ + Render platform module configuration page. + Super admin only - allows enabling/disabling feature modules + for the platform. + """ + # Only super admins can access module configuration + if not current_user.is_super_admin: + return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) + + return templates.TemplateResponse( + "admin/platform-modules.html", + { + "request": request, + "user": current_user, + "platform_code": platform_code, + }, + ) + + # ============================================================================ # CONTENT MANAGEMENT SYSTEM (CMS) ROUTES # ============================================================================ @@ -1215,7 +1283,7 @@ async def admin_platform_edit( @router.get("/platform-homepage", include_in_schema=False) async def admin_platform_homepage_manager( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1225,14 +1293,13 @@ async def admin_platform_homepage_manager( - /admin/platforms → Select platform → Homepage button - Or directly: /admin/content-pages?platform_code={code}&slug=home """ - from starlette.responses import RedirectResponse return RedirectResponse(url="/admin/platforms", status_code=302) @router.get("/content-pages", response_class=HTMLResponse, include_in_schema=False) async def admin_content_pages_list( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1253,7 +1320,7 @@ async def admin_content_pages_list( ) async def admin_content_page_create( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1278,7 +1345,7 @@ async def admin_content_page_create( async def admin_content_page_edit( request: Request, page_id: int = Path(..., description="Content page ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("content-pages", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1303,7 +1370,7 @@ async def admin_content_page_edit( @router.get("/components", response_class=HTMLResponse, include_in_schema=False) async def admin_components_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("components", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1322,7 +1389,7 @@ async def admin_components_page( @router.get("/icons", response_class=HTMLResponse, include_in_schema=False) async def admin_icons_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("icons", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1341,7 +1408,7 @@ async def admin_icons_page( @router.get("/testing", response_class=HTMLResponse, include_in_schema=False) async def admin_testing_dashboard( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1360,7 +1427,7 @@ async def admin_testing_dashboard( @router.get("/testing-hub", response_class=HTMLResponse, include_in_schema=False) async def admin_testing_hub( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1379,7 +1446,7 @@ async def admin_testing_hub( @router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False) async def admin_test_auth_flow( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1402,7 +1469,7 @@ async def admin_test_auth_flow( ) async def admin_test_vendors_users_migration( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1426,7 +1493,7 @@ async def admin_test_vendors_users_migration( @router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False) async def admin_code_quality_dashboard( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1447,7 +1514,7 @@ async def admin_code_quality_dashboard( ) async def admin_code_quality_violations( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1471,7 +1538,7 @@ async def admin_code_quality_violations( async def admin_code_quality_violation_detail( request: Request, violation_id: int = Path(..., description="Violation ID"), - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1496,7 +1563,7 @@ async def admin_code_quality_violation_detail( @router.get("/platform-health", response_class=HTMLResponse, include_in_schema=False) async def admin_platform_health( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("platform-health", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ @@ -1520,7 +1587,7 @@ async def admin_platform_health( @router.get("/features", response_class=HTMLResponse, include_in_schema=False) async def admin_features_page( request: Request, - current_user: User = Depends(get_current_admin_from_cookie_or_header), + current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)), db: Session = Depends(get_db), ): """ diff --git a/app/templates/admin/platform-detail.html b/app/templates/admin/platform-detail.html index 0d8c42fb..bb38dc97 100644 --- a/app/templates/admin/platform-detail.html +++ b/app/templates/admin/platform-detail.html @@ -90,6 +90,37 @@ + +
+

+ + Super Admin + Admin Only + +

+
+ + + +
+

Module Configuration

+

Enable/disable features

+
+
+ + + + +
+

Menu Configuration

+

Admin & vendor menus

+
+
+
+
+
diff --git a/app/templates/admin/platform-modules.html b/app/templates/admin/platform-modules.html new file mode 100644 index 00000000..bc7968fb --- /dev/null +++ b/app/templates/admin/platform-modules.html @@ -0,0 +1,252 @@ +{# app/templates/admin/platform-modules.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} +{% from 'shared/macros/headers.html' import page_header %} + +{% block title %}Module Configuration{% endblock %} + +{% block alpine_data %}adminPlatformModules('{{ platform_code }}'){% endblock %} + +{% block content %} +{{ page_header('Module Configuration', back_url='/admin/platforms/' + platform_code) }} + +{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }} +{{ error_state('Error', show_condition='error') }} + + +
+
+
+

+

+ Enable or disable feature modules for this platform. Core modules cannot be disabled. +

+
+ +
+
+ + +
+
+
+ +
+
+

Total Modules

+

+
+
+ +
+
+ +
+
+

Enabled

+

+
+
+ +
+
+ +
+
+

Disabled

+

+
+
+ +
+
+ +
+
+

Core Modules

+

+
+
+
+ + +
+

+ Toggle modules on/off. Dependencies are resolved automatically. +

+
+ + +
+
+ + +
+ + Loading module configuration... +
+ + +
+ +
+
+
+
+ +

Core Modules

+ + Always Enabled + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +

Optional Modules

+
+ +
+
+
+ +
+
+ + +
+ +

No modules available.

+
+
+ +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/static/admin/js/platform-modules.js b/static/admin/js/platform-modules.js new file mode 100644 index 00000000..e12dbbcf --- /dev/null +++ b/static/admin/js/platform-modules.js @@ -0,0 +1,227 @@ +// static/admin/js/platform-modules.js +// Platform module configuration management + +const moduleConfigLog = window.LogConfig?.loggers?.moduleConfig || window.LogConfig?.createLogger?.('moduleConfig') || console; + +function adminPlatformModules(platformCode) { + // Get base data with safety check for standalone usage + const baseData = typeof data === 'function' ? data() : {}; + + return { + // Inherit base layout functionality from init-alpine.js + ...baseData, + + // Page-specific state + currentPage: 'platforms', + platformCode: platformCode, + loading: true, + error: null, + successMessage: null, + saving: false, + + // Data + platform: null, + moduleConfig: null, + + // Computed properties + get coreModules() { + if (!this.moduleConfig?.modules) return []; + return this.moduleConfig.modules.filter(m => m.is_core); + }, + + get optionalModules() { + if (!this.moduleConfig?.modules) return []; + return this.moduleConfig.modules.filter(m => !m.is_core); + }, + + get coreModulesCount() { + return this.coreModules.length; + }, + + get enabledOptionalCount() { + return this.optionalModules.filter(m => m.is_enabled).length; + }, + + // Module icons mapping + getModuleIcon(moduleCode) { + const icons = { + 'core': 'home', + 'platform-admin': 'building-office', + 'billing': 'credit-card', + 'inventory': 'archive-box', + 'orders': 'shopping-cart', + 'marketplace': 'shopping-bag', + 'customers': 'users', + 'cms': 'document-text', + 'analytics': 'chart-bar', + 'messaging': 'chat-bubble-left-right', + 'dev-tools': 'code-bracket', + 'monitoring': 'chart-bar-square' + }; + return icons[moduleCode] || 'puzzle'; + }, + + async init() { + moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZING ==='); + moduleConfigLog.info('Platform code:', this.platformCode); + + try { + await this.loadPlatform(); + await this.loadModuleConfig(); + moduleConfigLog.info('=== PLATFORM MODULES PAGE INITIALIZED ==='); + } catch (error) { + moduleConfigLog.error('Failed to initialize modules page:', error); + this.error = 'Failed to load page data. Please refresh.'; + } + }, + + async refresh() { + this.error = null; + this.successMessage = null; + await this.loadModuleConfig(); + }, + + async loadPlatform() { + try { + this.platform = await apiClient.get(`/admin/platforms/${this.platformCode}`); + moduleConfigLog.info('Loaded platform:', this.platform?.name); + } catch (error) { + moduleConfigLog.error('Failed to load platform:', error); + throw error; + } + }, + + async loadModuleConfig() { + this.loading = true; + this.error = null; + + try { + const platformId = this.platform?.id; + if (!platformId) { + throw new Error('Platform not loaded'); + } + + this.moduleConfig = await apiClient.get(`/admin/modules/platforms/${platformId}`); + moduleConfigLog.info('Loaded module config:', { + total: this.moduleConfig?.total, + enabled: this.moduleConfig?.enabled, + disabled: this.moduleConfig?.disabled + }); + } catch (error) { + moduleConfigLog.error('Failed to load module config:', error); + this.error = error.message || 'Failed to load module configuration'; + } finally { + this.loading = false; + } + }, + + async toggleModule(module) { + if (module.is_core) { + moduleConfigLog.warn('Cannot toggle core module:', module.code); + return; + } + + this.saving = true; + this.error = null; + this.successMessage = null; + + const action = module.is_enabled ? 'disable' : 'enable'; + + try { + const platformId = this.platform?.id; + const endpoint = `/admin/modules/platforms/${platformId}/${action}`; + + const result = await apiClient.post(endpoint, { + module_code: module.code + }); + + moduleConfigLog.info(`${action}d module:`, module.code, result); + + // Show success message + if (result.also_enabled?.length > 0) { + this.successMessage = `Module '${module.name}' enabled. Also enabled dependencies: ${result.also_enabled.join(', ')}`; + } else if (result.also_disabled?.length > 0) { + this.successMessage = `Module '${module.name}' disabled. Also disabled dependents: ${result.also_disabled.join(', ')}`; + } else { + this.successMessage = `Module '${module.name}' ${action}d successfully`; + } + + // Reload module config to get updated state + await this.loadModuleConfig(); + + // Clear success message after delay + setTimeout(() => { + this.successMessage = null; + }, 5000); + + } catch (error) { + moduleConfigLog.error(`Failed to ${action} module:`, error); + this.error = error.message || `Failed to ${action} module`; + } finally { + this.saving = false; + } + }, + + async enableAll() { + if (!confirm('This will enable all modules. Continue?')) { + return; + } + + this.saving = true; + this.error = null; + this.successMessage = null; + + try { + const platformId = this.platform?.id; + const result = await apiClient.post(`/admin/modules/platforms/${platformId}/enable-all`); + + moduleConfigLog.info('Enabled all modules:', result); + this.successMessage = `All ${result.enabled_count} modules enabled`; + + // Reload module config + await this.loadModuleConfig(); + + setTimeout(() => { + this.successMessage = null; + }, 5000); + + } catch (error) { + moduleConfigLog.error('Failed to enable all modules:', error); + this.error = error.message || 'Failed to enable all modules'; + } finally { + this.saving = false; + } + }, + + async disableOptional() { + if (!confirm('This will disable all optional modules, keeping only core modules. Continue?')) { + return; + } + + this.saving = true; + this.error = null; + this.successMessage = null; + + try { + const platformId = this.platform?.id; + const result = await apiClient.post(`/admin/modules/platforms/${platformId}/disable-optional`); + + moduleConfigLog.info('Disabled optional modules:', result); + this.successMessage = `Optional modules disabled. Core modules kept: ${result.core_modules.join(', ')}`; + + // Reload module config + await this.loadModuleConfig(); + + setTimeout(() => { + this.successMessage = null; + }, 5000); + + } catch (error) { + moduleConfigLog.error('Failed to disable optional modules:', error); + this.error = error.message || 'Failed to disable optional modules'; + } finally { + this.saving = false; + } + } + }; +}