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

@@ -11,10 +11,10 @@
{# Breadcrumbs #}
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
<li><a href="{{ base_url }}shop/" class="hover:text-primary">Home</a></li>
<li><a href="{{ base_url }}storefront/" class="hover:text-primary">Home</a></li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
<a href="{{ base_url }}shop/cart" class="hover:text-primary">Cart</a>
<a href="{{ base_url }}storefront/cart" class="hover:text-primary">Cart</a>
</li>
<li class="flex items-center">
<span class="h-4 w-4 mx-2" x-html="$icon('chevron-right', 'h-4 w-4')"></span>
@@ -36,7 +36,7 @@
<span class="mx-auto h-12 w-12 text-gray-400 mb-4 block" x-html="$icon('shopping-bag', 'h-12 w-12 mx-auto')"></span>
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">Your cart is empty</h3>
<p class="text-gray-500 dark:text-gray-400 mb-6">Add some products before checking out.</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 text-white rounded-lg transition-colors" style="background-color: var(--color-primary)">
<a href="{{ base_url }}storefront/products" class="inline-block px-6 py-3 text-white rounded-lg transition-colors" style="background-color: var(--color-primary)">
Browse Products
</a>
</div>
@@ -384,8 +384,8 @@
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="item in cartItems" :key="item.product_id">
<div class="py-4 flex items-center gap-4">
<img loading="lazy" :src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<img loading="lazy" :src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
class="w-16 h-16 object-cover rounded-lg">
<div class="flex-1 min-w-0">
<p class="font-medium text-gray-900 dark:text-white truncate" x-text="item.name"></p>
@@ -428,8 +428,8 @@
<template x-for="item in cartItems" :key="item.product_id">
<div class="flex items-center gap-3">
<div class="relative">
<img loading="lazy" :src="item.image_url || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
<img loading="lazy" :src="item.image_url || '/static/storefront/img/placeholder.svg'"
@error="$el.src = '/static/storefront/img/placeholder.svg'"
class="w-12 h-12 object-cover rounded">
<span class="absolute -top-2 -right-2 w-5 h-5 bg-gray-500 text-white text-xs rounded-full flex items-center justify-center" x-text="item.quantity"></span>
</div>
@@ -611,7 +611,7 @@ function checkoutPage() {
async loadCustomerData() {
try {
const response = await fetch('/api/v1/shop/auth/me');
const response = await fetch('/api/v1/storefront/auth/me');
if (response.ok) {
const data = await response.json();
this.isLoggedIn = true;
@@ -639,7 +639,7 @@ function checkoutPage() {
async loadSavedAddresses() {
try {
const response = await fetch('/api/v1/shop/addresses');
const response = await fetch('/api/v1/storefront/addresses');
if (response.ok) {
const data = await response.json();
this.savedAddresses = data.addresses || [];
@@ -706,7 +706,7 @@ function checkoutPage() {
async loadCart() {
this.loading = true;
try {
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();
this.cartItems = data.items || [];
@@ -790,7 +790,7 @@ function checkoutPage() {
country_iso: this.shippingAddress.country_iso,
is_default: this.shippingAddresses.length === 0 // Make default if first address
};
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
@@ -820,7 +820,7 @@ function checkoutPage() {
country_iso: this.billingAddress.country_iso,
is_default: this.billingAddresses.length === 0 // Make default if first address
};
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
@@ -883,7 +883,7 @@ function checkoutPage() {
console.log('[CHECKOUT] Placing order:', orderData);
const response = await fetch('/api/v1/shop/orders', {
const response = await fetch('/api/v1/storefront/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -900,7 +900,7 @@ function checkoutPage() {
console.log('[CHECKOUT] Order placed:', order.order_number);
// Redirect to confirmation page
window.location.href = '{{ base_url }}shop/order-confirmation?order=' + order.order_number;
window.location.href = '{{ base_url }}storefront/order-confirmation?order=' + order.order_number;
} catch (error) {
console.error('[CHECKOUT] Error placing order:', error);