@liujitcn/kratos-uni-app-core 0.0.65 → 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.
- package/dist/module.mjs +16 -4
- package/package.json +1 -1
- package/src/api/base/v1/login.ts +0 -13
- package/src/api/base/v1/secret_crypto.ts +36 -0
- package/src/components/KratosTabBar.vue +21 -2
- package/src/components/MfaManageDialog.vue +3 -9
- package/src/components/PasswordVerifyDialog.vue +4 -7
- package/src/index.ts +1 -0
- package/src/locales/README.md +1 -1
- package/src/locales/en-US.json +4 -1
- package/src/locales/ja-JP.json +4 -1
- package/src/locales/zh-CN.json +4 -1
- package/src/locales/zh-TW.json +4 -1
- package/src/rpc/base/v1/login.ts +2 -42
- package/src/rpc/base/v1/mfa.ts +3 -3
- package/src/rpc/base/v1/oauth.ts +2 -2
- package/src/rpc/base/v1/secret_crypto.ts +49 -0
- package/src/rpc/common/v1/types.ts +4 -4
- package/src/utils/http.ts +3 -3
- package/src/utils/{passwordCrypto.ts → secretCrypto.ts} +35 -31
- package/src/views/pages/login/login.vue +3 -9
package/dist/module.mjs
CHANGED
|
@@ -167,7 +167,10 @@ var zh_CN_default = {
|
|
|
167
167
|
"common.resource.default": "{resource}",
|
|
168
168
|
"common.action.delete_resource": "\u5220\u9664{resource}",
|
|
169
169
|
"common.action.set_field_status": "\u8BBE\u7F6E{field}",
|
|
170
|
-
"common.action.load_more": "\u52A0\u8F7D\u66F4\u591A"
|
|
170
|
+
"common.action.load_more": "\u52A0\u8F7D\u66F4\u591A",
|
|
171
|
+
"common.status.loading": "\u52A0\u8F7D\u4E2D\u2026",
|
|
172
|
+
"core.navigation.analytics": "\u5206\u6790",
|
|
173
|
+
"core.navigation.report": "\u62A5\u8868"
|
|
171
174
|
};
|
|
172
175
|
|
|
173
176
|
// src/locales/en-US.json
|
|
@@ -314,7 +317,10 @@ var en_US_default = {
|
|
|
314
317
|
"common.resource.default": "{resource}",
|
|
315
318
|
"common.action.delete_resource": "Delete {resource}",
|
|
316
319
|
"common.action.set_field_status": "Set {field}",
|
|
317
|
-
"common.action.load_more": "Load more"
|
|
320
|
+
"common.action.load_more": "Load more",
|
|
321
|
+
"common.status.loading": "Loading\u2026",
|
|
322
|
+
"core.navigation.analytics": "Analytics",
|
|
323
|
+
"core.navigation.report": "Reports"
|
|
318
324
|
};
|
|
319
325
|
|
|
320
326
|
// src/locales/ja-JP.json
|
|
@@ -461,7 +467,10 @@ var ja_JP_default = {
|
|
|
461
467
|
"common.resource.default": "{resource}",
|
|
462
468
|
"common.action.delete_resource": "{resource}\u3092\u524A\u9664",
|
|
463
469
|
"common.action.set_field_status": "{field}\u3092\u8A2D\u5B9A",
|
|
464
|
-
"common.action.load_more": "\u3055\u3089\u306B\u8AAD\u307F\u8FBC\u3080"
|
|
470
|
+
"common.action.load_more": "\u3055\u3089\u306B\u8AAD\u307F\u8FBC\u3080",
|
|
471
|
+
"common.status.loading": "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
|
|
472
|
+
"core.navigation.analytics": "\u5206\u6790",
|
|
473
|
+
"core.navigation.report": "\u30EC\u30DD\u30FC\u30C8"
|
|
465
474
|
};
|
|
466
475
|
|
|
467
476
|
// src/locales/zh-TW.json
|
|
@@ -608,7 +617,10 @@ var zh_TW_default = {
|
|
|
608
617
|
"common.resource.default": "{resource}",
|
|
609
618
|
"common.action.delete_resource": "\u522A\u9664{resource}",
|
|
610
619
|
"common.action.set_field_status": "\u8A2D\u5B9A{field}",
|
|
611
|
-
"common.action.load_more": "\u8F09\u5165\u66F4\u591A"
|
|
620
|
+
"common.action.load_more": "\u8F09\u5165\u66F4\u591A",
|
|
621
|
+
"common.status.loading": "\u8F09\u5165\u4E2D\u2026",
|
|
622
|
+
"core.navigation.analytics": "\u5206\u6790",
|
|
623
|
+
"core.navigation.report": "\u5831\u8868"
|
|
612
624
|
};
|
|
613
625
|
|
|
614
626
|
// src/locales/generated.ts
|
package/package.json
CHANGED
package/src/api/base/v1/login.ts
CHANGED
|
@@ -5,8 +5,6 @@ import type {
|
|
|
5
5
|
LogoutRequest,
|
|
6
6
|
LoginRequest,
|
|
7
7
|
LoginResponse,
|
|
8
|
-
PasswordPublicKeyRequest,
|
|
9
|
-
PasswordPublicKeyResponse,
|
|
10
8
|
RefreshTokenRequest,
|
|
11
9
|
RefreshTokenResponse,
|
|
12
10
|
VerifyCaptchaRequest,
|
|
@@ -17,7 +15,6 @@ import type { Empty } from '../../../rpc/google/protobuf/empty'
|
|
|
17
15
|
|
|
18
16
|
const CAPTCHA_URL = '/v1/base/captcha'
|
|
19
17
|
const CAPTCHA_VERIFY_URL = '/v1/base/captcha/verify'
|
|
20
|
-
const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
|
|
21
18
|
const SESSION_URL = '/v1/base/session'
|
|
22
19
|
const TOKEN_URL = '/v1/base/token'
|
|
23
20
|
|
|
@@ -43,16 +40,6 @@ export class LoginServiceImpl implements LoginService {
|
|
|
43
40
|
header: { Authorization: 'no-auth' },
|
|
44
41
|
})
|
|
45
42
|
}
|
|
46
|
-
/** 获取密码临时公钥 */
|
|
47
|
-
PasswordPublicKey(request: PasswordPublicKeyRequest): Promise<PasswordPublicKeyResponse> {
|
|
48
|
-
return http<PasswordPublicKeyResponse>({
|
|
49
|
-
url: `${PASSWORD_PUBLIC_KEY_URL}`,
|
|
50
|
-
method: 'GET',
|
|
51
|
-
authMode: 'none',
|
|
52
|
-
data: request,
|
|
53
|
-
header: { Authorization: 'no-auth' },
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
43
|
/** 登出 */
|
|
57
44
|
Logout(request: LogoutRequest): Promise<Empty> {
|
|
58
45
|
return http<Empty>({
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
|
+
import type {
|
|
3
|
+
GetSecretPublicKeyRequest,
|
|
4
|
+
GetSecretPublicKeyResponse,
|
|
5
|
+
RevealSecretFieldRequest,
|
|
6
|
+
RevealSecretFieldResponse,
|
|
7
|
+
SecretCryptoService,
|
|
8
|
+
} from '../../../rpc/base/v1/secret_crypto'
|
|
9
|
+
|
|
10
|
+
const SECRET_PUBLIC_KEY_URL = '/v1/base/secret-public-key'
|
|
11
|
+
const SECRET_FIELD_REVEAL_URL = '/v1/base/secret-field/reveal'
|
|
12
|
+
|
|
13
|
+
/** 敏感字段加密公共服务 */
|
|
14
|
+
export class SecretCryptoServiceImpl implements SecretCryptoService {
|
|
15
|
+
/** 获取敏感字段临时公钥 */
|
|
16
|
+
GetSecretPublicKey(request: GetSecretPublicKeyRequest): Promise<GetSecretPublicKeyResponse> {
|
|
17
|
+
return http<GetSecretPublicKeyResponse>({
|
|
18
|
+
url: `${SECRET_PUBLIC_KEY_URL}`,
|
|
19
|
+
method: 'GET',
|
|
20
|
+
authMode: 'none',
|
|
21
|
+
data: request,
|
|
22
|
+
header: { Authorization: 'no-auth' },
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 查看密钥字段明文 */
|
|
27
|
+
RevealSecretField(request: RevealSecretFieldRequest): Promise<RevealSecretFieldResponse> {
|
|
28
|
+
return http<RevealSecretFieldResponse>({
|
|
29
|
+
url: `${SECRET_FIELD_REVEAL_URL}`,
|
|
30
|
+
method: 'POST',
|
|
31
|
+
data: request,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const defSecretCryptoService = new SecretCryptoServiceImpl()
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, onMounted } from 'vue'
|
|
3
3
|
import { resolveModuleIcon, resolveStaticView } from '../module'
|
|
4
4
|
import { APP_MENU_ROOT_ID, useAppMenuBadge, useAppNavigation } from '../navigation'
|
|
5
|
+
import { useI18n } from '../locales'
|
|
5
6
|
import { resolveRootMenuId } from '../navigation-tree.mjs'
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{ route: string }>()
|
|
8
9
|
const { menus, tabBar, navigate } = useAppNavigation()
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
|
|
12
|
+
/** 按稳定 viewKey 优先取界面语言标签,未配置的菜单回退后端标题。 */
|
|
13
|
+
const tabBarTitleKeys: Record<string, string> = {
|
|
14
|
+
HOME: 'core.navigation.home',
|
|
15
|
+
APP_ANALYTIC: 'core.navigation.analytics',
|
|
16
|
+
APP_REPORT: 'core.navigation.report',
|
|
17
|
+
MESSAGE_INBOX: 'core.navigation.message',
|
|
18
|
+
PROFILE_HOME: 'core.navigation.my',
|
|
19
|
+
}
|
|
20
|
+
const tabBarTitle = (item: { viewKey: string; title: string }): string =>
|
|
21
|
+
(tabBarTitleKeys[item.viewKey] ? t(tabBarTitleKeys[item.viewKey]) : '') || item.title
|
|
9
22
|
const activeMenu = computed(() => {
|
|
10
23
|
const routeMenu = menus.value.find((menu) => resolveStaticView(menu.viewKey) === props.route)
|
|
11
24
|
if (!routeMenu) return
|
|
@@ -14,6 +27,12 @@ const activeMenu = computed(() => {
|
|
|
14
27
|
})
|
|
15
28
|
const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
16
29
|
const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
|
|
30
|
+
// #ifdef H5
|
|
31
|
+
// H5 端 tabBar.custom 不隐藏内置 tabbar,需手动隐藏,否则其边线从自定义栏上方露出形成双线。
|
|
32
|
+
onMounted(() => {
|
|
33
|
+
uni.hideTabBar({ fail: () => {} })
|
|
34
|
+
})
|
|
35
|
+
// #endif
|
|
17
36
|
</script>
|
|
18
37
|
|
|
19
38
|
<template>
|
|
@@ -46,7 +65,7 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
|
|
|
46
65
|
aria-hidden="true"
|
|
47
66
|
/>
|
|
48
67
|
<text :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }">
|
|
49
|
-
{{ item
|
|
68
|
+
{{ tabBarTitle(item) }}
|
|
50
69
|
</text>
|
|
51
70
|
</view>
|
|
52
71
|
</view>
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
DisableMfaRequest,
|
|
9
9
|
} from '../rpc/base/v1/mfa'
|
|
10
10
|
import { createWebAuthnCredential, getWebAuthnAssertion } from '../utils/webauthn'
|
|
11
|
-
import {
|
|
11
|
+
import { encryptPassword } from '../utils/secretCrypto'
|
|
12
12
|
import { useI18n } from '../locales'
|
|
13
13
|
import { defMfaService } from '../api/base/v1/mfa'
|
|
14
14
|
|
|
@@ -92,10 +92,7 @@ async function beginMfaSetup() {
|
|
|
92
92
|
loading.value = true
|
|
93
93
|
errorMessage.value = ''
|
|
94
94
|
try {
|
|
95
|
-
const password = await encryptPassword(
|
|
96
|
-
setupPasswordInput.value,
|
|
97
|
-
PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
98
|
-
)
|
|
95
|
+
const password = await encryptPassword(setupPasswordInput.value)
|
|
99
96
|
const request: BeginMfaSetupRequest = { password, setup_ticket: '' }
|
|
100
97
|
const result = await defMfaService.BeginMfaSetup(request)
|
|
101
98
|
setupTicket.value = result.setup_ticket
|
|
@@ -136,10 +133,7 @@ async function handleDisableConfirm() {
|
|
|
136
133
|
loading.value = true
|
|
137
134
|
errorMessage.value = ''
|
|
138
135
|
try {
|
|
139
|
-
const password = await encryptPassword(
|
|
140
|
-
disablePasswordInput.value,
|
|
141
|
-
PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
142
|
-
)
|
|
136
|
+
const password = await encryptPassword(disablePasswordInput.value)
|
|
143
137
|
let request: DisableMfaRequest
|
|
144
138
|
if (disableRecoveryCode.value.trim()) {
|
|
145
139
|
request = {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { useI18n } from '../locales'
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import type { PasswordCrypto } from '../rpc/common/v1/types'
|
|
4
|
+
import { encryptPassword } from '../utils/secretCrypto'
|
|
5
|
+
import type { SecretCrypto } from '../rpc/common/v1/types'
|
|
7
6
|
|
|
8
7
|
const { t } = useI18n()
|
|
9
8
|
|
|
@@ -26,7 +25,6 @@ interface PasswordVerifyDialogProps {
|
|
|
26
25
|
/** 外部业务提交状态。 */
|
|
27
26
|
confirmLoading?: boolean
|
|
28
27
|
/** 密码加密场景。 */
|
|
29
|
-
scene?: PasswordCryptoScene
|
|
30
28
|
/** 是否允许点击遮罩关闭。 */
|
|
31
29
|
closeOnClickModal?: boolean
|
|
32
30
|
}
|
|
@@ -39,13 +37,12 @@ const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
|
|
|
39
37
|
confirmText: '',
|
|
40
38
|
cancelText: '',
|
|
41
39
|
confirmLoading: false,
|
|
42
|
-
scene: PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
43
40
|
closeOnClickModal: false,
|
|
44
41
|
})
|
|
45
42
|
|
|
46
43
|
const emit = defineEmits<{
|
|
47
44
|
'update:modelValue': [value: boolean]
|
|
48
|
-
confirm: [password:
|
|
45
|
+
confirm: [password: SecretCrypto]
|
|
49
46
|
cancel: []
|
|
50
47
|
closed: []
|
|
51
48
|
}>()
|
|
@@ -87,7 +84,7 @@ async function handleConfirm() {
|
|
|
87
84
|
errorMessage.value = ''
|
|
88
85
|
encrypting.value = true
|
|
89
86
|
try {
|
|
90
|
-
const encryptedPassword = await encryptPassword(password.value
|
|
87
|
+
const encryptedPassword = await encryptPassword(password.value)
|
|
91
88
|
emit('confirm', encryptedPassword)
|
|
92
89
|
} finally {
|
|
93
90
|
encrypting.value = false
|
package/src/index.ts
CHANGED
package/src/locales/README.md
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
| `en-US.json` | 英文 Core 语言包。 | uni-app 切换为英文时使用。 |
|
|
10
10
|
| `ja-JP.json` | 日文 Core 语言包。 | uni-app 切换为日文时使用。 |
|
|
11
11
|
|
|
12
|
-
语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[
|
|
12
|
+
语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化设计与语言扩展](../../../../../../docs/国际化最终方案.md)。
|
package/src/locales/en-US.json
CHANGED
|
@@ -141,5 +141,8 @@
|
|
|
141
141
|
"common.resource.default": "{resource}",
|
|
142
142
|
"common.action.delete_resource": "Delete {resource}",
|
|
143
143
|
"common.action.set_field_status": "Set {field}",
|
|
144
|
-
"common.action.load_more": "Load more"
|
|
144
|
+
"common.action.load_more": "Load more",
|
|
145
|
+
"common.status.loading": "Loading…",
|
|
146
|
+
"core.navigation.analytics": "Analytics",
|
|
147
|
+
"core.navigation.report": "Reports"
|
|
145
148
|
}
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -141,5 +141,8 @@
|
|
|
141
141
|
"common.resource.default": "{resource}",
|
|
142
142
|
"common.action.delete_resource": "{resource}を削除",
|
|
143
143
|
"common.action.set_field_status": "{field}を設定",
|
|
144
|
-
"common.action.load_more": "さらに読み込む"
|
|
144
|
+
"common.action.load_more": "さらに読み込む",
|
|
145
|
+
"common.status.loading": "読み込み中…",
|
|
146
|
+
"core.navigation.analytics": "分析",
|
|
147
|
+
"core.navigation.report": "レポート"
|
|
145
148
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -141,5 +141,8 @@
|
|
|
141
141
|
"common.resource.default": "{resource}",
|
|
142
142
|
"common.action.delete_resource": "删除{resource}",
|
|
143
143
|
"common.action.set_field_status": "设置{field}",
|
|
144
|
-
"common.action.load_more": "加载更多"
|
|
144
|
+
"common.action.load_more": "加载更多",
|
|
145
|
+
"common.status.loading": "加载中…",
|
|
146
|
+
"core.navigation.analytics": "分析",
|
|
147
|
+
"core.navigation.report": "报表"
|
|
145
148
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -141,5 +141,8 @@
|
|
|
141
141
|
"common.resource.default": "{resource}",
|
|
142
142
|
"common.action.delete_resource": "刪除{resource}",
|
|
143
143
|
"common.action.set_field_status": "設定{field}",
|
|
144
|
-
"common.action.load_more": "載入更多"
|
|
144
|
+
"common.action.load_more": "載入更多",
|
|
145
|
+
"common.status.loading": "載入中…",
|
|
146
|
+
"core.navigation.analytics": "分析",
|
|
147
|
+
"core.navigation.report": "報表"
|
|
145
148
|
}
|
package/src/rpc/base/v1/login.ts
CHANGED
|
@@ -5,27 +5,9 @@
|
|
|
5
5
|
// source: base/v1/login.proto
|
|
6
6
|
|
|
7
7
|
/* eslint-disable */
|
|
8
|
-
import type {
|
|
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
|
-
|
|
|
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
|
/** 刷新认证令牌 */
|
package/src/rpc/base/v1/mfa.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// source: base/v1/mfa.proto
|
|
6
6
|
|
|
7
7
|
/* eslint-disable */
|
|
8
|
-
import type {
|
|
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
|
-
|
|
|
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
|
-
|
|
|
138
|
+
| SecretCrypto
|
|
139
139
|
| undefined;
|
|
140
140
|
/** TOTP 动态口令 */
|
|
141
141
|
code: string;
|
package/src/rpc/base/v1/oauth.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// source: base/v1/oauth.proto
|
|
6
6
|
|
|
7
7
|
/* eslint-disable */
|
|
8
|
-
import type {
|
|
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
|
-
|
|
|
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
|
|
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
|
-
|
|
75
|
+
/** 传输时为密文,服务端解密后回填明文 */
|
|
76
|
+
text: string;
|
|
77
77
|
}
|
package/src/utils/http.ts
CHANGED
|
@@ -47,7 +47,7 @@ const REFRESH_TOKEN_URL = '/v1/base/token'
|
|
|
47
47
|
const CAPTCHA_URL = '/v1/base/captcha'
|
|
48
48
|
const CONFIG_URL = '/v1/base/config'
|
|
49
49
|
const LANGUAGE_URL = '/v1/base/language'
|
|
50
|
-
const
|
|
50
|
+
const SECRET_PUBLIC_KEY_URL = '/v1/base/secret-public-key'
|
|
51
51
|
// 认证公共接口不携带访问令牌。
|
|
52
52
|
const NO_AUTH_URL_SET = new Set([
|
|
53
53
|
SESSION_URL,
|
|
@@ -55,14 +55,14 @@ const NO_AUTH_URL_SET = new Set([
|
|
|
55
55
|
CAPTCHA_URL,
|
|
56
56
|
CONFIG_URL,
|
|
57
57
|
LANGUAGE_URL,
|
|
58
|
-
|
|
58
|
+
SECRET_PUBLIC_KEY_URL,
|
|
59
59
|
])
|
|
60
60
|
const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
61
61
|
SESSION_URL,
|
|
62
62
|
CAPTCHA_URL,
|
|
63
63
|
CONFIG_URL,
|
|
64
64
|
LANGUAGE_URL,
|
|
65
|
-
|
|
65
|
+
SECRET_PUBLIC_KEY_URL,
|
|
66
66
|
])
|
|
67
67
|
|
|
68
68
|
const AUTH_SILENT_LOGOUT_EVENT = 'auth:silent-logout'
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defSecretCryptoService } from '../api/base/v1/secret_crypto'
|
|
2
2
|
import { t } from '../locales'
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
3
|
+
import type { GetSecretPublicKeyResponse } from '../rpc/base/v1/secret_crypto'
|
|
4
|
+
import type { SecretCrypto } from '../rpc/common/v1/types'
|
|
5
5
|
import * as miniCrypto from 'asmcrypto.js'
|
|
6
6
|
|
|
7
7
|
type MiniCrypto = typeof import('asmcrypto.js')
|
|
8
8
|
|
|
9
|
-
export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene
|
|
10
|
-
export type { PasswordCryptoScene }
|
|
11
|
-
|
|
12
9
|
/** 将 PEM 公钥转换为二进制 DER 数据。 */
|
|
13
10
|
function pemToArrayBuffer(pem: string) {
|
|
14
11
|
const base64 = pem
|
|
@@ -179,19 +176,14 @@ function portableBytesToBase64(value: Uint8Array) {
|
|
|
179
176
|
}
|
|
180
177
|
|
|
181
178
|
/** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的密码加密协议。 */
|
|
182
|
-
async function
|
|
179
|
+
async function encryptPortableSecret(
|
|
183
180
|
crypto: MiniCrypto,
|
|
184
|
-
|
|
185
|
-
publicKeyResponse:
|
|
186
|
-
|
|
187
|
-
public_key: string
|
|
188
|
-
algorithm: string
|
|
189
|
-
nonce: string
|
|
190
|
-
},
|
|
191
|
-
): Promise<PasswordCrypto> {
|
|
181
|
+
value: string,
|
|
182
|
+
publicKeyResponse: GetSecretPublicKeyResponse,
|
|
183
|
+
): Promise<SecretCrypto> {
|
|
192
184
|
const aesKey = new Uint8Array(await getPortableRandomBytes(32))
|
|
193
185
|
const iv = new Uint8Array(await getPortableRandomBytes(12))
|
|
194
|
-
const plaintext = crypto.string_to_bytes(
|
|
186
|
+
const plaintext = crypto.string_to_bytes(value, true)
|
|
195
187
|
const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key)
|
|
196
188
|
const hash = new crypto.Sha256()
|
|
197
189
|
const keySize = Math.ceil(new crypto.BigNumber(modulus).bitLength / 8)
|
|
@@ -233,26 +225,24 @@ async function encryptPortablePassword(
|
|
|
233
225
|
algorithm: publicKeyResponse.algorithm,
|
|
234
226
|
encrypted_key: portableBytesToBase64(encryptedKey),
|
|
235
227
|
iv: portableBytesToBase64(iv),
|
|
236
|
-
|
|
228
|
+
text: portableBytesToBase64(ciphertext),
|
|
237
229
|
}
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
/**
|
|
241
|
-
export async function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
): Promise<PasswordCrypto> {
|
|
245
|
-
const plainPassword = password.trim()
|
|
246
|
-
if (!plainPassword) {
|
|
247
|
-
throw new Error(t('core.crypto.password_required'))
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
|
|
232
|
+
/** 获取敏感字段一次性临时公钥,同一次提交的多个密文字段共用该公钥。 */
|
|
233
|
+
export async function fetchSecretPublicKey(): Promise<GetSecretPublicKeyResponse> {
|
|
234
|
+
return defSecretCryptoService.GetSecretPublicKey({})
|
|
235
|
+
}
|
|
251
236
|
|
|
237
|
+
/** 使用已获取的临时公钥加密单个敏感值,供一次取钥加密多个字段的场景复用。 */
|
|
238
|
+
export async function encryptWithPublicKey(
|
|
239
|
+
publicKeyResponse: GetSecretPublicKeyResponse,
|
|
240
|
+
value: string,
|
|
241
|
+
): Promise<SecretCrypto> {
|
|
252
242
|
const cryptoApi = globalThis.crypto
|
|
253
243
|
const isSecureContext = typeof window === 'undefined' || window.isSecureContext
|
|
254
244
|
if (!cryptoApi?.subtle || !isSecureContext) {
|
|
255
|
-
return
|
|
245
|
+
return encryptPortableSecret(miniCrypto, value, publicKeyResponse)
|
|
256
246
|
}
|
|
257
247
|
|
|
258
248
|
const subtleCrypto = getSubtleCrypto()
|
|
@@ -271,7 +261,7 @@ export async function encryptPassword(
|
|
|
271
261
|
const ciphertext = await subtleCrypto.subtle.encrypt(
|
|
272
262
|
{ name: 'AES-GCM', iv },
|
|
273
263
|
aesKey,
|
|
274
|
-
new TextEncoder().encode(
|
|
264
|
+
new TextEncoder().encode(value),
|
|
275
265
|
)
|
|
276
266
|
const encryptedKey = await subtleCrypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawAesKey)
|
|
277
267
|
|
|
@@ -281,6 +271,20 @@ export async function encryptPassword(
|
|
|
281
271
|
algorithm: publicKeyResponse.algorithm,
|
|
282
272
|
encrypted_key: arrayBufferToBase64(encryptedKey),
|
|
283
273
|
iv: arrayBufferToBase64(iv.buffer),
|
|
284
|
-
|
|
274
|
+
text: arrayBufferToBase64(ciphertext),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 加密单个敏感字段值,自动获取一次性临时公钥。 */
|
|
279
|
+
export async function encryptSecret(value: string): Promise<SecretCrypto> {
|
|
280
|
+
return encryptWithPublicKey(await fetchSecretPublicKey(), value)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 加密密码字段:去除首尾空白且不允许为空。 */
|
|
284
|
+
export async function encryptPassword(password: string): Promise<SecretCrypto> {
|
|
285
|
+
const plainPassword = password.trim()
|
|
286
|
+
if (!plainPassword) {
|
|
287
|
+
throw new Error(t('core.crypto.password_required'))
|
|
285
288
|
}
|
|
289
|
+
return encryptSecret(plainPassword)
|
|
286
290
|
}
|
|
@@ -9,7 +9,7 @@ import { defMfaService } from '../../../api/base/v1/mfa'
|
|
|
9
9
|
import defaultLogo from '../../../static/images/logo_icon.png'
|
|
10
10
|
import { formatSrc } from '../../../utils'
|
|
11
11
|
import { homeTabPage } from '../../../utils/navigation'
|
|
12
|
-
import {
|
|
12
|
+
import { encryptPassword } from '../../../utils/secretCrypto'
|
|
13
13
|
import { createWebAuthnCredential, getWebAuthnAssertion } from '../../../utils/webauthn'
|
|
14
14
|
import { navigateAppRoute } from '../../../navigation'
|
|
15
15
|
import { getLanguageOptions, useI18n, type SupportedLocale } from '../../../locales'
|
|
@@ -274,10 +274,7 @@ const bindMiniAccount = async () => {
|
|
|
274
274
|
}
|
|
275
275
|
loading.value = true
|
|
276
276
|
try {
|
|
277
|
-
const password = await encryptPassword(
|
|
278
|
-
miniPasswordValue.value,
|
|
279
|
-
PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_LOGIN,
|
|
280
|
-
)
|
|
277
|
+
const password = await encryptPassword(miniPasswordValue.value)
|
|
281
278
|
const code = (await wx.login()).code
|
|
282
279
|
const response = await userStore.bindOauthSession({
|
|
283
280
|
provider: wechatMiniProvider,
|
|
@@ -685,10 +682,7 @@ const verifyCaptchaToken = async (captchaCode: string) => {
|
|
|
685
682
|
|
|
686
683
|
// 执行真正的账号登录流程。
|
|
687
684
|
const submitLogin = async (captchaCode: string) => {
|
|
688
|
-
const password = await encryptPassword(
|
|
689
|
-
passwordValue.value,
|
|
690
|
-
PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_LOGIN,
|
|
691
|
-
)
|
|
685
|
+
const password = await encryptPassword(passwordValue.value)
|
|
692
686
|
return userStore.login({
|
|
693
687
|
...form.value,
|
|
694
688
|
tenant_code: showTenantCode.value ? form.value.tenant_code : '0000',
|