@liujitcn/kratos-admin-core 0.0.66 → 0.0.68

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 (43) 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/index.vue +1 -0
  22. package/dist/package/src/components/ProForm/interface/index.ts +15 -0
  23. package/dist/package/src/components/ProTable/index.vue +8 -1
  24. package/dist/package/src/locales/README.md +1 -1
  25. package/dist/package/src/locales/en-US.json +9 -1
  26. package/dist/package/src/locales/ja-JP.json +9 -1
  27. package/dist/package/src/locales/zh-CN.json +9 -1
  28. package/dist/package/src/locales/zh-TW.json +9 -1
  29. package/dist/package/src/rpc/base/v1/login.ts +2 -42
  30. package/dist/package/src/rpc/base/v1/mfa.ts +3 -3
  31. package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
  32. package/dist/package/src/rpc/base/v1/secret_crypto.ts +49 -0
  33. package/dist/package/src/rpc/common/v1/types.ts +4 -4
  34. package/dist/package/src/rpc/system/admin/v1/auth.ts +3 -3
  35. package/dist/package/src/security.ts +1 -1
  36. package/dist/package/src/utils/request.ts +3 -3
  37. package/dist/package/src/utils/{passwordCrypto.ts → secretCrypto.ts} +112 -50
  38. package/dist/package/src/views/demo/components/DemoFormDialog.vue +3 -3
  39. package/dist/package/src/views/demo/table/index.vue +2 -2
  40. package/dist/package/src/views/demo/tree-grid/index.vue +1 -1
  41. package/dist/package/src/views/login/components/LoginForm.vue +2 -2
  42. package/package.json +6 -2
  43. package/dist/package/declarations/src/utils/passwordCrypto.d.ts +0 -6
@@ -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
  }
@@ -123,13 +123,13 @@ const deptTreeOptions: ProFormOption[] = demoDeptTree.map(dept => ({
123
123
 
124
124
  /** 租户静态选项:tenant-select 传入 options 后不再请求租户接口,纯静态可渲染。 */
125
125
  const tenantOptions: ProFormOption[] = [
126
- { label: "Demo 总租户", value: 1 },
127
- { label: "Demo 子租户", value: 2 }
126
+ { label: t("core.demo.form.tenant.main"), value: 1 },
127
+ { label: t("core.demo.form.tenant.sub"), value: 2 }
128
128
  ];
129
129
 
130
130
  /** 协作成员穿梭框数据源:value 即选中后的 key。 */
131
131
  const memberOptions: ProFormOption[] = demoDeptTree[0].children!.map((dept, index) => ({
132
- label: `${dept.name}负责人`,
132
+ label: t("core.demo.form.leader_title", { name: dept.name }),
133
133
  value: index + 1
134
134
  }));
135
135
 
@@ -68,7 +68,7 @@ const statusTagOptions = [
68
68
  ];
69
69
 
70
70
  /** select-v2 大数据下拉的静态选项(isShow: false,仅出现在搜索区)。 */
71
- const tagSearchOptions = Array.from({ length: 50 }, (_, index) => ({ label: `标签 ${index + 1}`, value: `tag-${index + 1}` }));
71
+ const tagSearchOptions = Array.from({ length: 50 }, (_, index) => ({ label: t("core.demo.table.tag_item", { index: index + 1 }), value: `tag-${index + 1}` }));
72
72
 
73
73
  /** tree-select 搜索项的静态团队树(isShow: false,仅出现在搜索区)。 */
74
74
  const ownerGroupOptions = demoRegionTree;
@@ -121,7 +121,7 @@ const columns = computed<ColumnProps[]>(() => [
121
121
  minWidth: 130,
122
122
  align: "right",
123
123
  cellType: "money",
124
- moneyProps: { prefix: "¥", suffix: " 元" }
124
+ moneyProps: { prefix: "¥", suffix: t("core.demo.table.money_unit") }
125
125
  },
126
126
  {
127
127
  // 多级表头:_children 嵌套的列渲染在“任务情况”分组下
@@ -180,7 +180,7 @@ function handleCreateChild(parentId?: number) {
180
180
  function handleFormSubmit(data: Record<string, any>) {
181
181
  const node: DemoCatalog = {
182
182
  id: ++nextId,
183
- name: String(data.name || `Demo 目录 ${nextId}`),
183
+ name: String(data.name || t("core.demo.tree.new_dir", { id: nextId })),
184
184
  code: `demo-${nextId}`,
185
185
  owner: data.visibility === "private" ? "--" : "Demo",
186
186
  status: data.enabled ? STATUS_ENABLE : STATUS_DISABLE,
@@ -217,7 +217,7 @@ import { useKeepAliveStore } from "../../../stores/modules/keepAlive";
217
217
  import { initDynamicRouter } from "../../../routers/modules/dynamicRouter";
218
218
  import { isUnmatchedRoute, navigateTo, resolveFrontendRouteURL } from "../../../utils/router";
219
219
  import type { ElForm, FormRules } from "element-plus";
220
- import { PASSWORD_CRYPTO_SCENE, encryptPassword } from "../../../utils/passwordCrypto";
220
+ import { encryptPassword } from "../../../utils/secretCrypto";
221
221
  import { useConfigStore } from "../../../stores/modules/config";
222
222
  import { Click as GoCaptchaClick, Rotate as GoCaptchaRotate, Slide as GoCaptchaSlide } from "go-captcha-vue";
223
223
  import "go-captcha-vue/dist/style.css";
@@ -797,7 +797,7 @@ const verifyCaptchaToken = async (captchaCode: string) => {
797
797
  const submitLogin = async (captchaToken: string) => {
798
798
  loading.value = true;
799
799
  try {
800
- const password = await encryptPassword(loginForm.password, PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_LOGIN);
800
+ const password = await encryptPassword(loginForm.password);
801
801
  const loginRequest: LoginRequest = {
802
802
  tenant_code: configStore.showTenantCode ? loginForm.tenant_code : "0000",
803
803
  user_name: loginForm.user_name,
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.66",
4
+ "version": "0.0.68",
5
5
  "type": "module",
6
6
  "description": "kratos-admin frontend runtime and base modules",
7
7
  "author": {
@@ -232,7 +232,11 @@
232
232
  "types": "./dist/package/declarations/vite.config.d.ts",
233
233
  "default": "./vite.config.ts"
234
234
  },
235
- "./package.json": "./package.json"
235
+ "./package.json": "./package.json",
236
+ "./utils/secretCrypto": {
237
+ "types": "./dist/package/declarations/src/utils/secretCrypto.d.ts",
238
+ "default": "./dist/package/src/utils/secretCrypto.ts"
239
+ }
236
240
  },
237
241
  "files": [
238
242
  "dist/package",
@@ -1,6 +0,0 @@
1
- import { PasswordCryptoScene } from "../rpc/base/v1/login";
2
- import type { PasswordCrypto } from "../rpc/common/v1/types";
3
- export declare const PASSWORD_CRYPTO_SCENE: typeof PasswordCryptoScene;
4
- export type { PasswordCryptoScene };
5
- /** 加密单个密码字段,返回后端可解析的密码密文。 */
6
- export declare function encryptPassword(password: string, scene: PasswordCryptoScene): Promise<PasswordCrypto>;