All checks were successful
Yesterday's redirectIfCustomerAreaUnauthorized was scoped to /account/*
only. Admin, store, and merchant pages still hit the same UX gap when
an AJAX call returned 401 on token expiry: apiClient cleared tokens
and threw, leaving the page in a broken state with whatever generic
error UI the caller had wired up — no redirect, no `?next=` round-trip,
identical bug to the customer flicker we fixed in `b04b36a2` /
`6564f138`.
Rename and dispatch by path:
- /account/* (not /account/login) → /account/login?next=…
- /admin/* (not /admin/login) → /admin/login?next=…
- /merchants/* (not /merchants/login) → /merchants/login?next=…
- /store/{code}/* (not /store/{code}/login) → /store/{code}/login?next=…
- anything else → return false (caller throws)
Store paths include the per-store code, so the helper does a small regex
to extract `{code}` from the current pathname and builds the persona's
login URL with the right prefix.
All three 401 handlers in apiClient (request, requestFormData, getBlob)
already wrap this with the `return new Promise(() => {})` pattern from
6564f138, so the caller's `.finally(() => loading = false)` doesn't fire
before navigation completes — kills the wrong-state UI flash on every
persona, not just customer.
Login pages updated to honour `?next=` precedence over the existing
`*_last_visited_page` localStorage fallback, with persona-specific
safety checks (must start with /admin/, /merchants/, /store/{code}/
respectively; must not be a login or onboarding URL). The store login
also normalises the basePath because the store-code path prefix can
flip between subdomain (/store/{code}/...) and dev/path-based
(/platforms/{platform}/store/{code}/...) modes.
Customer login already honoured `?next=` from bbb481aa; left unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
257 lines
10 KiB
JavaScript
257 lines
10 KiB
JavaScript
// app/static/store/js/login.js
|
|
// noqa: js-003 - Standalone login page without store layout
|
|
// noqa: js-004 - Standalone page has no currentPage sidebar highlight
|
|
/**
|
|
* Store login page logic
|
|
*/
|
|
|
|
// Create custom logger for store login page
|
|
const storeLoginLog = window.LogConfig.createLogger('STORE-LOGIN');
|
|
|
|
function languageSelector(currentLang, enabledLanguages) {
|
|
return {
|
|
currentLang: currentLang || 'fr',
|
|
languages: enabledLanguages || ['en', 'fr', 'de', 'lb'],
|
|
async setLanguage(lang) {
|
|
if (lang === this.currentLang) return;
|
|
try {
|
|
// noqa: JS-008 - Login page has no apiClient; raw fetch is intentional
|
|
await fetch('/api/v1/platform/language/set', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ language: lang }),
|
|
});
|
|
window.location.reload();
|
|
} catch (error) {
|
|
storeLoginLog.error('Failed to set language:', error);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function storeLogin() {
|
|
return {
|
|
credentials: {
|
|
username: '',
|
|
password: ''
|
|
},
|
|
store: null,
|
|
storeCode: null,
|
|
loading: false,
|
|
checked: false,
|
|
error: '',
|
|
success: '',
|
|
errors: {},
|
|
dark: false,
|
|
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._storeLoginInitialized) return;
|
|
window._storeLoginInitialized = true;
|
|
|
|
try {
|
|
storeLoginLog.info('=== STORE LOGIN PAGE INITIALIZING ===');
|
|
|
|
// Load theme
|
|
const theme = localStorage.getItem('theme');
|
|
if (theme === 'dark') {
|
|
this.dark = true;
|
|
}
|
|
storeLoginLog.debug('Dark mode:', this.dark);
|
|
|
|
// Get store code from URL path
|
|
// Supports both /store/{code}/login and /platforms/{platform}/store/{code}/login
|
|
const pathSegments = window.location.pathname.split('/').filter(Boolean);
|
|
const storeIndex = pathSegments.indexOf('store');
|
|
if (storeIndex !== -1 && pathSegments[storeIndex + 1]) {
|
|
this.storeCode = pathSegments[storeIndex + 1];
|
|
storeLoginLog.debug('Store code from URL:', this.storeCode);
|
|
await this.loadStore();
|
|
}
|
|
this.checked = true;
|
|
storeLoginLog.info('=== STORE LOGIN PAGE INITIALIZATION COMPLETE ===');
|
|
} catch (error) {
|
|
storeLoginLog.error('Failed to initialize login page:', error);
|
|
this.checked = true;
|
|
}
|
|
},
|
|
|
|
async loadStore() {
|
|
storeLoginLog.info('Loading store information...');
|
|
this.loading = true;
|
|
try {
|
|
const response = await apiClient.get(`/store/info/${this.storeCode}`);
|
|
this.store = response;
|
|
storeLoginLog.info('Store loaded successfully:', {
|
|
code: this.store.code,
|
|
name: this.store.name
|
|
});
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Load Store');
|
|
this.error = 'Failed to load store information';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async handleLogin() {
|
|
storeLoginLog.info('=== STORE LOGIN ATTEMPT STARTED ===');
|
|
this.clearErrors();
|
|
this.loading = true;
|
|
|
|
try {
|
|
if (!this.credentials.username) {
|
|
this.errors.username = 'Username is required';
|
|
}
|
|
if (!this.credentials.password) {
|
|
this.errors.password = 'Password is required';
|
|
}
|
|
|
|
if (Object.keys(this.errors).length > 0) {
|
|
storeLoginLog.warn('Validation failed:', this.errors);
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
|
|
storeLoginLog.info('Calling store login API...');
|
|
storeLoginLog.debug('Username:', this.credentials.username);
|
|
storeLoginLog.debug('Store code:', this.storeCode);
|
|
|
|
window.LogConfig.logApiCall('POST', '/store/auth/login', {
|
|
username: this.credentials.username,
|
|
store_code: this.storeCode
|
|
}, 'request');
|
|
|
|
const startTime = performance.now();
|
|
const response = await apiClient.post('/store/auth/login', {
|
|
email_or_username: this.credentials.username,
|
|
password: this.credentials.password,
|
|
store_code: this.storeCode,
|
|
platform_code: window.STORE_PLATFORM_CODE || localStorage.getItem('store_platform') || null
|
|
});
|
|
const duration = performance.now() - startTime;
|
|
|
|
window.LogConfig.logApiCall('POST', '/store/auth/login', {
|
|
hasToken: !!response.access_token,
|
|
user: response.user?.username
|
|
}, 'response');
|
|
window.LogConfig.logPerformance('Store Login', duration);
|
|
|
|
storeLoginLog.info('Login successful!');
|
|
storeLoginLog.debug('Storing authentication data...');
|
|
|
|
// Store token with correct key that apiClient expects
|
|
localStorage.setItem('store_token', response.access_token);
|
|
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
|
localStorage.setItem('storeCode', this.storeCode);
|
|
if (response.platform_code) {
|
|
localStorage.setItem('store_platform', response.platform_code);
|
|
}
|
|
storeLoginLog.debug('Token stored as store_token in localStorage');
|
|
|
|
this.success = 'Login successful! Redirecting...';
|
|
|
|
// Build platform-aware base path
|
|
const platformCode = window.STORE_PLATFORM_CODE;
|
|
const basePath = platformCode
|
|
? `/platforms/${platformCode}/store/${this.storeCode}`
|
|
: `/store/${this.storeCode}`;
|
|
|
|
// Decide where to land after login. Precedence:
|
|
// 1. ?next=<path> — set by apiClient on 401 mid-session;
|
|
// must be a /store/{code}/... URL that doesn't loop back
|
|
// to login or onboarding.
|
|
// 2. store_last_visited_page localStorage — fallback for
|
|
// organic logins (preserves the store-relative sub-path
|
|
// even if the basePath prefix changed, e.g. domain swap).
|
|
// 3. {basePath}/dashboard — last-resort default.
|
|
const nextParam = new URLSearchParams(window.location.search).get('next');
|
|
const lastPage = localStorage.getItem('store_last_visited_page');
|
|
const isSafeStoreUrl = (u) =>
|
|
u && u.includes(`/store/${this.storeCode}/`) &&
|
|
!u.includes('/login') && !u.includes('/onboarding');
|
|
|
|
let redirectTo = `${basePath}/dashboard`;
|
|
if (isSafeStoreUrl(nextParam)) {
|
|
redirectTo = nextParam;
|
|
} else if (lastPage && !lastPage.includes('/login') && !lastPage.includes('/onboarding')) {
|
|
// Preserve only the store-relative sub-path; basePath
|
|
// may have changed (subdomain ↔ /platforms/... etc.)
|
|
const storePathMatch = lastPage.match(/\/store\/[^/]+(\/.*)/);
|
|
if (storePathMatch) {
|
|
redirectTo = `${basePath}${storePathMatch[1]}`;
|
|
}
|
|
}
|
|
|
|
storeLoginLog.info('next param:', nextParam);
|
|
storeLoginLog.info('Last visited page:', lastPage);
|
|
storeLoginLog.info('Redirecting to:', redirectTo);
|
|
|
|
setTimeout(() => {
|
|
window.location.href = redirectTo;
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Store Login');
|
|
|
|
if (error.status === 401) {
|
|
this.error = 'Invalid username or password';
|
|
} else if (error.status === 403) {
|
|
this.error = 'Your account does not have access to this store';
|
|
} else {
|
|
this.error = error.message || 'Login failed. Please try again.';
|
|
}
|
|
storeLoginLog.info('Error message displayed to user:', this.error);
|
|
} finally {
|
|
this.loading = false;
|
|
storeLoginLog.info('=== STORE LOGIN ATTEMPT FINISHED ===');
|
|
}
|
|
},
|
|
|
|
// Forgot password state
|
|
rememberMe: false,
|
|
showForgotPassword: false,
|
|
forgotPasswordEmail: '',
|
|
forgotPasswordLoading: false,
|
|
|
|
async handleForgotPassword() {
|
|
storeLoginLog.info('=== FORGOT PASSWORD ATTEMPT ===');
|
|
if (!this.forgotPasswordEmail.trim()) {
|
|
this.error = 'Email is required';
|
|
return;
|
|
}
|
|
|
|
this.forgotPasswordLoading = true;
|
|
this.clearErrors();
|
|
|
|
try {
|
|
await apiClient.post('/store/auth/forgot-password', {
|
|
email: this.forgotPasswordEmail.trim()
|
|
});
|
|
this.success = 'If an account exists with this email, a password reset link has been sent.';
|
|
this.forgotPasswordEmail = '';
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'ForgotPassword');
|
|
this.error = error.message || 'Failed to send reset email. Please try again.';
|
|
} finally {
|
|
this.forgotPasswordLoading = false;
|
|
}
|
|
},
|
|
|
|
clearErrors() {
|
|
storeLoginLog.debug('Clearing form errors');
|
|
this.error = '';
|
|
this.errors = {};
|
|
},
|
|
|
|
toggleDarkMode() {
|
|
storeLoginLog.debug('Toggling dark mode...');
|
|
this.dark = !this.dark;
|
|
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
|
storeLoginLog.info('Dark mode:', this.dark ? 'ON' : 'OFF');
|
|
}
|
|
};
|
|
}
|
|
|
|
storeLoginLog.info('Store login module loaded');
|