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

@@ -173,6 +173,22 @@ messaging_module = ModuleDefinition(
],
),
],
FrontendType.STOREFRONT: [
MenuSectionDefinition(
id="account",
label_key=None,
order=10,
items=[
MenuItemDefinition(
id="messages",
label_key="storefront.account.messages",
icon="chat-bubble-left-right",
route="storefront/account/messages",
order=50,
),
],
),
],
},
is_core=True, # Core module - email/notifications required for registration, password reset, etc.
# =========================================================================

View File

@@ -13,7 +13,7 @@
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<li class="inline-flex items-center">
<a href="{{ base_url }}shop/account/dashboard"
<a href="{{ base_url }}storefront/account/dashboard"
class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary dark:text-gray-400 dark:hover:text-white"
style="--hover-color: var(--color-primary)">
<span class="w-4 h-4 mr-2" x-html="$icon('home', 'w-4 h-4')"></span>
@@ -292,7 +292,7 @@ function shopMessages() {
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;
}
@@ -304,7 +304,7 @@ function shopMessages() {
params.append('status', this.statusFilter);
}
const response = await fetch(`/api/v1/shop/messages?${params}`, {
const response = await fetch(`/api/v1/storefront/messages?${params}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -313,7 +313,7 @@ function shopMessages() {
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 conversations');
@@ -335,11 +335,11 @@ function shopMessages() {
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/messages/${conversationId}`, {
const response = await fetch(`/api/v1/storefront/messages/${conversationId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -357,7 +357,7 @@ function shopMessages() {
});
// Update URL without reload
const url = `{{ base_url }}shop/account/messages/${conversationId}`;
const url = `{{ base_url }}storefront/account/messages/${conversationId}`;
history.pushState({}, '', url);
} catch (error) {
console.error('Error loading conversation:', error);
@@ -372,7 +372,7 @@ function shopMessages() {
const token = localStorage.getItem('customer_token');
if (!token) return;
const response = await fetch(`/api/v1/shop/messages/${this.selectedConversation.id}`, {
const response = await fetch(`/api/v1/storefront/messages/${this.selectedConversation.id}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -404,7 +404,7 @@ function shopMessages() {
this.loadConversations();
// Update URL
history.pushState({}, '', '{{ base_url }}shop/account/messages');
history.pushState({}, '', '{{ base_url }}storefront/account/messages');
},
async sendReply() {
@@ -415,7 +415,7 @@ function shopMessages() {
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;
}
@@ -425,7 +425,7 @@ function shopMessages() {
formData.append('attachments', file);
}
const response = await fetch(`/api/v1/shop/messages/${this.selectedConversation.id}/messages`, {
const response = await fetch(`/api/v1/storefront/messages/${this.selectedConversation.id}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`