diff --git a/web/src/api/request.ts b/web/src/api/request.ts index a0f81523..7341b4a8 100644 --- a/web/src/api/request.ts +++ b/web/src/api/request.ts @@ -6,7 +6,6 @@ const API_BASE = '/api' // Toast debounce mechanism - prevent toast spam (5 seconds) const toastDebounceMap = new Map() const TOAST_DEBOUNCE_TIME = 5000 -let sessionExpiredNotified = false function shouldShowToast(key: string): boolean { const now = Date.now() @@ -84,24 +83,10 @@ export async function request( const message = getErrorMessage(data, `HTTP ${response.status}`) const normalized = message.toLowerCase() const isNotAuthenticated = normalized.includes('not authenticated') - if (response.status === 401 && !sessionExpiredNotified) { - const isLoggedInElsewhere = normalized.includes('logged in elsewhere') - const isSessionExpired = normalized.includes('session expired') - if (isLoggedInElsewhere || isSessionExpired) { - sessionExpiredNotified = true - const titleKey = isLoggedInElsewhere ? 'auth.loggedInElsewhere' : 'auth.sessionExpired' - if (toastOnError && shouldShowToast('error_session_expired')) { - toast.error(t(titleKey), { - description: message, - duration: 3000, - }) - } - setTimeout(() => { - window.location.reload() - }, 1200) - } - } - if (toastOnError && shouldShowToast(toastKey) && !(response.status === 401 && isNotAuthenticated)) { + const isSessionExpired = normalized.includes('session expired') + const isLoggedInElsewhere = normalized.includes('logged in elsewhere') + const isAuthIssue = response.status === 401 && (isNotAuthenticated || isSessionExpired || isLoggedInElsewhere) + if (toastOnError && shouldShowToast(toastKey) && !isAuthIssue) { toast.error(t('api.operationFailed'), { description: message, duration: 4000, diff --git a/web/src/router/index.ts b/web/src/router/index.ts index a3eb97b9..2f617e4e 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -1,4 +1,7 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' +import { toast } from 'vue-sonner' +import i18n from '@/i18n' +import { ApiError } from '@/api/request' import { useAuthStore } from '@/stores/auth' const routes: RouteRecordRaw[] = [ @@ -33,6 +36,12 @@ const router = createRouter({ routes, }) +let sessionExpiredNotified = false + +function t(key: string, params?: Record): string { + return String(i18n.global.t(key, params as any)) +} + // Navigation guard router.beforeEach(async (to, _from, next) => { const authStore = useAuthStore() @@ -54,8 +63,21 @@ router.beforeEach(async (to, _from, next) => { if (!authStore.isAuthenticated) { try { await authStore.checkAuth() - } catch { + } catch (e) { // Not authenticated + if (e instanceof ApiError && e.status === 401 && !sessionExpiredNotified) { + const normalized = e.message.toLowerCase() + const isLoggedInElsewhere = normalized.includes('logged in elsewhere') + const isSessionExpired = normalized.includes('session expired') + if (isLoggedInElsewhere || isSessionExpired) { + sessionExpiredNotified = true + const titleKey = isLoggedInElsewhere ? 'auth.loggedInElsewhere' : 'auth.sessionExpired' + toast.error(t(titleKey), { + description: e.message, + duration: 3000, + }) + } + } } if (!authStore.isAuthenticated) { diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index ca8da71e..45684943 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -32,10 +32,14 @@ export const useAuthStore = defineStore('auth', () => { user.value = result.user || null isAdmin.value = result.is_admin ?? false return result - } catch { + } catch (e) { isAuthenticated.value = false user.value = null isAdmin.value = false + error.value = e instanceof Error ? e.message : 'Not authenticated' + if (e instanceof Error) { + throw e + } throw new Error('Not authenticated') } }