diff --git a/client-v3/src/App.vue b/client-v3/src/App.vue index 77a78467..5b48ac76 100644 --- a/client-v3/src/App.vue +++ b/client-v3/src/App.vue @@ -111,9 +111,7 @@ {{ userStore.currentUser.username }} Settings - - Sign Out - + Sign Out @@ -401,6 +399,10 @@ async function switchServer(): Promise { window.location.reload(); } +async function handleLogout(): Promise { + await userStore.logout(); +} + async function goToLivePage(): Promise { const valid = await v$.value.$validate(); if (!valid) return; diff --git a/client-v3/src/components/show/config/script/ScriptEditor.vue b/client-v3/src/components/show/config/script/ScriptEditor.vue index b770119e..6e86ec12 100644 --- a/client-v3/src/components/show/config/script/ScriptEditor.vue +++ b/client-v3/src/components/show/config/script/ScriptEditor.vue @@ -121,14 +121,8 @@ - + Add Dialogue + Add Stage Direction diff --git a/client-v3/src/components/show/config/script/ScriptLineViewer.vue b/client-v3/src/components/show/config/script/ScriptLineViewer.vue index b8e0cfec..841d6f08 100644 --- a/client-v3/src/components/show/config/script/ScriptLineViewer.vue +++ b/client-v3/src/components/show/config/script/ScriptLineViewer.vue @@ -121,26 +121,26 @@ End - - Insert Dialogue - Insert Stage Direction + Edit - Insert Cue Line - Insert Spacing - Delete - + + Insert Dialogue + Insert Stage Direction + Insert Cue Line + Insert Spacing + Delete + + diff --git a/client-v3/src/composables/useWebSocket.ts b/client-v3/src/composables/useWebSocket.ts index ec5d1535..f41f0787 100644 --- a/client-v3/src/composables/useWebSocket.ts +++ b/client-v3/src/composables/useWebSocket.ts @@ -169,20 +169,24 @@ function connect(): void { ws = new WebSocket(wsURL); ws.onopen = async () => { - const wasErrored = errorCount > 0; - wsStore.$patch({ isConnected: true }); - if (wasErrored) { - toast.success( - `WebSocket reconnected after ${errorCount} attempt${errorCount > 1 ? 's' : ''}` - ); - } - log.info('WebSocket connected'); - if (wasErrored) { - const { useShowStore } = await import('@/stores/show'); - const showStore = useShowStore(); - if (showStore.currentSession != null) { - await showStore.getShowSessionData(); + try { + const wasErrored = errorCount > 0; + wsStore.$patch({ isConnected: true }); + if (wasErrored) { + toast.success( + `WebSocket reconnected after ${errorCount} attempt${errorCount > 1 ? 's' : ''}` + ); } + log.info('WebSocket connected'); + if (wasErrored) { + const { useShowStore } = await import('@/stores/show'); + const showStore = useShowStore(); + if (showStore.currentSession != null) { + await showStore.getShowSessionData(); + } + } + } catch (e) { + log.error('Error in WebSocket onopen handler:', e); } }; diff --git a/client-v3/src/js/http-interceptor.ts b/client-v3/src/js/http-interceptor.ts index f7b2f72b..933c4ac2 100644 --- a/client-v3/src/js/http-interceptor.ts +++ b/client-v3/src/js/http-interceptor.ts @@ -19,9 +19,29 @@ function buildAuthenticatedOptions( return { ...options, headers }; } +type QueueEntry = { + resolve: (r: Response) => void; + resource: string; + options: RequestInit & { headers: Record }; +}; + export default function setupHttpInterceptor(): void { const originalFetch = window.fetch; - const refreshState = { isRefreshing: false }; + const refreshState = { isRefreshing: false, queue: [] as QueueEntry[] }; + + function flushQueue(newToken: string | null): void { + const entries = refreshState.queue.splice(0); + entries.forEach(({ resolve, resource, options }) => { + if (newToken) { + originalFetch(resource, { + ...options, + headers: { ...options.headers, Authorization: `Bearer ${newToken}` }, + }).then(resolve); + } else { + resolve(new Response(JSON.stringify({ message: 'Session expired' }), { status: 401 })); + } + }); + } async function handle401Response( resource: string, @@ -30,16 +50,24 @@ export default function setupHttpInterceptor(): void { isRefreshRequest: boolean, response: Response ): Promise { - if (isRefreshRequest || refreshState.isRefreshing) { - log.warn('Token refresh failed with 401 or already refreshing, logging out'); + if (isRefreshRequest) { + log.warn('Token refresh request received 401, logging out'); + flushQueue(null); toast.warning('Your session has expired. Please log in again.'); await userStore.logout(); return response; } + if (refreshState.isRefreshing) { + return new Promise((resolve) => { + refreshState.queue.push({ resolve, resource, options: newOptions }); + }); + } + log.info('Attempting token refresh'); if (!userStore.authToken) { log.warn('401 received with no token present'); + flushQueue(null); await userStore.logout(); return response; } @@ -51,18 +79,21 @@ export default function setupHttpInterceptor(): void { if (!refreshSuccess) { log.warn('Token refresh failed, logging out'); + flushQueue(null); toast.warning('Your session has expired. Please log in again.'); await userStore.logout(); return response; } log.info('Token refresh successful, retrying original request'); + flushQueue(userStore.authToken); return await originalFetch(resource, { ...newOptions, headers: { ...newOptions.headers, Authorization: `Bearer ${userStore.authToken}` }, }); } catch (refreshError) { refreshState.isRefreshing = false; + flushQueue(null); log.error('Error during token refresh:', refreshError); toast.error('Authentication error - please log in again'); await userStore.logout(); diff --git a/client-v3/src/stores/show.ts b/client-v3/src/stores/show.ts index 39c2ea65..359a6ea2 100644 --- a/client-v3/src/stores/show.ts +++ b/client-v3/src/stores/show.ts @@ -753,7 +753,14 @@ export const useShowStore = defineStore('show', { await this.getScriptRevisions(); toast.success('Added new script revision!'); } else { - toast.error('Unable to add new script revision'); + let message = 'Unable to add new script revision'; + try { + const data = await response.json(); + if (data.message) message = data.message; + } catch { + /* non-JSON body */ + } + toast.error(message); } }, @@ -767,7 +774,14 @@ export const useShowStore = defineStore('show', { await this.getScriptRevisions(); toast.success('Deleted script revision!'); } else { - toast.error('Unable to delete script revision'); + let message = 'Unable to delete script revision'; + try { + const data = await response.json(); + if (data.message) message = data.message; + } catch { + /* non-JSON body */ + } + toast.error(message); } }, @@ -781,7 +795,14 @@ export const useShowStore = defineStore('show', { await this.getScriptRevisions(); toast.success('Loaded script revision!'); } else { - toast.error('Unable to load script revision'); + let message = 'Unable to load script revision'; + try { + const data = await response.json(); + if (data.message) message = data.message; + } catch { + /* non-JSON body */ + } + toast.error(message); } },