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:
@@ -5,15 +5,18 @@ Core module admin API routes.
|
||||
Aggregates all admin core routes:
|
||||
- /dashboard/* - Admin dashboard and statistics
|
||||
- /settings/* - Platform settings management
|
||||
- /menu-config/* - Menu visibility configuration
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .admin_dashboard import admin_dashboard_router
|
||||
from .admin_settings import admin_settings_router
|
||||
from .admin_menu_config import router as admin_menu_config_router
|
||||
|
||||
admin_router = APIRouter()
|
||||
|
||||
# Aggregate all core admin routes
|
||||
admin_router.include_router(admin_dashboard_router, tags=["admin-dashboard"])
|
||||
admin_router.include_router(admin_settings_router, tags=["admin-settings"])
|
||||
admin_router.include_router(admin_menu_config_router, tags=["admin-menu-config"])
|
||||
|
||||
463
app/modules/core/routes/api/admin_menu_config.py
Normal file
463
app/modules/core/routes/api/admin_menu_config.py
Normal file
@@ -0,0 +1,463 @@
|
||||
# app/modules/core/routes/api/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 app.modules.enums 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,
|
||||
)
|
||||
@@ -23,7 +23,7 @@ from app.modules.tenancy.exceptions import ConfirmationRequiredException
|
||||
from app.modules.monitoring.services.admin_audit_service import admin_audit_service
|
||||
from app.modules.core.services.admin_settings_service import admin_settings_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.admin import (
|
||||
from app.modules.tenancy.schemas.admin import (
|
||||
AdminSettingCreate,
|
||||
AdminSettingDefaultResponse,
|
||||
AdminSettingListResponse,
|
||||
@@ -528,7 +528,7 @@ def update_email_settings(
|
||||
Settings are stored in the database and override .env values.
|
||||
Only non-null values are updated.
|
||||
"""
|
||||
from models.schema.admin import AdminSettingCreate
|
||||
from app.modules.tenancy.schemas.admin import AdminSettingCreate
|
||||
|
||||
updated_keys = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user