@liujitcn/kratos-uni-app-core 0.0.28 → 0.0.31
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/README.md +28 -5
- package/dist/module.mjs +164 -4
- package/package.json +3 -1
- package/src/api/base/mfa.ts +127 -0
- package/src/components/KratosSettingsPage.vue +280 -0
- package/src/components/KratosTabBar.vue +50 -1
- package/src/components/MfaManageDialog.vue +469 -0
- package/src/components/MfaRecoveryCodesDialog.vue +192 -0
- package/src/components/MfaSetupPanel.vue +160 -0
- package/src/components/PasswordVerifyDialog.vue +277 -0
- package/src/index.ts +2 -0
- package/src/locales/README.md +2 -1
- package/src/locales/en-US.json +41 -1
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +41 -1
- package/src/locales/zh-CN.json +41 -1
- package/src/locales/zh-TW.json +41 -1
- package/src/navigation.ts +47 -1
- package/src/rpc/base/v1/login.ts +33 -2
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/stores/modules/user.ts +26 -7
- package/src/utils/clipboard.ts +33 -0
- package/src/utils/http.ts +8 -3
- package/src/utils/webauthn.ts +101 -0
- package/src/views/pages/login/login.vue +293 -12
package/src/locales/zh-CN.json
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"core.crypto.public_key_invalid": "公钥格式无效",
|
|
29
29
|
"core.crypto.random_failed": "随机数生成失败",
|
|
30
30
|
"core.crypto.unsupported": "当前环境不支持密码加密",
|
|
31
|
+
"core.password.verify_title": "验证密码",
|
|
32
|
+
"core.password.verify_label": "当前密码",
|
|
31
33
|
"core.file.upload_failed": "上传失败",
|
|
32
34
|
"core.home.account_capability": "账户能力",
|
|
33
35
|
"core.home.account_capability_description": "个人资料、应用设置与智能助手入口已保留",
|
|
@@ -58,6 +60,23 @@
|
|
|
58
60
|
"core.login.captcha_write_failed": "验证码图片写入失败",
|
|
59
61
|
"core.login.default_sub_title": "欢迎使用本应用",
|
|
60
62
|
"core.login.login_success": "登录成功",
|
|
63
|
+
"core.login.mfa_title": "多因素认证",
|
|
64
|
+
"core.login.mfa_code": "请输入动态口令",
|
|
65
|
+
"core.login.mfa_recovery_code": "恢复码(可选)",
|
|
66
|
+
"core.login.mfa_setup_title": "绑定多因素认证",
|
|
67
|
+
"core.login.mfa_copy_uri": "复制绑定地址",
|
|
68
|
+
"core.login.mfa_copy_success": "绑定地址已复制",
|
|
69
|
+
"core.login.mfa_recovery_codes_copied": "恢复码已复制",
|
|
70
|
+
"core.login.mfa_recovery_codes_title": "保存 MFA 恢复码",
|
|
71
|
+
"core.login.mfa_recovery_codes_warning": "每个恢复码只能使用一次,此弹窗关闭后不会再次显示。",
|
|
72
|
+
"core.login.mfa_copy_recovery_codes": "复制恢复码",
|
|
73
|
+
"core.login.mfa_recovery_codes_confirm": "我已保存",
|
|
74
|
+
"core.login.mfa_setup_success": "多因素认证绑定成功,请重新登录。",
|
|
75
|
+
"core.login.mfa_webauthn_action": "使用 Passkey",
|
|
76
|
+
"core.login.mfa_webauthn_setup_unsupported": "当前平台不支持 WebAuthn,请使用 TOTP 或恢复码。",
|
|
77
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 注册未返回有效凭据。",
|
|
78
|
+
"core.login.mfa_webauthn_login_unsupported": "当前平台不支持 WebAuthn,请使用恢复码。",
|
|
79
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登录未返回有效凭据。",
|
|
61
80
|
"core.login.password": "请输入密码",
|
|
62
81
|
"core.login.privacy": "《隐私协议》",
|
|
63
82
|
"core.login.protocol_missing": "服务条款和隐私协议未配置",
|
|
@@ -71,6 +90,7 @@
|
|
|
71
90
|
"core.login.wechat_bind_tip": "该微信尚未绑定,请登录已有账号完成绑定",
|
|
72
91
|
"core.login.wechat_failed": "微信登录失败,请稍后重试",
|
|
73
92
|
"core.navigation.home": "首页",
|
|
93
|
+
"core.navigation.message": "消息",
|
|
74
94
|
"core.navigation.error.access": "菜单访问模式无效:{access}",
|
|
75
95
|
"core.navigation.error.child_tab": "只有移动端二级菜单可以作为 tab:{name}",
|
|
76
96
|
"core.navigation.error.cycle": "移动端菜单存在循环父级:{name}",
|
|
@@ -92,6 +112,22 @@
|
|
|
92
112
|
"core.protocol.not_configured": "{title}未配置,暂时无法查看",
|
|
93
113
|
"core.protocol.privacy": "隐私协议",
|
|
94
114
|
"core.protocol.service": "服务条款",
|
|
115
|
+
"core.settings.authorization": "授权管理",
|
|
116
|
+
"core.settings.contact": "联系我们",
|
|
117
|
+
"core.settings.feedback": "问题反馈",
|
|
118
|
+
"core.settings.logout_confirm": "是否退出登录?",
|
|
119
|
+
"core.settings.logout_failed": "退出登录失败",
|
|
120
|
+
"core.settings.mfa_bind_success": "多因素认证绑定成功",
|
|
121
|
+
"core.settings.mfa_disable_action": "禁用",
|
|
122
|
+
"core.settings.mfa_disable_factor_required": "请输入动态口令或恢复码。",
|
|
123
|
+
"core.settings.mfa_disable_success": "多因素认证已禁用",
|
|
124
|
+
"core.settings.mfa_disable_title": "禁用多因素认证",
|
|
125
|
+
"core.settings.mfa_disabled": "未启用",
|
|
126
|
+
"core.settings.mfa_enabled_method": "已启用({method})",
|
|
127
|
+
"core.settings.mfa_method_totp": "TOTP",
|
|
128
|
+
"core.settings.mfa_method_webauthn": "WebAuthn",
|
|
129
|
+
"core.settings.mfa_title": "多因素认证",
|
|
130
|
+
"core.settings.title": "设置",
|
|
95
131
|
"core.status.CONFIG_ERROR": "导航配置无效",
|
|
96
132
|
"core.status.forbidden": "暂无访问权限",
|
|
97
133
|
"core.status.NOT_FOUND": "页面不存在",
|
|
@@ -100,5 +136,9 @@
|
|
|
100
136
|
"core.status.loading": "正在加载",
|
|
101
137
|
"core.webview.embed_blocked": "当前 H5 页面可能被目标站点限制嵌入",
|
|
102
138
|
"core.webview.invalid_url": "缺少有效链接地址",
|
|
103
|
-
"core.webview.open_failed": "链接无法打开"
|
|
139
|
+
"core.webview.open_failed": "链接无法打开",
|
|
140
|
+
"common.value.top_level": "顶级节点",
|
|
141
|
+
"common.resource.default": "{resource}",
|
|
142
|
+
"common.action.delete_resource": "删除{resource}",
|
|
143
|
+
"common.action.set_field_status": "设置{field}"
|
|
104
144
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"core.crypto.public_key_invalid": "公鑰格式無效",
|
|
29
29
|
"core.crypto.random_failed": "隨機數生成失敗",
|
|
30
30
|
"core.crypto.unsupported": "當前環境不支援密碼加密",
|
|
31
|
+
"core.password.verify_title": "驗證密碼",
|
|
32
|
+
"core.password.verify_label": "當前密碼",
|
|
31
33
|
"core.file.upload_failed": "上傳失敗",
|
|
32
34
|
"core.home.account_capability": "賬戶能力",
|
|
33
35
|
"core.home.account_capability_description": "個人資料、應用設定與智慧助手入口已保留",
|
|
@@ -58,6 +60,23 @@
|
|
|
58
60
|
"core.login.captcha_write_failed": "驗證碼圖片寫入失敗",
|
|
59
61
|
"core.login.default_sub_title": "歡迎使用本應用",
|
|
60
62
|
"core.login.login_success": "登入成功",
|
|
63
|
+
"core.login.mfa_title": "多因素認證",
|
|
64
|
+
"core.login.mfa_code": "請輸入動態密碼",
|
|
65
|
+
"core.login.mfa_recovery_code": "恢復碼(可選)",
|
|
66
|
+
"core.login.mfa_setup_title": "綁定多因素認證",
|
|
67
|
+
"core.login.mfa_copy_uri": "複製綁定地址",
|
|
68
|
+
"core.login.mfa_copy_success": "綁定地址已複製",
|
|
69
|
+
"core.login.mfa_recovery_codes_copied": "恢復碼已複製",
|
|
70
|
+
"core.login.mfa_recovery_codes_title": "儲存 MFA 恢復碼",
|
|
71
|
+
"core.login.mfa_recovery_codes_warning": "每個恢復碼只能使用一次,關閉此視窗後不會再次顯示。",
|
|
72
|
+
"core.login.mfa_copy_recovery_codes": "複製恢復碼",
|
|
73
|
+
"core.login.mfa_recovery_codes_confirm": "我已儲存",
|
|
74
|
+
"core.login.mfa_setup_success": "多因素認證綁定成功,請重新登入。",
|
|
75
|
+
"core.login.mfa_webauthn_action": "使用 Passkey",
|
|
76
|
+
"core.login.mfa_webauthn_setup_unsupported": "目前平台不支援 WebAuthn,請使用 TOTP 或恢復碼。",
|
|
77
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 註冊未回傳有效憑證。",
|
|
78
|
+
"core.login.mfa_webauthn_login_unsupported": "目前平台不支援 WebAuthn,請使用恢復碼。",
|
|
79
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登入未回傳有效憑證。",
|
|
61
80
|
"core.login.password": "請輸入密碼",
|
|
62
81
|
"core.login.privacy": "《隱私協議》",
|
|
63
82
|
"core.login.protocol_missing": "服務條款和隱私協議未配置",
|
|
@@ -71,6 +90,7 @@
|
|
|
71
90
|
"core.login.wechat_bind_tip": "該微信尚未繫結,請登入已有賬號完成繫結",
|
|
72
91
|
"core.login.wechat_failed": "微信登入失敗,請稍後重試",
|
|
73
92
|
"core.navigation.home": "首頁",
|
|
93
|
+
"core.navigation.message": "消息",
|
|
74
94
|
"core.navigation.error.access": "選單訪問模式無效:{access}",
|
|
75
95
|
"core.navigation.error.child_tab": "只有移動端二級選單可以作為 tab:{name}",
|
|
76
96
|
"core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
|
|
@@ -92,6 +112,22 @@
|
|
|
92
112
|
"core.protocol.not_configured": "{title}未配置,暫時無法檢視",
|
|
93
113
|
"core.protocol.privacy": "隱私協議",
|
|
94
114
|
"core.protocol.service": "服務條款",
|
|
115
|
+
"core.settings.authorization": "授權管理",
|
|
116
|
+
"core.settings.contact": "聯絡我們",
|
|
117
|
+
"core.settings.feedback": "問題反饋",
|
|
118
|
+
"core.settings.logout_confirm": "是否退出登入?",
|
|
119
|
+
"core.settings.logout_failed": "退出登入失敗",
|
|
120
|
+
"core.settings.mfa_bind_success": "多因素認證綁定成功",
|
|
121
|
+
"core.settings.mfa_disable_action": "停用",
|
|
122
|
+
"core.settings.mfa_disable_factor_required": "請輸入動態密碼或恢復碼。",
|
|
123
|
+
"core.settings.mfa_disable_success": "多因素認證已停用",
|
|
124
|
+
"core.settings.mfa_disable_title": "停用多因素認證",
|
|
125
|
+
"core.settings.mfa_disabled": "未啟用",
|
|
126
|
+
"core.settings.mfa_enabled_method": "已啟用({method})",
|
|
127
|
+
"core.settings.mfa_method_totp": "TOTP",
|
|
128
|
+
"core.settings.mfa_method_webauthn": "WebAuthn",
|
|
129
|
+
"core.settings.mfa_title": "多因素認證",
|
|
130
|
+
"core.settings.title": "設定",
|
|
95
131
|
"core.status.CONFIG_ERROR": "導航配置無效",
|
|
96
132
|
"core.status.forbidden": "暫無訪問許可權",
|
|
97
133
|
"core.status.NOT_FOUND": "頁面不存在",
|
|
@@ -100,5 +136,9 @@
|
|
|
100
136
|
"core.status.loading": "正在載入",
|
|
101
137
|
"core.webview.embed_blocked": "當前 H5 頁面可能被目標站點限制嵌入",
|
|
102
138
|
"core.webview.invalid_url": "缺少有效連結地址",
|
|
103
|
-
"core.webview.open_failed": "連結無法開啟"
|
|
139
|
+
"core.webview.open_failed": "連結無法開啟",
|
|
140
|
+
"common.value.top_level": "頂級節點",
|
|
141
|
+
"common.resource.default": "{resource}",
|
|
142
|
+
"common.action.delete_resource": "刪除{resource}",
|
|
143
|
+
"common.action.set_field_status": "設定{field}"
|
|
104
144
|
}
|
package/src/navigation.ts
CHANGED
|
@@ -59,6 +59,18 @@ const createDefaultMenus = (): AppMenu[] => [
|
|
|
59
59
|
icon: 'HOME_DEFAULT',
|
|
60
60
|
selectedIcon: 'HOME_SELECTED',
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
id: 9990904,
|
|
64
|
+
parentId: APP_MENU_ROOT_ID,
|
|
65
|
+
name: 'AppMessage',
|
|
66
|
+
path: 'app/message',
|
|
67
|
+
viewKey: 'MESSAGE_INBOX',
|
|
68
|
+
title: t('core.navigation.message'),
|
|
69
|
+
access: 'AUTHENTICATED',
|
|
70
|
+
inTabBar: true,
|
|
71
|
+
icon: 'MESSAGE_DEFAULT',
|
|
72
|
+
selectedIcon: 'MESSAGE_SELECTED',
|
|
73
|
+
},
|
|
62
74
|
{
|
|
63
75
|
id: 99909,
|
|
64
76
|
parentId: APP_MENU_ROOT_ID,
|
|
@@ -107,7 +119,7 @@ const createDefaultMenus = (): AppMenu[] => [
|
|
|
107
119
|
name: 'AppSettings',
|
|
108
120
|
path: 'app/settings',
|
|
109
121
|
viewKey: 'SETTINGS',
|
|
110
|
-
title: t('
|
|
122
|
+
title: t('core.settings.title'),
|
|
111
123
|
access: 'AUTHENTICATED',
|
|
112
124
|
inTabBar: false,
|
|
113
125
|
},
|
|
@@ -135,6 +147,7 @@ const createDefaultMenus = (): AppMenu[] => [
|
|
|
135
147
|
|
|
136
148
|
const menus = ref<AppMenu[]>([])
|
|
137
149
|
const ready = ref(false)
|
|
150
|
+
const appMenuBadges = ref<Record<string, number>>({})
|
|
138
151
|
let tabNavigationTarget: string | undefined
|
|
139
152
|
let adapter: AppNavigationAdapter = {
|
|
140
153
|
list: () => defBaseMenuService.ListBaseMenu(),
|
|
@@ -272,6 +285,39 @@ function navigateTabRouteInStack(
|
|
|
272
285
|
})
|
|
273
286
|
}
|
|
274
287
|
|
|
288
|
+
/** 按稳定 viewKey 跳转,并将动作参数作为查询参数传递。 */
|
|
289
|
+
export function navigateAppView(viewKey: string, params: Record<string, string> = {}): void {
|
|
290
|
+
const menu = menus.value.find((item) => item.viewKey === viewKey)
|
|
291
|
+
if (!menu) {
|
|
292
|
+
launchAppStatus('NOT_FOUND')
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
const remaining = { ...params }
|
|
296
|
+
const path = menu.path.replace(/:([A-Za-z0-9_]+)/g, (placeholder, key: string) => {
|
|
297
|
+
if (!(key in remaining)) return placeholder
|
|
298
|
+
const value = remaining[key]
|
|
299
|
+
delete remaining[key]
|
|
300
|
+
return encodeURIComponent(value)
|
|
301
|
+
})
|
|
302
|
+
const query = Object.entries(remaining)
|
|
303
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
304
|
+
.join('&')
|
|
305
|
+
navigateAppRoute(`${path}${query ? `?${query}` : ''}`)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 设置移动端菜单 badge 数量,供业务模块注册未读状态。 */
|
|
309
|
+
export function setAppMenuBadge(viewKey: string, count: number): void {
|
|
310
|
+
const next = { ...appMenuBadges.value }
|
|
311
|
+
if (count > 0) next[viewKey] = Math.min(Math.floor(count), 99)
|
|
312
|
+
else delete next[viewKey]
|
|
313
|
+
appMenuBadges.value = next
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** 读取移动端菜单 badge 数量。 */
|
|
317
|
+
export function useAppMenuBadge(viewKey: string) {
|
|
318
|
+
return computed(() => appMenuBadges.value[viewKey] ?? 0)
|
|
319
|
+
}
|
|
320
|
+
|
|
275
321
|
/** 获取导航响应式状态。 */
|
|
276
322
|
export function useAppNavigation() {
|
|
277
323
|
const menuTree = computed<AppMenuNode[]>(() => buildMenuTree(menus.value, APP_MENU_ROOT_ID))
|
package/src/rpc/base/v1/login.ts
CHANGED
|
@@ -20,6 +20,25 @@ export enum PasswordCryptoScene {
|
|
|
20
20
|
PASSWORD_CRYPTO_SCENE_RESET_BASE_USER_PASSWORD = 3,
|
|
21
21
|
/** PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD - 个人修改密码加密场景。 */
|
|
22
22
|
PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD = 4,
|
|
23
|
+
/** PASSWORD_CRYPTO_SCENE_MFA - 多因素认证敏感操作场景。 */
|
|
24
|
+
PASSWORD_CRYPTO_SCENE_MFA = 5,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* LoginStatus 表示登录请求当前所处的认证阶段。
|
|
29
|
+
* 只有 AUTHENTICATED 或 PASSWORD_CHANGE_REQUIRED 才会携带正式访问令牌,MFA 状态必须继续完成挑战。
|
|
30
|
+
*/
|
|
31
|
+
export enum LoginStatus {
|
|
32
|
+
/** LOGIN_STATUS_UNSPECIFIED - 未指定登录状态。 */
|
|
33
|
+
LOGIN_STATUS_UNSPECIFIED = 0,
|
|
34
|
+
/** LOGIN_STATUS_AUTHENTICATED - 已完成认证并签发令牌。 */
|
|
35
|
+
LOGIN_STATUS_AUTHENTICATED = 1,
|
|
36
|
+
/** LOGIN_STATUS_MFA_REQUIRED - 需要继续校验多因素认证。 */
|
|
37
|
+
LOGIN_STATUS_MFA_REQUIRED = 2,
|
|
38
|
+
/** LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED - 必须先完成多因素认证绑定。 */
|
|
39
|
+
LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED = 3,
|
|
40
|
+
/** LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED - 下次操作前必须修改临时密码。 */
|
|
41
|
+
LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED = 4,
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
/** 验证码获取条件 */
|
|
@@ -80,8 +99,8 @@ export interface PasswordPublicKeyResponse {
|
|
|
80
99
|
|
|
81
100
|
/** 刷新令牌请求参数 */
|
|
82
101
|
export interface RefreshTokenRequest {
|
|
83
|
-
/**
|
|
84
|
-
refresh_token
|
|
102
|
+
/** 更新令牌;未填写时由服务端从HttpOnly Cookie读取。 */
|
|
103
|
+
refresh_token?: string | undefined;
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
/** 刷新令牌响应 */
|
|
@@ -122,6 +141,18 @@ export interface LoginResponse {
|
|
|
122
141
|
token_type: string;
|
|
123
142
|
/** 令牌有效时间,单位为秒。如果访问令牌过期,服务器应回复授予访问令牌的持续时间。如果省略该参数,必须其他方式设置过期时间。 */
|
|
124
143
|
expires_in: number;
|
|
144
|
+
/** 登录认证状态 */
|
|
145
|
+
status: LoginStatus;
|
|
146
|
+
/** 多因素认证挑战ID */
|
|
147
|
+
mfa_challenge_id: string;
|
|
148
|
+
/** 多因素认证绑定临时票据 */
|
|
149
|
+
mfa_setup_ticket: string;
|
|
150
|
+
/** 多因素认证挑战或绑定票据有效时间,单位秒 */
|
|
151
|
+
mfa_expires_in: number;
|
|
152
|
+
/** 多因素认证方式:totp、webauthn */
|
|
153
|
+
mfa_method: string;
|
|
154
|
+
/** WebAuthn认证选项JSON */
|
|
155
|
+
mfa_webauthn_options_json: string;
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
/** Base登录公共服务 */
|
|
@@ -0,0 +1,180 @@
|
|
|
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/mfa.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
import type { PasswordCrypto } from "../../common/v1/types";
|
|
9
|
+
import type { Empty } from "../../google/protobuf/empty";
|
|
10
|
+
import type { LoginResponse } from "./login";
|
|
11
|
+
|
|
12
|
+
/** 多因素认证校验请求。 */
|
|
13
|
+
export interface VerifyMfaRequest {
|
|
14
|
+
/** 多因素认证挑战ID */
|
|
15
|
+
challenge_id: string;
|
|
16
|
+
/** TOTP动态口令 */
|
|
17
|
+
code: string;
|
|
18
|
+
/** 一次性恢复码 */
|
|
19
|
+
recovery_code: string;
|
|
20
|
+
/** WebAuthn认证响应JSON */
|
|
21
|
+
webauthn_response_json: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 登录强制绑定多因素认证请求。 */
|
|
25
|
+
export interface BeginMfaEnrollmentRequest {
|
|
26
|
+
/** 强制绑定临时票据 */
|
|
27
|
+
setup_ticket: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 登录强制绑定多因素认证响应。 */
|
|
31
|
+
export interface BeginMfaEnrollmentResponse {
|
|
32
|
+
/** 绑定临时票据 */
|
|
33
|
+
setup_ticket: string;
|
|
34
|
+
/** Authenticator应用使用的otpauth地址 */
|
|
35
|
+
otpauth_uri: string;
|
|
36
|
+
/** 绑定临时票据有效时间,单位秒 */
|
|
37
|
+
expires_in: number;
|
|
38
|
+
/** 多因素认证方式:totp、webauthn */
|
|
39
|
+
method: string;
|
|
40
|
+
/** WebAuthn注册选项JSON */
|
|
41
|
+
webauthn_options_json: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 登录强制绑定多因素认证确认请求。 */
|
|
45
|
+
export interface ConfirmMfaEnrollmentRequest {
|
|
46
|
+
/** 绑定临时票据 */
|
|
47
|
+
setup_ticket: string;
|
|
48
|
+
/** TOTP动态口令 */
|
|
49
|
+
code: string;
|
|
50
|
+
/** WebAuthn注册响应JSON */
|
|
51
|
+
webauthn_response_json: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 登录强制绑定多因素认证确认响应。 */
|
|
55
|
+
export interface ConfirmMfaEnrollmentResponse {
|
|
56
|
+
/** 是否启用成功 */
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
/** 一次性恢复码,仅本次返回 */
|
|
59
|
+
recovery_codes: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 查询当前用户多因素认证状态请求。 */
|
|
63
|
+
export interface GetMfaStatusRequest {
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 多因素认证状态响应。 */
|
|
67
|
+
export interface MfaStatusResponse {
|
|
68
|
+
/** 是否已启用多因素认证 */
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
/** 全局多因素认证策略 */
|
|
71
|
+
policy: string;
|
|
72
|
+
/** 认证方式 */
|
|
73
|
+
method: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 开始绑定多因素认证请求。 */
|
|
77
|
+
export interface BeginMfaSetupRequest {
|
|
78
|
+
/** 当前登录密码 */
|
|
79
|
+
password:
|
|
80
|
+
| PasswordCrypto
|
|
81
|
+
| undefined;
|
|
82
|
+
/** 强制绑定场景下的临时票据 */
|
|
83
|
+
setup_ticket: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 开始绑定多因素认证响应。 */
|
|
87
|
+
export interface BeginMfaSetupResponse {
|
|
88
|
+
/** 绑定临时票据 */
|
|
89
|
+
setup_ticket: string;
|
|
90
|
+
/** Authenticator 应用使用的 otpauth 地址 */
|
|
91
|
+
otpauth_uri: string;
|
|
92
|
+
/** 绑定临时票据有效时间,单位秒 */
|
|
93
|
+
expires_in: number;
|
|
94
|
+
/** 多因素认证方式:totp、webauthn */
|
|
95
|
+
method: string;
|
|
96
|
+
/** WebAuthn 注册选项 JSON */
|
|
97
|
+
webauthn_options_json: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 确认绑定多因素认证请求。 */
|
|
101
|
+
export interface ConfirmMfaSetupRequest {
|
|
102
|
+
/** 绑定临时票据 */
|
|
103
|
+
setup_ticket: string;
|
|
104
|
+
/** TOTP 动态口令 */
|
|
105
|
+
code: string;
|
|
106
|
+
/** WebAuthn 注册响应 JSON */
|
|
107
|
+
webauthn_response_json: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 确认绑定多因素认证响应。 */
|
|
111
|
+
export interface ConfirmMfaSetupResponse {
|
|
112
|
+
/** 是否启用成功 */
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
/** 一次性恢复码,仅本次返回 */
|
|
115
|
+
recovery_codes: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 开始禁用 WebAuthn 多因素认证请求。 */
|
|
119
|
+
export interface BeginMfaDisableRequest {
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 开始禁用 WebAuthn 多因素认证响应。 */
|
|
123
|
+
export interface BeginMfaDisableResponse {
|
|
124
|
+
/** WebAuthn 禁用挑战 ID */
|
|
125
|
+
challenge_id: string;
|
|
126
|
+
/** WebAuthn 禁用挑战有效时间,单位秒 */
|
|
127
|
+
expires_in: number;
|
|
128
|
+
/** WebAuthn 验证选项 JSON */
|
|
129
|
+
webauthn_options_json: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 禁用多因素认证请求。 */
|
|
133
|
+
export interface DisableMfaRequest {
|
|
134
|
+
/** 当前登录密码 */
|
|
135
|
+
password:
|
|
136
|
+
| PasswordCrypto
|
|
137
|
+
| undefined;
|
|
138
|
+
/** TOTP 动态口令 */
|
|
139
|
+
code: string;
|
|
140
|
+
/** WebAuthn 禁用挑战 ID */
|
|
141
|
+
webauthn_challenge_id: string;
|
|
142
|
+
/** WebAuthn 验证响应 JSON */
|
|
143
|
+
webauthn_response_json: string;
|
|
144
|
+
/** 一次性恢复码 */
|
|
145
|
+
recovery_code: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 重新生成恢复码请求。 */
|
|
149
|
+
export interface RegenerateMfaRecoveryCodesRequest {
|
|
150
|
+
/** TOTP 动态口令 */
|
|
151
|
+
code: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 恢复码响应。 */
|
|
155
|
+
export interface RecoveryCodesResponse {
|
|
156
|
+
/** 一次性恢复码,仅本次返回 */
|
|
157
|
+
recovery_codes: string[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Base 多因素认证公共服务。 */
|
|
161
|
+
export interface MfaService {
|
|
162
|
+
/** 校验多因素认证 */
|
|
163
|
+
VerifyMfa(request: VerifyMfaRequest): Promise<LoginResponse>;
|
|
164
|
+
/** 开始登录强制绑定配置的 MFA 方式 */
|
|
165
|
+
BeginMfaEnrollment(request: BeginMfaEnrollmentRequest): Promise<BeginMfaEnrollmentResponse>;
|
|
166
|
+
/** 确认登录强制绑定配置的 MFA 方式 */
|
|
167
|
+
ConfirmMfaEnrollment(request: ConfirmMfaEnrollmentRequest): Promise<ConfirmMfaEnrollmentResponse>;
|
|
168
|
+
/** 查询当前用户多因素认证状态。 */
|
|
169
|
+
GetMfaStatus(request: GetMfaStatusRequest): Promise<MfaStatusResponse>;
|
|
170
|
+
/** 开始绑定当前用户的多因素认证方式。 */
|
|
171
|
+
BeginMfaSetup(request: BeginMfaSetupRequest): Promise<BeginMfaSetupResponse>;
|
|
172
|
+
/** 确认绑定当前用户的多因素认证方式。 */
|
|
173
|
+
ConfirmMfaSetup(request: ConfirmMfaSetupRequest): Promise<ConfirmMfaSetupResponse>;
|
|
174
|
+
/** 开始禁用 WebAuthn 多因素认证的 Passkey 验证。 */
|
|
175
|
+
BeginMfaDisable(request: BeginMfaDisableRequest): Promise<BeginMfaDisableResponse>;
|
|
176
|
+
/** 禁用当前用户的多因素认证。 */
|
|
177
|
+
DisableMfa(request: DisableMfaRequest): Promise<Empty>;
|
|
178
|
+
/** 重新生成当前用户的多因素认证恢复码。 */
|
|
179
|
+
RegenerateMfaRecoveryCodes(request: RegenerateMfaRecoveryCodesRequest): Promise<RecoveryCodesResponse>;
|
|
180
|
+
}
|
package/src/rpc/base/v1/oauth.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
/* eslint-disable */
|
|
8
8
|
import type { PasswordCrypto } from "../../common/v1/types";
|
|
9
9
|
import type { Empty } from "../../google/protobuf/empty";
|
|
10
|
+
import type { LoginStatus } from "./login";
|
|
10
11
|
|
|
11
12
|
/** 个人中心三方账号绑定列表查询条件 */
|
|
12
13
|
export interface ListOauthBindingRequest {
|
|
@@ -96,6 +97,18 @@ export interface CreateOauthSessionResponse {
|
|
|
96
97
|
expires_in: number;
|
|
97
98
|
/** 是否需要绑定已有账号。 */
|
|
98
99
|
binding_required: boolean;
|
|
100
|
+
/** 登录认证状态 */
|
|
101
|
+
status: LoginStatus;
|
|
102
|
+
/** 多因素认证挑战ID */
|
|
103
|
+
mfa_challenge_id: string;
|
|
104
|
+
/** 多因素认证绑定临时票据 */
|
|
105
|
+
mfa_setup_ticket: string;
|
|
106
|
+
/** 多因素认证挑战或绑定票据有效时间,单位秒 */
|
|
107
|
+
mfa_expires_in: number;
|
|
108
|
+
/** 多因素认证方式:totp、webauthn */
|
|
109
|
+
mfa_method: string;
|
|
110
|
+
/** WebAuthn认证选项JSON */
|
|
111
|
+
mfa_webauthn_options_json: string;
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
/** 三方登录回调请求 */
|
|
@@ -132,6 +145,18 @@ export interface ExchangeOauthTicketResponse {
|
|
|
132
145
|
token_type: string;
|
|
133
146
|
/** 令牌有效时间,单位为秒。 */
|
|
134
147
|
expires_in: number;
|
|
148
|
+
/** 登录认证状态 */
|
|
149
|
+
status: LoginStatus;
|
|
150
|
+
/** 多因素认证挑战ID */
|
|
151
|
+
mfa_challenge_id: string;
|
|
152
|
+
/** 多因素认证绑定临时票据 */
|
|
153
|
+
mfa_setup_ticket: string;
|
|
154
|
+
/** 多因素认证挑战或绑定票据有效时间,单位秒 */
|
|
155
|
+
mfa_expires_in: number;
|
|
156
|
+
/** 多因素认证方式:totp、webauthn */
|
|
157
|
+
mfa_method: string;
|
|
158
|
+
/** WebAuthn认证选项JSON */
|
|
159
|
+
mfa_webauthn_options_json: string;
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/** 个人中心三方账号绑定回调请求 */
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
CreateOauthSessionResponse,
|
|
6
6
|
} from '../../rpc/base/v1/oauth'
|
|
7
7
|
import type { LoginRequest, LoginResponse } from '../../rpc/base/v1/login'
|
|
8
|
+
import type { VerifyMfaRequest } from '../../rpc/base/v1/mfa'
|
|
9
|
+
import { defMfaService } from '../../api/base/mfa'
|
|
8
10
|
import { defAuthService } from '../../api/system/auth'
|
|
9
11
|
import { defLoginService } from '../../api/base/login'
|
|
10
12
|
import { defOauthService } from '../../api/base/oauth'
|
|
@@ -82,12 +84,13 @@ export const useUserStore = defineStore(
|
|
|
82
84
|
* @returns
|
|
83
85
|
*/
|
|
84
86
|
function login(request: LoginRequest) {
|
|
85
|
-
return new Promise<
|
|
87
|
+
return new Promise<LoginResponse>((resolve, reject) => {
|
|
86
88
|
defLoginService
|
|
87
89
|
.Login(request)
|
|
88
90
|
.then(async (data) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
if (data.status === 1 || data.status === 0 || data.status === 4)
|
|
92
|
+
await applyLoginToken(data)
|
|
93
|
+
resolve(data)
|
|
91
94
|
})
|
|
92
95
|
.catch((error) => {
|
|
93
96
|
reject(error)
|
|
@@ -95,6 +98,19 @@ export const useUserStore = defineStore(
|
|
|
95
98
|
})
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/** 校验登录阶段的多因素认证并保存正式令牌。 */
|
|
102
|
+
function verifyMfa(request: VerifyMfaRequest) {
|
|
103
|
+
return new Promise<LoginResponse>((resolve, reject) => {
|
|
104
|
+
defMfaService
|
|
105
|
+
.VerifyMfa(request)
|
|
106
|
+
.then(async (data) => {
|
|
107
|
+
await applyLoginToken(data)
|
|
108
|
+
resolve(data)
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => reject(error))
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
/**
|
|
99
115
|
* 创建三方登录会话
|
|
100
116
|
*
|
|
@@ -110,7 +126,8 @@ export const useUserStore = defineStore(
|
|
|
110
126
|
resolve(data)
|
|
111
127
|
return
|
|
112
128
|
}
|
|
113
|
-
|
|
129
|
+
if (data.status === 1 || data.status === 0 || data.status === 4)
|
|
130
|
+
await applyLoginToken(data)
|
|
114
131
|
resolve(data)
|
|
115
132
|
})
|
|
116
133
|
.catch((error) => {
|
|
@@ -121,12 +138,13 @@ export const useUserStore = defineStore(
|
|
|
121
138
|
|
|
122
139
|
/** 绑定已有账号并创建三方登录会话。 */
|
|
123
140
|
function bindOauthSession(request: BindOauthSessionRequest) {
|
|
124
|
-
return new Promise<
|
|
141
|
+
return new Promise<CreateOauthSessionResponse>((resolve, reject) => {
|
|
125
142
|
defOauthService
|
|
126
143
|
.BindOauthSession(request)
|
|
127
144
|
.then(async (data) => {
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
if (data.status === 1 || data.status === 0 || data.status === 4)
|
|
146
|
+
await applyLoginToken(data)
|
|
147
|
+
resolve(data)
|
|
130
148
|
})
|
|
131
149
|
.catch((error) => {
|
|
132
150
|
reject(error)
|
|
@@ -239,6 +257,7 @@ export const useUserStore = defineStore(
|
|
|
239
257
|
isAuthenticated,
|
|
240
258
|
getUserProfile,
|
|
241
259
|
login,
|
|
260
|
+
verifyMfa,
|
|
242
261
|
createOauthSession,
|
|
243
262
|
bindOauthSession,
|
|
244
263
|
logout,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 通过当前平台能力复制文本,并在 H5 非安全上下文中降级到选区复制。 */
|
|
2
|
+
export async function copyText(value: string): Promise<void> {
|
|
3
|
+
if (!value) throw new Error('clipboard value is empty')
|
|
4
|
+
|
|
5
|
+
// #ifdef H5
|
|
6
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
7
|
+
try {
|
|
8
|
+
await navigator.clipboard.writeText(value)
|
|
9
|
+
return
|
|
10
|
+
} catch {
|
|
11
|
+
// 局域网 HTTP 等非安全上下文继续使用同步选区复制。
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const textarea = document.createElement('textarea')
|
|
15
|
+
textarea.value = value
|
|
16
|
+
textarea.setAttribute('readonly', '')
|
|
17
|
+
textarea.style.position = 'fixed'
|
|
18
|
+
textarea.style.opacity = '0'
|
|
19
|
+
textarea.style.pointerEvents = 'none'
|
|
20
|
+
document.body.appendChild(textarea)
|
|
21
|
+
textarea.select()
|
|
22
|
+
const copied = document.execCommand('copy')
|
|
23
|
+
document.body.removeChild(textarea)
|
|
24
|
+
if (!copied) throw new Error('clipboard copy failed')
|
|
25
|
+
return undefined
|
|
26
|
+
// #endif
|
|
27
|
+
|
|
28
|
+
// #ifndef H5
|
|
29
|
+
await new Promise<void>((resolve, reject) => {
|
|
30
|
+
uni.setClipboardData({ data: value, success: () => resolve(), fail: reject })
|
|
31
|
+
})
|
|
32
|
+
// #endif
|
|
33
|
+
}
|
package/src/utils/http.ts
CHANGED
|
@@ -22,6 +22,11 @@ const apiBasePath = import.meta.env.VITE_APP_BASE_API || '/api'
|
|
|
22
22
|
const apiTargetUrl = import.meta.env.VITE_APP_API_URL || ''
|
|
23
23
|
const normalizedApiBasePath = apiBasePath.startsWith('/') ? apiBasePath : `/${apiBasePath}`
|
|
24
24
|
|
|
25
|
+
let sourceClient = 'uni-h5'
|
|
26
|
+
// #ifdef MP-WEIXIN
|
|
27
|
+
sourceClient = 'uni-weapp'
|
|
28
|
+
// #endif
|
|
29
|
+
|
|
25
30
|
/**
|
|
26
31
|
* H5 开发环境优先走同源代理,避免浏览器直接请求后端产生跨域。
|
|
27
32
|
* 其它平台继续使用显式配置的后端地址。
|
|
@@ -87,10 +92,10 @@ const httpInterceptor = {
|
|
|
87
92
|
}
|
|
88
93
|
// 2. 请求超时,普通请求默认 10s;流式等长连接可自行传入更长超时。
|
|
89
94
|
options.timeout = options.timeout || 10000
|
|
90
|
-
// 3.
|
|
95
|
+
// 3. 添加当前客户端平台标识
|
|
91
96
|
options.header = {
|
|
92
97
|
...options.header,
|
|
93
|
-
'source-client':
|
|
98
|
+
'source-client': sourceClient,
|
|
94
99
|
...getLocaleRequestHeaders(),
|
|
95
100
|
}
|
|
96
101
|
// 4. 添加 token 请求头标识
|
|
@@ -330,7 +335,7 @@ async function refreshAccessToken() {
|
|
|
330
335
|
method: 'POST',
|
|
331
336
|
data: { refresh_token: refreshToken },
|
|
332
337
|
header: {
|
|
333
|
-
'source-client':
|
|
338
|
+
'source-client': sourceClient,
|
|
334
339
|
...getLocaleRequestHeaders(),
|
|
335
340
|
},
|
|
336
341
|
success: resolve,
|