fix: make FrontendType mandatory in require_module_access

The require_module_access dependency was using path-based detection to
determine admin vs vendor authentication, which failed for API routes
(/api/v1/admin/*) because it only checked for /admin/*.

Changes:
- Make frontend_type parameter mandatory (was optional with fallback)
- Remove path-based detection logic from require_module_access
- Update all 33 module route files to pass explicit FrontendType:
  - 15 admin routes use FrontendType.ADMIN
  - 18 vendor routes use FrontendType.VENDOR

This ensures authentication method is explicitly declared at route
definition time, making it independent of URL structure and future-proof
for API version changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 22:09:21 +01:00
parent 01e7602dcb
commit 9a0dd84035
34 changed files with 83 additions and 45 deletions

View File

@@ -54,6 +54,7 @@ from middleware.rate_limiter import RateLimiter
from app.modules.tenancy.models import User as UserModel
from app.modules.tenancy.models import Vendor
from models.schema.auth import UserContext
from app.modules.enums import FrontendType
# Initialize dependencies
security = HTTPBearer(auto_error=False) # auto_error=False prevents automatic 403
@@ -429,7 +430,7 @@ def get_admin_with_platform_context(
# ============================================================================
def require_module_access(module_code: str):
def require_module_access(module_code: str, frontend_type: FrontendType):
"""
Dependency factory for module-based route access control.
@@ -438,14 +439,18 @@ def require_module_access(module_code: str):
tied to a specific menu item.
Usage:
@router.get("/admin/billing/stripe-config")
async def stripe_config(
current_user: User = Depends(require_module_access("billing")),
):
...
admin_router = APIRouter(
dependencies=[Depends(require_module_access("messaging", FrontendType.ADMIN))]
)
vendor_router = APIRouter(
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))]
)
Args:
module_code: Module code to check (e.g., "billing", "marketplace")
frontend_type: Frontend type (ADMIN or VENDOR). Required to determine
which authentication method to use.
Returns:
Dependency function that validates module access and returns User
@@ -459,12 +464,13 @@ def require_module_access(module_code: str):
vendor_token: str | None = Cookie(None),
db: Session = Depends(get_db),
) -> UserContext:
# Try admin auth first, then vendor
user_context = None
platform_id = None
# Check if this is an admin request
if admin_token or (credentials and request.url.path.startswith("/admin")):
# Use explicit frontend_type to determine authentication method
is_admin_request = frontend_type == FrontendType.ADMIN
if is_admin_request:
try:
user_context = get_current_admin_from_cookie_or_header(
request, credentials, admin_token, db
@@ -477,12 +483,11 @@ def require_module_access(module_code: str):
platform = getattr(request.state, "admin_platform", None)
if platform:
platform_id = platform.id
# Note: token_platform_id is not on UserContext, would need to be added
except Exception:
pass
# Check if this is a vendor request
if not user_context and (vendor_token or (credentials and "/vendor/" in request.url.path)):
# Handle vendor request
if not user_context and frontend_type == FrontendType.VENDOR:
try:
user_context = get_current_vendor_from_cookie_or_header(
request, credentials, vendor_token, db

View File

@@ -25,11 +25,12 @@ from app.modules.analytics.schemas import (
VendorAnalyticsResponse,
)
from app.modules.billing.models import FeatureCode
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
router = APIRouter(
prefix="/analytics",
dependencies=[Depends(require_module_access("analytics"))],
dependencies=[Depends(require_module_access("analytics", FrontendType.VENDOR))],
)
vendor_router = router # Alias for discovery
logger = logging.getLogger(__name__)

View File

@@ -18,6 +18,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.billing.services import admin_subscription_service, subscription_service
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
from app.modules.billing.schemas import (
BillingHistoryListResponse,
@@ -39,7 +40,7 @@ logger = logging.getLogger(__name__)
# Admin router with module access control
admin_router = APIRouter(
prefix="/subscriptions",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.ADMIN))],
)

View File

@@ -20,11 +20,12 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.billing.services.feature_service import feature_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
admin_features_router = APIRouter(
prefix="/features",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -20,6 +20,7 @@ from app.api.deps import get_current_vendor_api, require_module_access
from app.core.config import settings
from app.core.database import get_db
from app.modules.billing.services import billing_service, subscription_service
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
logger = logging.getLogger(__name__)
@@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
# Vendor router with module access control
vendor_router = APIRouter(
prefix="/billing",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))],
)

View File

@@ -21,11 +21,12 @@ from app.api.deps import get_current_vendor_api, require_module_access
from app.core.config import settings
from app.core.database import get_db
from app.modules.billing.services import billing_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
vendor_addons_router = APIRouter(
prefix="/addons",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -22,10 +22,11 @@ from app.api.deps import get_current_vendor_api, require_module_access
from app.core.config import settings
from app.core.database import get_db
from app.modules.billing.services import billing_service, subscription_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
vendor_checkout_router = APIRouter(
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -26,11 +26,12 @@ from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.billing.exceptions import FeatureNotFoundError
from app.modules.billing.services.feature_service import feature_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
vendor_features_router = APIRouter(
prefix="/features",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -19,11 +19,12 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.analytics.services.usage_service import usage_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
vendor_usage_router = APIRouter(
prefix="/usage",
dependencies=[Depends(require_module_access("billing"))],
dependencies=[Depends(require_module_access("billing", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -19,6 +19,7 @@ from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.billing.services.subscription_service import subscription_service
from app.modules.catalog.services.vendor_product_service import vendor_product_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
from app.modules.catalog.schemas import (
CatalogVendor,
@@ -35,7 +36,7 @@ from app.modules.catalog.schemas import (
admin_router = APIRouter(
prefix="/vendor-products",
dependencies=[Depends(require_module_access("catalog"))],
dependencies=[Depends(require_module_access("catalog", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -18,6 +18,7 @@ from app.core.database import get_db
from app.modules.catalog.services.product_service import product_service
from app.modules.billing.services.subscription_service import subscription_service
from app.modules.catalog.services.vendor_product_service import vendor_product_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
from app.modules.catalog.schemas import (
ProductCreate,
@@ -33,7 +34,7 @@ from app.modules.catalog.schemas import (
vendor_router = APIRouter(
prefix="/products",
dependencies=[Depends(require_module_access("catalog"))],
dependencies=[Depends(require_module_access("catalog", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -12,6 +12,7 @@ Aggregates all admin CMS routes:
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from app.modules.enums import FrontendType
from .admin_content_pages import admin_content_pages_router
from .admin_images import admin_images_router
@@ -19,7 +20,7 @@ from .admin_media import admin_media_router
from .admin_vendor_themes import admin_vendor_themes_router
admin_router = APIRouter(
dependencies=[Depends(require_module_access("cms"))],
dependencies=[Depends(require_module_access("cms", FrontendType.ADMIN))],
)
# For backwards compatibility with existing imports

View File

@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.customers.services import admin_customer_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
from app.modules.customers.schemas import (
CustomerDetailResponse,
@@ -22,7 +23,7 @@ from app.modules.customers.schemas import (
# Create module-aware router
admin_router = APIRouter(
prefix="/customers",
dependencies=[Depends(require_module_access("customers"))],
dependencies=[Depends(require_module_access("customers", FrontendType.ADMIN))],
)

View File

@@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.customers.services import customer_service
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
from app.modules.customers.schemas import (
CustomerDetailResponse,
@@ -28,7 +29,7 @@ from app.modules.customers.schemas import (
# Create module-aware router
vendor_router = APIRouter(
prefix="/customers",
dependencies=[Depends(require_module_access("customers"))],
dependencies=[Depends(require_module_access("customers", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -20,6 +20,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.inventory.services.inventory_import_service import inventory_import_service
from app.modules.inventory.services.inventory_service import inventory_service
from app.modules.inventory.services.inventory_transaction_service import inventory_transaction_service
@@ -45,7 +46,7 @@ from app.modules.inventory.schemas import (
admin_router = APIRouter(
prefix="/inventory",
dependencies=[Depends(require_module_access("inventory"))],
dependencies=[Depends(require_module_access("inventory", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.inventory.services.inventory_service import inventory_service
from app.modules.inventory.services.inventory_transaction_service import inventory_transaction_service
from models.schema.auth import UserContext
@@ -33,7 +34,7 @@ from app.modules.inventory.schemas import (
vendor_router = APIRouter(
prefix="/inventory",
dependencies=[Depends(require_module_access("inventory"))],
dependencies=[Depends(require_module_access("inventory", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.loyalty.schemas import (
ProgramListResponse,
ProgramResponse,
@@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
# Admin router with module access control
admin_router = APIRouter(
prefix="/loyalty",
dependencies=[Depends(require_module_access("loyalty"))],
dependencies=[Depends(require_module_access("loyalty", FrontendType.ADMIN))],
)

View File

@@ -53,6 +53,7 @@ from app.modules.loyalty.services import (
stamp_service,
wallet_service,
)
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
logger = logging.getLogger(__name__)
@@ -60,7 +61,7 @@ logger = logging.getLogger(__name__)
# Vendor router with module access control
vendor_router = APIRouter(
prefix="/loyalty",
dependencies=[Depends(require_module_access("loyalty"))],
dependencies=[Depends(require_module_access("loyalty", FrontendType.VENDOR))],
)

View File

@@ -19,6 +19,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.exceptions import ResourceNotFoundException, ValidationException
from app.modules.enums import FrontendType
from app.modules.orders.exceptions import OrderHasUnresolvedExceptionsException
from app.modules.orders.services.order_item_exception_service import order_item_exception_service
from app.modules.marketplace.services.letzshop import (
@@ -65,7 +66,7 @@ from app.modules.marketplace.schemas import (
admin_letzshop_router = APIRouter(
prefix="/letzshop",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.marketplace.services.marketplace_import_job_service import marketplace_import_job_service
from app.modules.analytics.services.stats_service import stats_service
from app.modules.tenancy.services.vendor_service import vendor_service
@@ -29,7 +30,7 @@ from app.modules.analytics.schemas import ImportStatsResponse
admin_marketplace_router = APIRouter(
prefix="/marketplace-import-jobs",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -20,12 +20,13 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.marketplace.services.marketplace_product_service import marketplace_product_service
from models.schema.auth import UserContext
admin_products_router = APIRouter(
prefix="/products",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -30,6 +30,7 @@ from app.modules.marketplace.services.letzshop import (
LetzshopOrderService,
OrderNotFoundError,
)
from app.modules.enums import FrontendType
from models.schema.auth import UserContext
from app.modules.marketplace.schemas import (
FulfillmentConfirmRequest,
@@ -56,7 +57,7 @@ from app.modules.marketplace.schemas import (
vendor_letzshop_router = APIRouter(
prefix="/letzshop",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.marketplace.services.marketplace_import_job_service import marketplace_import_job_service
from app.modules.tenancy.services.vendor_service import vendor_service
from middleware.decorators import rate_limit
@@ -26,7 +27,7 @@ from app.modules.marketplace.schemas import (
vendor_marketplace_router = APIRouter(
prefix="/marketplace",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -20,6 +20,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.marketplace.services.onboarding_service import OnboardingService
from models.schema.auth import UserContext
from app.modules.marketplace.schemas import (
@@ -41,7 +42,7 @@ from app.modules.marketplace.schemas import (
vendor_onboarding_router = APIRouter(
prefix="/onboarding",
dependencies=[Depends(require_module_access("marketplace"))],
dependencies=[Depends(require_module_access("marketplace", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -11,13 +11,14 @@ Aggregates all admin messaging routes:
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from app.modules.enums import FrontendType
from .admin_messages import admin_messages_router
from .admin_notifications import admin_notifications_router
from .admin_email_templates import admin_email_templates_router
admin_router = APIRouter(
dependencies=[Depends(require_module_access("messaging"))],
dependencies=[Depends(require_module_access("messaging", FrontendType.ADMIN))],
)
# Aggregate all messaging admin routes

View File

@@ -12,6 +12,7 @@ Aggregates all vendor messaging routes:
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from app.modules.enums import FrontendType
from .vendor_messages import vendor_messages_router
from .vendor_notifications import vendor_notifications_router
@@ -19,7 +20,7 @@ from .vendor_email_settings import vendor_email_settings_router
from .vendor_email_templates import vendor_email_templates_router
vendor_router = APIRouter(
dependencies=[Depends(require_module_access("messaging"))],
dependencies=[Depends(require_module_access("messaging", FrontendType.VENDOR))],
)
# Aggregate all messaging vendor routes

View File

@@ -14,6 +14,7 @@ Aggregates all admin monitoring routes:
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from app.modules.enums import FrontendType
from .admin_logs import admin_logs_router
from .admin_tasks import admin_tasks_router
@@ -23,7 +24,7 @@ from .admin_audit import admin_audit_router
from .admin_platform_health import admin_platform_health_router
admin_router = APIRouter(
dependencies=[Depends(require_module_access("monitoring"))],
dependencies=[Depends(require_module_access("monitoring", FrontendType.ADMIN))],
)
# Aggregate all monitoring admin routes

View File

@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.orders.services.order_service import order_service
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
@@ -37,7 +38,7 @@ from app.modules.orders.schemas import (
# Base router for orders
_orders_router = APIRouter(
prefix="/orders",
dependencies=[Depends(require_module_access("orders"))],
dependencies=[Depends(require_module_access("orders", FrontendType.ADMIN))],
)
# Aggregate router that includes both orders and exceptions

View File

@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.orders.services.order_item_exception_service import order_item_exception_service
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
@@ -33,7 +34,7 @@ logger = logging.getLogger(__name__)
admin_exceptions_router = APIRouter(
prefix="/order-exceptions",
tags=["Order Item Exceptions"],
dependencies=[Depends(require_module_access("orders"))],
dependencies=[Depends(require_module_access("orders", FrontendType.ADMIN))],
)

View File

@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.orders.services.order_inventory_service import order_inventory_service
from app.modules.orders.services.order_service import order_service
from models.schema.auth import UserContext
@@ -29,7 +30,7 @@ from app.modules.orders.schemas import (
# Base router for orders
_orders_router = APIRouter(
prefix="/orders",
dependencies=[Depends(require_module_access("orders"))],
dependencies=[Depends(require_module_access("orders", FrontendType.VENDOR))],
)
# Aggregate router that includes both orders and exceptions

View File

@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.orders.services.order_item_exception_service import order_item_exception_service
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
@@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
vendor_exceptions_router = APIRouter(
prefix="/order-exceptions",
tags=["Vendor Order Item Exceptions"],
dependencies=[Depends(require_module_access("orders"))],
dependencies=[Depends(require_module_access("orders", FrontendType.VENDOR))],
)

View File

@@ -34,6 +34,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.billing.dependencies.feature_gate import RequireFeature
from app.modules.enums import FrontendType
from app.modules.orders.exceptions import (
InvoicePDFNotFoundException,
)
@@ -55,7 +56,7 @@ from app.modules.orders.schemas import (
vendor_invoices_router = APIRouter(
prefix="/invoices",
dependencies=[Depends(require_module_access("orders"))],
dependencies=[Depends(require_module_access("orders", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)

View File

@@ -13,10 +13,11 @@ import logging
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
from app.modules.enums import FrontendType
admin_router = APIRouter(
prefix="/payments",
dependencies=[Depends(require_module_access("payments"))],
dependencies=[Depends(require_module_access("payments", FrontendType.ADMIN))],
)
logger = logging.getLogger(__name__)

View File

@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.modules.enums import FrontendType
from app.modules.tenancy.services.vendor_service import vendor_service
from models.schema.auth import UserContext
from app.modules.payments.schemas import (
@@ -39,7 +40,7 @@ from app.modules.payments.schemas import (
vendor_router = APIRouter(
prefix="/payments",
dependencies=[Depends(require_module_access("payments"))],
dependencies=[Depends(require_module_access("payments", FrontendType.VENDOR))],
)
logger = logging.getLogger(__name__)