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'