feat(auth): 首登强制改密页面 + 创建用户开关
- 独立改密页 + 路由守卫拦截待改密 - auth store 持久化 mustChangePassword - 用户创建表单加「首登强制改密」开关(默认勾选) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ce45d93ce5
commit
5de298a956
@ -5,6 +5,7 @@ export interface LoginSuccess {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresIn: number
|
||||
mustChangePassword: boolean
|
||||
}
|
||||
|
||||
export interface LoginNeedTenant {
|
||||
|
||||
@ -42,6 +42,7 @@ export interface UserUpsertData {
|
||||
phone?: string
|
||||
orgCode?: string | null
|
||||
status?: UserStatus
|
||||
mustChangePassword?: boolean
|
||||
}
|
||||
|
||||
/** 用户分页列表。 */
|
||||
|
||||
78
src/pages/force-change-password.vue
Normal file
78
src/pages/force-change-password.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: 'blank'
|
||||
freeAccess: true
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div class="force-pwd">
|
||||
<el-card class="force-pwd__card" shadow="never">
|
||||
<h2 class="force-pwd__title">修改初始密码</h2>
|
||||
<p class="force-pwd__tip">首次登录请先修改初始密码</p>
|
||||
<change-password-form ref="formRef" />
|
||||
<el-button type="primary" class="force-pwd__submit" :loading="loading" @click="onSubmit">
|
||||
确认修改
|
||||
</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import { changePassword } from '@/api/auth'
|
||||
import ChangePasswordForm from '@/components/change-password-form.vue'
|
||||
|
||||
interface FormApi {
|
||||
submit: () => Promise<{ oldPassword: string; newPassword: string }>
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const formRef = ref<FormApi>()
|
||||
const loading = ref(false)
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
let result: { oldPassword: string; newPassword: string } | undefined
|
||||
try {
|
||||
result = await formRef.value?.submit()
|
||||
} catch {
|
||||
return // 校验失败
|
||||
}
|
||||
if (result == null) return
|
||||
loading.value = true
|
||||
try {
|
||||
await changePassword(result.oldPassword, result.newPassword)
|
||||
ElMessage.success('密码已修改,请重新登录')
|
||||
auth.reset()
|
||||
router.push('/login')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.force-pwd {
|
||||
@apply flex min-h-screen items-center justify-center;
|
||||
background: var(--wm-bg-color-page);
|
||||
}
|
||||
|
||||
.force-pwd__card {
|
||||
@apply w-[420px];
|
||||
}
|
||||
|
||||
.force-pwd__title {
|
||||
@apply text-center text-lg font-semibold;
|
||||
}
|
||||
|
||||
.force-pwd__tip {
|
||||
@apply mb-md text-center text-sm text-gray-500;
|
||||
}
|
||||
|
||||
.force-pwd__submit {
|
||||
@apply mt-md w-full;
|
||||
}
|
||||
</style>
|
||||
@ -98,6 +98,12 @@ const formSchema = computed<JsonSchema>(() => ({
|
||||
minLength: 6,
|
||||
showOn: ['create'],
|
||||
},
|
||||
mustChangePassword: {
|
||||
type: 'boolean',
|
||||
title: '首登强制改密',
|
||||
default: true,
|
||||
showOn: ['create'],
|
||||
},
|
||||
nickname: { type: 'string', title: '昵称', placeholder: '请输入昵称' },
|
||||
email: { type: 'string', title: '邮箱', format: 'email', placeholder: '请输入邮箱' },
|
||||
phone: { type: 'string', title: '手机', placeholder: '请输入手机号' },
|
||||
|
||||
@ -10,6 +10,7 @@ import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import i18n, { loadLanguage } from '@/i18n'
|
||||
import { getAccessToken } from '@/utils/token'
|
||||
import { useMenuStore } from '@/stores/modules/menu'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -66,6 +67,12 @@ router.beforeEach(async (to, from, next) => {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
// 首登强制改密:待改密用户仅放行改密页,其余路由重定向
|
||||
const auth = useAuthStore()
|
||||
if (auth.mustChangePassword && to.path !== '/force-change-password') {
|
||||
next('/force-change-password')
|
||||
return
|
||||
}
|
||||
// 路由级权限:自由路径或 meta.freeAccess 放行,其余须在可访问菜单中
|
||||
if (FREE_PATHS.has(to.path) || to.meta.freeAccess === true) {
|
||||
next()
|
||||
|
||||
@ -2,11 +2,12 @@ import { computed, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { http } from '@/api/http';
|
||||
import { login as loginApi, type LoginResult } from '@/api/auth';
|
||||
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token';
|
||||
import { clearTokens, getAccessToken, getMustChangePassword, getRefreshToken, setMustChangePassword, setTokens } from '@/utils/token';
|
||||
|
||||
export const useAuthStore = defineStore('authStore', () => {
|
||||
const accessToken = ref<string | null>(getAccessToken());
|
||||
const refreshToken = ref<string | null>(getRefreshToken());
|
||||
const mustChangePassword = ref<boolean>(getMustChangePassword());
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
|
||||
@ -16,6 +17,8 @@ export const useAuthStore = defineStore('authStore', () => {
|
||||
setTokens(data.accessToken, data.refreshToken);
|
||||
accessToken.value = data.accessToken;
|
||||
refreshToken.value = data.refreshToken;
|
||||
mustChangePassword.value = data.mustChangePassword;
|
||||
setMustChangePassword(data.mustChangePassword);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@ -29,9 +32,7 @@ export const useAuthStore = defineStore('authStore', () => {
|
||||
// 忽略登出接口错误
|
||||
}
|
||||
}
|
||||
clearTokens();
|
||||
accessToken.value = null;
|
||||
refreshToken.value = null;
|
||||
reset();
|
||||
}
|
||||
|
||||
/** 本地清空会话状态(不调用登出接口,供 401 强退使用)。 */
|
||||
@ -39,7 +40,9 @@ export const useAuthStore = defineStore('authStore', () => {
|
||||
clearTokens();
|
||||
accessToken.value = null;
|
||||
refreshToken.value = null;
|
||||
mustChangePassword.value = false;
|
||||
setMustChangePassword(false);
|
||||
}
|
||||
|
||||
return { accessToken, refreshToken, isAuthenticated, login, logout, reset };
|
||||
return { accessToken, refreshToken, isAuthenticated, mustChangePassword, login, logout, reset };
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const ACCESS_KEY = 'sysmgr_access_token';
|
||||
const REFRESH_KEY = 'sysmgr_refresh_token';
|
||||
const MUST_CHANGE_KEY = 'sysmgr_must_change_password';
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
return localStorage.getItem(ACCESS_KEY);
|
||||
@ -18,3 +19,12 @@ export function clearTokens(): void {
|
||||
localStorage.removeItem(ACCESS_KEY);
|
||||
localStorage.removeItem(REFRESH_KEY);
|
||||
}
|
||||
|
||||
export function getMustChangePassword(): boolean {
|
||||
return sessionStorage.getItem(MUST_CHANGE_KEY) === '1';
|
||||
}
|
||||
|
||||
export function setMustChangePassword(v: boolean): void {
|
||||
if (v) sessionStorage.setItem(MUST_CHANGE_KEY, '1');
|
||||
else sessionStorage.removeItem(MUST_CHANGE_KEY);
|
||||
}
|
||||
|
||||
13
typed-router.d.ts
vendored
13
typed-router.d.ts
vendored
@ -51,6 +51,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/force-change-password': RouteRecordInfo<
|
||||
'/force-change-password',
|
||||
'/force-change-password',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/forgot-password': RouteRecordInfo<
|
||||
'/forgot-password',
|
||||
'/forgot-password',
|
||||
@ -207,6 +214,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/force-change-password.vue': {
|
||||
routes:
|
||||
| '/force-change-password'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/forgot-password.vue': {
|
||||
routes:
|
||||
| '/forgot-password'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user