@liujitcn/kratos-uni-app-core 0.0.29 → 0.0.32
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 +30 -7
- package/dist/module.mjs +164 -16
- package/dist/vite.mjs +4 -5
- package/package.json +3 -1
- package/src/api/base/{config.ts → v1/config.ts} +6 -2
- package/src/api/base/{file.ts → v1/file.ts} +6 -2
- package/src/api/base/{language.ts → v1/language.ts} +2 -2
- package/src/api/base/{login.ts → v1/login.ts} +3 -3
- package/src/api/base/v1/mfa.ts +127 -0
- package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
- package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
- package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
- package/src/api/system/app/v1/base_dict.ts +21 -0
- package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
- package/src/bootstrap.ts +1 -1
- package/src/components/KratosSettingsPage.vue +292 -0
- package/src/components/KratosTabBar.vue +71 -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 -4
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +41 -4
- package/src/locales/zh-CN.json +41 -4
- package/src/locales/zh-TW.json +41 -4
- package/src/navigation.ts +115 -53
- package/src/rpc/base/v1/login.ts +37 -4
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/rpc/system/app/v1/auth.ts +7 -0
- package/src/rpc/system/common/v1/common.ts +35 -0
- package/src/stores/modules/setting.ts +1 -1
- package/src/stores/modules/user.ts +29 -10
- package/src/utils/clipboard.ts +33 -0
- package/src/utils/http.ts +10 -13
- package/src/utils/passwordCrypto.ts +1 -1
- package/src/utils/webauthn.ts +101 -0
- package/src/views/pages/login/login.vue +299 -13
- package/src/vite.ts +4 -7
- package/src/api/system/base_dict.ts +0 -24
|
@@ -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
|
/** 个人中心三方账号绑定回调请求 */
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
/* eslint-disable */
|
|
8
8
|
import type { Empty } from "../../../google/protobuf/empty";
|
|
9
|
+
import type { BaseUserIDType } from "../../common/v1/common";
|
|
9
10
|
|
|
10
11
|
/** 获取用户资料请求参数 */
|
|
11
12
|
export interface GetUserProfileRequest {
|
|
@@ -21,6 +22,12 @@ export interface UserProfileForm {
|
|
|
21
22
|
gender: number;
|
|
22
23
|
/** 手机号 */
|
|
23
24
|
phone: string;
|
|
25
|
+
/** 邮箱 */
|
|
26
|
+
email: string;
|
|
27
|
+
/** 证件类型:枚举【BaseUserIDType】 */
|
|
28
|
+
id_type: BaseUserIDType;
|
|
29
|
+
/** 证件号 */
|
|
30
|
+
id_code: string;
|
|
24
31
|
/** 头像 */
|
|
25
32
|
avatar: string;
|
|
26
33
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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: system/common/v1/common.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
/** 系统公共用户相关枚举。 */
|
|
10
|
+
export enum BaseUserIDType {
|
|
11
|
+
/** BASE_USER_ID_TYPE_UNSPECIFIED - 未指定证件类型。 */
|
|
12
|
+
BASE_USER_ID_TYPE_UNSPECIFIED = 0,
|
|
13
|
+
/** BASE_USER_ID_TYPE_ID_CARD - 居民身份证。 */
|
|
14
|
+
BASE_USER_ID_TYPE_ID_CARD = 1,
|
|
15
|
+
/** BASE_USER_ID_TYPE_PASSPORT - 护照。 */
|
|
16
|
+
BASE_USER_ID_TYPE_PASSPORT = 2,
|
|
17
|
+
/** BASE_USER_ID_TYPE_HK_MACAO_PERMIT - 港澳居民来往内地通行证。 */
|
|
18
|
+
BASE_USER_ID_TYPE_HK_MACAO_PERMIT = 3,
|
|
19
|
+
/** BASE_USER_ID_TYPE_TAIWAN_PERMIT - 台湾居民来往大陆通行证。 */
|
|
20
|
+
BASE_USER_ID_TYPE_TAIWAN_PERMIT = 4,
|
|
21
|
+
/** BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE - 外国人永久居留身份证。 */
|
|
22
|
+
BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE = 5,
|
|
23
|
+
/** BASE_USER_ID_TYPE_OTHER - 其他证件。 */
|
|
24
|
+
BASE_USER_ID_TYPE_OTHER = 6,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 用户密码修改状态。 */
|
|
28
|
+
export enum BaseUserPasswordChangeStatus {
|
|
29
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED - 未指定密码修改状态。 */
|
|
30
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED = 0,
|
|
31
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED - 当前必须修改密码。 */
|
|
32
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED = 1,
|
|
33
|
+
/** BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED - 当前无需修改密码。 */
|
|
34
|
+
BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED = 2,
|
|
35
|
+
}
|
|
@@ -5,9 +5,11 @@ 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 {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import type { VerifyMfaRequest } from '../../rpc/base/v1/mfa'
|
|
9
|
+
import { defMfaService } from '../../api/base/v1/mfa'
|
|
10
|
+
import { defAuthService } from '../../api/system/app/v1/auth'
|
|
11
|
+
import { defLoginService } from '../../api/base/v1/login'
|
|
12
|
+
import { defOauthService } from '../../api/base/v1/oauth'
|
|
11
13
|
import { defineStore } from 'pinia'
|
|
12
14
|
import { ref } from 'vue'
|
|
13
15
|
import {
|
|
@@ -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
|
* 其它平台继续使用显式配置的后端地址。
|
|
@@ -38,10 +43,7 @@ const CAPTCHA_URL = '/v1/base/captcha'
|
|
|
38
43
|
const CONFIG_URL = '/v1/base/config'
|
|
39
44
|
const LANGUAGE_URL = '/v1/base/language'
|
|
40
45
|
const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
|
|
41
|
-
|
|
42
|
-
const LEGACY_REFRESH_TOKEN_URL = `${LEGACY_AUTH_URL}/token`
|
|
43
|
-
const LEGACY_CAPTCHA_URL = '/login/captcha'
|
|
44
|
-
// 认证公共接口不携带旧 token,同时保留旧路径兼容迁移期间的灰度访问。
|
|
46
|
+
// 认证公共接口不携带访问令牌。
|
|
45
47
|
const NO_AUTH_URL_SET = new Set([
|
|
46
48
|
SESSION_URL,
|
|
47
49
|
REFRESH_TOKEN_URL,
|
|
@@ -49,9 +51,6 @@ const NO_AUTH_URL_SET = new Set([
|
|
|
49
51
|
CONFIG_URL,
|
|
50
52
|
LANGUAGE_URL,
|
|
51
53
|
PASSWORD_PUBLIC_KEY_URL,
|
|
52
|
-
LEGACY_AUTH_URL,
|
|
53
|
-
LEGACY_CAPTCHA_URL,
|
|
54
|
-
LEGACY_REFRESH_TOKEN_URL,
|
|
55
54
|
])
|
|
56
55
|
const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
57
56
|
SESSION_URL,
|
|
@@ -59,8 +58,6 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
|
59
58
|
CONFIG_URL,
|
|
60
59
|
LANGUAGE_URL,
|
|
61
60
|
PASSWORD_PUBLIC_KEY_URL,
|
|
62
|
-
LEGACY_AUTH_URL,
|
|
63
|
-
LEGACY_CAPTCHA_URL,
|
|
64
61
|
])
|
|
65
62
|
|
|
66
63
|
const AUTH_SILENT_LOGOUT_EVENT = 'auth:silent-logout'
|
|
@@ -87,10 +84,10 @@ const httpInterceptor = {
|
|
|
87
84
|
}
|
|
88
85
|
// 2. 请求超时,普通请求默认 10s;流式等长连接可自行传入更长超时。
|
|
89
86
|
options.timeout = options.timeout || 10000
|
|
90
|
-
// 3.
|
|
87
|
+
// 3. 添加当前客户端平台标识
|
|
91
88
|
options.header = {
|
|
92
89
|
...options.header,
|
|
93
|
-
'source-client':
|
|
90
|
+
'source-client': sourceClient,
|
|
94
91
|
...getLocaleRequestHeaders(),
|
|
95
92
|
}
|
|
96
93
|
// 4. 添加 token 请求头标识
|
|
@@ -142,7 +139,7 @@ function isAuthErrorResponse(data: unknown) {
|
|
|
142
139
|
)
|
|
143
140
|
}
|
|
144
141
|
|
|
145
|
-
//
|
|
142
|
+
// 当前请求属于公共认证接口时,不应自动附带登录态。
|
|
146
143
|
function isNoAuthRequest(url: string) {
|
|
147
144
|
return NO_AUTH_URL_SET.has(url)
|
|
148
145
|
}
|
|
@@ -330,7 +327,7 @@ async function refreshAccessToken() {
|
|
|
330
327
|
method: 'POST',
|
|
331
328
|
data: { refresh_token: refreshToken },
|
|
332
329
|
header: {
|
|
333
|
-
'source-client':
|
|
330
|
+
'source-client': sourceClient,
|
|
334
331
|
...getLocaleRequestHeaders(),
|
|
335
332
|
},
|
|
336
333
|
success: resolve,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { t } from '../locales'
|
|
2
|
+
|
|
3
|
+
/** WebAuthn 选项使用的 Base64URL 编解码。 */
|
|
4
|
+
function decodeBase64Url(value: string): ArrayBuffer {
|
|
5
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
6
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
|
|
7
|
+
const binary = atob(padded)
|
|
8
|
+
const bytes = new Uint8Array(binary.length)
|
|
9
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
|
|
10
|
+
return bytes.buffer
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 将 WebAuthn 二进制响应编码为服务端协议使用的 Base64URL。 */
|
|
14
|
+
function encodeBase64Url(value: ArrayBuffer | Uint8Array): string {
|
|
15
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value)
|
|
16
|
+
let binary = ''
|
|
17
|
+
bytes.forEach((byte) => {
|
|
18
|
+
binary += String.fromCharCode(byte)
|
|
19
|
+
})
|
|
20
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 将服务端返回的 WebAuthn 创建选项转换为浏览器类型。 */
|
|
24
|
+
function parseCreationOptions(optionsJson: string): PublicKeyCredentialCreationOptions {
|
|
25
|
+
const options = JSON.parse(optionsJson) as { publicKey: PublicKeyCredentialCreationOptions }
|
|
26
|
+
const publicKey = options.publicKey
|
|
27
|
+
publicKey.challenge = decodeBase64Url(publicKey.challenge as unknown as string)
|
|
28
|
+
publicKey.user.id = decodeBase64Url(publicKey.user.id as unknown as string)
|
|
29
|
+
publicKey.excludeCredentials = publicKey.excludeCredentials?.map((item) => ({
|
|
30
|
+
...item,
|
|
31
|
+
id: decodeBase64Url(item.id as unknown as string),
|
|
32
|
+
}))
|
|
33
|
+
return publicKey
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 将服务端返回的 WebAuthn 断言选项转换为浏览器类型。 */
|
|
37
|
+
function parseRequestOptions(optionsJson: string): PublicKeyCredentialRequestOptions {
|
|
38
|
+
const options = JSON.parse(optionsJson) as { publicKey: PublicKeyCredentialRequestOptions }
|
|
39
|
+
const publicKey = options.publicKey
|
|
40
|
+
publicKey.challenge = decodeBase64Url(publicKey.challenge as unknown as string)
|
|
41
|
+
publicKey.allowCredentials = publicKey.allowCredentials?.map((item) => ({
|
|
42
|
+
...item,
|
|
43
|
+
id: decodeBase64Url(item.id as unknown as string),
|
|
44
|
+
}))
|
|
45
|
+
return publicKey
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 将 PublicKeyCredential 序列化为服务端协议使用的 JSON。 */
|
|
49
|
+
function serializeCredential(credential: PublicKeyCredential): string {
|
|
50
|
+
const response = credential.response
|
|
51
|
+
const serializedResponse: Record<string, unknown> = {
|
|
52
|
+
clientDataJSON: encodeBase64Url(response.clientDataJSON),
|
|
53
|
+
}
|
|
54
|
+
const payload: Record<string, unknown> = {
|
|
55
|
+
id: credential.id,
|
|
56
|
+
rawId: encodeBase64Url(credential.rawId),
|
|
57
|
+
type: credential.type,
|
|
58
|
+
response: serializedResponse,
|
|
59
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
60
|
+
}
|
|
61
|
+
if ('attestationObject' in response) {
|
|
62
|
+
const attestation = response as AuthenticatorAttestationResponse
|
|
63
|
+
serializedResponse.attestationObject = encodeBase64Url(attestation.attestationObject)
|
|
64
|
+
} else {
|
|
65
|
+
const assertion = response as AuthenticatorAssertionResponse
|
|
66
|
+
Object.assign(serializedResponse, {
|
|
67
|
+
authenticatorData: encodeBase64Url(assertion.authenticatorData),
|
|
68
|
+
signature: encodeBase64Url(assertion.signature),
|
|
69
|
+
userHandle: assertion.userHandle ? encodeBase64Url(assertion.userHandle) : null,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
if (credential.authenticatorAttachment)
|
|
73
|
+
payload.authenticatorAttachment = credential.authenticatorAttachment
|
|
74
|
+
return JSON.stringify(payload)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 执行 WebAuthn 注册 ceremony。 */
|
|
78
|
+
export async function createWebAuthnCredential(optionsJson: string): Promise<string> {
|
|
79
|
+
if (typeof navigator === 'undefined' || !navigator.credentials?.create) {
|
|
80
|
+
throw new Error(t('core.login.mfa_webauthn_setup_unsupported'))
|
|
81
|
+
}
|
|
82
|
+
const credential = await navigator.credentials.create({
|
|
83
|
+
publicKey: parseCreationOptions(optionsJson),
|
|
84
|
+
})
|
|
85
|
+
if (!(credential instanceof PublicKeyCredential))
|
|
86
|
+
throw new Error(t('core.login.mfa_webauthn_setup_invalid'))
|
|
87
|
+
return serializeCredential(credential)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 执行 WebAuthn 登录 ceremony。 */
|
|
91
|
+
export async function getWebAuthnAssertion(optionsJson: string): Promise<string> {
|
|
92
|
+
if (typeof navigator === 'undefined' || !navigator.credentials?.get) {
|
|
93
|
+
throw new Error(t('core.login.mfa_webauthn_login_unsupported'))
|
|
94
|
+
}
|
|
95
|
+
const credential = await navigator.credentials.get({
|
|
96
|
+
publicKey: parseRequestOptions(optionsJson),
|
|
97
|
+
})
|
|
98
|
+
if (!(credential instanceof PublicKeyCredential))
|
|
99
|
+
throw new Error(t('core.login.mfa_webauthn_login_invalid'))
|
|
100
|
+
return serializeCredential(credential)
|
|
101
|
+
}
|