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

@@ -6,7 +6,13 @@ This module provides shopping cart functionality for customer storefronts.
It is session-based and does not require customer authentication.
"""
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.base import (
MenuItemDefinition,
MenuSectionDefinition,
ModuleDefinition,
PermissionDefinition,
)
from app.modules.enums import FrontendType
# =============================================================================
# Router Lazy Imports
@@ -53,7 +59,24 @@ cart_module = ModuleDefinition(
),
],
# Cart is storefront-only - no admin/store menus needed
menu_items={},
menus={
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="actions",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="cart",
label_key="storefront.actions.cart",
icon="shopping-cart",
route="storefront/cart",
order=20,
),
],
),
],
},
)

View File

@@ -13,7 +13,7 @@
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
<a href="{{ base_url }}storefront/products" class="hover:text-primary">Products</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium">Cart</span>
</div>
@@ -39,7 +39,7 @@
<p class="text-gray-600 dark:text-gray-400 mb-6">
Add some products to get started!
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
Browse Products
</a>
</div>
@@ -55,8 +55,8 @@
{# Item Image #}
<div class="flex-shrink-0">
<img
:src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
:src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
:alt="item.name"
class="w-24 h-24 object-cover rounded-lg"
>
@@ -153,7 +153,7 @@
Proceed to Checkout
</button>
<a href="{{ base_url }}shop/products" class="block w-full px-6 py-3 text-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<a href="{{ base_url }}storefront/products" class="block w-full px-6 py-3 text-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
Continue Shopping
</a>
@@ -218,7 +218,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log(`[SHOP] Loading cart for session ${this.sessionId}...`);
const response = await fetch(`/api/v1/shop/cart/${this.sessionId}`);
const response = await fetch(`/api/v1/storefront/cart/${this.sessionId}`);
if (response.ok) {
const data = await response.json();
@@ -245,7 +245,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Updating quantity:', productId, newQuantity);
const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
method: 'PUT',
headers: {
@@ -280,7 +280,7 @@ document.addEventListener('alpine:init', () => {
try {
console.log('[SHOP] Removing item:', productId);
const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
method: 'DELETE'
}
@@ -307,9 +307,9 @@ document.addEventListener('alpine:init', () => {
if (!token) {
// Redirect to login with return URL
window.location.href = '{{ base_url }}shop/account/login?return={{ base_url }}shop/checkout';
window.location.href = '{{ base_url }}storefront/account/login?return={{ base_url }}storefront/checkout';
} else {
window.location.href = '{{ base_url }}shop/checkout';
window.location.href = '{{ base_url }}storefront/checkout';
}
}
};