diff --git a/clinical-web/src/router/index.ts b/clinical-web/src/router/index.ts index e0b424e..3c19eec 100644 --- a/clinical-web/src/router/index.ts +++ b/clinical-web/src/router/index.ts @@ -6,12 +6,27 @@ import ForbiddenView from '@/views/auth/ForbiddenView.vue' import LoginView from '@/views/auth/LoginView.vue' import MenuResourceView from '@/views/management/MenuResourceView.vue' import { useLoginStore } from '@/stores/login' +import { ApiResponseError } from '@/utils/api-response' import { readStoredAuthSession } from '@/utils/auth-storage' function firstAuthorizedPath(loginStore: ReturnType) { return loginStore.firstAuthorizedPath ?? '/forbidden' } +function safeRedirectPath(value: unknown) { + return typeof value === 'string' && value.startsWith('/') && !value.startsWith('//') + ? value + : null +} + +function isPasswordChangeRequired(error: unknown) { + return ( + error instanceof ApiResponseError && + String(error.code) === '40300' && + error.message === '请先修改密码' + ) +} + const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ @@ -65,6 +80,7 @@ const router = createRouter({ router.beforeEach(async (to) => { const loginStore = useLoginStore() + const requestedRedirect = safeRedirectPath(to.query.redirect) if (to.name === 'login' && loginStore.isLoggedIn) { if (!readStoredAuthSession()) { @@ -72,9 +88,14 @@ router.beforeEach(async (to) => { return true } - return loginStore.user?.passwordChangeRequired - ? { name: 'change-password' } - : firstAuthorizedPath(loginStore) + if (loginStore.user?.passwordChangeRequired) { + return { + name: 'change-password', + query: requestedRedirect ? { redirect: requestedRedirect } : undefined, + } + } + + return requestedRedirect ?? firstAuthorizedPath(loginStore) } if (to.meta.requiresAuth && !loginStore.isLoggedIn) { @@ -88,15 +109,22 @@ router.beforeEach(async (to) => { return true } + // The backend intentionally blocks /auth/me and /authorization/me until a + // forced password change is complete. The login response already supplies + // the authenticated user's requirement, so don't call those endpoints here. + if (to.name === 'change-password') { + return true + } + + if (loginStore.user?.passwordChangeRequired) { + return { name: 'change-password', query: { redirect: to.fullPath } } + } + try { const user = await loginStore.ensureCurrentUser() - if (user?.passwordChangeRequired && to.name !== 'change-password') { - return { name: 'change-password' } - } - - if (!user?.passwordChangeRequired && to.name === 'change-password') { - return firstAuthorizedPath(loginStore) + if (user?.passwordChangeRequired) { + return { name: 'change-password', query: { redirect: to.fullPath } } } if (to.name === 'menu-resource') { @@ -113,7 +141,13 @@ router.beforeEach(async (to) => { } return true - } catch { + } catch (error: unknown) { + // A forced-change response is an authenticated session state, not an + // expired session. Keep the token so the user can call the change endpoint. + if (isPasswordChangeRequired(error)) { + return { name: 'change-password', query: { redirect: to.fullPath } } + } + loginStore.clearSession() return { name: 'login', diff --git a/clinical-web/src/views/auth/ChangePasswordView.vue b/clinical-web/src/views/auth/ChangePasswordView.vue index b91705a..e0e2ad2 100644 --- a/clinical-web/src/views/auth/ChangePasswordView.vue +++ b/clinical-web/src/views/auth/ChangePasswordView.vue @@ -1,9 +1,10 @@