@liujitcn/kratos-admin-core 0.0.31 → 0.0.33

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 (110) 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 +68 -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 +15 -2
  69. package/dist/package/src/locales/ja-JP.json +15 -2
  70. package/dist/package/src/locales/zh-CN.json +15 -2
  71. package/dist/package/src/locales/zh-TW.json +15 -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/config.ts +1 -1
  76. package/dist/package/src/rpc/base/v1/file.ts +1 -1
  77. package/dist/package/src/rpc/base/v1/language.ts +1 -1
  78. package/dist/package/src/rpc/base/v1/login.ts +5 -3
  79. package/dist/package/src/rpc/base/v1/mfa.ts +1 -1
  80. package/dist/package/src/rpc/base/v1/oauth.ts +1 -1
  81. package/dist/package/src/rpc/base/v1/oauth_client.ts +1 -1
  82. package/dist/package/src/rpc/common/v1/common.ts +1 -1
  83. package/dist/package/src/rpc/common/v1/enum.ts +1 -1
  84. package/dist/package/src/rpc/common/v1/types.ts +1 -1
  85. package/dist/package/src/rpc/google/protobuf/empty.ts +1 -1
  86. package/dist/package/src/rpc/google/protobuf/wrappers.ts +1 -1
  87. package/dist/package/src/rpc/system/admin/v1/auth.ts +14 -1
  88. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +1 -113
  89. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +1 -1
  90. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +1 -1
  91. package/dist/package/src/rpc/system/admin/v1/common.ts +1 -1
  92. package/dist/package/src/rpc/system/common/v1/common.ts +35 -0
  93. package/dist/package/src/stores/interface/index.ts +6 -1
  94. package/dist/package/src/stores/modules/auth.ts +1 -1
  95. package/dist/package/src/stores/modules/config.ts +2 -2
  96. package/dist/package/src/stores/modules/dict.ts +1 -1
  97. package/dist/package/src/stores/modules/user.ts +21 -22
  98. package/dist/package/src/styles/element.scss +35 -4
  99. package/dist/package/src/typings/global.d.ts +6 -0
  100. package/dist/package/src/utils/passwordCrypto.ts +169 -12
  101. package/dist/package/src/utils/passwordStrength.ts +7 -24
  102. package/dist/package/src/utils/request.ts +155 -36
  103. package/dist/package/src/utils/tenant.ts +1 -1
  104. package/dist/package/src/utils/utils.ts +25 -6
  105. package/dist/package/src/views/login/components/LoginForm.vue +36 -21
  106. package/dist/package/src/views/login/index.scss +2 -2
  107. package/dist/package/types/generated/auto-imports.d.ts +2 -0
  108. package/package.json +2 -1
  109. package/types/generated/auto-imports.d.ts +2 -0
  110. package/vite.config.ts +24 -1
@@ -1,6 +1,6 @@
1
1
  // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
2
  // versions:
3
- // protoc-gen-ts_proto v2.11.8
3
+ // protoc-gen-ts_proto v2.12.1
4
4
  // protoc unknown
5
5
  // source: system/admin/v1/common.proto
6
6
 
@@ -0,0 +1,35 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.12.1
4
+ // protoc unknown
5
+ // source: system/common/v1/common.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 系统公共用户相关枚举。 */
10
+ export enum BaseUserIDType {
11
+ /** BASE_USER_ID_TYPE_UNSPECIFIED - 未指定证件类型。 */
12
+ BASE_USER_ID_TYPE_UNSPECIFIED = 0,
13
+ /** BASE_USER_ID_TYPE_ID_CARD - 居民身份证。 */
14
+ BASE_USER_ID_TYPE_ID_CARD = 1,
15
+ /** BASE_USER_ID_TYPE_PASSPORT - 护照。 */
16
+ BASE_USER_ID_TYPE_PASSPORT = 2,
17
+ /** BASE_USER_ID_TYPE_HK_MACAO_PERMIT - 港澳居民来往内地通行证。 */
18
+ BASE_USER_ID_TYPE_HK_MACAO_PERMIT = 3,
19
+ /** BASE_USER_ID_TYPE_TAIWAN_PERMIT - 台湾居民来往大陆通行证。 */
20
+ BASE_USER_ID_TYPE_TAIWAN_PERMIT = 4,
21
+ /** BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE - 外国人永久居留身份证。 */
22
+ BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE = 5,
23
+ /** BASE_USER_ID_TYPE_OTHER - 其他证件。 */
24
+ BASE_USER_ID_TYPE_OTHER = 6,
25
+ }
26
+
27
+ /** 用户密码修改状态。 */
28
+ export enum BaseUserPasswordChangeStatus {
29
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED - 未指定密码修改状态。 */
30
+ BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED = 0,
31
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED - 当前必须修改密码。 */
32
+ BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED = 1,
33
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED - 当前无需修改密码。 */
34
+ BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED = 2,
35
+ }
@@ -53,9 +53,14 @@ export interface GlobalState {
53
53
  /** 当前登录用户认证状态。 */
54
54
  export interface UserState {
55
55
  token: string;
56
- refreshToken: string;
57
56
  tokenType: string;
58
57
  tokenExpiresAt: number;
58
+ /** 是否正在执行退出登录,阻止后台请求刷新令牌。 */
59
+ isLoggingOut: boolean;
60
+ /** 认证状态版本,用于丢弃退出期间完成的旧刷新请求。 */
61
+ authVersion: number;
62
+ /** 当前认证是否已被本地显式清理。 */
63
+ authInvalidated: boolean;
59
64
  userInfo: UserInfoForm;
60
65
  }
61
66
 
@@ -1,5 +1,5 @@
1
1
  import { defineStore } from "pinia";
2
- import { defAuthService } from "../../api/system/auth";
2
+ import { defAuthService } from "../../api/system/admin/v1/auth";
3
3
  import { AuthState } from "../../stores/interface";
4
4
  import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList, isExternalPath } from "../../utils";
5
5
  import type { RouteItem } from "../../rpc/system/admin/v1/auth";
@@ -1,5 +1,5 @@
1
1
  import { defineStore } from "pinia";
2
- import { defConfigService } from "../../api/base/config";
2
+ import { defConfigService } from "../../api/base/v1/config";
3
3
  import { BaseConfigSite } from "../../rpc/base/v1/config";
4
4
  import type { LoginCaptchaConfig, SiteConfigState, SiteDisplayConfig } from "../../stores/interface";
5
5
  import defaultLogoUrl from "../../assets/images/logo.svg";
@@ -9,7 +9,7 @@ const CAPTCHA_TYPE_KEY = "captchaType";
9
9
  const I18N_DRAFT_ENABLED_KEY = "i18n.draft.enabled";
10
10
 
11
11
  const DEFAULT_SITE_DISPLAY_CONFIG: SiteDisplayConfig = {
12
- sysName: "Admin",
12
+ sysName: import.meta.env.VITE_GLOB_APP_TITLE,
13
13
  icp: "",
14
14
  copyright: "2026 © Admin",
15
15
  watermark: "Admin",
@@ -1,6 +1,6 @@
1
1
  import { defineStore } from "pinia";
2
2
  import pinia from "../../stores";
3
- import { defBaseDictService } from "../../api/system/base_dict";
3
+ import { defBaseDictService } from "../../api/system/admin/v1/base_dict";
4
4
  import type { DictState } from "../../stores/interface";
5
5
  import piniaPersistConfig from "../../stores/helper/persist";
6
6
  import type { OptionBaseDictResponse_BaseDict, OptionBaseDictResponse_BaseDictItem } from "../../rpc/system/admin/v1/base_dict";
@@ -1,7 +1,7 @@
1
1
  import { defineStore } from "pinia";
2
- import { defAuthService } from "../../api/system/auth";
3
- import { defLoginService } from "../../api/base/login";
4
- import { defMfaService } from "../../api/base/mfa";
2
+ import { defAuthService } from "../../api/system/admin/v1/auth";
3
+ import { defLoginService } from "../../api/base/v1/login";
4
+ import { defMfaService } from "../../api/base/v1/mfa";
5
5
  import type { LoginRequest, LoginResponse } from "../../rpc/base/v1/login";
6
6
  import type { VerifyMfaRequest } from "../../rpc/base/v1/mfa";
7
7
  import type { UserInfoForm } from "../../rpc/system/admin/v1/auth";
@@ -9,12 +9,14 @@ import { UserState } from "../../stores/interface";
9
9
  import piniaPersistConfig from "../../stores/helper/persist";
10
10
  import { useDictStoreHook } from "../../stores/modules/dict";
11
11
  import { useLockScreenStore } from "../../stores/modules/lockScreen";
12
- import { t } from "../../locales";
13
12
 
14
13
  const defaultUserInfo: UserInfoForm = {
15
14
  user_name: "",
16
15
  nick_name: "",
17
16
  phone: "",
17
+ email: "",
18
+ id_type: 0,
19
+ id_code: "",
18
20
  avatar: "",
19
21
  role_code: "",
20
22
  role_name: "",
@@ -26,9 +28,11 @@ const defaultUserInfo: UserInfoForm = {
26
28
  export const useUserStore = defineStore("admin-user", {
27
29
  state: (): UserState => ({
28
30
  token: "",
29
- refreshToken: "",
30
31
  tokenType: "",
31
32
  tokenExpiresAt: 0,
33
+ isLoggingOut: false,
34
+ authVersion: 0,
35
+ authInvalidated: false,
32
36
  userInfo: defaultUserInfo
33
37
  }),
34
38
  getters: {},
@@ -37,10 +41,6 @@ export const useUserStore = defineStore("admin-user", {
37
41
  setToken(token: string) {
38
42
  this.token = token;
39
43
  },
40
- /** 设置刷新令牌 */
41
- setRefreshToken(refreshToken: string) {
42
- this.refreshToken = refreshToken;
43
- },
44
44
  /** 设置令牌类型 */
45
45
  setTokenType(tokenType: string) {
46
46
  this.tokenType = tokenType;
@@ -54,12 +54,12 @@ export const useUserStore = defineStore("admin-user", {
54
54
  this.userInfo = userInfo;
55
55
  },
56
56
  /** 根据接口返回统一更新令牌信息 */
57
- updateTokenAuth(accessToken: string, refreshToken: string, tokenType: string, expiresIn?: number) {
57
+ updateTokenAuth(accessToken: string, tokenType: string, expiresIn?: number) {
58
58
  const tokenPrefix = tokenType ? `${tokenType} ` : "";
59
59
  const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 : 0;
60
60
 
61
+ this.authInvalidated = false;
61
62
  this.setToken(`${tokenPrefix}${accessToken}`.trim());
62
- this.setRefreshToken(refreshToken ?? "");
63
63
  this.setTokenType(tokenType ?? "");
64
64
  this.setTokenExpiresAt(expiresAt);
65
65
  },
@@ -71,24 +71,20 @@ export const useUserStore = defineStore("admin-user", {
71
71
  async login(loginRequest: LoginRequest): Promise<LoginResponse> {
72
72
  const data = await defLoginService.Login(loginRequest);
73
73
  if (data.status === 1 || data.status === 0 || data.status === 4) {
74
- this.updateTokenAuth(data.access_token, data.refresh_token ?? "", data.token_type ?? "", data.expires_in);
74
+ this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
75
75
  }
76
76
  return data;
77
77
  },
78
78
  /** 校验登录阶段的多因素认证并保存正式令牌。 */
79
79
  async verifyMfa(request: VerifyMfaRequest): Promise<LoginResponse> {
80
80
  const data = await defMfaService.VerifyMfa(request);
81
- this.updateTokenAuth(data.access_token, data.refresh_token ?? "", data.token_type ?? "", data.expires_in);
81
+ this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
82
82
  return data;
83
83
  },
84
- /** 刷新认证令牌 */
84
+ /** 通过 HttpOnly Cookie 刷新认证令牌。 */
85
85
  async refreshAccessToken() {
86
- if (!this.refreshToken) {
87
- return Promise.reject(new Error(t("core.auth.refresh_token_missing")));
88
- }
89
-
90
- const data = await defLoginService.RefreshToken({ refresh_token: this.refreshToken });
91
- this.updateTokenAuth(data.access_token, data.refresh_token ?? this.refreshToken, data.token_type ?? "", data.expires_in);
86
+ const data = await defLoginService.RefreshToken({});
87
+ this.updateTokenAuth(data.access_token, data.token_type ?? "", data.expires_in);
92
88
  return data;
93
89
  },
94
90
  /** 获取用户信息 */
@@ -102,22 +98,25 @@ export const useUserStore = defineStore("admin-user", {
102
98
  // 清理登录态时同步清空字典缓存,避免切换账号后读到旧字典。
103
99
  useDictStoreHook().clearDictionaryCache();
104
100
  useLockScreenStore().clearLock();
101
+ this.authVersion += 1;
102
+ this.authInvalidated = true;
105
103
  this.setToken("");
106
- this.setRefreshToken("");
107
104
  this.setTokenType("");
108
105
  this.setTokenExpiresAt(0);
109
106
  this.setUserInfo({ ...defaultUserInfo });
110
107
  },
111
108
  /** 退出登录 */
112
109
  async logout() {
110
+ this.isLoggingOut = true;
113
111
  try {
114
112
  await defLoginService.Logout({});
115
113
  } catch {
116
114
  // 退出接口返回异常时,前端仍需清理本地登录态,避免用户卡在当前会话。
117
115
  } finally {
118
116
  this.clearAuthData();
117
+ this.isLoggingOut = false;
119
118
  }
120
119
  }
121
120
  },
122
- persist: piniaPersistConfig("admin-user", ["token", "tokenType", "tokenExpiresAt", "userInfo"])
121
+ persist: piniaPersistConfig("admin-user-security-v2", ["userInfo"])
123
122
  });
@@ -1,3 +1,9 @@
1
+ /* 统一 Element Plus 矩形控件与后台页面的圆角。 */
2
+ html:root {
3
+ --el-border-radius-base: var(--admin-page-radius);
4
+ --el-border-radius-small: 2px;
5
+ }
6
+
1
7
  /* el-alert */
2
8
  .el-alert {
3
9
  border: 1px solid;
@@ -27,7 +33,18 @@
27
33
  overflow-x: hidden;
28
34
  background-color: var(--el-bg-color);
29
35
  border: 1px solid var(--el-border-color-light);
30
- border-radius: 6px;
36
+ border-radius: var(--admin-page-radius);
37
+ box-shadow: 0 0 12px rgb(0 0 0 / 5%);
38
+ }
39
+
40
+ /* 非列表业务页沿用 ProTable 卡片外观 */
41
+ .el-card.admin-page-card {
42
+ background-color: var(--el-bg-color);
43
+ border: 1px solid var(--el-border-color-light);
44
+ border-radius: var(--admin-page-radius);
45
+ box-shadow: 0 0 12px rgb(0 0 0 / 5%);
46
+ }
47
+ .el-card.admin-page-card.is-always-shadow {
31
48
  box-shadow: 0 0 12px rgb(0 0 0 / 5%);
32
49
  }
33
50
 
@@ -165,10 +182,9 @@
165
182
  }
166
183
  }
167
184
 
168
- // 设置 el-table 中 header 文字不换行,并省略
185
+ // 设置 el-table 表头文字单行省略,完整内容由列标题悬浮提示展示。
169
186
  .el-table__header .el-table__cell > .cell {
170
- // white-space: nowrap;
171
- white-space: wrap;
187
+ white-space: nowrap;
172
188
  }
173
189
 
174
190
  // 空表时去掉底部那条默认分隔线,和菜单管理页保持一致。
@@ -328,6 +344,21 @@
328
344
  }
329
345
  }
330
346
 
347
+ // ProDialog 需要去掉 Element Plus 默认的外层内边距,避免异步样式重复加载后覆盖紧凑布局。
348
+ .el-dialog.pro-dialog {
349
+ padding: 0;
350
+
351
+ // 对齐原生 MessageBox 的紧凑上下间距,避免标题和操作区留白过大。
352
+ .el-dialog__header,
353
+ .el-dialog__body {
354
+ padding: 12px 20px;
355
+ }
356
+
357
+ .el-dialog__footer {
358
+ padding: 0 20px 12px;
359
+ }
360
+ }
361
+
331
362
  @media screen and (width <= 768px) {
332
363
  .el-dialog {
333
364
  margin-top: 4vh;
@@ -55,6 +55,12 @@ declare interface ViteEnv {
55
55
  VITE_PUBLIC_PATH: string;
56
56
  VITE_API_URL: string;
57
57
  VITE_PROXY: [string, string][];
58
+ /** 是否启用 Vite HTTPS 开发服务。 */
59
+ VITE_HTTPS: boolean;
60
+ /** HTTPS 私钥路径,相对于宿主 apps/admin 目录。 */
61
+ VITE_HTTPS_KEY: string;
62
+ /** HTTPS 证书路径,相对于宿主 apps/admin 目录。 */
63
+ VITE_HTTPS_CERT: string;
58
64
  VITE_CODEINSPECTOR: boolean;
59
65
  }
60
66
 
@@ -1,7 +1,10 @@
1
- import { defLoginService } from "../api/base/login";
2
- import { PasswordCryptoScene } from "../rpc/base/v1/login";
1
+ import { defLoginService } from "../api/base/v1/login";
2
+ import { PasswordCryptoScene, type PasswordPublicKeyResponse } from "../rpc/base/v1/login";
3
3
  import type { PasswordCrypto } from "../rpc/common/v1/types";
4
4
  import { t } from "../locales";
5
+ import * as portableCrypto from "asmcrypto.js";
6
+
7
+ type PortableCrypto = typeof import("asmcrypto.js");
5
8
 
6
9
  export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene;
7
10
  export type { PasswordCryptoScene };
@@ -12,7 +15,11 @@ function pemToArrayBuffer(pem: string) {
12
15
  .replace(/-----BEGIN PUBLIC KEY-----/g, "")
13
16
  .replace(/-----END PUBLIC KEY-----/g, "")
14
17
  .replace(/\s/g, "");
15
- const binary = window.atob(base64);
18
+ const decode = globalThis.atob;
19
+ if (!decode) {
20
+ throw new Error(t("core.password.base64_decode_unsupported"));
21
+ }
22
+ const binary = decode(base64);
16
23
  const buffer = new Uint8Array(binary.length);
17
24
  for (let i = 0; i < binary.length; i += 1) {
18
25
  buffer[i] = binary.charCodeAt(i);
@@ -27,18 +34,163 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
27
34
  for (let i = 0; i < bytes.byteLength; i += 1) {
28
35
  binary += String.fromCharCode(bytes[i]);
29
36
  }
30
- return window.btoa(binary);
37
+ const encode = globalThis.btoa;
38
+ if (!encode) {
39
+ throw new Error(t("core.password.base64_encode_unsupported"));
40
+ }
41
+ return encode(binary);
31
42
  }
32
43
 
33
44
  /** 获取浏览器 WebCrypto 能力。 */
34
45
  function getSubtleCrypto() {
35
46
  const cryptoApi = globalThis.crypto;
36
- if (!cryptoApi?.subtle) {
47
+ const isSecureContext = typeof window === "undefined" || window.isSecureContext;
48
+ if (!cryptoApi?.subtle || !isSecureContext) {
37
49
  throw new Error(t("core.password.crypto_unsupported"));
38
50
  }
39
51
  return cryptoApi;
40
52
  }
41
53
 
54
+ /** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
55
+ function readDerNode(data: Uint8Array, offset: number) {
56
+ if (offset + 2 > data.length) {
57
+ throw new Error(t("core.password.public_key_invalid"));
58
+ }
59
+ const tag = data[offset];
60
+ const firstLengthByte = data[offset + 1];
61
+ let length = firstLengthByte;
62
+ let valueOffset = offset + 2;
63
+ if (firstLengthByte & 0x80) {
64
+ const lengthByteCount = firstLengthByte & 0x7f;
65
+ if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
66
+ throw new Error(t("core.password.public_key_invalid"));
67
+ }
68
+ length = 0;
69
+ for (let index = 0; index < lengthByteCount; index += 1) {
70
+ length = length * 256 + data[valueOffset + index];
71
+ }
72
+ valueOffset += lengthByteCount;
73
+ }
74
+ const end = valueOffset + length;
75
+ if (end > data.length) {
76
+ throw new Error(t("core.password.public_key_invalid"));
77
+ }
78
+ return { tag, value: data.subarray(valueOffset, end), next: end };
79
+ }
80
+
81
+ /** 去除 DER 正整数前导零字节。 */
82
+ function trimDerInteger(value: Uint8Array) {
83
+ let offset = 0;
84
+ while (offset < value.length - 1 && value[offset] === 0) {
85
+ offset += 1;
86
+ }
87
+ return value.subarray(offset);
88
+ }
89
+
90
+ /** 从 PEM SubjectPublicKeyInfo 中提取 RSA 模数和指数。 */
91
+ function parsePortableRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
92
+ const der = new Uint8Array(pemToArrayBuffer(publicKey));
93
+ const subjectPublicKeyInfo = readDerNode(der, 0);
94
+ const algorithm = readDerNode(subjectPublicKeyInfo.value, 0);
95
+ const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next);
96
+ if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
97
+ throw new Error(t("core.password.public_key_invalid"));
98
+ }
99
+ const rsaPublicKey = readDerNode(bitString.value, 1);
100
+ const modulus = readDerNode(rsaPublicKey.value, 0);
101
+ const exponent = readDerNode(rsaPublicKey.value, modulus.next);
102
+ if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
103
+ throw new Error(t("core.password.public_key_invalid"));
104
+ }
105
+ return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)];
106
+ }
107
+
108
+ /** 计算 RSA-OAEP 所需的 MGF1 掩码。 */
109
+ function generateMgf1Mask(cryptoApi: PortableCrypto, seed: Uint8Array, length: number) {
110
+ const mask = new Uint8Array(length);
111
+ const counter = new Uint8Array(4);
112
+ const hash = new cryptoApi.Sha256();
113
+ const blockCount = Math.ceil(length / hash.HASH_SIZE);
114
+ for (let index = 0; index < blockCount; index += 1) {
115
+ counter[0] = index >>> 24;
116
+ counter[1] = index >>> 16;
117
+ counter[2] = index >>> 8;
118
+ counter[3] = index;
119
+ let block = hash.reset().process(seed).process(counter).finish().result;
120
+ if (!block) {
121
+ throw new Error(t("core.password.encrypt_failed"));
122
+ }
123
+ const target = mask.subarray(index * hash.HASH_SIZE);
124
+ if (block.length > target.length) {
125
+ block = block.subarray(0, target.length);
126
+ }
127
+ target.set(block);
128
+ }
129
+ return mask;
130
+ }
131
+
132
+ /** 生成纯 JavaScript 密码加密所需的随机字节。 */
133
+ function getPortableRandomBytes(length: number) {
134
+ const cryptoApi = globalThis.crypto;
135
+ if (!cryptoApi?.getRandomValues) {
136
+ throw new Error(t("core.password.random_failed"));
137
+ }
138
+ return cryptoApi.getRandomValues(new Uint8Array(length));
139
+ }
140
+
141
+ /** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的密码加密协议。 */
142
+ async function encryptPortablePassword(
143
+ cryptoApi: PortableCrypto,
144
+ password: string,
145
+ publicKeyResponse: PasswordPublicKeyResponse
146
+ ): Promise<PasswordCrypto> {
147
+ const aesKey = getPortableRandomBytes(32);
148
+ const iv = getPortableRandomBytes(12);
149
+ const plaintext = cryptoApi.string_to_bytes(password, true);
150
+ const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key);
151
+ const hash = new cryptoApi.Sha256();
152
+ const keySize = Math.ceil(new cryptoApi.BigNumber(modulus).bitLength / 8);
153
+ const seed = getPortableRandomBytes(hash.HASH_SIZE);
154
+ const dataBlockLength = keySize - hash.HASH_SIZE - 1;
155
+ const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1;
156
+ if (paddingLength < 0) {
157
+ throw new Error(t("core.password.password_too_long"));
158
+ }
159
+ const encodedMessage = new Uint8Array(keySize);
160
+ const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1);
161
+ const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1);
162
+ const labelHash = hash.reset().process(new Uint8Array()).finish().result;
163
+ if (!labelHash) {
164
+ throw new Error(t("core.password.encrypt_failed"));
165
+ }
166
+ dataBlock.set(labelHash, 0);
167
+ dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1);
168
+ dataBlock[hash.HASH_SIZE + paddingLength] = 1;
169
+ const dataBlockMask = generateMgf1Mask(cryptoApi, seed, dataBlock.length);
170
+ for (let index = 0; index < dataBlock.length; index += 1) {
171
+ dataBlock[index] ^= dataBlockMask[index];
172
+ }
173
+ const seedMask = generateMgf1Mask(cryptoApi, dataBlock, maskedSeed.length);
174
+ maskedSeed.set(seed);
175
+ for (let index = 0; index < maskedSeed.length; index += 1) {
176
+ maskedSeed[index] ^= seedMask[index];
177
+ }
178
+ const encryptedKey = new cryptoApi.RSA([modulus, exponent]).encrypt(new cryptoApi.BigNumber(encodedMessage)).result;
179
+ if (!encryptedKey) {
180
+ throw new Error(t("core.password.encrypt_failed"));
181
+ }
182
+ const ciphertext = cryptoApi.AES_GCM.encrypt(plaintext, aesKey, iv);
183
+
184
+ return {
185
+ key_id: publicKeyResponse.key_id,
186
+ nonce: publicKeyResponse.nonce,
187
+ algorithm: publicKeyResponse.algorithm,
188
+ encrypted_key: arrayBufferToBase64(encryptedKey.slice().buffer),
189
+ iv: arrayBufferToBase64(iv.buffer),
190
+ ciphertext: arrayBufferToBase64(ciphertext.slice().buffer)
191
+ };
192
+ }
193
+
42
194
  /** 加密单个密码字段,返回后端可解析的密码密文。 */
43
195
  export async function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto> {
44
196
  const plainPassword = password.trim();
@@ -46,20 +198,25 @@ export async function encryptPassword(password: string, scene: PasswordCryptoSce
46
198
  throw new Error(t("core.password.required"));
47
199
  }
48
200
 
49
- const cryptoApi = getSubtleCrypto();
50
201
  const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene });
51
- const publicKey = await cryptoApi.subtle.importKey(
202
+ const cryptoApi = globalThis.crypto;
203
+ const isSecureContext = typeof window === "undefined" || window.isSecureContext;
204
+ if (!cryptoApi?.subtle || !isSecureContext) {
205
+ return encryptPortablePassword(portableCrypto, plainPassword, publicKeyResponse);
206
+ }
207
+ const subtleCrypto = getSubtleCrypto();
208
+ const publicKey = await subtleCrypto.subtle.importKey(
52
209
  "spki",
53
210
  pemToArrayBuffer(publicKeyResponse.public_key),
54
211
  { name: "RSA-OAEP", hash: "SHA-256" },
55
212
  false,
56
213
  ["encrypt"]
57
214
  );
58
- const aesKey = await cryptoApi.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
59
- const rawAesKey = await cryptoApi.subtle.exportKey("raw", aesKey);
60
- const iv = cryptoApi.getRandomValues(new Uint8Array(12));
61
- const ciphertext = await cryptoApi.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(plainPassword));
62
- const encryptedKey = await cryptoApi.subtle.encrypt({ name: "RSA-OAEP" }, publicKey, rawAesKey);
215
+ const aesKey = await subtleCrypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
216
+ const rawAesKey = await subtleCrypto.subtle.exportKey("raw", aesKey);
217
+ const iv = subtleCrypto.getRandomValues(new Uint8Array(12));
218
+ const ciphertext = await subtleCrypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(plainPassword));
219
+ const encryptedKey = await subtleCrypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKey, rawAesKey);
63
220
 
64
221
  return {
65
222
  key_id: publicKeyResponse.key_id,
@@ -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
- }