diff --git a/src/api/auth.js b/src/api/auth.js index 5c37109..fd45a26 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -67,6 +67,7 @@ export async function logout(store) { application: TRYTON_APPLICATION, }, includeKitchen: false, + skipAuthFailureHandler: true, }); } } finally { diff --git a/src/api/client.js b/src/api/client.js index 0a01ffb..323aa2a 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -183,6 +183,32 @@ function logApiFailure(message, context) { console.error(message, context); } +function isAuthErrorStatus(status) { + return status === 401 || status === 403; +} + +function isKitchensPath(path) { + return String(path || '').replace(/^\/+/, '').replace(/\/+$/, '') === API_PATHS.kitchens; +} + +function shouldInvalidateValidatedSession(store, path, options = {}) { + if (options.skipAuthFailureHandler) { + return false; + } + + if (!store.session?.applicationKey || !store.session?.hasValidated) { + return false; + } + + return ( + isKitchensPath(path) || + options.includeKitchen !== false || + path === API_PATHS.items || + path === API_PATHS.locations || + String(path || '').startsWith(`${API_PATHS.items}/`) + ); +} + export async function apiRequest(store, path, options = {}) { const { config, session, activeKitchen } = store; @@ -240,6 +266,9 @@ export async function apiRequest(store, path, options = {}) { method, error, }); + if (shouldInvalidateValidatedSession(store, path, options)) { + window.__loncApp?.handleAuthFailure?.(networkError); + } throw networkError; } @@ -252,6 +281,15 @@ export async function apiRequest(store, path, options = {}) { status: response.status, payload, }); + if ( + isAuthErrorStatus(response.status) && + ( + shouldInvalidateValidatedSession(store, path, options) || + (store.session?.state === 'connected' && response.status === 403 && isKitchensPath(path)) + ) + ) { + window.__loncApp?.handleAuthFailure?.(apiError); + } throw apiError; } diff --git a/src/app/bootstrap.js b/src/app/bootstrap.js index d034f68..576792a 100644 --- a/src/app/bootstrap.js +++ b/src/app/bootstrap.js @@ -44,6 +44,7 @@ export function bootstrapApp() { store, outlet: document.querySelector('#route-view'), }); + let authFailureHandled = false; function applyKitchens(kitchens) { store.setKitchens(kitchens); @@ -67,6 +68,9 @@ export function bootstrapApp() { } else if (store.isConnected) { await window.__loncApp.refreshKitchens(); } + if (store.isConnected) { + authFailureHandled = false; + } renderNav(); } catch (error) { renderNav(); @@ -82,11 +86,36 @@ export function bootstrapApp() { } else if (store.isConnected) { await window.__loncApp.refreshKitchens(); } + if (store.isConnected) { + authFailureHandled = false; + } renderNav(); return result; }, + handleAuthFailure(error) { + if (!store.session?.applicationKey || !store.session?.hasValidated || authFailureHandled) { + return; + } + + authFailureHandled = true; + store.markSessionInvalid(); + renderNav(); + const status = error?.status || error?.cause?.status; + const message = + status === 401 || status === 403 + ? 'This application key is no longer accepted by Tryton. Please verify it again or disconnect and create a new key.' + : 'Authenticated requests are no longer succeeding. The application key may have been cancelled, or access is being denied by the server. Please reconnect or create a new key.'; + store.addAlert({ + type: 'warning', + timeout: 0, + message, + }); + navigate('/login'); + window.setTimeout(() => router.render(), 0); + }, async logout() { await logout(store); + authFailureHandled = false; renderNav(); navigate('/login'); }, diff --git a/src/app/store.js b/src/app/store.js index 49132b0..10ee154 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -108,5 +108,18 @@ export function createAppStore() { this.setKitchens([]); this.setActiveKitchen(null); }, + markSessionInvalid() { + if (!this.session) { + return; + } + + this.setSession({ + ...this.session, + state: CONNECTION_STATES.invalidKey, + hasValidated: true, + }); + this.setKitchens([]); + this.setActiveKitchen(null); + }, }; } diff --git a/src/features/labels/label-create-page.js b/src/features/labels/label-create-page.js index 9fc7bf7..e9faadc 100644 --- a/src/features/labels/label-create-page.js +++ b/src/features/labels/label-create-page.js @@ -515,6 +515,9 @@ export function labelCreatePageData(store) { ...loadLabelDraft(), }, async init() { + if (!store.isConnected) { + return; + } await this.loadLocations(); this.$watch('form', () => this.persistDraft(), { deep: true }); this.$watch('form.stockType', (value) => { @@ -536,6 +539,10 @@ export function labelCreatePageData(store) { }, 250); }, async loadLocations() { + if (!store.isConnected) { + return; + } + try { const { flat } = await fetchLocations(store); this.locations = flat; diff --git a/src/features/stock/stock-detail-page.js b/src/features/stock/stock-detail-page.js index 4f9886b..dd0af21 100644 --- a/src/features/stock/stock-detail-page.js +++ b/src/features/stock/stock-detail-page.js @@ -152,6 +152,10 @@ export function stockDetailPageData(store) { level: 'plenty', }, async init() { + if (!store.isConnected) { + return; + } + const { params } = getRouteContext(); await runAsyncState(this.state, async () => { this.entry = await getStockEntry(store, params.id); diff --git a/src/features/stock/stock-list-page.js b/src/features/stock/stock-list-page.js index 25d7cd5..fc5677d 100644 --- a/src/features/stock/stock-list-page.js +++ b/src/features/stock/stock-list-page.js @@ -442,9 +442,16 @@ export function stockListPageData(store) { location: '', }, async init() { + if (!store.isConnected) { + return; + } await Promise.all([this.loadLocations(), this.loadEntries()]); }, async loadEntries() { + if (!store.isConnected) { + return; + } + await runAsyncState(this.state, async () => { const loadedEntries = await listStockEntries(store); this.entries = sortEntries(loadedEntries); @@ -461,6 +468,10 @@ export function stockListPageData(store) { }).catch(() => {}); }, async loadLocations() { + if (!store.isConnected) { + return; + } + try { const { flat } = await fetchLocations(store); this.locations = flat;