@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.
Files changed (93) hide show
  1. package/README.md +10 -4
  2. package/dist/package/declarations/src/api/base/{config.d.ts → v1/config.d.ts} +1 -1
  3. package/dist/package/declarations/src/api/base/{file.d.ts → v1/file.d.ts} +3 -1
  4. package/dist/package/declarations/src/api/base/{language.d.ts → v1/language.d.ts} +1 -1
  5. package/dist/package/declarations/src/api/base/{login.d.ts → v1/login.d.ts} +2 -2
  6. package/dist/package/declarations/src/api/base/{mfa.d.ts → v1/mfa.d.ts} +3 -3
  7. package/dist/package/declarations/src/api/base/{oauth.d.ts → v1/oauth.d.ts} +1 -1
  8. package/dist/package/declarations/src/api/system/{auth.d.ts → admin/v1/auth.d.ts} +2 -2
  9. package/dist/package/declarations/src/api/system/{base_dict.d.ts → admin/v1/base_dict.d.ts} +1 -1
  10. package/dist/package/declarations/src/api/system/{base_tenant.d.ts → admin/v1/base_tenant.d.ts} +2 -2
  11. package/dist/package/declarations/src/components/ProTable/index.vue.d.ts +114 -0
  12. package/dist/package/declarations/src/components/TreeFilter/index.vue.d.ts +84 -76
  13. package/dist/package/declarations/src/components/Upload/File.vue.d.ts +1 -1
  14. package/dist/package/declarations/src/components/Upload/Files.vue.d.ts +1 -1
  15. package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +1 -1
  16. package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +1 -1
  17. package/dist/package/declarations/src/components/WangEditor/index.vue.d.ts +2 -0
  18. package/dist/package/declarations/src/documentTitle.d.ts +2 -0
  19. package/dist/package/declarations/src/format.d.ts +1 -1
  20. package/dist/package/declarations/src/index.d.ts +2 -1
  21. package/dist/package/declarations/src/locales/generated.d.ts +40 -0
  22. package/dist/package/declarations/src/modules/index.d.ts +1 -3
  23. package/dist/package/declarations/src/request.d.ts +1 -1
  24. package/dist/package/declarations/src/routers/index.d.ts +1 -1
  25. package/dist/package/declarations/src/rpc/base/v1/login.d.ts +5 -3
  26. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +13 -0
  27. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +0 -101
  28. package/dist/package/declarations/src/rpc/system/common/v1/common.d.ts +26 -0
  29. package/dist/package/declarations/src/stores/interface/index.d.ts +6 -1
  30. package/dist/package/declarations/src/stores/modules/user.d.ts +2 -4
  31. package/dist/package/declarations/src/utils/passwordStrength.d.ts +1 -12
  32. package/dist/package/declarations/src/utils/request.d.ts +13 -2
  33. package/dist/package/declarations/src/utils/utils.d.ts +4 -0
  34. package/dist/package/src/api/base/{config.ts → v1/config.ts} +2 -2
  35. package/dist/package/src/api/base/{file.ts → v1/file.ts} +20 -24
  36. package/dist/package/src/api/base/{language.ts → v1/language.ts} +2 -2
  37. package/dist/package/src/api/base/{login.ts → v1/login.ts} +3 -3
  38. package/dist/package/src/api/base/{mfa.ts → v1/mfa.ts} +4 -4
  39. package/dist/package/src/api/base/{oauth.ts → v1/oauth.ts} +2 -2
  40. package/dist/package/src/api/system/{auth.ts → admin/v1/auth.ts} +3 -3
  41. package/dist/package/src/api/system/{base_dict.ts → admin/v1/base_dict.ts} +2 -2
  42. package/dist/package/src/api/system/{base_tenant.ts → admin/v1/base_tenant.ts} +3 -3
  43. package/dist/package/src/bootstrap.ts +6 -5
  44. package/dist/package/src/components/CronExpression/index.vue +4 -3
  45. package/dist/package/src/components/Dialog/ProDialog.vue +1 -0
  46. package/dist/package/src/components/ImportExcel/index.vue +4 -2
  47. package/dist/package/src/components/MarkdownPreview/index.vue +1 -1
  48. package/dist/package/src/components/PasswordStrength/index.vue +1 -1
  49. package/dist/package/src/components/ProTable/components/TableColumn.vue +14 -6
  50. package/dist/package/src/components/ProTable/index.vue +3 -3
  51. package/dist/package/src/components/RichTextPreview/index.vue +12 -1
  52. package/dist/package/src/components/SelectIcon/index.vue +3 -2
  53. package/dist/package/src/components/Upload/File.vue +2 -2
  54. package/dist/package/src/components/Upload/Files.vue +2 -2
  55. package/dist/package/src/components/Upload/Img.vue +7 -5
  56. package/dist/package/src/components/Upload/Imgs.vue +6 -5
  57. package/dist/package/src/components/WangEditor/index.vue +10 -7
  58. package/dist/package/src/documentTitle.ts +8 -0
  59. package/dist/package/src/format.ts +1 -1
  60. package/dist/package/src/index.ts +2 -1
  61. package/dist/package/src/layouts/components/Header/components/Avatar.vue +7 -6
  62. package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +3 -2
  63. package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +3 -2
  64. package/dist/package/src/layouts/components/Header/components/SearchMenu.vue +16 -8
  65. package/dist/package/src/layouts/components/LockScreen.vue +175 -52
  66. package/dist/package/src/layouts/components/Tabs/index.vue +12 -3
  67. package/dist/package/src/layouts/components/ThemeDrawer/index.scss +4 -4
  68. package/dist/package/src/locales/en-US.json +8 -2
  69. package/dist/package/src/locales/ja-JP.json +8 -2
  70. package/dist/package/src/locales/zh-CN.json +8 -2
  71. package/dist/package/src/locales/zh-TW.json +8 -2
  72. package/dist/package/src/modules/index.ts +2 -4
  73. package/dist/package/src/request.ts +3 -0
  74. package/dist/package/src/routers/index.ts +15 -14
  75. package/dist/package/src/rpc/base/v1/login.ts +4 -2
  76. package/dist/package/src/rpc/system/admin/v1/auth.ts +13 -0
  77. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +0 -112
  78. package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
  79. package/dist/package/src/stores/interface/index.ts +6 -1
  80. package/dist/package/src/stores/modules/auth.ts +1 -1
  81. package/dist/package/src/stores/modules/config.ts +2 -2
  82. package/dist/package/src/stores/modules/dict.ts +1 -1
  83. package/dist/package/src/stores/modules/user.ts +21 -22
  84. package/dist/package/src/styles/element.scss +35 -4
  85. package/dist/package/src/utils/passwordCrypto.ts +1 -1
  86. package/dist/package/src/utils/passwordStrength.ts +7 -24
  87. package/dist/package/src/utils/request.ts +155 -36
  88. package/dist/package/src/utils/tenant.ts +1 -1
  89. package/dist/package/src/utils/utils.ts +25 -6
  90. package/dist/package/src/views/login/components/LoginForm.vue +32 -20
  91. package/dist/package/src/views/login/index.scss +2 -2
  92. package/package.json +1 -1
  93. 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.length >= 8) ruleScore += 1;
34
- if (/[a-z]/.test(password) && /[A-Z]/.test(password)) ruleScore += 1;
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
- const LEGACY_AUTH_URL = "/auth";
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 (requestUrl === SESSION_URL || requestUrl === LEGACY_AUTH_URL) && requestMethod === "delete";
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 (!userStore.token && (userStore.refreshToken || hasRefreshCookieHint())) {
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
- const userStore = getUserStore();
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
- // 登录、验证码、刷新令牌接口不携带旧 token,避免请求头污染。
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 { code, message, reason, metadata } = response.data;
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
- ElMessage.error(message || t("common.message.system_error"));
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 { code?: string | number; message?: string } | undefined;
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
- if (requestConfig && !requestConfig._authRetried && (getUserStore().refreshToken || hasRefreshCookieHint())) {
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 (message) {
260
- ElMessage.error(message);
366
+ if (isPasswordChangeRequired(data)) {
367
+ handlePasswordChangeRequired(message || t("common.message.system_error"));
368
+ } else if (message) {
369
+ showRequestError(message);
261
370
  } else {
262
- ElMessage.error(t("common.message.system_error"));
371
+ showRequestError(t("common.message.system_error"));
263
372
  }
264
373
  }
265
374
  } else if (!shouldSkipErrorMessage(requestConfig)) {
266
- ElMessage.error(error.message || t("common.message.system_error"));
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
- export default service;
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
- { refresh_token: userStore.refreshToken || undefined },
308
- { headers: { Authorization: "no-auth", ...getLocaleRequestHeaders() } }
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.updateTokenAuth(
312
- data.access_token,
313
- data.refresh_token ?? userStore.refreshToken,
314
- data.token_type ?? "",
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
  }
@@ -1,4 +1,4 @@
1
- import { defBaseTenantService } from "../api/system/base_tenant";
1
+ import { defBaseTenantService } from "../api/system/admin/v1/base_tenant";
2
2
 
3
3
  /** 默认租户编码。 */
4
4
  export const DEFAULT_TENANT_CODE = "0000";
@@ -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}/admin/`;
38
- const normalizedPath = value.replace(/^\/+/, "").replace(/^admin\/+/, "");
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
- <el-dialog
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
- </el-dialog>
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
- if (mustChangePassword) {
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
- if (result.status === LoginStatus.LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED) {
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
- return;
590
- }
591
- userStore.updateTokenAuth(result.access_token, result.refresh_token ?? "", result.token_type ?? "", result.expires_in);
592
- await finishLogin(result.status === LoginStatus.LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED);
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: 6px;
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: 10px;
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: 6px;
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: 10px;
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: 10px;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-core",
3
3
  "private": false,
4
- "version": "0.0.31",
4
+ "version": "0.0.32",
5
5
  "type": "module",
6
6
  "description": "kratos-admin frontend runtime and base modules",
7
7
  "author": {
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",