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