diff --git a/src/api/auth.ts b/src/api/auth.ts index 207365b..d50429b 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -5,6 +5,7 @@ export interface LoginSuccess { accessToken: string refreshToken: string expiresIn: number + mustChangePassword: boolean } export interface LoginNeedTenant { diff --git a/src/api/user.ts b/src/api/user.ts index 063fefc..4c13ea3 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -42,6 +42,7 @@ export interface UserUpsertData { phone?: string orgCode?: string | null status?: UserStatus + mustChangePassword?: boolean } /** 用户分页列表。 */ diff --git a/src/pages/force-change-password.vue b/src/pages/force-change-password.vue new file mode 100644 index 0000000..a1622ce --- /dev/null +++ b/src/pages/force-change-password.vue @@ -0,0 +1,78 @@ + +meta: + layout: 'blank' + freeAccess: true + + + + + + + diff --git a/src/pages/system/user/index.vue b/src/pages/system/user/index.vue index b6a22c9..4b59694 100644 --- a/src/pages/system/user/index.vue +++ b/src/pages/system/user/index.vue @@ -98,6 +98,12 @@ const formSchema = computed(() => ({ 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: '请输入手机号' }, diff --git a/src/router/index.ts b/src/router/index.ts index ccb08a7..1b46ffa 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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() diff --git a/src/stores/modules/auth.ts b/src/stores/modules/auth.ts index 2e95ceb..9435e39 100644 --- a/src/stores/modules/auth.ts +++ b/src/stores/modules/auth.ts @@ -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(getAccessToken()); const refreshToken = ref(getRefreshToken()); + const mustChangePassword = ref(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 }; }); diff --git a/src/utils/token.ts b/src/utils/token.ts index b206547..a3908d9 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -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); +} diff --git a/typed-router.d.ts b/typed-router.d.ts index f110b09..2f3c9dc 100644 --- a/typed-router.d.ts +++ b/typed-router.d.ts @@ -51,6 +51,13 @@ declare module 'vue-router/auto-routes' { Record, | never >, + '/force-change-password': RouteRecordInfo< + '/force-change-password', + '/force-change-password', + Record, + Record, + | 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'