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
|
accessToken: string
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
expiresIn: number
|
expiresIn: number
|
||||||
|
mustChangePassword: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginNeedTenant {
|
export interface LoginNeedTenant {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export interface UserUpsertData {
|
|||||||
phone?: string
|
phone?: string
|
||||||
orgCode?: string | null
|
orgCode?: string | null
|
||||||
status?: UserStatus
|
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,
|
minLength: 6,
|
||||||
showOn: ['create'],
|
showOn: ['create'],
|
||||||
},
|
},
|
||||||
|
mustChangePassword: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: '首登强制改密',
|
||||||
|
default: true,
|
||||||
|
showOn: ['create'],
|
||||||
|
},
|
||||||
nickname: { type: 'string', title: '昵称', placeholder: '请输入昵称' },
|
nickname: { type: 'string', title: '昵称', placeholder: '请输入昵称' },
|
||||||
email: { type: 'string', title: '邮箱', format: 'email', placeholder: '请输入邮箱' },
|
email: { type: 'string', title: '邮箱', format: 'email', placeholder: '请输入邮箱' },
|
||||||
phone: { type: 'string', title: '手机', placeholder: '请输入手机号' },
|
phone: { type: 'string', title: '手机', placeholder: '请输入手机号' },
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { setupLayouts } from 'virtual:generated-layouts'
|
|||||||
import i18n, { loadLanguage } from '@/i18n'
|
import i18n, { loadLanguage } from '@/i18n'
|
||||||
import { getAccessToken } from '@/utils/token'
|
import { getAccessToken } from '@/utils/token'
|
||||||
import { useMenuStore } from '@/stores/modules/menu'
|
import { useMenuStore } from '@/stores/modules/menu'
|
||||||
|
import { useAuthStore } from '@/stores/modules/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -66,6 +67,12 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
next('/login')
|
next('/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 首登强制改密:待改密用户仅放行改密页,其余路由重定向
|
||||||
|
const auth = useAuthStore()
|
||||||
|
if (auth.mustChangePassword && to.path !== '/force-change-password') {
|
||||||
|
next('/force-change-password')
|
||||||
|
return
|
||||||
|
}
|
||||||
// 路由级权限:自由路径或 meta.freeAccess 放行,其余须在可访问菜单中
|
// 路由级权限:自由路径或 meta.freeAccess 放行,其余须在可访问菜单中
|
||||||
if (FREE_PATHS.has(to.path) || to.meta.freeAccess === true) {
|
if (FREE_PATHS.has(to.path) || to.meta.freeAccess === true) {
|
||||||
next()
|
next()
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import { computed, ref } from 'vue';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { http } from '@/api/http';
|
import { http } from '@/api/http';
|
||||||
import { login as loginApi, type LoginResult } from '@/api/auth';
|
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', () => {
|
export const useAuthStore = defineStore('authStore', () => {
|
||||||
const accessToken = ref<string | null>(getAccessToken());
|
const accessToken = ref<string | null>(getAccessToken());
|
||||||
const refreshToken = ref<string | null>(getRefreshToken());
|
const refreshToken = ref<string | null>(getRefreshToken());
|
||||||
|
const mustChangePassword = ref<boolean>(getMustChangePassword());
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!accessToken.value);
|
const isAuthenticated = computed(() => !!accessToken.value);
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ export const useAuthStore = defineStore('authStore', () => {
|
|||||||
setTokens(data.accessToken, data.refreshToken);
|
setTokens(data.accessToken, data.refreshToken);
|
||||||
accessToken.value = data.accessToken;
|
accessToken.value = data.accessToken;
|
||||||
refreshToken.value = data.refreshToken;
|
refreshToken.value = data.refreshToken;
|
||||||
|
mustChangePassword.value = data.mustChangePassword;
|
||||||
|
setMustChangePassword(data.mustChangePassword);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -29,9 +32,7 @@ export const useAuthStore = defineStore('authStore', () => {
|
|||||||
// 忽略登出接口错误
|
// 忽略登出接口错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearTokens();
|
reset();
|
||||||
accessToken.value = null;
|
|
||||||
refreshToken.value = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 本地清空会话状态(不调用登出接口,供 401 强退使用)。 */
|
/** 本地清空会话状态(不调用登出接口,供 401 强退使用)。 */
|
||||||
@ -39,7 +40,9 @@ export const useAuthStore = defineStore('authStore', () => {
|
|||||||
clearTokens();
|
clearTokens();
|
||||||
accessToken.value = null;
|
accessToken.value = null;
|
||||||
refreshToken.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 ACCESS_KEY = 'sysmgr_access_token';
|
||||||
const REFRESH_KEY = 'sysmgr_refresh_token';
|
const REFRESH_KEY = 'sysmgr_refresh_token';
|
||||||
|
const MUST_CHANGE_KEY = 'sysmgr_must_change_password';
|
||||||
|
|
||||||
export function getAccessToken(): string | null {
|
export function getAccessToken(): string | null {
|
||||||
return localStorage.getItem(ACCESS_KEY);
|
return localStorage.getItem(ACCESS_KEY);
|
||||||
@ -18,3 +19,12 @@ export function clearTokens(): void {
|
|||||||
localStorage.removeItem(ACCESS_KEY);
|
localStorage.removeItem(ACCESS_KEY);
|
||||||
localStorage.removeItem(REFRESH_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>,
|
Record<never, 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': RouteRecordInfo<
|
||||||
'/forgot-password',
|
'/forgot-password',
|
||||||
'/forgot-password',
|
'/forgot-password',
|
||||||
@ -207,6 +214,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/force-change-password.vue': {
|
||||||
|
routes:
|
||||||
|
| '/force-change-password'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/forgot-password.vue': {
|
'src/pages/forgot-password.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/forgot-password'
|
| '/forgot-password'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user