@liujitcn/kratos-admin-core 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/package/declarations/src/api/base/{config.d.ts → v1/config.d.ts} +1 -1
- package/dist/package/declarations/src/api/base/{file.d.ts → v1/file.d.ts} +3 -1
- package/dist/package/declarations/src/api/base/{language.d.ts → v1/language.d.ts} +1 -1
- package/dist/package/declarations/src/api/base/{login.d.ts → v1/login.d.ts} +2 -2
- package/dist/package/declarations/src/api/base/{mfa.d.ts → v1/mfa.d.ts} +3 -3
- package/dist/package/declarations/src/api/base/{oauth.d.ts → v1/oauth.d.ts} +1 -1
- package/dist/package/declarations/src/api/system/{auth.d.ts → admin/v1/auth.d.ts} +2 -2
- package/dist/package/declarations/src/api/system/{base_dict.d.ts → admin/v1/base_dict.d.ts} +1 -1
- package/dist/package/declarations/src/api/system/{base_tenant.d.ts → admin/v1/base_tenant.d.ts} +2 -2
- package/dist/package/declarations/src/components/ProTable/index.vue.d.ts +114 -0
- package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +84 -76
- package/dist/package/declarations/src/components/Upload/File.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/WangEditor/index.vue.d.ts +2 -0
- package/dist/package/declarations/src/documentTitle.d.ts +2 -0
- package/dist/package/declarations/src/format.d.ts +1 -1
- package/dist/package/declarations/src/index.d.ts +2 -1
- package/dist/package/declarations/src/locales/generated.d.ts +40 -0
- package/dist/package/declarations/src/modules/index.d.ts +1 -3
- package/dist/package/declarations/src/request.d.ts +1 -1
- package/dist/package/declarations/src/routers/index.d.ts +1 -1
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +5 -3
- package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +13 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +0 -101
- package/dist/package/declarations/src/rpc/system/common/v1/common.d.ts +26 -0
- package/dist/package/declarations/src/stores/interface/index.d.ts +6 -1
- package/dist/package/declarations/src/stores/modules/user.d.ts +2 -4
- package/dist/package/declarations/src/utils/passwordStrength.d.ts +1 -12
- package/dist/package/declarations/src/utils/request.d.ts +13 -2
- package/dist/package/declarations/src/utils/utils.d.ts +4 -0
- package/dist/package/src/api/base/{config.ts → v1/config.ts} +2 -2
- package/dist/package/src/api/base/{file.ts → v1/file.ts} +20 -24
- package/dist/package/src/api/base/{language.ts → v1/language.ts} +2 -2
- package/dist/package/src/api/base/{login.ts → v1/login.ts} +3 -3
- package/dist/package/src/api/base/{mfa.ts → v1/mfa.ts} +4 -4
- package/dist/package/src/api/base/{oauth.ts → v1/oauth.ts} +2 -2
- package/dist/package/src/api/system/{auth.ts → admin/v1/auth.ts} +3 -3
- package/dist/package/src/api/system/{base_dict.ts → admin/v1/base_dict.ts} +2 -2
- package/dist/package/src/api/system/{base_tenant.ts → admin/v1/base_tenant.ts} +3 -3
- package/dist/package/src/bootstrap.ts +6 -5
- package/dist/package/src/components/CronExpression/index.vue +4 -3
- package/dist/package/src/components/Dialog/ProDialog.vue +1 -0
- package/dist/package/src/components/ImportExcel/index.vue +4 -2
- package/dist/package/src/components/MarkdownPreview/index.vue +1 -1
- package/dist/package/src/components/PasswordStrength/index.vue +1 -1
- package/dist/package/src/components/ProTable/components/TableColumn.vue +14 -6
- package/dist/package/src/components/ProTable/index.vue +3 -3
- package/dist/package/src/components/RichTextPreview/index.vue +12 -1
- package/dist/package/src/components/SelectIcon/index.vue +3 -2
- package/dist/package/src/components/Upload/File.vue +2 -2
- package/dist/package/src/components/Upload/Files.vue +2 -2
- package/dist/package/src/components/Upload/Img.vue +7 -5
- package/dist/package/src/components/Upload/Imgs.vue +6 -5
- package/dist/package/src/components/WangEditor/index.vue +10 -7
- package/dist/package/src/documentTitle.ts +8 -0
- package/dist/package/src/format.ts +1 -1
- package/dist/package/src/index.ts +2 -1
- package/dist/package/src/layouts/components/Header/components/Avatar.vue +7 -6
- package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +3 -2
- package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +3 -2
- package/dist/package/src/layouts/components/Header/components/SearchMenu.vue +16 -8
- package/dist/package/src/layouts/components/LockScreen.vue +175 -52
- package/dist/package/src/layouts/components/Tabs/index.vue +12 -3
- package/dist/package/src/layouts/components/ThemeDrawer/index.scss +4 -4
- package/dist/package/src/locales/en-US.json +8 -2
- package/dist/package/src/locales/ja-JP.json +8 -2
- package/dist/package/src/locales/zh-CN.json +8 -2
- package/dist/package/src/locales/zh-TW.json +8 -2
- package/dist/package/src/modules/index.ts +2 -4
- package/dist/package/src/request.ts +3 -0
- package/dist/package/src/routers/index.ts +15 -14
- package/dist/package/src/rpc/base/v1/login.ts +4 -2
- package/dist/package/src/rpc/system/admin/v1/auth.ts +13 -0
- package/dist/package/src/rpc/system/admin/v1/base_dict.ts +0 -112
- package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
- package/dist/package/src/stores/interface/index.ts +6 -1
- package/dist/package/src/stores/modules/auth.ts +1 -1
- package/dist/package/src/stores/modules/config.ts +2 -2
- package/dist/package/src/stores/modules/dict.ts +1 -1
- package/dist/package/src/stores/modules/user.ts +21 -22
- package/dist/package/src/styles/element.scss +35 -4
- package/dist/package/src/utils/passwordCrypto.ts +1 -1
- package/dist/package/src/utils/passwordStrength.ts +7 -24
- package/dist/package/src/utils/request.ts +155 -36
- package/dist/package/src/utils/tenant.ts +1 -1
- package/dist/package/src/utils/utils.ts +25 -6
- package/dist/package/src/views/login/components/LoginForm.vue +32 -20
- package/dist/package/src/views/login/index.scss +2 -2
- package/package.json +1 -1
- package/vite.config.ts +1 -0
|
@@ -9,12 +9,10 @@ export interface PasswordStrengthResult {
|
|
|
9
9
|
strengthScore: number;
|
|
10
10
|
/** 强度等级。 */
|
|
11
11
|
level: PasswordStrengthLevel;
|
|
12
|
-
/** 是否达到允许提交的最高强度。 */
|
|
13
|
-
isValid: boolean;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* 计算密码强度结果,供表单展示使用;最终规则由服务端密码策略校验。
|
|
18
16
|
*
|
|
19
17
|
* @param password 当前密码
|
|
20
18
|
* @returns 密码强度分析结果
|
|
@@ -24,14 +22,13 @@ export function getPasswordStrength(password?: string): PasswordStrengthResult {
|
|
|
24
22
|
return {
|
|
25
23
|
ruleScore: 0,
|
|
26
24
|
strengthScore: 0,
|
|
27
|
-
level: "empty"
|
|
28
|
-
isValid: false
|
|
25
|
+
level: "empty"
|
|
29
26
|
};
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
let ruleScore = 0;
|
|
33
|
-
if (password
|
|
34
|
-
if (/[
|
|
30
|
+
if (/[a-z]/.test(password)) ruleScore += 1;
|
|
31
|
+
if (/[A-Z]/.test(password)) ruleScore += 1;
|
|
35
32
|
if (/\d/.test(password)) ruleScore += 1;
|
|
36
33
|
if (/[^A-Za-z0-9]/.test(password)) ruleScore += 1;
|
|
37
34
|
|
|
@@ -39,33 +36,19 @@ export function getPasswordStrength(password?: string): PasswordStrengthResult {
|
|
|
39
36
|
return {
|
|
40
37
|
ruleScore,
|
|
41
38
|
strengthScore: 3,
|
|
42
|
-
level: "high"
|
|
43
|
-
isValid: true
|
|
39
|
+
level: "high"
|
|
44
40
|
};
|
|
45
41
|
}
|
|
46
42
|
if (ruleScore === 3) {
|
|
47
43
|
return {
|
|
48
44
|
ruleScore,
|
|
49
45
|
strengthScore: 2,
|
|
50
|
-
level: "medium"
|
|
51
|
-
isValid: false
|
|
46
|
+
level: "medium"
|
|
52
47
|
};
|
|
53
48
|
}
|
|
54
49
|
return {
|
|
55
50
|
ruleScore,
|
|
56
51
|
strengthScore: 1,
|
|
57
|
-
level: "low"
|
|
58
|
-
isValid: false
|
|
52
|
+
level: "low"
|
|
59
53
|
};
|
|
60
54
|
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 校验密码是否达到最高强度,便于表单规则直接复用。
|
|
64
|
-
*
|
|
65
|
-
* @param password 当前密码
|
|
66
|
-
* @returns 校验结果
|
|
67
|
-
*/
|
|
68
|
-
export function validatePasswordStrengthValue(password?: string) {
|
|
69
|
-
const result = getPasswordStrength(password);
|
|
70
|
-
return { valid: result.isValid };
|
|
71
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import axios, { type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from "axios";
|
|
1
|
+
import axios, { type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError, type AxiosRequestConfig } from "axios";
|
|
2
2
|
import qs from "qs";
|
|
3
|
+
import { reactive } from "vue";
|
|
3
4
|
import router from "../routers";
|
|
4
5
|
import { LOGIN_URL } from "../config";
|
|
5
6
|
import pinia from "../stores";
|
|
@@ -12,6 +13,10 @@ export const requestBaseURL = `${apiTargetUrl}${apiBasePath}`;
|
|
|
12
13
|
const SESSION_URL = "/v1/base/session";
|
|
13
14
|
const TOKEN_URL = "/v1/base/token";
|
|
14
15
|
const REFRESH_EXPIRY_COOKIE = "kratos_refresh_exp";
|
|
16
|
+
const REFRESH_TOKEN_TRANSPORT_HEADER = "X-Refresh-Token-Transport";
|
|
17
|
+
const REFRESH_TOKEN_TRANSPORT_COOKIE = "cookie";
|
|
18
|
+
const PASSWORD_CHANGE_REQUIRED_METADATA_KEY = "password_change_required";
|
|
19
|
+
const PASSWORD_CHANGE_STATE_STORAGE_KEY = "admin-password-change-required";
|
|
15
20
|
const CAPTCHA_URL = "/v1/base/captcha";
|
|
16
21
|
const CONFIG_URL = "/v1/base/config";
|
|
17
22
|
const LANGUAGE_URL = "/v1/base/language";
|
|
@@ -22,10 +27,7 @@ const OAUTH_TICKET_URL = "/v1/base/oauth/ticket";
|
|
|
22
27
|
const MFA_VERIFY_URL = "/v1/base/mfa/verify";
|
|
23
28
|
const MFA_ENROLLMENT_URL = "/v1/base/mfa/enrollment";
|
|
24
29
|
const MFA_ENROLLMENT_CONFIRM_URL = "/v1/base/mfa/enrollment/confirm";
|
|
25
|
-
|
|
26
|
-
const LEGACY_REFRESH_TOKEN_URL = `${LEGACY_AUTH_URL}/token`;
|
|
27
|
-
const LEGACY_CAPTCHA_URL = "/login/captcha";
|
|
28
|
-
// 认证公共接口不携带旧 token,同时保留旧路径兼容迁移期间的灰度访问。
|
|
30
|
+
// 认证公共接口不携带访问令牌。
|
|
29
31
|
const NO_AUTH_URL_SET = new Set([
|
|
30
32
|
SESSION_URL,
|
|
31
33
|
TOKEN_URL,
|
|
@@ -38,10 +40,7 @@ const NO_AUTH_URL_SET = new Set([
|
|
|
38
40
|
OAUTH_TICKET_URL,
|
|
39
41
|
MFA_VERIFY_URL,
|
|
40
42
|
MFA_ENROLLMENT_URL,
|
|
41
|
-
MFA_ENROLLMENT_CONFIRM_URL
|
|
42
|
-
LEGACY_AUTH_URL,
|
|
43
|
-
LEGACY_CAPTCHA_URL,
|
|
44
|
-
LEGACY_REFRESH_TOKEN_URL
|
|
43
|
+
MFA_ENROLLMENT_CONFIRM_URL
|
|
45
44
|
]);
|
|
46
45
|
const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
47
46
|
SESSION_URL,
|
|
@@ -55,10 +54,7 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
|
55
54
|
OAUTH_TICKET_URL,
|
|
56
55
|
MFA_VERIFY_URL,
|
|
57
56
|
MFA_ENROLLMENT_URL,
|
|
58
|
-
MFA_ENROLLMENT_CONFIRM_URL
|
|
59
|
-
LEGACY_AUTH_URL,
|
|
60
|
-
LEGACY_REFRESH_TOKEN_URL,
|
|
61
|
-
LEGACY_CAPTCHA_URL
|
|
57
|
+
MFA_ENROLLMENT_CONFIRM_URL
|
|
62
58
|
]);
|
|
63
59
|
|
|
64
60
|
/** 支持自动重试的 Axios 请求配置。 */
|
|
@@ -67,12 +63,52 @@ type RetryableRequestConfig = InternalAxiosRequestConfig & {
|
|
|
67
63
|
_authRetried?: boolean;
|
|
68
64
|
};
|
|
69
65
|
|
|
66
|
+
/** 统一的结构化错误响应。 */
|
|
67
|
+
type ErrorResponseData = {
|
|
68
|
+
code?: string | number;
|
|
69
|
+
message?: string;
|
|
70
|
+
reason?: string;
|
|
71
|
+
metadata?: Record<string, string>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** 本地持久化的强制改密状态,仅包含非敏感的显示信息。 */
|
|
75
|
+
interface PersistedPasswordChangeState {
|
|
76
|
+
/** 是否需要强制修改密码。 */
|
|
77
|
+
required: boolean;
|
|
78
|
+
/** 后端返回的提示文案。 */
|
|
79
|
+
message: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 读取刷新前保存的强制改密状态。 */
|
|
83
|
+
function getPersistedPasswordChangeState(): PersistedPasswordChangeState {
|
|
84
|
+
const emptyState = { required: false, message: "" };
|
|
85
|
+
if (typeof window === "undefined") return emptyState;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const value = window.localStorage.getItem(PASSWORD_CHANGE_STATE_STORAGE_KEY);
|
|
89
|
+
if (!value) return emptyState;
|
|
90
|
+
const parsed = JSON.parse(value) as unknown;
|
|
91
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return emptyState;
|
|
92
|
+
const persisted = parsed as { message?: unknown };
|
|
93
|
+
return {
|
|
94
|
+
required: true,
|
|
95
|
+
message: typeof persisted.message === "string" ? persisted.message : ""
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return emptyState;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 强制改密弹窗状态。 */
|
|
103
|
+
export const passwordChangeState = reactive(getPersistedPasswordChangeState());
|
|
104
|
+
|
|
70
105
|
// 创建 axios 实例
|
|
71
106
|
const service = axios.create({
|
|
72
107
|
baseURL: requestBaseURL,
|
|
73
108
|
timeout: 50000,
|
|
74
109
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
|
75
|
-
paramsSerializer: params => qs.stringify(params)
|
|
110
|
+
paramsSerializer: params => qs.stringify(params),
|
|
111
|
+
withCredentials: true
|
|
76
112
|
});
|
|
77
113
|
|
|
78
114
|
// 刷新令牌请求使用独立实例,避免与主请求拦截器互相递归。
|
|
@@ -93,6 +129,9 @@ function shouldSkipAuth(config: InternalAxiosRequestConfig) {
|
|
|
93
129
|
if (config.headers.Authorization === "no-auth") return true;
|
|
94
130
|
|
|
95
131
|
const requestUrl = config.url ?? "";
|
|
132
|
+
const requestMethod = String(config.method ?? "").toLowerCase();
|
|
133
|
+
// 登录请求会显式声明 no-auth,登出请求则必须带访问令牌让服务端清理会话和刷新令牌。
|
|
134
|
+
if (requestUrl === SESSION_URL && requestMethod === "delete") return false;
|
|
96
135
|
return NO_AUTH_URL_SET.has(requestUrl);
|
|
97
136
|
}
|
|
98
137
|
|
|
@@ -110,7 +149,57 @@ function shouldSkipErrorMessage(config?: InternalAxiosRequestConfig) {
|
|
|
110
149
|
|
|
111
150
|
const requestUrl = config.url ?? "";
|
|
112
151
|
const requestMethod = String(config.method ?? "").toLowerCase();
|
|
113
|
-
return
|
|
152
|
+
return requestUrl === SESSION_URL && requestMethod === "delete";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 判断响应是否要求用户先修改密码。 */
|
|
156
|
+
function isPasswordChangeRequired(data?: ErrorResponseData) {
|
|
157
|
+
return data?.metadata?.[PASSWORD_CHANGE_REQUIRED_METADATA_KEY] === "true";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 判断最近是否已经处理过强制改密响应。 */
|
|
161
|
+
function isPasswordChangeRequiredPromptActive() {
|
|
162
|
+
return passwordChangePromptUntil > Date.now();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 处理强制改密响应,避免并发请求重复提示并打开全局改密弹窗。 */
|
|
166
|
+
export function handlePasswordChangeRequired(message: string) {
|
|
167
|
+
if (isPasswordChangeRequiredPromptActive()) return;
|
|
168
|
+
passwordChangePromptUntil = Date.now() + 10 * 1000;
|
|
169
|
+
passwordChangeState.required = true;
|
|
170
|
+
passwordChangeState.message = message;
|
|
171
|
+
persistPasswordChangeState();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 清理强制改密弹窗状态。 */
|
|
175
|
+
export function clearPasswordChangeRequired() {
|
|
176
|
+
passwordChangeState.required = false;
|
|
177
|
+
passwordChangeState.message = "";
|
|
178
|
+
passwordChangePromptUntil = 0;
|
|
179
|
+
persistPasswordChangeState();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 持久化或删除强制改密状态,兼容浏览器禁用本地存储的场景。 */
|
|
183
|
+
function persistPasswordChangeState() {
|
|
184
|
+
if (typeof window === "undefined") return;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (passwordChangeState.required) {
|
|
188
|
+
window.localStorage.setItem(
|
|
189
|
+
PASSWORD_CHANGE_STATE_STORAGE_KEY,
|
|
190
|
+
JSON.stringify({ message: passwordChangeState.message })
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
window.localStorage.removeItem(PASSWORD_CHANGE_STATE_STORAGE_KEY);
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// 本地存储不可用时仍保留当前页面内的响应式状态。
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 统一展示请求错误,合并并发请求产生的相同提示。 */
|
|
201
|
+
function showRequestError(message: string) {
|
|
202
|
+
ElMessage.error({ message, grouping: true });
|
|
114
203
|
}
|
|
115
204
|
|
|
116
205
|
/** 读取访问令牌过期时间 */
|
|
@@ -136,7 +225,13 @@ export function hasValidAccessToken() {
|
|
|
136
225
|
/** 读取最新可用访问令牌,必要时先串行刷新,供 axios、fetch 与 SSE 共用。 */
|
|
137
226
|
export async function getRequestAccessToken(): Promise<string> {
|
|
138
227
|
const userStore = getUserStore();
|
|
139
|
-
if (
|
|
228
|
+
if (userStore.isLoggingOut) {
|
|
229
|
+
return userStore.token.trim();
|
|
230
|
+
}
|
|
231
|
+
if (userStore.authInvalidated) {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
if (!userStore.token && hasRefreshCookieHint()) {
|
|
140
235
|
await handleTokenRefresh(false);
|
|
141
236
|
}
|
|
142
237
|
|
|
@@ -151,12 +246,15 @@ export async function getRequestAccessToken(): Promise<string> {
|
|
|
151
246
|
|
|
152
247
|
/** 尝试通过刷新令牌恢复访问令牌,供路由守卫进入页面前调用。 */
|
|
153
248
|
export async function ensureAccessToken() {
|
|
249
|
+
const userStore = getUserStore();
|
|
250
|
+
if (userStore.isLoggingOut || userStore.authInvalidated) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
154
253
|
if (hasValidAccessToken()) {
|
|
155
254
|
return true;
|
|
156
255
|
}
|
|
157
256
|
|
|
158
|
-
|
|
159
|
-
if (!userStore.refreshToken && !hasRefreshCookieHint()) {
|
|
257
|
+
if (!hasRefreshCookieHint()) {
|
|
160
258
|
return false;
|
|
161
259
|
}
|
|
162
260
|
|
|
@@ -171,6 +269,7 @@ export async function ensureAccessToken() {
|
|
|
171
269
|
|
|
172
270
|
// 防止并发 401 重复弹出认证失效确认框。
|
|
173
271
|
let isHandlingAuthExpired = false;
|
|
272
|
+
let passwordChangePromptUntil = 0;
|
|
174
273
|
|
|
175
274
|
/** 统一处理认证失效 */
|
|
176
275
|
export function handleAuthExpired() {
|
|
@@ -190,6 +289,7 @@ export function handleAuthExpired() {
|
|
|
190
289
|
const userStore = getUserStore();
|
|
191
290
|
const currentRoute = router.currentRoute.value;
|
|
192
291
|
const redirect = currentRoute.path === LOGIN_URL ? undefined : currentRoute.fullPath;
|
|
292
|
+
clearPasswordChangeRequired();
|
|
193
293
|
userStore.clearAuthData();
|
|
194
294
|
return router.replace({
|
|
195
295
|
path: LOGIN_URL,
|
|
@@ -207,7 +307,8 @@ service.interceptors.request.use(
|
|
|
207
307
|
const skipAuth = shouldSkipAuth(config);
|
|
208
308
|
const accessToken = skipAuth ? "" : await getRequestAccessToken();
|
|
209
309
|
Object.assign(config.headers, getLocaleRequestHeaders());
|
|
210
|
-
|
|
310
|
+
config.headers[REFRESH_TOKEN_TRANSPORT_HEADER] = REFRESH_TOKEN_TRANSPORT_COOKIE;
|
|
311
|
+
// 登录、验证码、刷新令牌接口不携带访问令牌,避免请求头污染。
|
|
211
312
|
if (!skipAuth && accessToken) {
|
|
212
313
|
config.headers.Authorization = accessToken;
|
|
213
314
|
} else {
|
|
@@ -226,24 +327,30 @@ service.interceptors.response.use(
|
|
|
226
327
|
return response;
|
|
227
328
|
}
|
|
228
329
|
|
|
229
|
-
const
|
|
330
|
+
const responseData = response.data as ErrorResponseData;
|
|
331
|
+
const { code, message, reason, metadata } = responseData;
|
|
230
332
|
if (code === undefined || message === undefined || reason === undefined || metadata === undefined) {
|
|
231
333
|
return response.data;
|
|
232
334
|
}
|
|
233
335
|
|
|
234
|
-
|
|
336
|
+
if (isPasswordChangeRequired(responseData)) {
|
|
337
|
+
handlePasswordChangeRequired(message || t("common.message.system_error"));
|
|
338
|
+
} else {
|
|
339
|
+
showRequestError(message || t("common.message.system_error"));
|
|
340
|
+
}
|
|
235
341
|
return Promise.reject(new Error(message || t("common.message.system_error")));
|
|
236
342
|
},
|
|
237
343
|
async (error: AxiosError) => {
|
|
238
344
|
const status = error.response?.status;
|
|
239
|
-
const data = error.response?.data as
|
|
345
|
+
const data = error.response?.data as ErrorResponseData | undefined;
|
|
240
346
|
const code = data?.code;
|
|
241
347
|
const message = data?.message;
|
|
242
348
|
const requestConfig = error.config as RetryableRequestConfig | undefined;
|
|
243
349
|
|
|
244
350
|
// 业务请求仅在 401 时尝试刷新并重放,403 直接展示后端权限错误。
|
|
245
351
|
if ((status === 401 || code === 401) && !shouldSkipAuthExpiredPrompt(requestConfig)) {
|
|
246
|
-
|
|
352
|
+
const userStore = getUserStore();
|
|
353
|
+
if (requestConfig && !requestConfig._authRetried && !userStore.isLoggingOut && !userStore.authInvalidated && hasRefreshCookieHint()) {
|
|
247
354
|
requestConfig._authRetried = true;
|
|
248
355
|
try {
|
|
249
356
|
await handleTokenRefresh(false);
|
|
@@ -256,20 +363,27 @@ service.interceptors.response.use(
|
|
|
256
363
|
handleAuthExpired();
|
|
257
364
|
} else if (data) {
|
|
258
365
|
if (!shouldSkipErrorMessage(requestConfig)) {
|
|
259
|
-
if (
|
|
260
|
-
|
|
366
|
+
if (isPasswordChangeRequired(data)) {
|
|
367
|
+
handlePasswordChangeRequired(message || t("common.message.system_error"));
|
|
368
|
+
} else if (message) {
|
|
369
|
+
showRequestError(message);
|
|
261
370
|
} else {
|
|
262
|
-
|
|
371
|
+
showRequestError(t("common.message.system_error"));
|
|
263
372
|
}
|
|
264
373
|
}
|
|
265
374
|
} else if (!shouldSkipErrorMessage(requestConfig)) {
|
|
266
|
-
|
|
375
|
+
showRequestError(error.message || t("common.message.system_error"));
|
|
267
376
|
}
|
|
268
377
|
return Promise.reject(error.message);
|
|
269
378
|
}
|
|
270
379
|
);
|
|
271
380
|
|
|
272
|
-
|
|
381
|
+
/** 以请求体、响应体顺序提供带类型约束的 HTTP 请求函数。 */
|
|
382
|
+
function request<Request = unknown, Response = unknown>(config: AxiosRequestConfig<Request>): Promise<Response> {
|
|
383
|
+
return service<Response, Response, Request>(config) as Promise<Response>;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export default request;
|
|
273
387
|
|
|
274
388
|
// 刷新 Token 的锁
|
|
275
389
|
let isRefreshing = false;
|
|
@@ -301,17 +415,22 @@ async function handleTokenRefresh(promptOnFailure = true) {
|
|
|
301
415
|
/** 调用刷新令牌接口并回写最新认证信息 */
|
|
302
416
|
async function refreshAccessToken() {
|
|
303
417
|
const userStore = getUserStore();
|
|
418
|
+
const authVersion = userStore.authVersion;
|
|
304
419
|
|
|
305
420
|
const response = await refreshService.post(
|
|
306
421
|
TOKEN_URL,
|
|
307
|
-
{
|
|
308
|
-
{
|
|
422
|
+
{},
|
|
423
|
+
{
|
|
424
|
+
headers: {
|
|
425
|
+
Authorization: "no-auth",
|
|
426
|
+
[REFRESH_TOKEN_TRANSPORT_HEADER]: REFRESH_TOKEN_TRANSPORT_COOKIE,
|
|
427
|
+
...getLocaleRequestHeaders()
|
|
428
|
+
}
|
|
429
|
+
}
|
|
309
430
|
);
|
|
310
431
|
const data = response.data;
|
|
311
|
-
userStore.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
data.expires_in
|
|
316
|
-
);
|
|
432
|
+
if (userStore.authVersion !== authVersion || userStore.isLoggingOut || userStore.authInvalidated) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
userStore.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
|
|
317
436
|
}
|
|
@@ -28,13 +28,32 @@ export function formatSrc(src: string) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const configuredBase = String(import.meta.env.VITE_APP_STATIC_URL ?? "").trim();
|
|
31
|
-
if (value.startsWith("/admin/")) {
|
|
32
|
-
return new URL(value, `${window.location.origin}/`).toString();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
const staticBase = configuredBase
|
|
36
32
|
? new URL(`${configuredBase.replace(/\/$/, "")}/`, `${window.location.origin}/`).toString()
|
|
37
|
-
: `${window.location.origin}
|
|
38
|
-
const normalizedPath = value.replace(/^\/+/, "")
|
|
33
|
+
: `${window.location.origin}/`;
|
|
34
|
+
const normalizedPath = normalizeStaticAssetPath(value).replace(/^\/+/, "");
|
|
39
35
|
return new URL(normalizedPath, staticBase).toString();
|
|
40
36
|
}
|
|
37
|
+
|
|
38
|
+
/** 将对象路径转换为统一的数据访问路径。 */
|
|
39
|
+
export function normalizeStaticAssetPath(src: string) {
|
|
40
|
+
const value = String(src ?? "");
|
|
41
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (value.startsWith("/data/") || value.startsWith("/admin/")) return value;
|
|
45
|
+
if (value.startsWith("data/")) return `/${value}`;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 将富文本中的媒体路径转换为统一的数据访问路径。 */
|
|
50
|
+
export function normalizeRichTextMediaPaths(html: string) {
|
|
51
|
+
if (typeof DOMParser === "undefined") return html;
|
|
52
|
+
|
|
53
|
+
const document = new DOMParser().parseFromString(html, "text/html");
|
|
54
|
+
document.body.querySelectorAll<HTMLElement>("img[src], video[src]").forEach(element => {
|
|
55
|
+
const src = element.getAttribute("src");
|
|
56
|
+
if (src) element.setAttribute("src", normalizeStaticAssetPath(src));
|
|
57
|
+
});
|
|
58
|
+
return document.body.innerHTML;
|
|
59
|
+
}
|
|
@@ -99,11 +99,12 @@
|
|
|
99
99
|
</el-tooltip>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
102
|
-
<
|
|
102
|
+
<ProDialog
|
|
103
103
|
v-model="behaviorDialogVisible"
|
|
104
104
|
width="364px"
|
|
105
105
|
top="16vh"
|
|
106
106
|
:show-close="false"
|
|
107
|
+
:show-footer="false"
|
|
107
108
|
append-to-body
|
|
108
109
|
class="behavior-captcha-dialog"
|
|
109
110
|
>
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
:events="rotateCaptchaEvents"
|
|
131
132
|
/>
|
|
132
133
|
</div>
|
|
133
|
-
</
|
|
134
|
+
</ProDialog>
|
|
134
135
|
<ProDialog v-model="mfaDialogVisible" :title="t('core.login.mfa_title')" width="360px" :close-on-click-modal="false">
|
|
135
136
|
<ProForm
|
|
136
137
|
ref="mfaLoginFormRef"
|
|
@@ -180,9 +181,9 @@ import { computed, ref, reactive, onMounted, watch } from "vue";
|
|
|
180
181
|
import { useRoute, useRouter } from "vue-router";
|
|
181
182
|
import { HOME_URL } from "../../../config";
|
|
182
183
|
import { getTimeState } from "../../../utils";
|
|
183
|
-
import { defLoginService } from "../../../api/base/login";
|
|
184
|
-
import { defMfaService } from "../../../api/base/mfa";
|
|
185
|
-
import { defOauthService } from "../../../api/base/oauth";
|
|
184
|
+
import { defLoginService } from "../../../api/base/v1/login";
|
|
185
|
+
import { defMfaService } from "../../../api/base/v1/mfa";
|
|
186
|
+
import { defOauthService } from "../../../api/base/v1/oauth";
|
|
186
187
|
import { createWebAuthnCredential, getWebAuthnAssertion } from "../../../security";
|
|
187
188
|
import ProDialog from "../../../components/Dialog/ProDialog.vue";
|
|
188
189
|
import ProForm from "../../../components/ProForm/index.vue";
|
|
@@ -204,6 +205,7 @@ import { useConfigStore } from "../../../stores/modules/config";
|
|
|
204
205
|
import { Click as GoCaptchaClick, Rotate as GoCaptchaRotate, Slide as GoCaptchaSlide } from "go-captcha-vue";
|
|
205
206
|
import "go-captcha-vue/dist/style.css";
|
|
206
207
|
import { useLocaleStore } from "../../../locales";
|
|
208
|
+
import { clearPasswordChangeRequired, handlePasswordChangeRequired } from "../../../utils/request";
|
|
207
209
|
|
|
208
210
|
const router = useRouter();
|
|
209
211
|
const route = useRoute();
|
|
@@ -556,13 +558,9 @@ const finishLogin = async (mustChangePassword = false) => {
|
|
|
556
558
|
tabsStore.setTabs([]);
|
|
557
559
|
keepAliveStore.setKeepAliveName([]);
|
|
558
560
|
|
|
559
|
-
// 5
|
|
561
|
+
// 5.无论是否需要改密,都回到登录前页面;强制改密状态由布局中的全局弹窗展示。
|
|
560
562
|
// 统一走动态路由感知跳转,避免首次登录后目标页面尚未完成挂载时直接进入 404。
|
|
561
|
-
|
|
562
|
-
await navigateTo(router, "/profile", { tab: "password" });
|
|
563
|
-
} else {
|
|
564
|
-
await navigateTo(router, getLoginRedirectPath());
|
|
565
|
-
}
|
|
563
|
+
await navigateTo(router, getLoginRedirectPath());
|
|
566
564
|
ElNotification({
|
|
567
565
|
title: getTimeState(),
|
|
568
566
|
message: t("core.login.welcome"),
|
|
@@ -582,14 +580,20 @@ const handleLoginResponse = async (result: LoginResponse) => {
|
|
|
582
580
|
mfaDialogVisible.value = true;
|
|
583
581
|
return;
|
|
584
582
|
}
|
|
585
|
-
|
|
583
|
+
if (result.status === LoginStatus.LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED) {
|
|
586
584
|
mfaSetupTicket.value = result.mfa_setup_ticket;
|
|
587
585
|
mfaSetupMethod.value = result.mfa_method || "totp";
|
|
588
586
|
await beginMfaSetup(result.mfa_setup_ticket);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const mustChangePassword = result.status === LoginStatus.LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED;
|
|
590
|
+
if (mustChangePassword) {
|
|
591
|
+
handlePasswordChangeRequired(t("core.layout.password_change_required"));
|
|
592
|
+
} else {
|
|
593
|
+
clearPasswordChangeRequired();
|
|
594
|
+
}
|
|
595
|
+
userStore.updateTokenAuth(result.access_token, result.token_type ?? "", result.expires_in);
|
|
596
|
+
await finishLogin(mustChangePassword);
|
|
593
597
|
};
|
|
594
598
|
|
|
595
599
|
/** 校验登录阶段 MFA 并完成登录。 */
|
|
@@ -859,7 +863,7 @@ watch(
|
|
|
859
863
|
height: 40px;
|
|
860
864
|
cursor: pointer;
|
|
861
865
|
object-fit: contain;
|
|
862
|
-
border-radius:
|
|
866
|
+
border-radius: var(--admin-page-radius);
|
|
863
867
|
}
|
|
864
868
|
.behavior-captcha-body {
|
|
865
869
|
min-height: 0;
|
|
@@ -969,15 +973,19 @@ watch(
|
|
|
969
973
|
--go-captcha-theme-dot-color-color: #ffffff;
|
|
970
974
|
--go-captcha-theme-dot-bg-color: color-mix(in srgb, var(--el-color-primary) 68%, transparent);
|
|
971
975
|
--go-captcha-theme-dot-border-color: var(--el-bg-color);
|
|
972
|
-
border-radius:
|
|
976
|
+
border-radius: var(--admin-page-radius);
|
|
973
977
|
box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px;
|
|
974
978
|
}
|
|
975
979
|
|
|
980
|
+
:global(.behavior-captcha-dialog .go-captcha.gc-theme) {
|
|
981
|
+
border-radius: var(--admin-page-radius);
|
|
982
|
+
}
|
|
983
|
+
|
|
976
984
|
:global(.behavior-captcha-dialog .el-dialog__header) {
|
|
977
985
|
display: none;
|
|
978
986
|
}
|
|
979
987
|
|
|
980
|
-
:global(.behavior-captcha-dialog .el-dialog__body) {
|
|
988
|
+
:global(.behavior-captcha-dialog.pro-dialog .el-dialog__body) {
|
|
981
989
|
padding: 10px 12px 12px;
|
|
982
990
|
}
|
|
983
991
|
|
|
@@ -999,7 +1007,11 @@ watch(
|
|
|
999
1007
|
|
|
1000
1008
|
:global(.behavior-captcha-dialog .go-captcha .gc-body) {
|
|
1001
1009
|
margin-top: 0;
|
|
1002
|
-
border-radius:
|
|
1010
|
+
border-radius: var(--admin-page-radius);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
:global(.behavior-captcha-dialog .go-captcha .gc-button-block button) {
|
|
1014
|
+
border-radius: var(--admin-page-radius);
|
|
1003
1015
|
}
|
|
1004
1016
|
|
|
1005
1017
|
:global(.behavior-captcha-dialog .go-captcha .gc-footer) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
height: 94%;
|
|
16
16
|
padding: 0 50px;
|
|
17
17
|
background-color: rgb(255 255 255 / 80%);
|
|
18
|
-
border-radius:
|
|
18
|
+
border-radius: var(--admin-page-radius);
|
|
19
19
|
.dark {
|
|
20
20
|
position: absolute;
|
|
21
21
|
top: 13px;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
max-width: 100%;
|
|
42
42
|
padding: 50px 40px 45px;
|
|
43
43
|
background-color: var(--el-bg-color);
|
|
44
|
-
border-radius:
|
|
44
|
+
border-radius: var(--admin-page-radius);
|
|
45
45
|
box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px;
|
|
46
46
|
.login-logo {
|
|
47
47
|
display: flex;
|
package/package.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -85,6 +85,7 @@ export function defineAdminViteConfig(options: AdminViteConfigOptions = {}) {
|
|
|
85
85
|
"@liujitcn/kratos-admin-core > dayjs/plugin/advancedFormat.js",
|
|
86
86
|
"@liujitcn/kratos-admin-core > dayjs/plugin/customParseFormat.js",
|
|
87
87
|
"@liujitcn/kratos-admin-core > dayjs/plugin/dayOfYear.js",
|
|
88
|
+
"@liujitcn/kratos-admin-core > dayjs/plugin/quarterOfYear.js",
|
|
88
89
|
"@liujitcn/kratos-admin-core > dayjs/plugin/isSameOrAfter.js",
|
|
89
90
|
"@liujitcn/kratos-admin-core > dayjs/plugin/isSameOrBefore.js",
|
|
90
91
|
"@liujitcn/kratos-admin-core > dayjs/plugin/localeData.js",
|