fix: storefront login 403, cookie path, double-storefront URLs, and auth redirects
Some checks failed
CI / ruff (push) Successful in 9s
CI / pytest (push) Failing after 46m52s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 30s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- Extract store/platform context from Referer header for storefront API requests
  (StoreContextMiddleware and PlatformContextMiddleware) so login POST works in
  dev mode where API paths lack /platforms/{code}/ prefix
- Set customer token cookie path to "/" for cross-route compatibility
- Fix double storefront in URLs: replace {{ base_url }}storefront/ with {{ base_url }}
  across all 24 storefront templates
- Fix auth error redirect to include platform prefix and use store_code
- Update seed script to output correct storefront login URLs
- Add 20 new unit tests covering all fixes; fix 9 pre-existing test failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:29:52 +01:00
parent 32e4aa6564
commit f47c680cb8
38 changed files with 759 additions and 165 deletions

View File

@@ -178,23 +178,8 @@ def customer_login(
},
)
# Calculate cookie path based on store access method
store_context = getattr(request.state, "store_context", None)
access_method = (
store_context.get("detection_method", "unknown")
if store_context
else "unknown"
)
cookie_path = "/storefront"
if access_method == "path":
full_prefix = (
store_context.get("full_prefix", "/store/")
if store_context
else "/store/"
)
cookie_path = f"{full_prefix}{store.subdomain}/storefront"
# Set cookie with path=/ so it's sent with all requests
# (platform prefix varies between dev and prod, broad path avoids mismatch)
response.set_cookie(
key="customer_token",
value=login_result["token_data"]["access_token"],
@@ -202,12 +187,12 @@ def customer_login(
secure=should_use_secure_cookies(),
samesite="lax",
max_age=login_result["token_data"]["expires_in"],
path=cookie_path,
path="/",
)
logger.debug(
f"Set customer_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path={cookie_path}, httponly=True, secure={should_use_secure_cookies()})",
f"(path=/, httponly=True, secure={should_use_secure_cookies()})",
)
return CustomerLoginResponse(
@@ -237,25 +222,9 @@ def customer_logout(request: Request, response: Response):
},
)
store_context = getattr(request.state, "store_context", None)
access_method = (
store_context.get("detection_method", "unknown")
if store_context
else "unknown"
)
response.delete_cookie(key="customer_token", path="/")
cookie_path = "/storefront"
if access_method == "path" and store:
full_prefix = (
store_context.get("full_prefix", "/store/")
if store_context
else "/store/"
)
cookie_path = f"{full_prefix}{store.subdomain}/storefront"
response.delete_cookie(key="customer_token", path=cookie_path)
logger.debug(f"Deleted customer_token cookie (path={cookie_path})")
logger.debug("Deleted customer_token cookie (path=/)")
return LogoutResponse(message="Logged out successfully")

View File

@@ -153,14 +153,19 @@ async def shop_account_root(request: Request):
base_url = "/"
if access_method == "path" and store:
full_prefix = (
store_context.get("full_prefix", "/store/")
if store_context
else "/store/"
)
base_url = f"{full_prefix}{store.subdomain}/"
platform = getattr(request.state, "platform", None)
platform_original_path = getattr(request.state, "platform_original_path", None)
if platform and platform_original_path and platform_original_path.startswith("/platforms/"):
base_url = f"/platforms/{platform.code}/storefront/{store.store_code}/"
else:
full_prefix = (
store_context.get("full_prefix", "/storefront/")
if store_context
else "/storefront/"
)
base_url = f"{full_prefix}{store.store_code}/"
return RedirectResponse(url=f"{base_url}storefront/account/dashboard", status_code=302)
return RedirectResponse(url=f"{base_url}account/dashboard", status_code=302)
# ============================================================================

View File

@@ -400,7 +400,7 @@ function addressesPage() {
if (!response.ok) {
if (response.status === 401) {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
return;
}
throw new Error('Failed to load addresses');

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 }}storefront/account/orders"
<a href="{{ base_url }}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 }}storefront/account/profile"
<a href="{{ base_url }}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 }}storefront/account/addresses"
<a href="{{ base_url }}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,7 +67,7 @@
</a>
<!-- Messages Card -->
<a href="{{ base_url }}storefront/account/messages"
<a href="{{ base_url }}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/storefront/messages/unread-count').then(r => r.json()).then(d => unreadCount = d.unread_count).catch(() => {})">
@@ -157,14 +157,14 @@ function accountDashboard() {
// Redirect to login page
setTimeout(() => {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}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 }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
}, 1000);
}
})
@@ -173,7 +173,7 @@ function accountDashboard() {
this.showToast('Logout failed', 'error');
// Redirect anyway
setTimeout(() => {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
}, 1000);
});
}

View File

@@ -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 }}storefront/account/login">
href="{{ base_url }}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 }}storefront/">
href="{{ base_url }}">
← Continue shopping
</a>
</p>

View File

@@ -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 }}storefront/account/forgot-password"
<a href="{{ base_url }}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 }}storefront/account/register">
href="{{ base_url }}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 }}storefront/">
href="{{ base_url }}">
← Continue shopping
</a>
</p>
@@ -263,7 +263,7 @@
// Redirect to account page or return URL
setTimeout(() => {
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}storefront/account';
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}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 }}storefront/account/dashboard" class="hover:text-primary">My Account</a>
<a href="{{ base_url }}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,7 +330,7 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
@@ -344,7 +344,7 @@ function shopProfilePage() {
if (response.status === 401) {
localStorage.removeItem('customer_token');
localStorage.removeItem('customer_user');
window.location.href = '{{ base_url }}storefront/account/login?next=' + encodeURIComponent(window.location.pathname);
window.location.href = '{{ base_url }}account/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
throw new Error('Failed to load profile');
@@ -380,7 +380,7 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
return;
}
@@ -429,7 +429,7 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
return;
}
@@ -472,7 +472,7 @@ function shopProfilePage() {
try {
const token = localStorage.getItem('customer_token');
if (!token) {
window.location.href = '{{ base_url }}storefront/account/login';
window.location.href = '{{ base_url }}account/login';
return;
}

View File

@@ -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 }}storefront/account/login">
href="{{ base_url }}account/login">
Sign in instead
</a>
</p>
@@ -360,7 +360,7 @@
// Redirect to login after 2 seconds
setTimeout(() => {
window.location.href = '{{ base_url }}storefront/account/login?registered=true';
window.location.href = '{{ base_url }}account/login?registered=true';
}, 2000);
} catch (error) {

View File

@@ -80,7 +80,7 @@
Please request a new password reset link.
</p>
<a href="{{ base_url }}storefront/account/forgot-password"
<a href="{{ base_url }}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 }}storefront/account/login"
<a href="{{ base_url }}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 }}storefront/account/login">
href="{{ base_url }}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 }}storefront/">
href="{{ base_url }}">
← Continue shopping
</a>
</p>