fix(认证): 修复首次登录改密跳转
This commit is contained in:
@@ -6,12 +6,27 @@ import ForbiddenView from '@/views/auth/ForbiddenView.vue'
|
|||||||
import LoginView from '@/views/auth/LoginView.vue'
|
import LoginView from '@/views/auth/LoginView.vue'
|
||||||
import MenuResourceView from '@/views/management/MenuResourceView.vue'
|
import MenuResourceView from '@/views/management/MenuResourceView.vue'
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
|
import { ApiResponseError } from '@/utils/api-response'
|
||||||
import { readStoredAuthSession } from '@/utils/auth-storage'
|
import { readStoredAuthSession } from '@/utils/auth-storage'
|
||||||
|
|
||||||
function firstAuthorizedPath(loginStore: ReturnType<typeof useLoginStore>) {
|
function firstAuthorizedPath(loginStore: ReturnType<typeof useLoginStore>) {
|
||||||
return loginStore.firstAuthorizedPath ?? '/forbidden'
|
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({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
@@ -65,6 +80,7 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
const loginStore = useLoginStore()
|
const loginStore = useLoginStore()
|
||||||
|
const requestedRedirect = safeRedirectPath(to.query.redirect)
|
||||||
|
|
||||||
if (to.name === 'login' && loginStore.isLoggedIn) {
|
if (to.name === 'login' && loginStore.isLoggedIn) {
|
||||||
if (!readStoredAuthSession()) {
|
if (!readStoredAuthSession()) {
|
||||||
@@ -72,9 +88,14 @@ router.beforeEach(async (to) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return loginStore.user?.passwordChangeRequired
|
if (loginStore.user?.passwordChangeRequired) {
|
||||||
? { name: 'change-password' }
|
return {
|
||||||
: firstAuthorizedPath(loginStore)
|
name: 'change-password',
|
||||||
|
query: requestedRedirect ? { redirect: requestedRedirect } : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestedRedirect ?? firstAuthorizedPath(loginStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !loginStore.isLoggedIn) {
|
if (to.meta.requiresAuth && !loginStore.isLoggedIn) {
|
||||||
@@ -88,15 +109,22 @@ router.beforeEach(async (to) => {
|
|||||||
return true
|
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 {
|
try {
|
||||||
const user = await loginStore.ensureCurrentUser()
|
const user = await loginStore.ensureCurrentUser()
|
||||||
|
|
||||||
if (user?.passwordChangeRequired && to.name !== 'change-password') {
|
if (user?.passwordChangeRequired) {
|
||||||
return { name: 'change-password' }
|
return { name: 'change-password', query: { redirect: to.fullPath } }
|
||||||
}
|
|
||||||
|
|
||||||
if (!user?.passwordChangeRequired && to.name === 'change-password') {
|
|
||||||
return firstAuthorizedPath(loginStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.name === 'menu-resource') {
|
if (to.name === 'menu-resource') {
|
||||||
@@ -113,7 +141,13 @@ router.beforeEach(async (to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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()
|
loginStore.clearSession()
|
||||||
return {
|
return {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loginStore = useLoginStore()
|
const loginStore = useLoginStore()
|
||||||
|
|
||||||
@@ -13,6 +14,19 @@ const confirmPassword = ref('')
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
function getAfterChangeRedirect() {
|
||||||
|
const redirect = route.query.redirect
|
||||||
|
if (
|
||||||
|
typeof redirect === 'string' &&
|
||||||
|
redirect.startsWith('/') &&
|
||||||
|
!redirect.startsWith('//') &&
|
||||||
|
redirect !== '/change-password'
|
||||||
|
) {
|
||||||
|
return redirect
|
||||||
|
}
|
||||||
|
return '/workbench/home'
|
||||||
|
}
|
||||||
|
|
||||||
function validatePassword() {
|
function validatePassword() {
|
||||||
if (!currentPassword.value || !newPassword.value || !confirmPassword.value) {
|
if (!currentPassword.value || !newPassword.value || !confirmPassword.value) {
|
||||||
return '请完整填写当前密码、新密码和确认密码'
|
return '请完整填写当前密码、新密码和确认密码'
|
||||||
@@ -51,7 +65,10 @@ async function handleSubmit() {
|
|||||||
currentPassword: currentPassword.value,
|
currentPassword: currentPassword.value,
|
||||||
newPassword: newPassword.value,
|
newPassword: newPassword.value,
|
||||||
})
|
})
|
||||||
await router.replace({ name: 'login', query: { passwordChanged: '1' } })
|
await router.replace({
|
||||||
|
name: 'login',
|
||||||
|
query: { passwordChanged: '1', redirect: getAfterChangeRedirect() },
|
||||||
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
errorMessage.value =
|
errorMessage.value =
|
||||||
error instanceof Error && error.message ? error.message : '密码修改失败,请稍后重试'
|
error instanceof Error && error.message ? error.message : '密码修改失败,请稍后重试'
|
||||||
@@ -74,8 +91,14 @@ async function handleLogout() {
|
|||||||
<main class="password-page">
|
<main class="password-page">
|
||||||
<form class="password-card" novalidate @submit.prevent="handleSubmit">
|
<form class="password-card" novalidate @submit.prevent="handleSubmit">
|
||||||
<div class="password-brand">医签通</div>
|
<div class="password-brand">医签通</div>
|
||||||
<h1>首次登录,请修改密码</h1>
|
<h1>{{ loginStore.user?.passwordChangeRequired ? '首次登录,请修改密码' : '修改密码' }}</h1>
|
||||||
<p class="password-description">为保护患者信息,设置新密码后需要重新登录。</p>
|
<p class="password-description">
|
||||||
|
{{
|
||||||
|
loginStore.user?.passwordChangeRequired
|
||||||
|
? '为保护患者信息,设置新密码后需要重新登录。'
|
||||||
|
: '修改密码后需要重新登录。'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>当前密码</span>
|
<span>当前密码</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { getAuthErrorMessage, getCaptcha } from '@/api/auth'
|
import { getAuthErrorMessage, getCaptcha } from '@/api/auth'
|
||||||
@@ -18,6 +18,7 @@ const captchaLoading = ref(false)
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
const passwordChanged = route.query.passwordChanged === '1'
|
const passwordChanged = route.query.passwordChanged === '1'
|
||||||
|
const passwordChangeRequested = computed(() => route.query.redirect === '/change-password')
|
||||||
|
|
||||||
async function loadCaptcha() {
|
async function loadCaptcha() {
|
||||||
captchaLoading.value = true
|
captchaLoading.value = true
|
||||||
@@ -41,6 +42,13 @@ function getRedirectPath() {
|
|||||||
: '/workbench/home'
|
: '/workbench/home'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestPasswordChange() {
|
||||||
|
void router.replace({
|
||||||
|
name: 'login',
|
||||||
|
query: { ...route.query, redirect: '/change-password' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
const normalizedUsername = username.value.trim()
|
const normalizedUsername = username.value.trim()
|
||||||
|
|
||||||
@@ -70,9 +78,12 @@ async function handleLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await loginStore.login(payload)
|
const session = await loginStore.login(payload)
|
||||||
|
const redirect = getRedirectPath()
|
||||||
|
|
||||||
await router.replace(
|
await router.replace(
|
||||||
session.user.passwordChangeRequired ? { name: 'change-password' } : getRedirectPath(),
|
session.user.passwordChangeRequired
|
||||||
|
? { name: 'change-password', query: { redirect } }
|
||||||
|
: redirect,
|
||||||
)
|
)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
errorMessage.value = getAuthErrorMessage(error)
|
errorMessage.value = getAuthErrorMessage(error)
|
||||||
@@ -175,6 +186,12 @@ onMounted(() => {
|
|||||||
{{ isSubmitting ? '登录中…' : '登 录' }}
|
{{ isSubmitting ? '登录中…' : '登 录' }}
|
||||||
</button>
|
</button>
|
||||||
<p class="login-tip">请使用医院分配的账号登录</p>
|
<p class="login-tip">请使用医院分配的账号登录</p>
|
||||||
|
<p v-if="passwordChangeRequested" class="login-change-hint" role="status">
|
||||||
|
登录后将进入修改密码页面
|
||||||
|
</p>
|
||||||
|
<button class="login-change-link" type="button" @click="requestPasswordChange">
|
||||||
|
修改密码
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="login-footer">© 2026 医签通 MEDISIGN · 患者电子签署系统 V1.1 原型</p>
|
<p class="login-footer">© 2026 医签通 MEDISIGN · 患者电子签署系统 V1.1 原型</p>
|
||||||
@@ -348,6 +365,27 @@ onMounted(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-change-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-change-link {
|
||||||
|
display: block;
|
||||||
|
padding: 7px 12px;
|
||||||
|
margin: 5px auto 0;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-change-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.login-footer {
|
.login-footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 18px;
|
bottom: 18px;
|
||||||
|
|||||||
Reference in New Issue
Block a user