fix(auth): 完善权限控制与强制改密流程

This commit is contained in:
xy
2026-09-17 16:33:09 +08:00
parent 81922d774f
commit 61b4b19f55
22 changed files with 618 additions and 59 deletions
@@ -0,0 +1,197 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useLoginStore } from '@/stores/login'
const router = useRouter()
const loginStore = useLoginStore()
const currentPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const errorMessage = ref('')
const isSubmitting = ref(false)
function validatePassword() {
if (!currentPassword.value || !newPassword.value || !confirmPassword.value) {
return '请完整填写当前密码、新密码和确认密码'
}
if (newPassword.value.length < 12 || newPassword.value.length > 128) {
return '新密码长度必须为 12 至 128 位'
}
if (
!/[A-Z]/.test(newPassword.value) ||
!/[a-z]/.test(newPassword.value) ||
!/\d/.test(newPassword.value) ||
!/[^A-Za-z0-9]/.test(newPassword.value)
) {
return '新密码必须包含大写字母、小写字母、数字和特殊字符'
}
if (newPassword.value !== confirmPassword.value) {
return '两次输入的新密码不一致'
}
if (newPassword.value === currentPassword.value) {
return '新密码不能与当前密码相同'
}
return ''
}
async function handleSubmit() {
const validationMessage = validatePassword()
if (validationMessage) {
errorMessage.value = validationMessage
return
}
errorMessage.value = ''
isSubmitting.value = true
try {
await loginStore.changePassword({
currentPassword: currentPassword.value,
newPassword: newPassword.value,
})
await router.replace({ name: 'login', query: { passwordChanged: '1' } })
} catch (error: unknown) {
errorMessage.value =
error instanceof Error && error.message ? error.message : '密码修改失败,请稍后重试'
} finally {
isSubmitting.value = false
}
}
async function handleLogout() {
try {
await loginStore.logout()
} catch {
loginStore.clearSession()
}
await router.replace({ name: 'login' })
}
</script>
<template>
<main class="password-page">
<form class="password-card" novalidate @submit.prevent="handleSubmit">
<div class="password-brand">医签通</div>
<h1>首次登录,请修改密码</h1>
<p class="password-description">为保护患者信息,设置新密码后需要重新登录。</p>
<label>
<span>当前密码</span>
<input v-model="currentPassword" type="password" autocomplete="current-password" />
</label>
<label>
<span>新密码</span>
<input v-model="newPassword" type="password" autocomplete="new-password" />
</label>
<label>
<span>确认新密码</span>
<input v-model="confirmPassword" type="password" autocomplete="new-password" />
</label>
<p class="password-policy">12–128 位,须同时包含大小写字母、数字和特殊字符。</p>
<p v-if="errorMessage" class="password-error" role="alert">{{ errorMessage }}</p>
<button class="password-submit" type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '修改中…' : '修改密码并重新登录' }}
</button>
<button class="password-logout" type="button" :disabled="isSubmitting" @click="handleLogout">
退出当前账号
</button>
</form>
</main>
</template>
<style scoped>
.password-page {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: var(--login-gradient);
}
.password-card {
width: min(420px, calc(100% - 32px));
padding: 32px 36px;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-login);
}
.password-brand {
color: var(--brand);
font-size: 15px;
font-weight: 800;
text-align: center;
}
h1 {
margin: 8px 0;
color: var(--ink);
font-size: 22px;
text-align: center;
}
.password-description,
.password-policy {
color: var(--mut);
font-size: 12px;
line-height: 1.6;
text-align: center;
}
label {
display: block;
margin-top: 14px;
}
label span {
display: block;
margin-bottom: 6px;
color: var(--mut);
font-size: 12px;
}
input {
width: 100%;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 6px;
outline: none;
}
input:focus {
border-color: var(--brand);
}
.password-error {
color: var(--err);
font-size: 12px;
}
.password-submit,
.password-logout {
width: 100%;
padding: 10px;
border-radius: 7px;
}
.password-submit {
color: #fff;
font-weight: 700;
background: var(--brand);
}
.password-logout {
margin-top: 8px;
color: var(--mut);
background: transparent;
}
button:disabled {
cursor: wait;
opacity: 0.65;
}
</style>
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useLoginStore } from '@/stores/login'
const router = useRouter()
const loginStore = useLoginStore()
async function handleLogout() {
try {
await loginStore.logout()
} catch {
loginStore.clearSession()
}
await router.replace({ name: 'login' })
}
</script>
<template>
<main class="forbidden-page">
<section class="forbidden-card">
<span class="forbidden-code">403</span>
<h1>当前账号没有可访问的功能</h1>
<p>请联系系统管理员分配相应权限,或退出后更换账号。</p>
<button type="button" @click="handleLogout">退出登录</button>
</section>
</main>
</template>
<style scoped>
.forbidden-page {
display: grid;
min-height: 100vh;
padding: 24px;
background: var(--bg);
place-items: center;
}
.forbidden-card {
max-width: 460px;
padding: 36px;
text-align: center;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-login);
}
.forbidden-code {
color: var(--brand);
font-size: 36px;
font-weight: 800;
}
h1 {
color: var(--ink);
font-size: 20px;
}
p {
color: var(--mut);
font-size: 13px;
line-height: 1.7;
}
button {
margin-top: 12px;
padding: 9px 24px;
color: #fff;
font-weight: 700;
background: var(--brand);
border-radius: 7px;
}
</style>
+19 -2
View File
@@ -17,6 +17,7 @@ const captchaCode = ref('')
const captchaLoading = ref(false)
const errorMessage = ref('')
const isSubmitting = ref(false)
const passwordChanged = route.query.passwordChanged === '1'
async function loadCaptcha() {
captchaLoading.value = true
@@ -68,9 +69,11 @@ async function handleLogin() {
: {}),
}
await loginStore.login(payload)
const session = await loginStore.login(payload)
await router.replace(getRedirectPath())
await router.replace(
session.user.passwordChangeRequired ? { name: 'change-password' } : getRedirectPath(),
)
} catch (error: unknown) {
errorMessage.value = getAuthErrorMessage(error)
@@ -116,6 +119,10 @@ onMounted(() => {
<p class="login-subtitle">安全留痕 · 高效签署 · 医患无忧</p>
<p v-if="passwordChanged" class="login-success" role="status">
密码修改成功,请使用新密码重新登录
</p>
<div class="login-field">
<label for="login-username">账号</label>
<input
@@ -301,6 +308,16 @@ onMounted(() => {
font-size: 12px;
}
.login-success {
margin: 0 0 12px;
padding: 8px 10px;
color: #16794d;
font-size: 12px;
text-align: center;
background: #edf9f3;
border-radius: 7px;
}
.login-submit {
width: 100%;
margin-top: 6px;