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:
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user