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

@@ -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';
}
}
};