@liujitcn/kratos-admin-core 0.0.66 → 0.0.67

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 (41) hide show
  1. package/dist/package/declarations/src/api/base/v1/login.d.ts +1 -3
  2. package/dist/package/declarations/src/api/base/v1/secret_crypto.d.ts +9 -0
  3. package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +3 -7
  4. package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +6 -6
  5. package/dist/package/declarations/src/components/ProForm/components/SecretInput.vue.d.ts +23 -0
  6. package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +14 -1
  7. package/dist/package/declarations/src/locales/generated.d.ts +32 -0
  8. package/dist/package/declarations/src/rpc/base/v1/login.d.ts +2 -39
  9. package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +3 -3
  10. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +2 -2
  11. package/dist/package/declarations/src/rpc/base/v1/secret_crypto.d.ts +37 -0
  12. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +4 -4
  13. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +3 -3
  14. package/dist/package/declarations/src/security.d.ts +1 -1
  15. package/dist/package/declarations/src/utils/secretCrypto.d.ts +19 -0
  16. package/dist/package/src/api/base/v1/login.ts +0 -12
  17. package/dist/package/src/api/base/v1/secret_crypto.ts +35 -0
  18. package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +4 -8
  19. package/dist/package/src/components/ProForm/components/ProFormItem.vue +24 -1
  20. package/dist/package/src/components/ProForm/components/SecretInput.vue +117 -0
  21. package/dist/package/src/components/ProForm/interface/index.ts +15 -0
  22. package/dist/package/src/locales/README.md +1 -1
  23. package/dist/package/src/locales/en-US.json +9 -1
  24. package/dist/package/src/locales/ja-JP.json +9 -1
  25. package/dist/package/src/locales/zh-CN.json +9 -1
  26. package/dist/package/src/locales/zh-TW.json +9 -1
  27. package/dist/package/src/rpc/base/v1/login.ts +2 -42
  28. package/dist/package/src/rpc/base/v1/mfa.ts +3 -3
  29. package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
  30. package/dist/package/src/rpc/base/v1/secret_crypto.ts +49 -0
  31. package/dist/package/src/rpc/common/v1/types.ts +4 -4
  32. package/dist/package/src/rpc/system/admin/v1/auth.ts +3 -3
  33. package/dist/package/src/security.ts +1 -1
  34. package/dist/package/src/utils/request.ts +3 -3
  35. package/dist/package/src/utils/{passwordCrypto.ts → secretCrypto.ts} +112 -50
  36. package/dist/package/src/views/demo/components/DemoFormDialog.vue +3 -3
  37. package/dist/package/src/views/demo/table/index.vue +2 -2
  38. package/dist/package/src/views/demo/tree-grid/index.vue +1 -1
  39. package/dist/package/src/views/login/components/LoginForm.vue +2 -2
  40. package/package.json +6 -2
  41. package/dist/package/declarations/src/utils/passwordCrypto.d.ts +0 -6
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <el-input
3
+ :model-value="modelValue"
4
+ :type="visible ? 'text' : 'password'"
5
+ autocomplete="new-password"
6
+ v-bind="inputAttrs"
7
+ @update:model-value="onInput"
8
+ >
9
+ <template #suffix>
10
+ <el-icon class="secret-input__toggle" @click="toggle">
11
+ <Loading v-if="loading" />
12
+ <View v-else-if="visible" />
13
+ <Hide v-else />
14
+ </el-icon>
15
+ </template>
16
+ </el-input>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed, ref, useAttrs } from "vue";
21
+ import { ElMessage } from "element-plus";
22
+ import { Hide, Loading, View } from "@element-plus/icons-vue";
23
+ import type { ProFormSecretConfig } from "../../../components/ProForm/interface";
24
+ import { revealSecret } from "../../../utils/secretCrypto";
25
+ import { t } from "../../../locales";
26
+
27
+ /** SecretInput 组件属性。 */
28
+ interface SecretInputProps {
29
+ /** 字段值。 */
30
+ modelValue?: string;
31
+ /** 查看配置:声明后眼睛点击时按需拉取明文。 */
32
+ secret?: boolean | ProFormSecretConfig;
33
+ /** 查看的资源标识。 */
34
+ resource?: string;
35
+ /** 查看的字段名。 */
36
+ fieldName?: string;
37
+ /** 记录ID。 */
38
+ recordId?: number;
39
+ /** 是否已配置密钥。 */
40
+ configured?: boolean;
41
+ }
42
+
43
+ const props = defineProps<SecretInputProps>();
44
+
45
+ const emit = defineEmits<{
46
+ (event: "update:modelValue", value: string): void;
47
+ }>();
48
+
49
+ /** 明文是否可见。 */
50
+ const visible = ref(false);
51
+ /** 明文是否来自远端查看。 */
52
+ const revealed = ref(false);
53
+ /** 查看请求进行中。 */
54
+ const loading = ref(false);
55
+
56
+ /** 解析查看配置。 */
57
+ const secretConfig = computed<ProFormSecretConfig | null>(() =>
58
+ typeof props.secret === "object" ? props.secret : null
59
+ );
60
+
61
+ // 透传调用方的 el-input 参数(maxlength、placeholder 等)。
62
+ const attrs = useAttrs();
63
+
64
+ /** 过滤掉原生密码框专属参数后透传其余输入参数。 */
65
+ const inputAttrs = computed(() => {
66
+ const { showPassword: _ignored, ...rest } = { ...(attrs as Record<string, unknown>) };
67
+ return rest;
68
+ });
69
+
70
+ /** 判断当前是否可以查看远端明文。 */
71
+ function canReveal() {
72
+ return Boolean(
73
+ secretConfig.value && props.resource && props.fieldName && (props.recordId ?? 0) > 0 && props.configured
74
+ );
75
+ }
76
+
77
+ /** 切换明文可见状态:查看态隐藏时清空回填的明文,保持"留空表示保留"语义。 */
78
+ async function toggle() {
79
+ if (loading.value) return;
80
+ if (visible.value) {
81
+ visible.value = false;
82
+ if (revealed.value) {
83
+ revealed.value = false;
84
+ emit("update:modelValue", "");
85
+ }
86
+ return;
87
+ }
88
+ if (props.modelValue) {
89
+ visible.value = true;
90
+ return;
91
+ }
92
+ if (!canReveal()) return;
93
+ loading.value = true;
94
+ try {
95
+ const text = await revealSecret(props.resource!, (props.recordId ?? 0), props.fieldName!);
96
+ emit("update:modelValue", text);
97
+ revealed.value = true;
98
+ visible.value = true;
99
+ } catch (error) {
100
+ ElMessage.error(error instanceof Error ? error.message : t("core.password.reveal_failed"));
101
+ } finally {
102
+ loading.value = false;
103
+ }
104
+ }
105
+
106
+ /** 输入回调:本地输入的值不再视为远端明文。 */
107
+ function onInput(value: string) {
108
+ revealed.value = false;
109
+ emit("update:modelValue", value);
110
+ }
111
+ </script>
112
+
113
+ <style scoped lang="scss">
114
+ .secret-input__toggle {
115
+ cursor: pointer;
116
+ }
117
+ </style>
@@ -13,6 +13,7 @@ export type ProFormComponentType =
13
13
  | "switch"
14
14
  | "checkbox"
15
15
  | "select"
16
+ | "cascader"
16
17
  | "tenant-select"
17
18
  | "dict"
18
19
  | "radio-group"
@@ -45,6 +46,18 @@ export interface ProFormOption {
45
46
  isLeaf?: boolean;
46
47
  }
47
48
 
49
+ /** secret 字段的查看配置:声明后输入框眼睛点击时按需拉取明文。 */
50
+ export interface ProFormSecretConfig {
51
+ /** 查看明文的资源标识。 */
52
+ resource: string;
53
+ /** 查看明文的字段名。 */
54
+ field: string;
55
+ /** 记录ID字段名,默认 "id"。 */
56
+ idProp?: string;
57
+ /** 已配置标记字段名,默认 `${field}_configured`。 */
58
+ configuredProp?: string;
59
+ }
60
+
48
61
  /** ProForm 字段配置。 */
49
62
  export interface ProFormField {
50
63
  /** 字段绑定路径,支持点路径访问嵌套对象。 */
@@ -69,6 +82,8 @@ export interface ProFormField {
69
82
  suffixSlotName?: string;
70
83
  /** 标题提示文案。 */
71
84
  labelTooltip?: string;
85
+ /** 提交时是否加密为 SecretCrypto 结构;配置对象时输入框眼睛支持按需查看明文。 */
86
+ secret?: boolean | ProFormSecretConfig;
72
87
  /** 字段校验规则。 */
73
88
  rules?: FormItemRule[];
74
89
  /** 字段是否显示。 */
@@ -9,4 +9,4 @@
9
9
  | `en-US.json` | 英文公共语言包。 | 管理端切换为英文时由 Vue I18n 使用。 |
10
10
  | `ja-JP.json` | 日文公共语言包。 | 管理端切换为日文时由 Vue I18n 使用。 |
11
11
 
12
- 语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n`,脚本会校验所有语言并生成注册产物。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化语言扩展指南](../../../../../../docs/国际化语言扩展指南.md)。
12
+ 语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n`,脚本会校验所有语言并生成注册产物。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化设计与语言扩展](../../../../../../docs/国际化最终方案.md)。
@@ -475,5 +475,13 @@
475
475
  "core.demo.detail.timeline.created": "Record created",
476
476
  "core.demo.detail.timeline.updated": "Base info updated",
477
477
  "core.demo.detail.timeline.enabled": "Record enabled",
478
- "core.demo.detail.timeline.disabled": "Record disabled"
478
+ "core.demo.detail.timeline.disabled": "Record disabled",
479
+ "core.password.reveal_unsupported": "Secret reveal is not registered in this environment",
480
+ "core.password.reveal_failed": "Failed to reveal the secret",
481
+ "core.demo.table.tag_item": "Tag {index}",
482
+ "core.demo.table.money_unit": " CNY",
483
+ "core.demo.form.tenant.main": "Demo main tenant",
484
+ "core.demo.form.tenant.sub": "Demo sub-tenant",
485
+ "core.demo.form.leader_title": "{name} Lead",
486
+ "core.demo.tree.new_dir": "Demo directory {id}"
479
487
  }
@@ -475,5 +475,13 @@
475
475
  "core.demo.detail.timeline.created": "記録を作成しました",
476
476
  "core.demo.detail.timeline.updated": "基本情報を更新しました",
477
477
  "core.demo.detail.timeline.enabled": "記録を有効化しました",
478
- "core.demo.detail.timeline.disabled": "記録を無効化しました"
478
+ "core.demo.detail.timeline.disabled": "記録を無効化しました",
479
+ "core.password.reveal_unsupported": "この環境ではシークレット表示が登録されていません",
480
+ "core.password.reveal_failed": "シークレットの表示に失敗しました",
481
+ "core.demo.table.tag_item": "タグ {index}",
482
+ "core.demo.table.money_unit": " 円",
483
+ "core.demo.form.tenant.main": "Demoメインテナント",
484
+ "core.demo.form.tenant.sub": "Demoサブテナント",
485
+ "core.demo.form.leader_title": "{name}リーダー",
486
+ "core.demo.tree.new_dir": "Demoディレクトリ {id}"
479
487
  }
@@ -475,5 +475,13 @@
475
475
  "core.demo.detail.timeline.created": "创建了记录",
476
476
  "core.demo.detail.timeline.updated": "更新了基础信息",
477
477
  "core.demo.detail.timeline.enabled": "启用了记录",
478
- "core.demo.detail.timeline.disabled": "停用了记录"
478
+ "core.demo.detail.timeline.disabled": "停用了记录",
479
+ "core.password.reveal_unsupported": "当前环境未注册密钥查看能力",
480
+ "core.password.reveal_failed": "查看密钥失败",
481
+ "core.demo.table.tag_item": "标签 {index}",
482
+ "core.demo.table.money_unit": " 元",
483
+ "core.demo.form.tenant.main": "Demo 总租户",
484
+ "core.demo.form.tenant.sub": "Demo 子租户",
485
+ "core.demo.form.leader_title": "{name}负责人",
486
+ "core.demo.tree.new_dir": "Demo 目录 {id}"
479
487
  }
@@ -475,5 +475,13 @@
475
475
  "core.demo.detail.timeline.created": "創建了記錄",
476
476
  "core.demo.detail.timeline.updated": "更新了基礎資訊",
477
477
  "core.demo.detail.timeline.enabled": "啟用了記錄",
478
- "core.demo.detail.timeline.disabled": "停用了記錄"
478
+ "core.demo.detail.timeline.disabled": "停用了記錄",
479
+ "core.password.reveal_unsupported": "目前環境未註冊密鑰查看能力",
480
+ "core.password.reveal_failed": "查看密鑰失敗",
481
+ "core.demo.table.tag_item": "標籤 {index}",
482
+ "core.demo.table.money_unit": " 元",
483
+ "core.demo.form.tenant.main": "Demo 總租戶",
484
+ "core.demo.form.tenant.sub": "Demo 子租戶",
485
+ "core.demo.form.leader_title": "{name}負責人",
486
+ "core.demo.tree.new_dir": "Demo 目錄 {id}"
479
487
  }
@@ -5,27 +5,9 @@
5
5
  // source: base/v1/login.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { PasswordCrypto } from "../../common/v1/types";
8
+ import type { SecretCrypto } from "../../common/v1/types";
9
9
  import type { Empty } from "../../google/protobuf/empty";
10
10
 
11
- /** 密码加密场景枚举。 */
12
- export enum PasswordCryptoScene {
13
- /** PASSWORD_CRYPTO_SCENE_UNSPECIFIED - 未指定密码加密场景。 */
14
- PASSWORD_CRYPTO_SCENE_UNSPECIFIED = 0,
15
- /** PASSWORD_CRYPTO_SCENE_LOGIN - 登录密码加密场景。 */
16
- PASSWORD_CRYPTO_SCENE_LOGIN = 1,
17
- /** PASSWORD_CRYPTO_SCENE_CREATE_BASE_USER - 新增后台用户密码加密场景。 */
18
- PASSWORD_CRYPTO_SCENE_CREATE_BASE_USER = 2,
19
- /** PASSWORD_CRYPTO_SCENE_RESET_BASE_USER_PASSWORD - 重置后台用户密码加密场景。 */
20
- PASSWORD_CRYPTO_SCENE_RESET_BASE_USER_PASSWORD = 3,
21
- /** PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD - 个人修改密码加密场景。 */
22
- PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD = 4,
23
- /** PASSWORD_CRYPTO_SCENE_MFA - 多因素认证敏感操作场景。 */
24
- PASSWORD_CRYPTO_SCENE_MFA = 5,
25
- /** PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY - 密码策略初始化密码配置场景。 */
26
- PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY = 6,
27
- }
28
-
29
11
  /**
30
12
  * LoginStatus 表示登录请求当前所处的认证阶段。
31
13
  * 只有 AUTHENTICATED 或 PASSWORD_CHANGE_REQUIRED 才会携带正式访问令牌,MFA 状态必须继续完成挑战。
@@ -49,12 +31,6 @@ export interface CaptchaRequest {
49
31
  type: string;
50
32
  }
51
33
 
52
- /** 密码临时公钥获取条件 */
53
- export interface PasswordPublicKeyRequest {
54
- /** 使用场景 */
55
- scene: PasswordCryptoScene;
56
- }
57
-
58
34
  /** 登出请求条件 */
59
35
  export interface LogoutRequest {
60
36
  }
@@ -85,20 +61,6 @@ export interface VerifyCaptchaResponse {
85
61
  expires_in: number;
86
62
  }
87
63
 
88
- /** 密码临时公钥响应 */
89
- export interface PasswordPublicKeyResponse {
90
- /** 临时密钥ID */
91
- key_id: string;
92
- /** 临时公钥 */
93
- public_key: string;
94
- /** 加密算法 */
95
- algorithm: string;
96
- /** 随机值 */
97
- nonce: string;
98
- /** 有效时间,单位秒 */
99
- expires_in: number;
100
- }
101
-
102
64
  /** 刷新令牌请求参数 */
103
65
  export interface RefreshTokenRequest {
104
66
  /** 更新令牌;未填写时由服务端从HttpOnly Cookie读取。 */
@@ -125,7 +87,7 @@ export interface LoginRequest {
125
87
  user_name: string;
126
88
  /** 用户的密码,必选项。 */
127
89
  password:
128
- | PasswordCrypto
90
+ | SecretCrypto
129
91
  | undefined;
130
92
  /** 验证码 */
131
93
  captcha_code: string;
@@ -165,8 +127,6 @@ export interface LoginService {
165
127
  Captcha(request: CaptchaRequest): Promise<CaptchaResponse>;
166
128
  /** 验证码预校验 */
167
129
  VerifyCaptcha(request: VerifyCaptchaRequest): Promise<VerifyCaptchaResponse>;
168
- /** 获取密码临时公钥 */
169
- PasswordPublicKey(request: PasswordPublicKeyRequest): Promise<PasswordPublicKeyResponse>;
170
130
  /** 登出 */
171
131
  Logout(request: LogoutRequest): Promise<Empty>;
172
132
  /** 刷新认证令牌 */
@@ -5,7 +5,7 @@
5
5
  // source: base/v1/mfa.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { PasswordCrypto } from "../../common/v1/types";
8
+ import type { SecretCrypto } from "../../common/v1/types";
9
9
  import type { Empty } from "../../google/protobuf/empty";
10
10
  import type { LoginResponse } from "./login";
11
11
 
@@ -79,7 +79,7 @@ export interface MfaStatusResponse {
79
79
  export interface BeginMfaSetupRequest {
80
80
  /** 当前登录密码 */
81
81
  password:
82
- | PasswordCrypto
82
+ | SecretCrypto
83
83
  | undefined;
84
84
  /** 强制绑定场景下的临时票据 */
85
85
  setup_ticket: string;
@@ -135,7 +135,7 @@ export interface BeginMfaDisableResponse {
135
135
  export interface DisableMfaRequest {
136
136
  /** 当前登录密码 */
137
137
  password:
138
- | PasswordCrypto
138
+ | SecretCrypto
139
139
  | undefined;
140
140
  /** TOTP 动态口令 */
141
141
  code: string;
@@ -5,7 +5,7 @@
5
5
  // source: base/v1/oauth.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { PasswordCrypto } from "../../common/v1/types";
8
+ import type { SecretCrypto } from "../../common/v1/types";
9
9
  import type { Empty } from "../../google/protobuf/empty";
10
10
  import type { LoginStatus } from "./login";
11
11
 
@@ -77,7 +77,7 @@ export interface BindOauthSessionRequest {
77
77
  user_name: string;
78
78
  /** 用户密码 */
79
79
  password:
80
- | PasswordCrypto
80
+ | SecretCrypto
81
81
  | undefined;
82
82
  /** 验证码 */
83
83
  captcha_code: string;
@@ -0,0 +1,49 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.8
4
+ // protoc unknown
5
+ // source: base/v1/secret_crypto.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 敏感字段临时公钥获取条件 */
10
+ export interface GetSecretPublicKeyRequest {
11
+ }
12
+
13
+ /** 敏感字段临时公钥响应 */
14
+ export interface GetSecretPublicKeyResponse {
15
+ /** 临时密钥ID */
16
+ key_id: string;
17
+ /** 临时公钥 */
18
+ public_key: string;
19
+ /** 加密算法 */
20
+ algorithm: string;
21
+ /** 随机值 */
22
+ nonce: string;
23
+ /** 有效时间,单位秒 */
24
+ expires_in: number;
25
+ }
26
+
27
+ /** 密钥字段查看条件 */
28
+ export interface RevealSecretFieldRequest {
29
+ /** 资源标识,如 ai_provider、base_oauth_provider、base_message_provider */
30
+ resource: string;
31
+ /** 记录ID */
32
+ id: number;
33
+ /** 字段名,如 api_key、client_secret */
34
+ field: string;
35
+ }
36
+
37
+ /** 密钥字段查看响应 */
38
+ export interface RevealSecretFieldResponse {
39
+ /** 密钥明文 */
40
+ text: string;
41
+ }
42
+
43
+ /** 敏感字段加密公共服务 */
44
+ export interface SecretCryptoService {
45
+ /** 获取敏感字段临时公钥,同一次提交的多个密文字段共用该公钥 */
46
+ GetSecretPublicKey(request: GetSecretPublicKeyRequest): Promise<GetSecretPublicKeyResponse>;
47
+ /** 查看密钥字段明文,仅允许查询注册过的密钥字段 */
48
+ RevealSecretField(request: RevealSecretFieldRequest): Promise<RevealSecretFieldResponse>;
49
+ }
@@ -60,8 +60,8 @@ export interface BytesValues {
60
60
  value: Uint8Array[];
61
61
  }
62
62
 
63
- /** 密码密文。 */
64
- export interface PasswordCrypto {
63
+ /** 敏感字段密文,密码与各类密钥字段统一使用。 */
64
+ export interface SecretCrypto {
65
65
  /** 临时密钥ID */
66
66
  key_id: string;
67
67
  /** 随机值 */
@@ -72,6 +72,6 @@ export interface PasswordCrypto {
72
72
  encrypted_key: string;
73
73
  /** 初始化向量 */
74
74
  iv: string;
75
- /** 密码加密数据 */
76
- ciphertext: string;
75
+ /** 传输时为密文,服务端解密后回填明文 */
76
+ text: string;
77
77
  }
@@ -5,7 +5,7 @@
5
5
  // source: system/admin/v1/auth.proto
6
6
 
7
7
  /* eslint-disable */
8
- import type { PasswordCrypto, StringValues } from "../../../common/v1/types";
8
+ import type { SecretCrypto, StringValues } from "../../../common/v1/types";
9
9
  import type { Empty } from "../../../google/protobuf/empty";
10
10
  import type { BaseUserIDType } from "../../common/v1/common";
11
11
  import type { BaseMenuType } from "./common";
@@ -202,10 +202,10 @@ export interface RouteParams {
202
202
  export interface UserPasswordForm {
203
203
  /** 原密码 */
204
204
  old_pwd:
205
- | PasswordCrypto
205
+ | SecretCrypto
206
206
  | undefined;
207
207
  /** 新密码 */
208
- new_pwd: PasswordCrypto | undefined;
208
+ new_pwd: SecretCrypto | undefined;
209
209
  }
210
210
 
211
211
  /** 用户手机号表单 */
@@ -1,5 +1,5 @@
1
1
  export * from "./utils/oauthProvider";
2
- export * from "./utils/passwordCrypto";
2
+ export * from "./utils/secretCrypto";
3
3
  export * from "./utils/passwordStrength";
4
4
  export * from "./utils/webauthn";
5
5
  export * from "./utils/clipboard";
@@ -22,7 +22,7 @@ const PASSWORD_CHANGE_STATE_STORAGE_KEY = "admin-password-change-required";
22
22
  const CAPTCHA_URL = "/v1/base/captcha";
23
23
  const CONFIG_URL = "/v1/base/config";
24
24
  const LANGUAGE_URL = "/v1/base/language";
25
- const PASSWORD_PUBLIC_KEY_URL = "/v1/base/password-public-key";
25
+ const SECRET_PUBLIC_KEY_URL = "/v1/base/secret-public-key";
26
26
  const OAUTH_PROVIDER_URL = "/v1/base/oauth/provider";
27
27
  const OAUTH_AUTHORIZATION_URL = "/v1/base/oauth/authorization";
28
28
  const OAUTH_TICKET_URL = "/v1/base/oauth/ticket";
@@ -36,7 +36,7 @@ const NO_AUTH_URL_SET = new Set([
36
36
  CAPTCHA_URL,
37
37
  CONFIG_URL,
38
38
  LANGUAGE_URL,
39
- PASSWORD_PUBLIC_KEY_URL,
39
+ SECRET_PUBLIC_KEY_URL,
40
40
  OAUTH_PROVIDER_URL,
41
41
  OAUTH_AUTHORIZATION_URL,
42
42
  OAUTH_TICKET_URL,
@@ -50,7 +50,7 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
50
50
  CAPTCHA_URL,
51
51
  CONFIG_URL,
52
52
  LANGUAGE_URL,
53
- PASSWORD_PUBLIC_KEY_URL,
53
+ SECRET_PUBLIC_KEY_URL,
54
54
  OAUTH_PROVIDER_URL,
55
55
  OAUTH_AUTHORIZATION_URL,
56
56
  OAUTH_TICKET_URL,
@@ -1,13 +1,112 @@
1
- import { defLoginService } from "../api/base/v1/login";
2
- import { PasswordCryptoScene, type PasswordPublicKeyResponse } from "../rpc/base/v1/login";
3
- import type { PasswordCrypto } from "../rpc/common/v1/types";
1
+ import { defSecretCryptoService } from "../api/base/v1/secret_crypto";
2
+ import type { GetSecretPublicKeyResponse } from "../rpc/base/v1/secret_crypto";
3
+ import type { SecretCrypto } from "../rpc/common/v1/types";
4
+ import type { ProFormField } from "../components/ProForm/interface";
4
5
  import { t } from "../locales";
5
6
  import * as portableCrypto from "asmcrypto.js";
6
7
 
7
8
  type PortableCrypto = typeof import("asmcrypto.js");
8
9
 
9
- export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene;
10
- export type { PasswordCryptoScene };
10
+ /** 获取敏感字段一次性临时公钥,同一次提交的多个密文字段共用该公钥。 */
11
+ export async function fetchSecretPublicKey(): Promise<GetSecretPublicKeyResponse> {
12
+ return defSecretCryptoService.GetSecretPublicKey({});
13
+ }
14
+
15
+ /** encodeSecretFields 在提交前把声明 secret 的字段统一加密为 SecretCrypto,返回模型浅克隆且不改动原表单。 */
16
+ export async function encodeSecretFields(
17
+ fields: ProFormField[],
18
+ model: Record<string, any>
19
+ ): Promise<Record<string, any>> {
20
+ const secretFields = fields.filter(field => field.secret);
21
+ const filled = secretFields
22
+ .map(field => ({ field, value: getByPath(model, field.prop) }))
23
+ .filter((item): item is { field: ProFormField; value: string } => typeof item.value === "string" && item.value.trim() !== "");
24
+ if (secretFields.length === 0) {
25
+ return { ...model };
26
+ }
27
+ const publicKey = filled.length > 0 ? await fetchSecretPublicKey() : null;
28
+ const payload = { ...model };
29
+ for (const item of secretFields) {
30
+ const value = getByPath(model, item.prop);
31
+ if (typeof value === "string" && value.trim() !== "") {
32
+ payload[item.prop] = await encryptWithPublicKey(publicKey!, value);
33
+ } else {
34
+ payload[item.prop] = undefined;
35
+ }
36
+ }
37
+ return payload;
38
+ }
39
+
40
+ /** 按点路径读取对象字段值。 */
41
+ function getByPath(model: Record<string, any>, path: string) {
42
+ return path.split(".").reduce((current, key) => (current == null ? current : current[key]), model as any);
43
+ }
44
+
45
+ /** 使用已获取的临时公钥加密单个敏感值,供一次取钥加密多个字段的场景复用。 */
46
+ export async function encryptWithPublicKey(publicKey: GetSecretPublicKeyResponse, value: string): Promise<SecretCrypto> {
47
+ if (!value) {
48
+ throw new Error(t("core.password.required"));
49
+ }
50
+ const cryptoApi = globalThis.crypto;
51
+ const isSecureContext = typeof window === "undefined" || window.isSecureContext;
52
+ if (!cryptoApi?.subtle || !isSecureContext) {
53
+ return encryptPortableSecret(portableCrypto, value, publicKey);
54
+ }
55
+ const subtleCrypto = getSubtleCrypto();
56
+ const publicKeyKey = await subtleCrypto.subtle.importKey(
57
+ "spki",
58
+ pemToArrayBuffer(publicKey.public_key),
59
+ { name: "RSA-OAEP", hash: "SHA-256" },
60
+ false,
61
+ ["encrypt"]
62
+ );
63
+ const aesKey = await subtleCrypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt"]);
64
+ const rawAesKey = await subtleCrypto.subtle.exportKey("raw", aesKey);
65
+ const iv = subtleCrypto.getRandomValues(new Uint8Array(12));
66
+ const ciphertext = await subtleCrypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, new TextEncoder().encode(value));
67
+ const encryptedKey = await subtleCrypto.subtle.encrypt({ name: "RSA-OAEP" }, publicKeyKey, rawAesKey);
68
+
69
+ return {
70
+ key_id: publicKey.key_id,
71
+ nonce: publicKey.nonce,
72
+ algorithm: publicKey.algorithm,
73
+ encrypted_key: arrayBufferToBase64(encryptedKey),
74
+ iv: arrayBufferToBase64(iv.buffer),
75
+ text: arrayBufferToBase64(ciphertext)
76
+ };
77
+ }
78
+
79
+ /** 加密单个敏感字段值,自动获取一次性临时公钥。 */
80
+ export async function encryptSecret(value: string): Promise<SecretCrypto> {
81
+ return encryptWithPublicKey(await fetchSecretPublicKey(), value);
82
+ }
83
+
84
+ /** SecretRevealHandler 按资源标识与字段名查看密钥明文的实现。 */
85
+ export type SecretRevealHandler = (resource: string, id: number, field: string) => Promise<string>;
86
+
87
+ let secretRevealHandler: SecretRevealHandler | null = null;
88
+
89
+ /** registerSecretRevealHandler 注册密钥明文查看实现,由业务模块在启动时注入。 */
90
+ export function registerSecretRevealHandler(handler: SecretRevealHandler) {
91
+ secretRevealHandler = handler;
92
+ }
93
+
94
+ /** revealSecret 查看注册过的密钥字段明文。 */
95
+ export async function revealSecret(resource: string, id: number, field: string): Promise<string> {
96
+ if (!secretRevealHandler) {
97
+ throw new Error(t("core.password.reveal_unsupported"));
98
+ }
99
+ return secretRevealHandler(resource, id, field);
100
+ }
101
+
102
+ /** 加密密码字段:去除首尾空白且不允许为空。 */
103
+ export async function encryptPassword(value: string): Promise<SecretCrypto> {
104
+ const plainPassword = value.trim();
105
+ if (!plainPassword) {
106
+ throw new Error(t("core.password.required"));
107
+ }
108
+ return encryptSecret(plainPassword);
109
+ }
11
110
 
12
111
  /** 将 PEM 公钥转换为二进制 DER 数据。 */
13
112
  function pemToArrayBuffer(pem: string) {
@@ -129,7 +228,7 @@ function generateMgf1Mask(cryptoApi: PortableCrypto, seed: Uint8Array, length: n
129
228
  return mask;
130
229
  }
131
230
 
132
- /** 生成纯 JavaScript 密码加密所需的随机字节。 */
231
+ /** 生成纯 JavaScript 加密所需的随机字节。 */
133
232
  function getPortableRandomBytes(length: number) {
134
233
  const cryptoApi = globalThis.crypto;
135
234
  if (!cryptoApi?.getRandomValues) {
@@ -138,15 +237,15 @@ function getPortableRandomBytes(length: number) {
138
237
  return cryptoApi.getRandomValues(new Uint8Array(length));
139
238
  }
140
239
 
141
- /** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的密码加密协议。 */
142
- async function encryptPortablePassword(
240
+ /** 使用纯 JavaScript 实现兼容缺少 WebCrypto 环境的敏感值加密协议。 */
241
+ async function encryptPortableSecret(
143
242
  cryptoApi: PortableCrypto,
144
- password: string,
145
- publicKeyResponse: PasswordPublicKeyResponse
146
- ): Promise<PasswordCrypto> {
243
+ value: string,
244
+ publicKeyResponse: GetSecretPublicKeyResponse
245
+ ): Promise<SecretCrypto> {
147
246
  const aesKey = getPortableRandomBytes(32);
148
247
  const iv = getPortableRandomBytes(12);
149
- const plaintext = cryptoApi.string_to_bytes(password, true);
248
+ const plaintext = cryptoApi.string_to_bytes(value, true);
150
249
  const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key);
151
250
  const hash = new cryptoApi.Sha256();
152
251
  const keySize = Math.ceil(new cryptoApi.BigNumber(modulus).bitLength / 8);
@@ -187,43 +286,6 @@ async function encryptPortablePassword(
187
286
  algorithm: publicKeyResponse.algorithm,
188
287
  encrypted_key: arrayBufferToBase64(encryptedKey.slice().buffer),
189
288
  iv: arrayBufferToBase64(iv.buffer),
190
- ciphertext: arrayBufferToBase64(ciphertext.slice().buffer)
191
- };
192
- }
193
-
194
- /** 加密单个密码字段,返回后端可解析的密码密文。 */
195
- export async function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto> {
196
- const plainPassword = password.trim();
197
- if (!plainPassword) {
198
- throw new Error(t("core.password.required"));
199
- }
200
-
201
- const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene });
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(
209
- "spki",
210
- pemToArrayBuffer(publicKeyResponse.public_key),
211
- { name: "RSA-OAEP", hash: "SHA-256" },
212
- false,
213
- ["encrypt"]
214
- );
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);
220
-
221
- return {
222
- key_id: publicKeyResponse.key_id,
223
- nonce: publicKeyResponse.nonce,
224
- algorithm: publicKeyResponse.algorithm,
225
- encrypted_key: arrayBufferToBase64(encryptedKey),
226
- iv: arrayBufferToBase64(iv.buffer),
227
- ciphertext: arrayBufferToBase64(ciphertext)
289
+ text: arrayBufferToBase64(ciphertext.slice().buffer)
228
290
  };
229
291
  }