@cloudbase/auth 3.0.0 → 3.0.2

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/src/index.ts CHANGED
@@ -1,11 +1,20 @@
1
1
  import type { ICloudbase, ICloudbaseConfig, ICloudbasePlatformInfo } from '@cloudbase/types'
2
2
  import type { ICloudbaseCache } from '@cloudbase/types/cache'
3
3
  import type { ICloudbaseRequest } from '@cloudbase/types/request'
4
- import type { ICloudbaseAuthConfig, IUser, IUserInfo, ILoginState } from '@cloudbase/types/auth'
4
+ import type { ICloudbaseAuthConfig, IUser, ILoginState } from '@cloudbase/types/auth'
5
5
  import type { ICloudbaseComponent } from '@cloudbase/types/component'
6
- import type { GrantProviderTokenResponse, ProviderSubType } from '@cloudbase/oauth/dist/cjs/auth/models'
7
- import type { authModels, AuthOptions, Credentials } from '@cloudbase/oauth'
8
- import { CloudbaseOAuth, AUTH_API_PREFIX } from '@cloudbase/oauth'
6
+ import type { AuthOptions, Credentials } from '@cloudbase/oauth'
7
+ import {
8
+ CloudbaseOAuth,
9
+ AUTH_API_PREFIX,
10
+ LOGIN_STATE_CHANGED_TYPE,
11
+ EVENTS,
12
+ AUTH_STATE_CHANGED_TYPE,
13
+ OAUTH_TYPE,
14
+ weAppJwtDecodeAll,
15
+ AuthError,
16
+ authModels,
17
+ } from '@cloudbase/oauth'
9
18
  import { useAuthAdapter } from './adapter'
10
19
  import {
11
20
  printWarn,
@@ -18,23 +27,77 @@ import {
18
27
  adapterForWxMp,
19
28
  useDefaultAdapter,
20
29
  } from './utilities'
30
+ import { saveToBrowserSession, getBrowserSession, removeBrowserSession, addUrlSearch } from './utils'
31
+ import { utils } from '@cloudbase/utilities'
32
+ import {
33
+ CommonRes,
34
+ DeleteMeReq,
35
+ GetClaimsRes,
36
+ GetUserIdentitiesRes,
37
+ GetUserRes,
38
+ LinkIdentityReq,
39
+ LinkIdentityRes,
40
+ OnAuthStateChangeCallback,
41
+ ReauthenticateRes,
42
+ ResendReq,
43
+ ResendRes,
44
+ ResetPasswordForEmailRes,
45
+ ResetPasswordForOldReq,
46
+ SetSessionReq,
47
+ SignInAnonymouslyReq,
48
+ SignInOAuthRes,
49
+ SignInRes,
50
+ SignInWithIdTokenReq,
51
+ SignInWithOAuthReq,
52
+ SignInWithOtpReq,
53
+ SignInWithOtpRes,
54
+ SignInWithPasswordReq,
55
+ SignUpRes,
56
+ UnlinkIdentityReq,
57
+ UpdateUserAttributes,
58
+ UpdateUserReq,
59
+ UpdateUserWithVerificationRes,
60
+ VerifyOAuthReq,
61
+ VerifyOtpReq,
62
+ } from './type'
63
+
64
+ const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
65
+
66
+ export type {
67
+ SignInRes,
68
+ GetUserRes,
69
+ CommonRes,
70
+ SignInWithOtpRes,
71
+ SignInOAuthRes,
72
+ GetClaimsRes,
73
+ ResetPasswordForEmailRes,
74
+ GetUserIdentitiesRes,
75
+ LinkIdentityRes,
76
+ ReauthenticateRes,
77
+ ResendRes,
78
+ UpdateUserWithVerificationRes,
79
+ OnAuthStateChangeCallback,
80
+ SignInWithPasswordReq,
81
+ SignInWithIdTokenReq,
82
+ SignInWithOAuthReq,
83
+ VerifyOAuthReq,
84
+ VerifyOtpReq,
85
+ LinkIdentityReq,
86
+ UnlinkIdentityReq,
87
+ UpdateUserReq,
88
+ SignInWithOtpReq,
89
+ ResetPasswordForOldReq,
90
+ ResendReq,
91
+ SetSessionReq,
92
+ DeleteMeReq,
93
+ SignUpRes,
94
+ } from './type'
21
95
 
22
96
  declare const cloudbase: ICloudbase
23
97
  declare const wx: any
24
98
 
25
99
  const COMPONENT_NAME = 'auth'
26
100
 
27
- const LOGIN_STATE_CHANGED_TYPE = {
28
- SIGN_OUT: 'sign_out',
29
- SIGN_IN: 'sign_in',
30
- CREDENTIALS_ERROR: 'credentials_error',
31
- }
32
-
33
- const EVENTS = {
34
- // 登录态改变后触发
35
- LOGIN_STATE_CHANGED: 'loginStateChanged',
36
- }
37
-
38
101
  interface UserInfo {
39
102
  uid?: string
40
103
  gender?: string
@@ -51,9 +114,7 @@ interface UserInfo {
51
114
  created_from?: string
52
115
  }
53
116
 
54
- const eventBus = new CloudbaseEventEmitter()
55
-
56
- const onCredentialsError = (params) => {
117
+ const onCredentialsError = eventBus => (params) => {
57
118
  eventBus.fire(EVENTS.LOGIN_STATE_CHANGED, { ...params, eventType: LOGIN_STATE_CHANGED_TYPE.CREDENTIALS_ERROR })
58
119
  }
59
120
 
@@ -62,14 +123,14 @@ interface IUserOptions {
62
123
  oauthInstance: CloudbaseOAuth
63
124
  }
64
125
 
65
- class User implements IUser {
126
+ export class User implements IUser {
66
127
  public uid?: string
67
128
  public gender?: string
68
129
  public picture?: string
69
130
  public email?: string
70
131
  public emailVerified?: boolean
71
132
  public phoneNumber?: string
72
- public username?: string
133
+ public username?: string // 用户名称,长度 5-24 位,支持字符中英文、数字、特殊字符(仅支持_-),不支持中文
73
134
  public name?: string
74
135
  public providers?: {
75
136
  id?: string
@@ -153,7 +214,7 @@ class User implements IUser {
153
214
 
154
215
  public async updateUserBasicInfo(params: authModels.ModifyUserBasicInfoRequest): Promise<void> {
155
216
  await this.oauthInstance.authApi.updateUserBasicInfo({ ...params })
156
- this.setLocalUserInfo({ username: params.username })
217
+ await this.refresh()
157
218
  }
158
219
 
159
220
  /**
@@ -211,18 +272,31 @@ class User implements IUser {
211
272
  `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
212
273
  ],
213
274
  })
214
- public async refresh(params?: { version?: string; query?: any }): Promise<IUserInfo> {
275
+ public async refresh(params?: { version?: string; query?: any }): Promise<authModels.UserInfo> {
215
276
  const newUserInfo = await this.oauthInstance.authApi.getUserInfo(params)
277
+ if ((newUserInfo as any).code === 'INVALID_ACCESS_TOKEN') {
278
+ this.setLocalUserInfo({})
279
+ throw newUserInfo
280
+ }
216
281
  this.setLocalUserInfo(newUserInfo)
217
282
  return newUserInfo
218
283
  }
219
284
 
220
- private getLocalUserInfo(key: string): string | boolean {
285
+ public getLocalUserInfo(key?: string): string | boolean | Record<string, any> {
221
286
  const { userInfoKey } = this.cache.keys
222
287
  const userInfo = this.cache.getStore(userInfoKey)
288
+
289
+ if (!key) return userInfo || {}
290
+
223
291
  return userInfo[key]
224
292
  }
225
293
 
294
+ public setLocalUserInfo(userInfo: any) {
295
+ const { userInfoKey } = this.cache.keys
296
+ this.cache.setStore(userInfoKey, userInfo)
297
+ this.setUserInfo()
298
+ }
299
+
226
300
  private async getLocalUserInfoAsync(key: string): Promise<string> {
227
301
  const { userInfoKey } = this.cache.keys
228
302
  const userInfo = await this.cache.getStoreAsync(userInfoKey)
@@ -247,16 +321,11 @@ class User implements IUser {
247
321
  'created_from',
248
322
  'providers',
249
323
  'username',
324
+ 'created_at',
250
325
  ].forEach((infoKey) => {
251
326
  this[infoKey] = userInfo[infoKey]
252
327
  })
253
328
  }
254
-
255
- private setLocalUserInfo(userInfo: any) {
256
- const { userInfoKey } = this.cache.keys
257
- this.cache.setStore(userInfoKey, userInfo)
258
- this.setUserInfo()
259
- }
260
329
  }
261
330
  interface ILoginStateOptions extends IUserOptions {
262
331
  envId: string
@@ -294,19 +363,22 @@ export class LoginState implements ILoginState {
294
363
  }
295
364
 
296
365
  class Auth {
297
- private readonly config: ICloudbaseAuthConfig
298
- private readonly cache: ICloudbaseCache
299
-
300
- private oauthInstance: CloudbaseOAuth
301
-
302
- constructor(config: ICloudbaseAuthConfig & { cache: ICloudbaseCache; request?: ICloudbaseRequest; runtime?: string },) {
366
+ readonly config: ICloudbaseAuthConfig
367
+ oauthInstance: CloudbaseOAuth
368
+ readonly cache: ICloudbaseCache
369
+ private listeners: Map<string, Set<OnAuthStateChangeCallback>> = new Map()
370
+ private hasListenerSetUp = false
371
+
372
+ constructor(config: ICloudbaseAuthConfig & {
373
+ cache: ICloudbaseCache
374
+ request?: ICloudbaseRequest
375
+ runtime?: string
376
+ },) {
303
377
  this.config = config
304
- this.cache = config.cache
305
378
  this.oauthInstance = config.oauthInstance
306
-
307
- if (config.publishable_key) {
308
- this.createLoginState()
309
- }
379
+ this.cache = config.cache
380
+ this.init()
381
+ this.setAccessKey()
310
382
  }
311
383
 
312
384
  /**
@@ -412,7 +484,7 @@ class Auth {
412
484
  // async storage的平台调用此API提示
413
485
  printWarn(
414
486
  ERRORS.INVALID_OPERATION,
415
- 'current platform\'s storage is asynchronous, please use getCurrentUser insteed',
487
+ 'current platform\'s storage is asynchronous, please use getCurrentUser instead',
416
488
  )
417
489
  return
418
490
  }
@@ -436,35 +508,36 @@ class Auth {
436
508
  `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
437
509
  ],
438
510
  })
439
- public async getCurrentUser() {
511
+ public async getCurrentUser(): Promise<(authModels.UserInfo & Partial<User>) | null> {
440
512
  const loginState = await this.getLoginState()
441
513
  if (loginState) {
514
+ const userInfo = loginState.user.getLocalUserInfo() as authModels.UserInfo
442
515
  await loginState.user.checkLocalInfoAsync()
443
- return loginState.user || null
516
+ return { ...loginState.user, ...userInfo } as unknown as authModels.UserInfo & Partial<User>
444
517
  }
445
518
  return null
446
519
  }
447
520
 
448
- /**
449
- * 匿名登录
450
- * @returns {Promise<LoginState>}
451
- * @memberof Auth
452
- */
453
- @catchErrorsDecorator({
454
- title: '匿名登录失败',
455
- messages: [
456
- '请确认以下各项:',
457
- ' 1 - 当前环境是否开启了匿名登录',
458
- ' 2 - 调用 auth().signInAnonymously() 的语法或参数是否正确',
459
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
460
- ],
461
- })
462
- public async signInAnonymously(data: {
463
- provider_token?: string
464
- } = {},): Promise<LoginState> {
465
- await this.oauthInstance.authApi.signInAnonymously(data)
466
- return this.createLoginState()
467
- }
521
+ // /**
522
+ // * 匿名登录
523
+ // * @returns {Promise<LoginState>}
524
+ // * @memberof Auth
525
+ // */
526
+ // @catchErrorsDecorator({
527
+ // title: '匿名登录失败',
528
+ // messages: [
529
+ // '请确认以下各项:',
530
+ // ' 1 - 当前环境是否开启了匿名登录',
531
+ // ' 2 - 调用 auth().signInAnonymously() 的语法或参数是否正确',
532
+ // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
533
+ // ],
534
+ // })
535
+ // public async signInAnonymously(data: {
536
+ // provider_token?: string
537
+ // } = {},): Promise<LoginState> {
538
+ // await this.oauthInstance.authApi.signInAnonymously(data)
539
+ // return this.createLoginState()
540
+ // }
468
541
 
469
542
  /**
470
543
  * 匿名登录
@@ -491,7 +564,7 @@ class Auth {
491
564
  const wxInfo = wx.getAccountInfoSync().miniProgram
492
565
 
493
566
  const mainFunc = async (code) => {
494
- let result: GrantProviderTokenResponse | undefined = undefined
567
+ let result: authModels.GrantProviderTokenResponse | undefined = undefined
495
568
  let credentials: Credentials | undefined = undefined
496
569
 
497
570
  try {
@@ -566,7 +639,7 @@ class Auth {
566
639
  const wxInfo = wx.getAccountInfoSync().miniProgram
567
640
 
568
641
  const mainFunc = async (code) => {
569
- let result: GrantProviderTokenResponse | undefined = undefined
642
+ let result: authModels.GrantProviderTokenResponse | undefined = undefined
570
643
 
571
644
  try {
572
645
  result = await this.oauthInstance.authApi.grantProviderToken({
@@ -609,82 +682,6 @@ class Auth {
609
682
  return
610
683
  }
611
684
 
612
- /**
613
- * 小程序openId静默登录
614
- * @returns {Promise<LoginState>}
615
- * @memberof Auth
616
- */
617
- @catchErrorsDecorator({
618
- title: '小程序openId静默登录失败',
619
- messages: [
620
- '请确认以下各项:',
621
- ' 1 - 当前环境是否开启了小程序openId静默登录',
622
- ' 2 - 调用 auth().signInWithOpenId() 的语法或参数是否正确',
623
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
624
- ],
625
- })
626
- public async signInWithOpenId({ useWxCloud = true } = {}): Promise<LoginState> {
627
- if (!adapterForWxMp.isMatch()) {
628
- throw Error('wx api undefined')
629
- }
630
- const wxInfo = wx.getAccountInfoSync().miniProgram
631
-
632
- const mainFunc = async (code) => {
633
- let result: GrantProviderTokenResponse | undefined = undefined
634
- let credentials: Credentials | undefined = undefined
635
-
636
- try {
637
- result = await this.oauthInstance.authApi.grantProviderToken(
638
- {
639
- provider_id: wxInfo?.appId,
640
- provider_code: code,
641
- provider_params: {
642
- provider_code_type: 'open_id',
643
- appid: wxInfo?.appId,
644
- },
645
- },
646
- useWxCloud,
647
- )
648
-
649
- if ((result as any)?.error_code || !result.provider_token) {
650
- throw result
651
- }
652
-
653
- credentials = await this.oauthInstance.authApi.signInWithProvider(
654
- { provider_token: result.provider_token },
655
- useWxCloud,
656
- )
657
-
658
- if ((credentials as any)?.error_code) {
659
- throw credentials
660
- }
661
- } catch (error) {
662
- throw error
663
- }
664
- await this.oauthInstance.oauth2client.setCredentials(credentials as Credentials)
665
- }
666
-
667
- await new Promise((resolve, reject) => {
668
- wx.login({
669
- success: async (res: { code: string }) => {
670
- try {
671
- await mainFunc(res.code)
672
- resolve(true)
673
- } catch (error) {
674
- reject(error)
675
- }
676
- },
677
- fail: (res: any) => {
678
- const error = new Error(res?.errMsg)
679
- ;(error as any).code = res?.errno
680
- reject(error)
681
- },
682
- })
683
- })
684
-
685
- return this.createLoginState()
686
- }
687
-
688
685
  /**
689
686
  * 小程序unionId静默登录
690
687
  * @returns {Promise<LoginState>}
@@ -755,63 +752,63 @@ class Auth {
755
752
  }
756
753
 
757
754
  /**
758
- * 小程序手机号授权登录
755
+ * 小程序手机号授权登录,目前只支持全托管手机号授权登录
759
756
  * @returns {Promise<LoginState>}
760
757
  * @memberof Auth
761
758
  */
762
- @catchErrorsDecorator({
763
- title: '小程序手机号授权登录失败',
764
- messages: [
765
- '请确认以下各项:',
766
- ' 1 - 当前环境是否开启了小程序手机号授权登录',
767
- ' 2 - 调用 auth().signInWithPhoneAuth() 的语法或参数是否正确',
768
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
769
- ],
770
- })
771
- public async signInWithPhoneAuth({ phoneCode = '' }): Promise<LoginState> {
772
- if (!adapterForWxMp.isMatch()) {
773
- throw Error('wx api undefined')
774
- }
775
- const wxInfo = wx.getAccountInfoSync().miniProgram
776
- const providerInfo = {
777
- provider_params: { provider_code_type: 'phone' },
778
- provider_id: wxInfo.appId,
779
- }
780
-
781
- const { code } = await wx.login()
782
- ;(providerInfo as any).provider_code = code
783
-
784
- try {
785
- let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo)
786
- if (providerToken.error_code) {
787
- throw providerToken
788
- }
789
-
790
- providerToken = await this.oauthInstance.authApi.patchProviderToken({
791
- provider_token: providerToken.provider_token,
792
- provider_id: wxInfo.appId,
793
- provider_params: {
794
- code: phoneCode,
795
- provider_code_type: 'phone',
796
- },
797
- })
798
- if (providerToken.error_code) {
799
- throw providerToken
800
- }
801
-
802
- const signInRes = await this.oauthInstance.authApi.signInWithProvider({
803
- provider_token: providerToken.provider_token,
804
- })
805
-
806
- if ((signInRes as any)?.error_code) {
807
- throw signInRes
808
- }
809
- } catch (error) {
810
- throw error
811
- }
812
-
813
- return this.createLoginState()
814
- }
759
+ // @catchErrorsDecorator({
760
+ // title: '小程序手机号授权登录失败',
761
+ // messages: [
762
+ // '请确认以下各项:',
763
+ // ' 1 - 当前环境是否开启了小程序手机号授权登录',
764
+ // ' 2 - 调用 auth().signInWithPhoneAuth() 的语法或参数是否正确',
765
+ // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
766
+ // ],
767
+ // })
768
+ // public async signInWithPhoneAuth({ phoneCode = '' }): Promise<LoginState> {
769
+ // if (!adapterForWxMp.isMatch()) {
770
+ // throw Error('wx api undefined')
771
+ // }
772
+ // const wxInfo = wx.getAccountInfoSync().miniProgram
773
+ // const providerInfo = {
774
+ // provider_params: { provider_code_type: 'phone' },
775
+ // provider_id: wxInfo.appId,
776
+ // }
777
+
778
+ // const { code } = await wx.login()
779
+ // ;(providerInfo as any).provider_code = code
780
+
781
+ // try {
782
+ // let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo)
783
+ // if (providerToken.error_code) {
784
+ // throw providerToken
785
+ // }
786
+
787
+ // providerToken = await this.oauthInstance.authApi.patchProviderToken({
788
+ // provider_token: providerToken.provider_token,
789
+ // provider_id: wxInfo.appId,
790
+ // provider_params: {
791
+ // code: phoneCode,
792
+ // provider_code_type: 'phone',
793
+ // },
794
+ // })
795
+ // if (providerToken.error_code) {
796
+ // throw providerToken
797
+ // }
798
+
799
+ // const signInRes = await this.oauthInstance.authApi.signInWithProvider({
800
+ // provider_token: providerToken.provider_token,
801
+ // })
802
+
803
+ // if ((signInRes as any)?.error_code) {
804
+ // throw signInRes
805
+ // }
806
+ // } catch (error) {
807
+ // throw error
808
+ // }
809
+
810
+ // return this.createLoginState()
811
+ // }
815
812
 
816
813
  /**
817
814
  * 小程序短信验证码登陆
@@ -893,21 +890,21 @@ class Auth {
893
890
  * @returns {Promise<LoginState>}
894
891
  * @memberof Auth
895
892
  */
896
- @catchErrorsDecorator({
897
- title: '自定义登录失败',
898
- messages: [
899
- '请确认以下各项:',
900
- ' 1 - 当前环境是否开启了自定义登录',
901
- ' 2 - 调用 auth().signInWithCustomTicket() 的语法或参数是否正确',
902
- ' 3 - ticket 是否归属于当前环境',
903
- ' 4 - 创建 ticket 的自定义登录私钥是否过期',
904
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
905
- ],
906
- })
907
- public async signInWithCustomTicket(): Promise<LoginState> {
908
- await this.oauthInstance.authApi.signInWithCustomTicket()
909
- return this.createLoginState()
910
- }
893
+ // @catchErrorsDecorator({
894
+ // title: '自定义登录失败',
895
+ // messages: [
896
+ // '请确认以下各项:',
897
+ // ' 1 - 当前环境是否开启了自定义登录',
898
+ // ' 2 - 调用 auth().signInWithCustomTicket() 的语法或参数是否正确',
899
+ // ' 3 - ticket 是否归属于当前环境',
900
+ // ' 4 - 创建 ticket 的自定义登录私钥是否过期',
901
+ // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
902
+ // ],
903
+ // })
904
+ // public async signInWithCustomTicket(): Promise<LoginState> {
905
+ // await this.oauthInstance.authApi.signInWithCustomTicket()
906
+ // return this.createLoginState()
907
+ // }
911
908
 
912
909
  /**
913
910
  *
@@ -920,25 +917,25 @@ class Auth {
920
917
  return this.createLoginState(params)
921
918
  }
922
919
 
923
- /**
924
- *
925
- * @param {authModels.SignUpRequest} params
926
- * @returns {Promise<LoginState>}
927
- * @memberof Auth
928
- */
929
- @catchErrorsDecorator({
930
- title: '注册失败',
931
- messages: [
932
- '请确认以下各项:',
933
- ' 1 - 当前环境是否开启了指定登录方式',
934
- ' 2 - 调用 auth().signUp() 的语法或参数是否正确',
935
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
936
- ],
937
- })
938
- public async signUp(params: authModels.SignUpRequest): Promise<LoginState> {
939
- await this.oauthInstance.authApi.signUp(params)
940
- return this.createLoginState()
941
- }
920
+ // /**
921
+ // *
922
+ // * @param {authModels.SignUpRequest} params
923
+ // * @returns {Promise<LoginState>}
924
+ // * @memberof Auth
925
+ // */
926
+ // @catchErrorsDecorator({
927
+ // title: '注册失败',
928
+ // messages: [
929
+ // '请确认以下各项:',
930
+ // ' 1 - 当前环境是否开启了指定登录方式',
931
+ // ' 2 - 调用 auth().signUp() 的语法或参数是否正确',
932
+ // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
933
+ // ],
934
+ // })
935
+ // public async signUp(params: authModels.SignUpRequest): Promise<any> {
936
+ // await this.oauthInstance.authApi.signUp(params)
937
+ // return this.createLoginState()
938
+ // }
942
939
 
943
940
  /**
944
941
  * 设置密码
@@ -971,26 +968,6 @@ class Auth {
971
968
  return exist
972
969
  }
973
970
 
974
- /**
975
- * 登出
976
- */
977
- @catchErrorsDecorator({
978
- title: '用户登出失败',
979
- messages: [
980
- '请确认以下各项:',
981
- ' 1 - 调用 auth().signOut() 的语法或参数是否正确',
982
- ' 2 - 当前用户是否为匿名登录(匿名登录不支持signOut)',
983
- `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
984
- ],
985
- })
986
- public async signOut(params?: authModels.SignoutRequest): Promise<void | authModels.SignoutReponse> {
987
- const { userInfoKey } = this.cache.keys
988
- const res = await this.oauthInstance.authApi.signOut(params)
989
- await this.cache.removeStoreAsync(userInfoKey)
990
- eventBus.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_OUT })
991
- return res
992
- }
993
-
994
971
  /**
995
972
  * 获取本地登录态-同步
996
973
  */
@@ -999,7 +976,7 @@ class Auth {
999
976
  // async storage的平台调用此API提示
1000
977
  printWarn(
1001
978
  ERRORS.INVALID_OPERATION,
1002
- 'current platform\'s storage is asynchronous, please use getLoginState insteed',
979
+ 'current platform\'s storage is asynchronous, please use getLoginState instead',
1003
980
  )
1004
981
  return
1005
982
  }
@@ -1058,8 +1035,8 @@ class Auth {
1058
1035
  `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
1059
1036
  ],
1060
1037
  })
1061
- public async getUserInfo(params?: { version?: string }): Promise<IUserInfo> {
1062
- return this.oauthInstance.authApi.getUserInfo(params)
1038
+ public async getUserInfo(): Promise<(authModels.UserInfo & Partial<User>) | null> {
1039
+ return this.getCurrentUser()
1063
1040
  }
1064
1041
 
1065
1042
  @catchErrorsDecorator({
@@ -1185,7 +1162,7 @@ class Auth {
1185
1162
  }
1186
1163
 
1187
1164
  public async onLoginStateChanged(callback: Function) {
1188
- eventBus.on(EVENTS.LOGIN_STATE_CHANGED, async (params) => {
1165
+ this.config.eventBus?.on(EVENTS.LOGIN_STATE_CHANGED, async (params) => {
1189
1166
  // getLoginState会重复触发getCredentials,导致死循环,所以getCredentials出错不再出发getLoginState
1190
1167
  const loginState = params?.data?.eventType !== LOGIN_STATE_CHANGED_TYPE.CREDENTIALS_ERROR ? await this.getLoginState() : {}
1191
1168
  callback.call(this, { ...params, ...loginState })
@@ -1226,7 +1203,7 @@ class Auth {
1226
1203
  `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`,
1227
1204
  ],
1228
1205
  })
1229
- public async getProviderSubType(): Promise<ProviderSubType> {
1206
+ public async getProviderSubType(): Promise<authModels.ProviderSubType> {
1230
1207
  return this.oauthInstance.authApi.getProviderSubType()
1231
1208
  }
1232
1209
 
@@ -1258,55 +1235,21 @@ class Auth {
1258
1235
  return this.oauthInstance.authApi.getUserBehaviorLog(params)
1259
1236
  }
1260
1237
 
1261
- /**
1262
- * 跳转系统默认登录页
1263
- * @returns {Promise<authModels.ToDefaultLoginPage>}
1264
- * @memberof Auth
1265
- */
1266
- public async toDefaultLoginPage(params: authModels.ToDefaultLoginPage = {}): Promise<void> {
1267
- const configVersion = params.config_version || 'env'
1268
-
1269
- if (adapterForWxMp.isMatch()) {
1270
- wx.navigateTo({ url: '/packages/$wd_system/pages/login/index' })
1271
- } else {
1272
- const redirectUri = params.redirect_uri || window.location.href
1273
- const urlObj = new URL(redirectUri)
1274
- const loginPage = `${urlObj.origin}/__auth/?app_id=${params.app_id || ''}&env_id=${this.config.env}&client_id=${
1275
- this.config.clientId
1276
- }&config_version=${configVersion}&redirect_uri=${encodeURIComponent(redirectUri)}`
1277
- window.location.href = loginPage
1278
- }
1279
- }
1280
-
1281
- private async createLoginState(
1282
- params?: { version?: string; query?: any },
1283
- options?: { asyncRefreshUser?: boolean },
1284
- ): Promise<LoginState> {
1285
- const loginState = new LoginState({
1286
- envId: this.config.env,
1287
- cache: this.cache,
1288
- oauthInstance: this.oauthInstance,
1289
- })
1290
-
1291
- await loginState.checkLocalStateAsync()
1292
- if (options?.asyncRefreshUser) {
1293
- loginState.user.refresh(params)
1294
- } else {
1295
- await loginState.user.refresh(params)
1296
- }
1297
- eventBus.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_IN })
1298
- return loginState
1299
- }
1300
-
1301
1238
  /**
1302
1239
  * sms/email 验证码登录/注册,逻辑一致收敛
1303
1240
  */
1304
- private async signInWithUsername({
1241
+ public async signInWithUsername({
1305
1242
  verificationInfo = { verification_id: '', is_user: false },
1306
1243
  verificationCode = '',
1307
1244
  username: rawUsername = '',
1308
1245
  bindInfo = undefined,
1309
1246
  loginType = '',
1247
+ }: {
1248
+ verificationInfo?: authModels.GetVerificationResponse
1249
+ verificationCode?: string
1250
+ username?: string
1251
+ bindInfo?: any
1252
+ loginType?: string
1310
1253
  }): Promise<LoginState> {
1311
1254
  try {
1312
1255
  // 1. 验证验证码
@@ -1319,10 +1262,11 @@ class Auth {
1319
1262
  throw verifyRes
1320
1263
  }
1321
1264
 
1265
+ // eslint-disable-next-line @typescript-eslint/naming-convention
1322
1266
  const { verification_token } = verifyRes
1323
1267
 
1324
1268
  // 手机登录参数
1325
- let username = `+86 ${rawUsername}`
1269
+ let username = /^\+\d{1,3}\s+/.test(rawUsername) ? rawUsername : `+86 ${rawUsername}`
1326
1270
  let signUpParam: any = { phone_number: username }
1327
1271
 
1328
1272
  // 邮箱登录参数
@@ -1370,37 +1314,1296 @@ class Auth {
1370
1314
  throw error
1371
1315
  }
1372
1316
  }
1373
- }
1374
1317
 
1375
- type TInitAuthOptions = Pick<ICloudbaseAuthConfig, 'region' | 'persistence' | 'i18n' | 'publishable_key'> & Partial<AuthOptions>
1318
+ async createLoginState(
1319
+ params?: { version?: string; query?: any },
1320
+ options?: { asyncRefreshUser?: boolean; userInfo?: any },
1321
+ ): Promise<LoginState> {
1322
+ const loginState = new LoginState({
1323
+ envId: this.config.env,
1324
+ cache: this.cache,
1325
+ oauthInstance: this.oauthInstance,
1326
+ })
1327
+
1328
+ await loginState.checkLocalStateAsync()
1376
1329
 
1377
- export function generateAuthInstance(
1378
- config: TInitAuthOptions,
1379
- options?: {
1380
- clientId: ICloudbaseConfig['clientId']
1381
- env: ICloudbaseConfig['env']
1382
- apiOrigin: string
1383
- cache?: ICloudbaseCache
1384
- platform?: ICloudbase['platform']
1385
- app?: ICloudbase
1386
- debug?: ICloudbaseAuthConfig['debug']
1387
- },
1388
- ) {
1389
- const { region = 'ap-shanghai', i18n, publishable_key } = config
1390
- const platform = options?.platform || (useDefaultAdapter() as ICloudbasePlatformInfo)
1391
- const { runtime, adapter } = platform
1330
+ if (options?.userInfo) {
1331
+ loginState.user.setLocalUserInfo(options.userInfo)
1332
+ } else {
1333
+ if (options?.asyncRefreshUser) {
1334
+ loginState.user.refresh(params)
1335
+ } else {
1336
+ await loginState.user.refresh(params)
1337
+ }
1338
+ }
1392
1339
 
1393
- const { env, clientId, debug, cache, app: cloudbase } = options || {}
1394
- let { apiOrigin } = options || {}
1395
- if (!apiOrigin) {
1396
- apiOrigin = `https://${env}.${region}.tcb-api.tencentcloudapi.com`
1340
+ this.config.eventBus?.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_IN })
1341
+
1342
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN })
1343
+ return loginState
1397
1344
  }
1398
1345
 
1399
- const commonOpts = {
1400
- env,
1346
+ async setAccessKey() {
1347
+ if (this.config.accessKey) {
1348
+ try {
1349
+ this.oauthInstance.oauth2client.setAccessKeyCredentials({
1350
+ access_token: this.config.accessKey,
1351
+ token_type: 'Bearer',
1352
+ scope: 'accessKey',
1353
+ expires_at: new Date(+new Date() + +new Date()),
1354
+ expires_in: +new Date() + +new Date(),
1355
+ })
1356
+ } catch (error) {
1357
+ console.warn('accessKey error: ', error)
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ // ========== new auth api methods merged below ==========
1363
+
1364
+ /**
1365
+ * https://supabase.com/docs/reference/javascript/auth-signinanonymously
1366
+ * Sign in a user anonymously.
1367
+ * const { data, error } = await auth.signInAnonymously();
1368
+ * @param params
1369
+ * @returns Promise<SignInRes>
1370
+ */
1371
+ async signInAnonymously(params: SignInAnonymouslyReq): Promise<SignInRes> {
1372
+ try {
1373
+ await this.oauthInstance.authApi.signInAnonymously(params)
1374
+ const loginState = await this.createLoginState()
1375
+
1376
+ const { data: { session } = {} } = await this.getSession()
1377
+
1378
+ // loginState返回是为了兼容v2版本
1379
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
1380
+ } catch (error) {
1381
+ return { data: {}, error: new AuthError(error) }
1382
+ }
1383
+ }
1384
+
1385
+ /**
1386
+ * https://supabase.com/docs/reference/javascript/auth-signup
1387
+ * Sign up a new user with email or phone using a one-time password (OTP). If the account not exist, a new account will be created.
1388
+ * @param params
1389
+ * @returns Promise<SignUpRes>
1390
+ */
1391
+ async signUp(params: authModels.SignUpRequest & { phone?: string }): Promise<SignUpRes> {
1392
+ if (params.phone_number || params.verification_code || params.verification_token || params.provider_token) {
1393
+ await this.oauthInstance.authApi.signUp(params)
1394
+ return this.createLoginState() as any
1395
+ }
1396
+ try {
1397
+ // 参数校验:email或phone必填其一
1398
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
1399
+
1400
+ // 第一步:发送验证码并存储 verificationInfo
1401
+ const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },)
1402
+
1403
+ return {
1404
+ data: {
1405
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1406
+ verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise<SignInRes> => {
1407
+ try {
1408
+ // 第三步:待用户输入完验证码之后,验证短信验证码
1409
+ const verificationTokenRes = await this.verify({
1410
+ verification_id: messageId || verificationInfo.verification_id,
1411
+ verification_code: token,
1412
+ })
1413
+
1414
+ // 第四步:注册并登录或直接登录
1415
+ // 如果用户已经存在,直接登录
1416
+ if (verificationInfo.is_user) {
1417
+ await this.signIn({
1418
+ username: params.email || this.formatPhone(params.phone),
1419
+ verification_token: verificationTokenRes.verification_token,
1420
+ })
1421
+ } else {
1422
+ // 如果用户不存在,注册用户
1423
+ const data = JSON.parse(JSON.stringify(params))
1424
+ delete data.email
1425
+ delete data.phone
1426
+
1427
+ await this.oauthInstance.authApi.signUp({
1428
+ ...data,
1429
+ ...(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) }),
1430
+ verification_token: verificationTokenRes.verification_token,
1431
+ verification_code: token,
1432
+ })
1433
+ await this.createLoginState()
1434
+ }
1435
+
1436
+ const { data: { session } = {} } = await this.getSession()
1437
+
1438
+ return { data: { user: session.user, session }, error: null }
1439
+ } catch (error) {
1440
+ return { data: {}, error: new AuthError(error) }
1441
+ }
1442
+ },
1443
+ },
1444
+ error: null,
1445
+ }
1446
+ } catch (error) {
1447
+ return { data: {}, error: new AuthError(error) }
1448
+ }
1449
+ }
1450
+
1451
+ /**
1452
+ * https://supabase.com/docs/reference/javascript/auth-signout
1453
+ * const result = await auth.signOut();
1454
+ *
1455
+ * @param params
1456
+ */
1457
+ async signOut(params?: authModels.SignoutRequest,): Promise<authModels.SignoutResponse & { data: Object; error: AuthError }> {
1458
+ try {
1459
+ const { userInfoKey } = this.cache.keys
1460
+ const res = await this.oauthInstance.authApi.signOut(params)
1461
+ await this.cache.removeStoreAsync(userInfoKey)
1462
+ this.setAccessKey()
1463
+
1464
+ this.config.eventBus?.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_OUT })
1465
+
1466
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_OUT })
1467
+
1468
+ // res返回是为了兼容v2版本
1469
+ return { ...res, data: {}, error: null }
1470
+ } catch (error) {
1471
+ return { data: {}, error: new AuthError(error) }
1472
+ }
1473
+ }
1474
+
1475
+ /**
1476
+ * https://supabase.com/docs/reference/javascript/auth-onauthstatechange
1477
+ * Receive a notification every time an auth event happens.
1478
+ * @param callback
1479
+ * @returns Promise<{ data: { subscription: Subscription }, error: Error | null }>
1480
+ */
1481
+ onAuthStateChange(callback: OnAuthStateChangeCallback) {
1482
+ if (!this.hasListenerSetUp) {
1483
+ this.setupListeners()
1484
+ this.hasListenerSetUp = true
1485
+ }
1486
+
1487
+ const id = Math.random().toString(36)
1488
+
1489
+ if (!this.listeners.has(id)) {
1490
+ this.listeners.set(id, new Set())
1491
+ }
1492
+
1493
+ this.listeners.get(id)!.add(callback)
1494
+
1495
+ // 返回 Subscription 对象
1496
+ const subscription = {
1497
+ id,
1498
+ callback,
1499
+ unsubscribe: () => {
1500
+ const callbacks = this.listeners.get(id)
1501
+ if (callbacks) {
1502
+ callbacks.delete(callback)
1503
+ if (callbacks.size === 0) {
1504
+ this.listeners.delete(id)
1505
+ }
1506
+ }
1507
+ },
1508
+ }
1509
+
1510
+ return {
1511
+ data: { subscription },
1512
+ }
1513
+ }
1514
+
1515
+ /**
1516
+ * https://supabase.com/docs/reference/javascript/auth-signinwithpassword
1517
+ * Log in an existing user with an email and password or phone and password or username and password.
1518
+ * @param params
1519
+ * @returns Promise<SignInRes>
1520
+ */
1521
+ async signInWithPassword(params: SignInWithPasswordReq): Promise<SignInRes> {
1522
+ try {
1523
+ // 参数校验:username/email/phone三选一,password必填
1524
+ this.validateAtLeastOne(
1525
+ params,
1526
+ [['username'], ['email'], ['phone']],
1527
+ 'You must provide either username, email, or phone',
1528
+ )
1529
+ this.validateParams(params, {
1530
+ password: { required: true, message: 'Password is required' },
1531
+ })
1532
+
1533
+ await this.signIn({
1534
+ username: params.username || params.email || this.formatPhone(params.phone),
1535
+ password: params.password,
1536
+ ...(params.is_encrypt ? { isEncrypt: true, version: 'v2' } : {}),
1537
+ })
1538
+ const { data: { session } = {} } = await this.getSession()
1539
+
1540
+ return { data: { user: session.user, session }, error: null }
1541
+ } catch (error) {
1542
+ return { data: {}, error: new AuthError(error) }
1543
+ }
1544
+ }
1545
+
1546
+ /**
1547
+ * https://supabase.com/docs/reference/javascript/auth-signinwithidtoken
1548
+ * 第三方平台登录。如果用户不存在,会根据云开发平台-登录方式中对应身份源的登录模式配置,判断是否自动注册
1549
+ * @param params
1550
+ * @returns Promise<SignInRes>
1551
+ */
1552
+ async signInWithIdToken(params: SignInWithIdTokenReq): Promise<SignInRes> {
1553
+ try {
1554
+ // 参数校验:token必填
1555
+ this.validateParams(params, {
1556
+ token: { required: true, message: 'Token is required' },
1557
+ })
1558
+
1559
+ await this.signInWithProvider({
1560
+ provider_token: params.token,
1561
+ })
1562
+ const { data: { session } = {} } = await this.getSession()
1563
+
1564
+ return { data: { user: session.user, session }, error: null }
1565
+ } catch (error) {
1566
+ return { data: {}, error: new AuthError(error) }
1567
+ }
1568
+ }
1569
+
1570
+ /**
1571
+ * https://supabase.com/docs/reference/javascript/auth-signinwithotp
1572
+ * Log in a user using a one-time password (OTP).
1573
+ * @param params
1574
+ * @returns Promise<SignInWithOtpRes>
1575
+ */
1576
+ async signInWithOtp(params: SignInWithOtpReq): Promise<SignInWithOtpRes> {
1577
+ try {
1578
+ // 参数校验:email或phone必填其一
1579
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
1580
+
1581
+ // 第一步:发送验证码并存储 verificationInfo
1582
+ const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },)
1583
+
1584
+ return {
1585
+ data: {
1586
+ user: null,
1587
+ session: null,
1588
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1589
+ verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise<SignInRes> => this.verifyOtp({
1590
+ email: params.email,
1591
+ phone: params.phone,
1592
+ token,
1593
+ messageId,
1594
+ }),
1595
+ },
1596
+ error: null,
1597
+ }
1598
+ } catch (error) {
1599
+ return { data: {}, error: new AuthError(error) }
1600
+ }
1601
+ }
1602
+
1603
+ /**
1604
+ * 校验第三方平台授权登录回调
1605
+ * @param params
1606
+ * @returns Promise<SignInRes>
1607
+ */
1608
+ async verifyOAuth(params?: VerifyOAuthReq): Promise<SignInRes | LinkIdentityRes> {
1609
+ const data: any = {}
1610
+ try {
1611
+ // 回调至 provider_redirect_uri 地址(url query中携带 授权code,state等参数),此时检查 state 是否符合预期(如 自己设置的 wx_open)
1612
+ const code = params?.code || utils.getQuery('code')
1613
+ const state = params?.state || utils.getQuery('state')
1614
+
1615
+ // 参数校验:code和state必填
1616
+ if (!code) {
1617
+ return { data: {}, error: new AuthError({ message: 'Code is required' }) }
1618
+ }
1619
+
1620
+ if (!state) {
1621
+ return { data: {}, error: new AuthError({ message: 'State is required' }) }
1622
+ }
1623
+
1624
+ const cacheData = getBrowserSession(state)
1625
+ data.type = cacheData?.type
1626
+
1627
+ const provider = params?.provider || cacheData?.provider || utils.getQuery('provider')
1628
+
1629
+ if (!provider) {
1630
+ return { data, error: new AuthError({ message: 'Provider is required' }) }
1631
+ }
1632
+
1633
+ // state符合预期,则获取该三方平台token
1634
+ const { provider_token: token } = await this.grantProviderToken({
1635
+ provider_id: provider,
1636
+ provider_redirect_uri: location.origin + location.pathname, // 指定三方平台跳回的 url 地址
1637
+ provider_code: code, // 第三方平台跳转回页面时,url param 中携带的 code 参数
1638
+ })
1639
+
1640
+ let res: SignInRes | LinkIdentityRes
1641
+
1642
+ if (cacheData.type === OAUTH_TYPE.BIND_IDENTITY) {
1643
+ res = await this.oauthInstance.authApi.toBindIdentity({ provider_token: token, provider, fireEvent: true })
1644
+ } else {
1645
+ // 通过 provider_token 仅登录或登录并注册(与云开发平台-登录方式-身份源登录模式配置有关)
1646
+ res = await this.signInWithIdToken({
1647
+ token,
1648
+ })
1649
+ res.data = { ...data, ...res.data }
1650
+ }
1651
+
1652
+ const localSearch = new URLSearchParams(location?.search)
1653
+ localSearch.delete('code')
1654
+ localSearch.delete('state')
1655
+ addUrlSearch(
1656
+ cacheData?.search === undefined ? `?${localSearch.toString()}` : cacheData?.search,
1657
+ cacheData?.hash || location.hash,
1658
+ )
1659
+ removeBrowserSession(state)
1660
+
1661
+ return res
1662
+ } catch (error) {
1663
+ return { data, error: new AuthError(error) }
1664
+ }
1665
+ }
1666
+
1667
+ /**
1668
+ * https://supabase.com/docs/reference/javascript/auth-signinwithoauth
1669
+ * 生成第三方平台授权 Uri (如微信二维码扫码授权网页)
1670
+ * @param params
1671
+ * @returns Promise<SignInOAuthRes>
1672
+ */
1673
+ async signInWithOAuth(params: SignInWithOAuthReq): Promise<SignInOAuthRes> {
1674
+ try {
1675
+ // 参数校验:provider必填
1676
+ this.validateParams(params, {
1677
+ provider: { required: true, message: 'Provider is required' },
1678
+ })
1679
+
1680
+ const href = params.options?.redirectTo || location.href
1681
+
1682
+ const urlObject = new URL(href)
1683
+
1684
+ const provider_redirect_uri = urlObject.origin + urlObject.pathname
1685
+
1686
+ const state = params.options?.state || `prd-${params.provider}-${Math.random().toString(36)
1687
+ .slice(2)}`
1688
+
1689
+ const { uri } = await this.genProviderRedirectUri({
1690
+ provider_id: params.provider,
1691
+ provider_redirect_uri,
1692
+ state,
1693
+ })
1694
+
1695
+ // 对 URL 进行解码
1696
+ const decodedUri = decodeURIComponent(uri)
1697
+
1698
+ // 合并额外的查询参数
1699
+ let finalUri = decodedUri
1700
+
1701
+ if (params.options?.queryParams) {
1702
+ const url = new URL(decodedUri)
1703
+ Object.entries(params.options.queryParams).forEach(([key, value]) => {
1704
+ url.searchParams.set(key, value)
1705
+ })
1706
+ finalUri = url.toString()
1707
+ }
1708
+
1709
+ saveToBrowserSession(state, {
1710
+ provider: params.provider,
1711
+ search: urlObject.search,
1712
+ hash: urlObject.hash,
1713
+ type: params.options?.type || OAUTH_TYPE.SIGN_IN,
1714
+ })
1715
+
1716
+ if (isBrowser() && !params.options?.skipBrowserRedirect) {
1717
+ window.location.assign(finalUri)
1718
+ }
1719
+
1720
+ return { data: { url: finalUri, provider: params.provider }, error: null }
1721
+ } catch (error) {
1722
+ return { data: {}, error: new AuthError(error) }
1723
+ }
1724
+ }
1725
+
1726
+ // https://supabase.com/docs/reference/javascript/auth-getclaims
1727
+ async getClaims(): Promise<GetClaimsRes> {
1728
+ try {
1729
+ const { accessToken } = await this.getAccessToken()
1730
+ const parsedToken = weAppJwtDecodeAll(accessToken)
1731
+ return { data: parsedToken, error: null }
1732
+ } catch (error) {
1733
+ return { data: {}, error: new AuthError(error) }
1734
+ }
1735
+ }
1736
+
1737
+ /**
1738
+ * https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail
1739
+ * 通过 email 或手机号重置密码
1740
+ * @param emailOrPhone 邮箱或手机号
1741
+ * @returns Promise<ResetPasswordForEmailRes>
1742
+ */
1743
+ async resetPasswordForEmail(
1744
+ emailOrPhone: string,
1745
+ options?: { redirectTo?: string },
1746
+ ): Promise<ResetPasswordForEmailRes> {
1747
+ try {
1748
+ // 参数校验:emailOrPhone必填
1749
+ this.validateParams(
1750
+ { emailOrPhone },
1751
+ {
1752
+ emailOrPhone: { required: true, message: 'Email or phone is required' },
1753
+ },
1754
+ )
1755
+
1756
+ const { redirectTo } = options || {}
1757
+
1758
+ // 判断是邮箱还是手机号
1759
+ const isEmail = emailOrPhone.includes('@')
1760
+ let verificationParams: { email?: string; phone_number?: string }
1761
+
1762
+ if (isEmail) {
1763
+ verificationParams = { email: emailOrPhone }
1764
+ } else {
1765
+ // 正规化手机号
1766
+ const formattedPhone = this.formatPhone(emailOrPhone)
1767
+ verificationParams = { phone_number: formattedPhone }
1768
+ }
1769
+
1770
+ // 第一步:发送验证码并存储 verificationInfo
1771
+ const verificationInfo = await this.getVerification(verificationParams)
1772
+
1773
+ return {
1774
+ data: {
1775
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1776
+ updateUser: async (attributes: UpdateUserAttributes): Promise<SignInRes> => {
1777
+ this.validateParams(attributes, {
1778
+ nonce: { required: true, message: 'Nonce is required' },
1779
+ password: { required: true, message: 'Password is required' },
1780
+ })
1781
+ try {
1782
+ // 第三步:待用户输入完验证码之后,验证验证码
1783
+ const verificationTokenRes = await this.verify({
1784
+ verification_id: verificationInfo.verification_id,
1785
+ verification_code: attributes.nonce,
1786
+ })
1787
+
1788
+ await this.oauthInstance.authApi.resetPassword({
1789
+ email: isEmail ? emailOrPhone : undefined,
1790
+ phone_number: !isEmail ? emailOrPhone : undefined,
1791
+ new_password: attributes.password,
1792
+ verification_token: verificationTokenRes.verification_token,
1793
+ })
1794
+
1795
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, {
1796
+ event: AUTH_STATE_CHANGED_TYPE.PASSWORD_RECOVERY,
1797
+ })
1798
+
1799
+ const res = await this.signInWithPassword({
1800
+ email: isEmail ? emailOrPhone : undefined,
1801
+ phone: !isEmail ? emailOrPhone : undefined,
1802
+ password: attributes.password,
1803
+ })
1804
+
1805
+ if (redirectTo && isBrowser()) {
1806
+ window.location.assign(redirectTo)
1807
+ }
1808
+
1809
+ return res
1810
+ } catch (error) {
1811
+ return { data: {}, error: new AuthError(error) }
1812
+ }
1813
+ },
1814
+ },
1815
+ error: null,
1816
+ }
1817
+ } catch (error) {
1818
+ return { data: {}, error: new AuthError(error) }
1819
+ }
1820
+ }
1821
+
1822
+ /**
1823
+ * 通过旧密码重置密码
1824
+ * @param new_password
1825
+ * @param old_password
1826
+ * @returns
1827
+ */
1828
+ async resetPasswordForOld(params: ResetPasswordForOldReq) {
1829
+ try {
1830
+ await this.oauthInstance.authApi.updatePasswordByOld({
1831
+ old_password: params.old_password,
1832
+ new_password: params.new_password,
1833
+ })
1834
+
1835
+ const { data: { session } = {} } = await this.getSession()
1836
+
1837
+ return { data: { user: session.user, session }, error: null }
1838
+ } catch (error) {
1839
+ return { data: {}, error: new AuthError(error) }
1840
+ }
1841
+ }
1842
+
1843
+ /**
1844
+ * https://supabase.com/docs/reference/javascript/auth-verifyotp
1845
+ * Log in a user given a User supplied OTP and verificationId received through mobile or email.
1846
+ * @param params
1847
+ * @returns Promise<SignInRes>
1848
+ */
1849
+ async verifyOtp(params: VerifyOtpReq): Promise<SignInRes> {
1850
+ try {
1851
+ const { type } = params
1852
+ // 参数校验:token和verificationInfo必填
1853
+ this.validateParams(params, {
1854
+ token: { required: true, message: 'Token is required' },
1855
+ messageId: { required: true, message: 'messageId is required' },
1856
+ })
1857
+
1858
+ if (['phone_change', 'email_change'].includes(type)) {
1859
+ await this.verify({
1860
+ verification_id: params.messageId,
1861
+ verification_code: params.token,
1862
+ })
1863
+ } else {
1864
+ await this.signInWithUsername({
1865
+ verificationInfo: { verification_id: params.messageId, is_user: true },
1866
+ verificationCode: params.token,
1867
+ username: params.email || this.formatPhone(params.phone) || '',
1868
+ loginType: params.email ? 'email' : 'phone',
1869
+ })
1870
+ }
1871
+
1872
+ const { data: { session } = {} } = await this.getSession()
1873
+
1874
+ return { data: { user: session.user, session }, error: null }
1875
+ } catch (error) {
1876
+ return { data: {}, error: new AuthError(error) }
1877
+ }
1878
+ }
1879
+
1880
+ /**
1881
+ * https://supabase.com/docs/reference/javascript/auth-getSession
1882
+ * Returns the session, refreshing it if necessary.
1883
+ * @returns Promise<SignInRes>
1884
+ */
1885
+ async getSession(): Promise<SignInRes> {
1886
+ try {
1887
+ const credentials: Credentials = await this.oauthInstance.oauth2client.getCredentials()
1888
+
1889
+ if (!credentials || credentials.scope === 'accessKey') {
1890
+ return { data: { session: null }, error: null }
1891
+ }
1892
+
1893
+ const { data: { user } = {} } = await this.getUser()
1894
+
1895
+ return { data: { session: { ...credentials, user }, user }, error: null }
1896
+ } catch (error) {
1897
+ return { data: {}, error: new AuthError(error) }
1898
+ }
1899
+ }
1900
+
1901
+ /**
1902
+ * https://supabase.com/docs/reference/javascript/auth-refreshsession
1903
+ * 无论过期状态如何,都返回一个新的会话
1904
+ * @param refresh_token
1905
+ * @returns Promise<SignInRes>
1906
+ */
1907
+ async refreshSession(refresh_token?: string): Promise<SignInRes> {
1908
+ try {
1909
+ const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials()
1910
+ credentials.refresh_token = refresh_token || credentials.refresh_token
1911
+ const newTokens = await this.oauthInstance.oauth2client.refreshToken(credentials)
1912
+ const { data: { user } = {} } = await this.getUser()
1913
+
1914
+ return { data: { user, session: { ...newTokens, user } }, error: null }
1915
+ } catch (error) {
1916
+ return { data: {}, error: new AuthError(error) }
1917
+ }
1918
+ }
1919
+
1920
+ /**
1921
+ * https://supabase.com/docs/reference/javascript/auth-getuser
1922
+ * 如果存在现有会话,则获取当前用户详细信息
1923
+ * @returns Promise<GetUserRes>
1924
+ */
1925
+ async getUser(): Promise<GetUserRes> {
1926
+ try {
1927
+ const user = this.convertToUser(await this.getUserInfo())
1928
+ return { data: { user }, error: null }
1929
+ } catch (error) {
1930
+ return { data: {}, error: new AuthError(error) }
1931
+ }
1932
+ }
1933
+
1934
+ /**
1935
+ * 刷新用户信息
1936
+ * @returns Promise<CommonRes>
1937
+ */
1938
+ async refreshUser(): Promise<CommonRes> {
1939
+ try {
1940
+ await this.currentUser.refresh()
1941
+
1942
+ const { data: { session } = {} } = await this.getSession()
1943
+ return { data: { user: session.user, session }, error: null }
1944
+ } catch (error) {
1945
+ return { data: {}, error: new AuthError(error) }
1946
+ }
1947
+ }
1948
+
1949
+ /**
1950
+ * https://supabase.com/docs/reference/javascript/auth-updateuser
1951
+ * 更新用户信息
1952
+ * @param params
1953
+ * @returns Promise<GetUserRes | UpdateUserWithVerificationRes>
1954
+ */
1955
+ async updateUser(params: UpdateUserReq): Promise<GetUserRes | UpdateUserWithVerificationRes> {
1956
+ try {
1957
+ // 参数校验:至少有一个更新字段被提供
1958
+ const hasValue = Object.keys(params).some(key => params[key] !== undefined && params[key] !== null && params[key] !== '',)
1959
+ if (!hasValue) {
1960
+ throw new AuthError({ message: 'At least one field must be provided for update' })
1961
+ }
1962
+
1963
+ const { email, phone, ...restParams } = params
1964
+
1965
+ // 检查是否需要更新 email 或 phone
1966
+ const needsEmailVerification = email !== undefined
1967
+ const needsPhoneVerification = phone !== undefined
1968
+
1969
+ let extraRes = {}
1970
+
1971
+ if (needsEmailVerification || needsPhoneVerification) {
1972
+ // 需要发送验证码
1973
+ let verificationParams: { email?: string; phone_number?: string }
1974
+ let verificationType: 'email_change' | 'phone_change'
1975
+
1976
+ if (needsEmailVerification) {
1977
+ verificationParams = { email: params.email }
1978
+ verificationType = 'email_change'
1979
+ } else {
1980
+ // 正规化手机号
1981
+ const formattedPhone = this.formatPhone(params.phone)
1982
+ verificationParams = { phone_number: formattedPhone }
1983
+ verificationType = 'phone_change'
1984
+ }
1985
+
1986
+ // 发送验证码
1987
+ const verificationInfo = await this.getVerification(verificationParams)
1988
+
1989
+ Object.keys(restParams).length > 0 && (await this.updateUserBasicInfo(restParams))
1990
+
1991
+ extraRes = {
1992
+ messageId: verificationInfo.verification_id,
1993
+ verifyOtp: async (verifyParams: { email?: string; phone?: string; token: string }): Promise<GetUserRes> => {
1994
+ try {
1995
+ if (verifyParams.email && params.email === verifyParams.email) {
1996
+ // 验证码验证
1997
+ await this.verifyOtp({
1998
+ type: 'email_change',
1999
+ email: params.email,
2000
+ token: verifyParams.token,
2001
+ messageId: verificationInfo.verification_id,
2002
+ })
2003
+ await this.updateUserBasicInfo({ email: params.email })
2004
+ } else if (verifyParams.phone && params.phone === verifyParams.phone) {
2005
+ // 验证码验证
2006
+ await this.verifyOtp({
2007
+ type: 'phone_change',
2008
+ phone: params.phone,
2009
+ token: verifyParams.token,
2010
+ messageId: verificationInfo.verification_id,
2011
+ })
2012
+ await this.updateUserBasicInfo({ phone: this.formatPhone(params.phone) })
2013
+ } else {
2014
+ await this.verifyOtp({
2015
+ type: verificationType,
2016
+ email: needsEmailVerification ? params.email : undefined,
2017
+ phone: !needsEmailVerification ? params.phone : undefined,
2018
+ token: verifyParams.token,
2019
+ messageId: verificationInfo.verification_id,
2020
+ })
2021
+ // 验证成功后更新用户信息
2022
+ await this.updateUserBasicInfo(params)
2023
+ }
2024
+
2025
+ const {
2026
+ data: { user },
2027
+ } = await this.getUser()
2028
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED })
2029
+
2030
+ return { data: { user }, error: null }
2031
+ } catch (error) {
2032
+ return { data: {}, error: new AuthError(error) }
2033
+ }
2034
+ },
2035
+ }
2036
+ } else {
2037
+ // 不需要验证,直接更新
2038
+ await this.updateUserBasicInfo(params)
2039
+ }
2040
+ const {
2041
+ data: { user },
2042
+ } = await this.getUser()
2043
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED })
2044
+
2045
+ return { data: { user, ...extraRes }, error: null }
2046
+ } catch (error) {
2047
+ return { data: {}, error: new AuthError(error) }
2048
+ }
2049
+ }
2050
+
2051
+ /**
2052
+ * https://supabase.com/docs/reference/javascript/auth-getuseridentities
2053
+ * 获取所有身份源
2054
+ * @returns Promise<GetUserIdentitiesRes>
2055
+ */
2056
+ async getUserIdentities(): Promise<GetUserIdentitiesRes> {
2057
+ try {
2058
+ const providers = await this.oauthInstance.authApi.getProviders()
2059
+
2060
+ return { data: { identities: providers?.data?.filter(v => !!v.bind) as unknown as GetUserIdentitiesRes['data']['identities'] }, error: null }
2061
+ } catch (error) {
2062
+ return { data: {}, error: new AuthError(error) }
2063
+ }
2064
+ }
2065
+
2066
+ /**
2067
+ * https://supabase.com/docs/reference/javascript/auth-linkidentity
2068
+ * 绑定身份源到当前用户
2069
+ * @param params
2070
+ * @returns Promise<LinkIdentityRes>
2071
+ */
2072
+ async linkIdentity(params: LinkIdentityReq): Promise<LinkIdentityRes> {
2073
+ try {
2074
+ // 参数校验:provider必填
2075
+ this.validateParams(params, {
2076
+ provider: { required: true, message: 'Provider is required' },
2077
+ })
2078
+
2079
+ await this.signInWithOAuth({
2080
+ provider: params.provider,
2081
+ options: {
2082
+ type: OAUTH_TYPE.BIND_IDENTITY,
2083
+ },
2084
+ })
2085
+
2086
+ return { data: { provider: params.provider }, error: null }
2087
+ } catch (error) {
2088
+ return { data: {}, error: new AuthError(error) }
2089
+ }
2090
+ }
2091
+
2092
+ /**
2093
+ * https://supabase.com/docs/reference/javascript/auth-unlinkidentity
2094
+ * 解绑身份源
2095
+ * @param params
2096
+ * @returns Promise<CommonRes>
2097
+ */
2098
+ async unlinkIdentity(params: UnlinkIdentityReq): Promise<CommonRes> {
2099
+ try {
2100
+ // 参数校验:provider必填
2101
+ this.validateParams(params, {
2102
+ provider: { required: true, message: 'Provider is required' },
2103
+ })
2104
+
2105
+ await this.oauthInstance.authApi.unbindProvider({ provider_id: params.provider })
2106
+
2107
+ return { data: {}, error: null }
2108
+ } catch (error) {
2109
+ return { data: {}, error: new AuthError(error) }
2110
+ }
2111
+ }
2112
+
2113
+ /**
2114
+ * https://supabase.com/docs/reference/javascript/auth-reauthentication
2115
+ * 重新认证
2116
+ * @returns Promise<ReauthenticateRes>
2117
+ */
2118
+ async reauthenticate(): Promise<ReauthenticateRes> {
2119
+ try {
2120
+ const {
2121
+ data: { user },
2122
+ } = await this.getUser()
2123
+
2124
+ this.validateAtLeastOne(user, [['email', 'phone']], 'You must provide either an email or phone number')
2125
+ const userInfo = user.email ? { email: user.email } : { phone_number: this.formatPhone(user.phone) }
2126
+
2127
+ // 第一步:发送验证码并存储 verificationInfo
2128
+ const verificationInfo = await this.getVerification(userInfo)
2129
+
2130
+ return {
2131
+ data: {
2132
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
2133
+ updateUser: async (attributes: UpdateUserAttributes): Promise<SignInRes> => {
2134
+ this.validateParams(attributes, {
2135
+ nonce: { required: true, message: 'Nonce is required' },
2136
+ })
2137
+ try {
2138
+ if (attributes.password) {
2139
+ // 第三步:待用户输入完验证码之后,验证验证码
2140
+ const verificationTokenRes = await this.verify({
2141
+ verification_id: verificationInfo.verification_id,
2142
+ verification_code: attributes.nonce,
2143
+ })
2144
+
2145
+ // 第四步:获取 sudo_token
2146
+ const sudoRes = await this.oauthInstance.authApi.sudo({
2147
+ verification_token: verificationTokenRes.verification_token,
2148
+ })
2149
+
2150
+ await this.oauthInstance.authApi.setPassword({
2151
+ new_password: attributes.password,
2152
+ sudo_token: sudoRes.sudo_token,
2153
+ })
2154
+ } else {
2155
+ await this.signInWithUsername({
2156
+ verificationInfo,
2157
+ verificationCode: attributes.nonce,
2158
+ ...userInfo,
2159
+ loginType: userInfo.email ? 'email' : 'phone',
2160
+ })
2161
+ }
2162
+
2163
+ const { data: { session } = {} } = await this.getSession()
2164
+
2165
+ return { data: { user: session.user, session }, error: null }
2166
+ } catch (error) {
2167
+ return { data: {}, error: new AuthError(error) }
2168
+ }
2169
+ },
2170
+ },
2171
+ error: null,
2172
+ }
2173
+ } catch (error) {
2174
+ return { data: {}, error: new AuthError(error) }
2175
+ }
2176
+ }
2177
+
2178
+ /**
2179
+ * https://supabase.com/docs/reference/javascript/auth-resend
2180
+ * 重新发送验证码
2181
+ * @param params
2182
+ * @returns Promise<ResendRes>
2183
+ */
2184
+ async resend(params: ResendReq): Promise<ResendRes> {
2185
+ try {
2186
+ // 参数校验:email或phone必填其一
2187
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
2188
+
2189
+ const target = params.type === 'signup' ? 'ANY' : 'USER'
2190
+ const data: { email?: string; phone_number?: string; target: 'USER' | 'ANY' } = { target }
2191
+ if ('email' in params) {
2192
+ data.email = params.email
2193
+ }
2194
+
2195
+ if ('phone' in params) {
2196
+ data.phone_number = this.formatPhone(params.phone)
2197
+ }
2198
+
2199
+ // 重新发送验证码
2200
+ const { verification_id: verificationId } = await this.oauthInstance.authApi.getVerification(data)
2201
+
2202
+ return {
2203
+ data: { messageId: verificationId },
2204
+ error: null,
2205
+ }
2206
+ } catch (error: any) {
2207
+ return {
2208
+ data: {},
2209
+ error: new AuthError(error),
2210
+ }
2211
+ }
2212
+ }
2213
+
2214
+ /**
2215
+ * https://supabase.com/docs/reference/javascript/auth-setsession
2216
+ * 使用access_token和refresh_token来设置会话
2217
+ * @param params
2218
+ * @returns Promise<SignInRes>
2219
+ */
2220
+ async setSession(params: SetSessionReq): Promise<SignInRes> {
2221
+ try {
2222
+ this.validateParams(params, {
2223
+ access_token: { required: true, message: 'Access token is required' },
2224
+ refresh_token: { required: true, message: 'Refresh token is required' },
2225
+ })
2226
+
2227
+ await this.oauthInstance.oauth2client.refreshToken(params, { throwError: true })
2228
+
2229
+ const { data: { session } = {} } = await this.getSession()
2230
+
2231
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN })
2232
+
2233
+ return { data: { user: session.user, session }, error: null }
2234
+ } catch (error) {
2235
+ return { data: {}, error: new AuthError(error) }
2236
+ }
2237
+ }
2238
+
2239
+ // https://supabase.com/docs/reference/javascript/auth-exchangecodeforsession
2240
+ async exchangeCodeForSession() {
2241
+ //
2242
+ }
2243
+
2244
+ /**
2245
+ * 删除当前用户
2246
+ * @param params
2247
+ * @returns
2248
+ */
2249
+ async deleteUser(params: DeleteMeReq): Promise<CommonRes> {
2250
+ try {
2251
+ this.validateParams(params, {
2252
+ password: { required: true, message: 'Password is required' },
2253
+ })
2254
+
2255
+ const { sudo_token } = await this.oauthInstance.authApi.sudo(params)
2256
+
2257
+ await this.oauthInstance.authApi.deleteMe({ sudo_token })
2258
+
2259
+ return { data: {}, error: null }
2260
+ } catch (error) {
2261
+ return { data: {}, error: new AuthError(error) }
2262
+ }
2263
+ }
2264
+
2265
+ /**
2266
+ * 跳转系统默认登录页
2267
+ * @returns {Promise<authModels.ToDefaultLoginPage>}
2268
+ * @memberof Auth
2269
+ */
2270
+ async toDefaultLoginPage(params: authModels.ToDefaultLoginPage = {}): Promise<CommonRes> {
2271
+ try {
2272
+ const configVersion = params.config_version || 'env'
2273
+ const query = Object.keys(params.query || {})
2274
+ .map(key => `${key}=${params.query[key]}`)
2275
+ .join('&')
2276
+
2277
+ if (adapterForWxMp.isMatch()) {
2278
+ wx.navigateTo({ url: `/packages/$wd_system/pages/login/index${query ? `?${query}` : ''}` })
2279
+ } else {
2280
+ const redirectUri = params.redirect_uri || window.location.href
2281
+ const urlObj = new URL(redirectUri)
2282
+ const loginPage = `${urlObj.origin}/__auth/?app_id=${params.app_id || ''}&env_id=${this.config.env}&client_id=${
2283
+ this.config.clientId || this.config.env
2284
+ }&config_version=${configVersion}&redirect_uri=${encodeURIComponent(redirectUri)}${query ? `&${query}` : ''}`
2285
+ window.location.href = loginPage
2286
+ }
2287
+ return { data: {}, error: null }
2288
+ } catch (error) {
2289
+ return { data: {}, error: new AuthError(error) }
2290
+ }
2291
+ }
2292
+
2293
+ /**
2294
+ * 自定义登录
2295
+ * @param getTickFn () => Promise<string>, 获取自定义登录 ticket 的函数
2296
+ * @returns
2297
+ */
2298
+ async signInWithCustomTicket(getTickFn?: authModels.GetCustomSignTicketFn): Promise<SignInRes> {
2299
+ if (getTickFn) {
2300
+ this.setCustomSignFunc(getTickFn)
2301
+ }
2302
+
2303
+ try {
2304
+ await this.oauthInstance.authApi.signInWithCustomTicket()
2305
+ const loginState = await this.createLoginState()
2306
+
2307
+ const { data: { session } = {} } = await this.getSession()
2308
+
2309
+ // loginState返回是为了兼容v2版本
2310
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2311
+ } catch (error) {
2312
+ return { data: {}, error: new AuthError(error) }
2313
+ }
2314
+ }
2315
+
2316
+ /**
2317
+ * 小程序openId静默登录
2318
+ * @param params
2319
+ * @returns Promise<SignInRes>
2320
+ */
2321
+ async signInWithOpenId({ useWxCloud = true } = {}): Promise<SignInRes> {
2322
+ if (!adapterForWxMp.isMatch()) {
2323
+ throw Error('wx api undefined')
2324
+ }
2325
+ const wxInfo = wx.getAccountInfoSync().miniProgram
2326
+
2327
+ const mainFunc = async (code) => {
2328
+ let result: authModels.GrantProviderTokenResponse | undefined = undefined
2329
+ let credentials: Credentials | undefined = undefined
2330
+
2331
+ try {
2332
+ result = await this.oauthInstance.authApi.grantProviderToken(
2333
+ {
2334
+ provider_id: wxInfo?.appId,
2335
+ provider_code: code,
2336
+ provider_params: {
2337
+ provider_code_type: 'open_id',
2338
+ appid: wxInfo?.appId,
2339
+ },
2340
+ },
2341
+ useWxCloud,
2342
+ )
2343
+
2344
+ if ((result as any)?.error_code || !result.provider_token) {
2345
+ throw result
2346
+ }
2347
+
2348
+ credentials = await this.oauthInstance.authApi.signInWithProvider(
2349
+ { provider_token: result.provider_token },
2350
+ useWxCloud,
2351
+ )
2352
+
2353
+ if ((credentials as any)?.error_code) {
2354
+ throw credentials
2355
+ }
2356
+ } catch (error) {
2357
+ throw error
2358
+ }
2359
+ await this.oauthInstance.oauth2client.setCredentials(credentials as Credentials)
2360
+ }
2361
+
2362
+ try {
2363
+ await new Promise((resolve, reject) => {
2364
+ wx.login({
2365
+ success: async (res: { code: string }) => {
2366
+ try {
2367
+ await mainFunc(res.code)
2368
+ resolve(true)
2369
+ } catch (error) {
2370
+ reject(error)
2371
+ }
2372
+ },
2373
+ fail: (res: any) => {
2374
+ const error = new Error(res?.errMsg)
2375
+ ;(error as any).code = res?.errno
2376
+ reject(error)
2377
+ },
2378
+ })
2379
+ })
2380
+
2381
+ const loginState = await this.createLoginState()
2382
+
2383
+ const { data: { session } = {} } = await this.getSession()
2384
+
2385
+ // loginState返回是为了兼容v2版本
2386
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2387
+ } catch (error) {
2388
+ return { data: {}, error: new AuthError(error) }
2389
+ }
2390
+ }
2391
+
2392
+ /**
2393
+ * 小程序手机号授权登录,目前只支持全托管手机号授权登录
2394
+ * @param params
2395
+ * @returns Promise<SignInRes>
2396
+ */
2397
+ async signInWithPhoneAuth({ phoneCode = '' }): Promise<SignInRes> {
2398
+ if (!adapterForWxMp.isMatch()) {
2399
+ return { data: {}, error: new AuthError({ message: 'wx api undefined' }) }
2400
+ }
2401
+ const wxInfo = wx.getAccountInfoSync().miniProgram
2402
+ const providerInfo = {
2403
+ provider_params: { provider_code_type: 'phone' },
2404
+ provider_id: wxInfo.appId,
2405
+ }
2406
+
2407
+ const { code } = await wx.login()
2408
+ ;(providerInfo as any).provider_code = code
2409
+
2410
+ try {
2411
+ let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo)
2412
+ if (providerToken.error_code) {
2413
+ throw providerToken
2414
+ }
2415
+
2416
+ providerToken = await this.oauthInstance.authApi.patchProviderToken({
2417
+ provider_token: providerToken.provider_token,
2418
+ provider_id: wxInfo.appId,
2419
+ provider_params: {
2420
+ code: phoneCode,
2421
+ provider_code_type: 'phone',
2422
+ },
2423
+ })
2424
+ if (providerToken.error_code) {
2425
+ throw providerToken
2426
+ }
2427
+
2428
+ const signInRes = await this.oauthInstance.authApi.signInWithProvider({
2429
+ provider_token: providerToken.provider_token,
2430
+ })
2431
+
2432
+ if ((signInRes as any)?.error_code) {
2433
+ throw signInRes
2434
+ }
2435
+ } catch (error) {
2436
+ return { data: {}, error: new AuthError(error) }
2437
+ }
2438
+
2439
+ const loginState = await this.createLoginState()
2440
+
2441
+ const { data: { session } = {} } = await this.getSession()
2442
+
2443
+ // loginState返回是为了兼容v2版本
2444
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2445
+ }
2446
+
2447
+ private formatPhone(phone: string) {
2448
+ if (!/\s+/.test(phone) && /^\+\d{1,3}\d+/.test(phone)) {
2449
+ return phone.replace(/^(\+\d{1,2})(\d+)$/, '$1 $2')
2450
+ }
2451
+ return /^\+\d{1,3}\s+/.test(phone) ? phone : `+86 ${phone}`
2452
+ }
2453
+
2454
+ private notifyListeners(event, session, info): OnAuthStateChangeCallback {
2455
+ this.listeners.forEach((callbacks) => {
2456
+ callbacks.forEach((callback) => {
2457
+ try {
2458
+ callback(event, session, info)
2459
+ } catch (error) {
2460
+ console.error('Error in auth state change callback:', error)
2461
+ }
2462
+ })
2463
+ })
2464
+ return
2465
+ }
2466
+
2467
+ private setupListeners() {
2468
+ this.config.eventBus?.on(EVENTS.AUTH_STATE_CHANGED, async (params) => {
2469
+ const event = params?.data?.event
2470
+ const info = params?.data?.info
2471
+ const {
2472
+ data: { session },
2473
+ } = await this.getSession()
2474
+
2475
+ this.notifyListeners(event, session, info)
2476
+ })
2477
+ }
2478
+
2479
+ private convertToUser(userInfo: authModels.UserInfo & Partial<User>) {
2480
+ if (!userInfo) return null
2481
+
2482
+ // 优先使用 userInfo 中的数据(V3 API)
2483
+ const email = userInfo?.email || ''
2484
+ const phone = userInfo?.phone_number || ''
2485
+ const userId = userInfo?.sub || userInfo?.uid || ''
2486
+
2487
+ return {
2488
+ id: userId,
2489
+ aud: 'authenticated',
2490
+ role: userInfo.groups?.map?.(group => (typeof group === 'string' ? group : group.id)),
2491
+ email: email || '',
2492
+ email_confirmed_at: userInfo?.email_verified ? userInfo.created_at : userInfo.created_at,
2493
+ phone,
2494
+ phone_confirmed_at: phone ? userInfo.created_at : undefined,
2495
+ confirmed_at: userInfo.created_at,
2496
+ last_sign_in_at: (userInfo as any).last_sign_in_at,
2497
+ app_metadata: {
2498
+ provider: userInfo.loginType?.toLowerCase() || 'cloudbase',
2499
+ providers: [userInfo.loginType?.toLowerCase() || 'cloudbase'],
2500
+ },
2501
+ user_metadata: {
2502
+ // V3 API 用户信息
2503
+ name: userInfo?.name,
2504
+ picture: userInfo?.picture,
2505
+ username: userInfo?.username, // 用户名称,长度 5-24 位,支持字符中英文、数字、特殊字符(仅支持_-),不支持中文
2506
+ gender: userInfo?.gender,
2507
+ locale: userInfo?.locale,
2508
+ // V2 API 兼容(使用 any 避免类型错误)
2509
+ uid: userInfo.uid,
2510
+ nickName: userInfo.nickName,
2511
+ avatarUrl: userInfo.avatarUrl || userInfo.picture,
2512
+ location: userInfo.location,
2513
+ hasPassword: userInfo.hasPassword,
2514
+ },
2515
+ identities:
2516
+ userInfo?.providers?.map(p => ({
2517
+ id: p.id || '',
2518
+ identity_id: p.id || '',
2519
+ user_id: userId,
2520
+ identity_data: {
2521
+ provider_id: p.id,
2522
+ provider_user_id: p.provider_user_id,
2523
+ name: p.name,
2524
+ },
2525
+ provider: p.id || 'cloudbase',
2526
+ created_at: userInfo.created_at,
2527
+ updated_at: userInfo.updated_at,
2528
+ last_sign_in_at: (userInfo as any).last_sign_in_at,
2529
+ })) || [],
2530
+ created_at: userInfo.created_at,
2531
+ updated_at: userInfo.updated_at,
2532
+ is_anonymous: userInfo.name === 'anonymous',
2533
+ }
2534
+ }
2535
+
2536
+ /**
2537
+ * 参数校验辅助方法
2538
+ */
2539
+ private validateParams(params: any, rules: { [key: string]: { required?: boolean; message: string } }): void {
2540
+ for (const [key, rule] of Object.entries(rules)) {
2541
+ if (rule.required && (params?.[key] === undefined || params?.[key] === null || params?.[key] === '')) {
2542
+ throw new AuthError({ message: rule.message })
2543
+ }
2544
+ }
2545
+ }
2546
+
2547
+ /**
2548
+ * 校验必填参数组(至少有一个参数必须有值)
2549
+ */
2550
+ private validateAtLeastOne(params: any, fieldGroups: string[][], message: string): void {
2551
+ const hasValue = fieldGroups.some(group => group.some(field => params?.[field] !== undefined && params?.[field] !== null && params?.[field] !== ''),)
2552
+
2553
+ if (!hasValue) {
2554
+ throw new AuthError({ message })
2555
+ }
2556
+ }
2557
+
2558
+ private async init(): Promise<{ error: Error | null }> {
2559
+ try {
2560
+ const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials()
2561
+ if (credentials) {
2562
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.INITIAL_SESSION })
2563
+ }
2564
+ } catch (error) {
2565
+ // Ignore errors when checking for existing credentials
2566
+ }
2567
+
2568
+ return { error: null }
2569
+ }
2570
+ }
2571
+
2572
+ type TInitAuthOptions = Pick<ICloudbaseAuthConfig, 'region' | 'persistence' | 'i18n' | 'accessKey' | 'useWxCloud'> &
2573
+ Partial<AuthOptions> & {
2574
+ detectSessionInUrl?: boolean
2575
+ }
2576
+
2577
+ export function generateAuthInstance(
2578
+ config: TInitAuthOptions & { sdkVersion?: string },
2579
+ options?: {
2580
+ clientId: ICloudbaseConfig['clientId']
2581
+ env: ICloudbaseConfig['env']
2582
+ apiOrigin: string
2583
+ cache?: ICloudbaseCache
2584
+ platform?: ICloudbase['platform']
2585
+ app?: ICloudbase
2586
+ debug?: ICloudbaseAuthConfig['debug']
2587
+ detectSessionInUrl?: ICloudbaseAuthConfig['detectSessionInUrl']
2588
+ },
2589
+ ) {
2590
+ const { region = 'ap-shanghai', i18n, accessKey, useWxCloud } = config
2591
+ const platform = options?.platform || (useDefaultAdapter.bind(options)() as ICloudbasePlatformInfo)
2592
+ const { runtime, adapter } = platform
2593
+
2594
+ const { env, clientId, debug, cache, app: cloudbase } = options || {}
2595
+ let { apiOrigin } = options || {}
2596
+ if (!apiOrigin) {
2597
+ apiOrigin = `https://${env}.${region}.tcb-api.tencentcloudapi.com`
2598
+ }
2599
+
2600
+ const commonOpts = {
2601
+ env,
1401
2602
  clientId,
1402
2603
  i18n,
1403
- publishable_key,
2604
+ accessKey,
2605
+ useWxCloud,
2606
+ eventBus: new CloudbaseEventEmitter(),
1404
2607
  }
1405
2608
 
1406
2609
  const oauthInstance = new CloudbaseOAuth(useAuthAdapter({
@@ -1415,8 +2618,10 @@ export function generateAuthInstance(
1415
2618
  captchaOptions: config?.captchaOptions,
1416
2619
  wxCloud: config?.wxCloud,
1417
2620
  adapter,
1418
- onCredentialsError,
1419
- headers: config.headers || {},
2621
+ onCredentialsError: onCredentialsError(commonOpts.eventBus),
2622
+ headers: { 'X-SDK-Version': `@cloudbase/js-sdk/${config.sdkVersion}`, ...(config.headers || {}) },
2623
+ detectSessionInUrl: config.detectSessionInUrl,
2624
+ debug,
1420
2625
  }),)
1421
2626
 
1422
2627
  const authInstance = new Auth({
@@ -1425,8 +2630,7 @@ export function generateAuthInstance(
1425
2630
  persistence: config.persistence,
1426
2631
  debug,
1427
2632
  cache:
1428
- cache
1429
- || new CloudbaseCache({
2633
+ cache || new CloudbaseCache({
1430
2634
  persistence: config.persistence,
1431
2635
  keys: { userInfoKey: `user_info_${env}` },
1432
2636
  platformInfo: platform,
@@ -1436,53 +2640,92 @@ export function generateAuthInstance(
1436
2640
  oauthInstance,
1437
2641
  })
1438
2642
 
2643
+ // Initialize session with user info callback
2644
+ // This handles OAuth callback URL detection and creates login state atomically
2645
+ oauthInstance.initializeSession(async (data, error) => {
2646
+ if (!data) return
2647
+
2648
+ if (data.type === OAUTH_TYPE.SIGN_IN) {
2649
+ if (error) {
2650
+ commonOpts.eventBus.fire(EVENTS.AUTH_STATE_CHANGED, {
2651
+ event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN,
2652
+ info: { ...data, error },
2653
+ })
2654
+ } else if (data.user) {
2655
+ // 使用已获取的 user 信息创建 LoginState,复用 createLoginState 逻辑
2656
+ // 但跳过 refresh() 避免再次请求 API 导致死锁
2657
+ authInstance.createLoginState({}, { userInfo: data.user })
2658
+ }
2659
+ } else if (data.type === OAUTH_TYPE.BIND_IDENTITY) {
2660
+ commonOpts.eventBus.fire(EVENTS.AUTH_STATE_CHANGED, {
2661
+ event: AUTH_STATE_CHANGED_TYPE.BIND_IDENTITY,
2662
+ info: { ...data, error },
2663
+ })
2664
+ }
2665
+ })
2666
+
1439
2667
  return { authInstance, oauthInstance }
1440
2668
  }
1441
2669
 
2670
+ const NAMESPACE = 'auth'
2671
+
1442
2672
  const component: ICloudbaseComponent = {
1443
2673
  name: COMPONENT_NAME,
1444
- namespace: 'auth',
1445
- entity(config: TInitAuthOptions = {
1446
- region: '',
1447
- persistence: 'local',
1448
- apiPath: AUTH_API_PREFIX,
1449
- },) {
1450
- if (this.authInstance) {
1451
- printWarn(ERRORS.INVALID_OPERATION, 'every cloudbase instance should has only one auth object')
1452
- return this.authInstance
1453
- }
1454
- const { adapter } = this.platform
1455
- // 如不明确指定persistence则优先取各平台adapter首选,其次localStorage
1456
- const newPersistence = config.persistence || adapter.primaryStorage
1457
- if (newPersistence && newPersistence !== this.config.persistence) {
1458
- this.updateConfig({ persistence: newPersistence })
1459
- }
1460
-
1461
- const { authInstance, oauthInstance } = generateAuthInstance(
1462
- {
1463
- wxCloud: this.config.wxCloud,
1464
- storage: this.config.storage,
1465
- ...config,
1466
- persistence: this.config.persistence,
1467
- i18n: this.config.i18n,
1468
- publishable_key: this.config.publishable_key,
1469
- },
1470
- {
1471
- env: this.config.env,
1472
- clientId: this.config.clientId,
1473
- apiOrigin: this.request.getBaseEndPoint(),
1474
- platform: this.platform,
1475
- cache: this.cache,
1476
- app: this,
1477
- debug: this.config.debug,
1478
- },
1479
- )
2674
+ namespace: NAMESPACE,
2675
+ entity(config?: TInitAuthOptions) {
2676
+ const auth = function (config?: TInitAuthOptions) {
2677
+ if (this.authInstance && !config) {
2678
+ // printWarn(ERRORS.INVALID_OPERATION, 'every cloudbase instance should has only one auth object')
2679
+ return this.authInstance
2680
+ }
1480
2681
 
1481
- this.oauthInstance = oauthInstance
2682
+ config = config || {
2683
+ region: '',
2684
+ persistence: 'local',
2685
+ apiPath: AUTH_API_PREFIX,
2686
+ }
2687
+ const { adapter } = this.platform
2688
+ // 如不明确指定persistence则优先取各平台adapter首选,其次localStorage
2689
+ const newPersistence = config.persistence || adapter.primaryStorage
2690
+ if (newPersistence && newPersistence !== this.config.persistence) {
2691
+ this.updateConfig({ persistence: newPersistence })
2692
+ }
1482
2693
 
1483
- this.authInstance = authInstance
2694
+ const { authInstance, oauthInstance } = generateAuthInstance(
2695
+ {
2696
+ wxCloud: this.config.wxCloud,
2697
+ storage: this.config.storage,
2698
+ ...config,
2699
+ persistence: this.config.persistence,
2700
+ i18n: this.config.i18n,
2701
+ accessKey: this.config.accessKey,
2702
+ useWxCloud: this.config.useWxCloud,
2703
+ sdkVersion: this.version,
2704
+ detectSessionInUrl: this.config.auth?.detectSessionInUrl,
2705
+ },
2706
+ {
2707
+ env: this.config.env,
2708
+ clientId: this.config.clientId,
2709
+ apiOrigin: this.request.getBaseEndPoint(this.config.endPointMode || 'CLOUD_API'),
2710
+ platform: this.platform,
2711
+ cache: this.cache,
2712
+ app: this,
2713
+ debug: this.config.debug,
2714
+ },
2715
+ )
2716
+
2717
+ this.oauthInstance = oauthInstance
2718
+
2719
+ this.authInstance = authInstance
2720
+
2721
+ return this.authInstance
2722
+ }
1484
2723
 
1485
- return this.authInstance
2724
+ const authProto = auth.call(this, config)
2725
+ Object.assign(auth, authProto)
2726
+ Object.setPrototypeOf(auth, Object.getPrototypeOf(authProto))
2727
+ this[NAMESPACE] = auth
2728
+ return auth
1486
2729
  },
1487
2730
  }
1488
2731