diff --git a/src/api/client.ts b/src/api/client.ts index bb6b9bf..be18326 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -36,10 +36,18 @@ client.interceptors.response.use( // the global handler for those and let the caller deal with it. const url = error?.config?.url ?? '' const isAuthEndpoint = typeof url === 'string' && url.includes('/auth/') - if (error?.response?.status === 401 && !isAuthEndpoint) { + const status = error?.response?.status + if (!isAuthEndpoint && (status === 401 || status === 403)) { const auth = useAuthStore() - auth.clear() - router.replace({ name: 'login' }) + // 401 = unauthenticated. 403 with an expired token = the kit rejects a stale + // session as "forbidden" (the expired JWT falls back to anonymous). Either way + // the session is dead → sign out and bounce to login with an "expired" hint. + // A genuine 403 (valid token, missing permission) is left alone so the calling + // view can surface a "no permission" message instead of logging the user out. + if (status === 401 || auth.isExpired()) { + auth.clear() + router.replace({ name: 'login', query: { expired: '1' } }) + } } return Promise.reject(error) }, diff --git a/src/components/EmptyState.vue b/src/components/EmptyState.vue new file mode 100644 index 0000000..4cb92c7 --- /dev/null +++ b/src/components/EmptyState.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index d513cc0..8c59960 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -102,6 +102,7 @@ export default { password: 'Password', submit: 'Sign in', failed: 'Login failed', + sessionExpired: 'Your session expired. Please sign in again.', }, changePassword: { title: 'Change your password', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index b0ca8e4..f70429f 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -102,6 +102,7 @@ export default { password: '비밀번호', submit: '로그인', failed: '로그인 실패', + sessionExpired: '세션이 만료되었습니다. 다시 로그인해 주세요.', }, changePassword: { title: '비밀번호 변경', diff --git a/src/layout/AppLayout.vue b/src/layout/AppLayout.vue index 9f0295a..8fbaad4 100644 --- a/src/layout/AppLayout.vue +++ b/src/layout/AppLayout.vue @@ -108,7 +108,7 @@ function toggleLocale() { text rounded severity="secondary" - :aria-label="t('app.toggleLocale')" + v-tooltip.top="t('app.toggleLocale')" :aria-label="t('app.toggleLocale')" data-testid="locale-toggle" @click="toggleLocale" > @@ -119,7 +119,7 @@ function toggleLocale() { rounded :icon="ui.theme === 'dark' ? 'pi pi-sun' : 'pi pi-moon'" severity="secondary" - :aria-label="t('app.toggleTheme')" + v-tooltip.top="t('app.toggleTheme')" :aria-label="t('app.toggleTheme')" @click="ui.toggleTheme()" />