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

@@ -132,6 +132,43 @@ customers_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="dashboard",
label_key="storefront.account.dashboard",
icon="home",
route="storefront/account/dashboard",
order=10,
),
MenuItemDefinition(
id="profile",
label_key="storefront.account.profile",
icon="user",
route="storefront/account/profile",
order=20,
),
MenuItemDefinition(
id="addresses",
label_key="storefront.account.addresses",
icon="map-pin",
route="storefront/account/addresses",
order=30,
),
MenuItemDefinition(
id="settings",
label_key="storefront.account.settings",
icon="cog",
route="storefront/account/settings",
order=90,
),
],
),
],
},
is_core=True, # Customers is a core module - customer data is fundamental
# =========================================================================

View File

@@ -392,7 +392,7 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch('/api/v1/shop/addresses', {
const response = await fetch('/api/v1/storefront/addresses', {
headers: {
'Authorization': token ? `Bearer ${token}` : '',
}
@@ -400,7 +400,7 @@ function addressesPage() {
if (!response.ok) {
if (response.status === 401) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
throw new Error('Failed to load addresses');
@@ -461,8 +461,8 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const url = this.editingAddress
? `/api/v1/shop/addresses/${this.editingAddress.id}`
: '/api/v1/shop/addresses';
? `/api/v1/storefront/addresses/${this.editingAddress.id}`
: '/api/v1/storefront/addresses';
const method = this.editingAddress ? 'PUT' : 'POST';
const response = await fetch(url, {
@@ -500,7 +500,7 @@ function addressesPage() {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch(`/api/v1/shop/addresses/${this.deletingAddressId}`, {
const response = await fetch(`/api/v1/storefront/addresses/${this.deletingAddressId}`, {
method: 'DELETE',
headers: {
'Authorization': token ? `Bearer ${token}` : '',
@@ -525,7 +525,7 @@ function addressesPage() {
async setAsDefault(addressId) {
try {
const token = localStorage.getItem('customer_token');
const response = await fetch(`/api/v1/shop/addresses/${addressId}/default`, {
const response = await fetch(`/api/v1/storefront/addresses/${addressId}/default`, {
method: 'PUT',
headers: {
'Authorization': token ? `Bearer ${token}` : '',

View File

@@ -18,7 +18,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- Orders Card -->
<a href="{{ base_url }}shop/account/orders"
<a href="{{ base_url }}storefront/account/orders"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -36,7 +36,7 @@
</a>
<!-- Profile Card -->
<a href="{{ base_url }}shop/account/profile"
<a href="{{ base_url }}storefront/account/profile"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -53,7 +53,7 @@
</a>
<!-- Addresses Card -->
<a href="{{ base_url }}shop/account/addresses"
<a href="{{ base_url }}storefront/account/addresses"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
@@ -67,10 +67,10 @@
</a>
<!-- Messages Card -->
<a href="{{ base_url }}shop/account/messages"
<a href="{{ base_url }}storefront/account/messages"
class="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-6 border border-gray-200 dark:border-gray-700"
x-data="{ unreadCount: 0 }"
x-init="fetch('/api/v1/shop/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
x-init="fetch('/api/v1/storefront/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
<div class="flex items-center mb-4">
<div class="flex-shrink-0 relative">
<span class="h-8 w-8 text-primary" style="color: var(--color-primary)" x-html="$icon('chat-bubble-left', 'h-8 w-8')"></span>
@@ -141,7 +141,7 @@ function accountDashboard() {
// Close modal
this.showLogoutModal = false;
fetch('/api/v1/shop/auth/logout', {
fetch('/api/v1/storefront/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -157,14 +157,14 @@ function accountDashboard() {
// Redirect to login page
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 500);
} else {
console.error('Logout failed with status:', response.status);
this.showToast('Logout failed', 'error');
// Still redirect on failure (cookie might be deleted)
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 1000);
}
})
@@ -173,7 +173,7 @@ function accountDashboard() {
this.showToast('Logout failed', 'error');
// Redirect anyway
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
}, 1000);
});
}

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -143,13 +143,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -218,7 +218,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/forgot-password', {
const response = await fetch('/api/v1/storefront/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -38,7 +38,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -127,7 +127,7 @@
style="color: var(--color-primary);">
<span class="ml-2 text-gray-700 dark:text-gray-400">Remember me</span>
</label>
<a href="{{ base_url }}shop/account/forgot-password"
<a href="{{ base_url }}storefront/account/forgot-password"
class="text-sm font-medium hover:underline"
style="color: var(--color-primary);">
Forgot password?
@@ -150,13 +150,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Don't have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/register">
href="{{ base_url }}storefront/account/register">
Create an account
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -238,7 +238,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/login', {
const response = await fetch('/api/v1/storefront/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -263,7 +263,7 @@
// Redirect to account page or return URL
setTimeout(() => {
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}shop/account';
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}storefront/account';
window.location.href = returnUrl;
}, 1000);

View File

@@ -11,7 +11,7 @@
<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/account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}storefront/account/dashboard" class="hover:text-primary">My Account</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>
@@ -330,11 +330,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -344,7 +344,7 @@ function shopProfilePage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}shop/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load profile');
@@ -380,11 +380,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
@@ -429,11 +429,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile', {
const response = await fetch('/api/v1/storefront/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
@@ -472,11 +472,11 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}shop/account/login';
window.location.href = '{{ base_url }}storefront/account/login';
return;
}
const response = await fetch('/api/v1/shop/profile/password', {
const response = await fetch('/api/v1/storefront/profile/password', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -218,7 +218,7 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Already have an account?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in instead
</a>
</p>
@@ -341,7 +341,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/register', {
const response = await fetch('/api/v1/storefront/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -360,7 +360,7 @@
// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '{{ base_url }}shop/account/login?registered=true';
window.location.href = '{{ base_url }}storefront/account/login?registered=true';
}, 2000);
} catch (error) {

View File

@@ -39,7 +39,7 @@
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='storefront/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
@@ -80,7 +80,7 @@
Please request a new password reset link.
</p>
<a href="{{ base_url }}shop/account/forgot-password"
<a href="{{ base_url }}storefront/account/forgot-password"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Request New Link
</a>
@@ -164,7 +164,7 @@
You can now sign in with your new password.
</p>
<a href="{{ base_url }}shop/account/login"
<a href="{{ base_url }}storefront/account/login"
class="btn-primary-theme inline-block px-6 py-2 text-sm font-medium text-white rounded-lg">
Sign In
</a>
@@ -177,13 +177,13 @@
<span class="text-sm text-gray-600 dark:text-gray-400">Remember your password?</span>
<a class="text-sm font-medium hover:underline ml-1"
style="color: var(--color-primary);"
href="{{ base_url }}shop/account/login">
href="{{ base_url }}storefront/account/login">
Sign in
</a>
</p>
<p class="mt-2 text-center">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="{{ base_url }}shop/">
href="{{ base_url }}storefront/">
← Continue shopping
</a>
</p>
@@ -273,7 +273,7 @@
this.loading = true;
try {
const response = await fetch('/api/v1/shop/auth/reset-password', {
const response = await fetch('/api/v1/storefront/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'