feat: storefront subscription access guard + module-driven nav + URL rename

Add StorefrontAccessMiddleware that blocks storefront access for stores
without an active subscription, returning a multilingual unavailable page
(en/fr/de/lb) for page requests and JSON 403 for API requests. Multi-platform
aware: resolves subscription for detected platform with fallback to primary.

Also includes yesterday's session work:
- Module-driven storefront navigation via FrontendType.STOREFRONT menu declarations
- shop/ → storefront/ URL rename across 30+ templates
- Subscription context (tier_code) passed to storefront templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 13:27:31 +01:00
parent 682213fdee
commit 2c710ad416
46 changed files with 1484 additions and 231 deletions

View File

@@ -146,6 +146,29 @@ def get_context_for_frontend(
f"[CONTEXT] {len(modules_with_providers)} modules have providers but none contributed"
)
# Pass enabled module codes to templates for conditional rendering
context["enabled_modules"] = enabled_module_codes
# For storefront, build nav menu structure from module declarations
if frontend_type == FrontendType.STOREFRONT:
from app.modules.core.services.menu_discovery_service import (
menu_discovery_service,
)
platform_id = platform.id if platform else None
sections = menu_discovery_service.get_menu_sections_for_frontend(
db, FrontendType.STOREFRONT, platform_id
)
# Build dict of section_id -> list of enabled items for easy template access
storefront_nav: dict[str, list] = {}
for section in sections:
enabled_items = [
item for item in section.items if item.is_module_enabled
]
if enabled_items:
storefront_nav[section.id] = enabled_items
context["storefront_nav"] = storefront_nav
# Add any extra context passed by the caller
if extra_context:
context.update(extra_context)
@@ -318,6 +341,10 @@ def get_storefront_context(
)
base_url = f"{full_prefix}{store.subdomain}/"
# Read subscription info set by StorefrontAccessMiddleware
subscription = getattr(request.state, "subscription", None)
subscription_tier = getattr(request.state, "subscription_tier", None)
# Build storefront-specific base context
storefront_base = {
"store": store,
@@ -325,6 +352,11 @@ def get_storefront_context(
"clean_path": clean_path,
"access_method": access_method,
"base_url": base_url,
"enabled_modules": set(),
"storefront_nav": {},
"subscription": subscription,
"subscription_tier": subscription_tier,
"tier_code": subscription_tier.code if subscription_tier else None,
}
# If no db session, return just the base context