@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.
Files changed (43) hide show
  1. package/README.md +30 -7
  2. package/dist/module.mjs +164 -16
  3. package/dist/vite.mjs +4 -5
  4. package/package.json +3 -1
  5. package/src/api/base/{config.ts → v1/config.ts} +6 -2
  6. package/src/api/base/{file.ts → v1/file.ts} +6 -2
  7. package/src/api/base/{language.ts → v1/language.ts} +2 -2
  8. package/src/api/base/{login.ts → v1/login.ts} +3 -3
  9. package/src/api/base/v1/mfa.ts +127 -0
  10. package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
  11. package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
  12. package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
  13. package/src/api/system/app/v1/base_dict.ts +21 -0
  14. package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
  15. package/src/bootstrap.ts +1 -1
  16. package/src/components/KratosSettingsPage.vue +292 -0
  17. package/src/components/KratosTabBar.vue +71 -1
  18. package/src/components/MfaManageDialog.vue +469 -0
  19. package/src/components/MfaRecoveryCodesDialog.vue +192 -0
  20. package/src/components/MfaSetupPanel.vue +160 -0
  21. package/src/components/PasswordVerifyDialog.vue +277 -0
  22. package/src/index.ts +2 -0
  23. package/src/locales/README.md +2 -1
  24. package/src/locales/en-US.json +41 -4
  25. package/src/locales/index.ts +10 -1
  26. package/src/locales/ja-JP.json +41 -4
  27. package/src/locales/zh-CN.json +41 -4
  28. package/src/locales/zh-TW.json +41 -4
  29. package/src/navigation.ts +115 -53
  30. package/src/rpc/base/v1/login.ts +37 -4
  31. package/src/rpc/base/v1/mfa.ts +180 -0
  32. package/src/rpc/base/v1/oauth.ts +25 -0
  33. package/src/rpc/system/app/v1/auth.ts +7 -0
  34. package/src/rpc/system/common/v1/common.ts +35 -0
  35. package/src/stores/modules/setting.ts +1 -1
  36. package/src/stores/modules/user.ts +29 -10
  37. package/src/utils/clipboard.ts +33 -0
  38. package/src/utils/http.ts +10 -13
  39. package/src/utils/passwordCrypto.ts +1 -1
  40. package/src/utils/webauthn.ts +101 -0
  41. package/src/views/pages/login/login.vue +299 -13
  42. package/src/vite.ts +4 -7
  43. 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
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { defConfigService } from '../../api/base/config'
1
+ import { defConfigService } from '../../api/base/v1/config'
2
2
  import { defineStore } from 'pinia'
3
3
  import { ref } from 'vue'
4
4
  import { BaseConfigSite } from '../../rpc/base/v1/config'
@@ -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 { defAuthService } from '../../api/system/auth'
9
- import { defLoginService } from '../../api/base/login'
10
- import { defOauthService } from '../../api/base/oauth'
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<void>((resolve, reject) => {
87
+ return new Promise<LoginResponse>((resolve, reject) => {
86
88
  defLoginService
87
89
  .Login(request)
88
90
  .then(async (data) => {
89
- await applyLoginToken(data)
90
- resolve()
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
- await applyLoginToken(data)
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<void>((resolve, reject) => {
141
+ return new Promise<CreateOauthSessionResponse>((resolve, reject) => {
125
142
  defOauthService
126
143
  .BindOauthSession(request)
127
144
  .then(async (data) => {
128
- await applyLoginToken(data)
129
- resolve()
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
- const LEGACY_AUTH_URL = '/auth'
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': 'miniapp',
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': 'miniapp',
330
+ 'source-client': sourceClient,
334
331
  ...getLocaleRequestHeaders(),
335
332
  },
336
333
  success: resolve,
@@ -1,4 +1,4 @@
1
- import { defLoginService } from '../api/base/login'
1
+ import { defLoginService } from '../api/base/v1/login'
2
2
  import { t } from '../locales'
3
3
  import { PasswordCryptoScene } from '../rpc/base/v1/login'
4
4
  import type { PasswordCrypto } from '../rpc/common/v1/types'
@@ -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
+ }