import type { ICloudbase, ICloudbaseConfig, ICloudbasePlatformInfo } from '@cloudbase/types' import type { ICloudbaseCache } from '@cloudbase/types/cache' import type { ICloudbaseRequest } from '@cloudbase/types/request' import type { ICloudbaseAuthConfig, IUser, ILoginState } from '@cloudbase/types/auth' import type { ICloudbaseComponent } from '@cloudbase/types/component' import type { AuthOptions, Credentials } from '@cloudbase/oauth' import { CloudbaseOAuth, AUTH_API_PREFIX, LOGIN_STATE_CHANGED_TYPE, EVENTS, AUTH_STATE_CHANGED_TYPE, OAUTH_TYPE, weAppJwtDecodeAll, AuthError, authModels, } from '@cloudbase/oauth' import { useAuthAdapter } from './adapter' import { printWarn, throwError, ERRORS, COMMUNITY_SITE_URL, catchErrorsDecorator, CloudbaseEventEmitter, CloudbaseCache, adapterForWxMp, useDefaultAdapter, } from './utilities' import { saveToBrowserSession, getBrowserSession, removeBrowserSession, addUrlSearch } from './utils' import { utils } from '@cloudbase/utilities' import { CommonRes, DeleteMeReq, GetClaimsRes, GetUserIdentitiesRes, GetUserRes, LinkIdentityReq, LinkIdentityRes, OnAuthStateChangeCallback, ReauthenticateRes, ResendReq, ResendRes, ResetPasswordForEmailRes, ResetPasswordForOldReq, SetSessionReq, SignInAnonymouslyReq, SignInOAuthRes, SignInRes, SignInWithIdTokenReq, SignInWithOAuthReq, SignInWithOtpReq, SignInWithOtpRes, SignInWithPasswordReq, SignUpRes, UnlinkIdentityReq, UpdateUserAttributes, UpdateUserReq, UpdateUserWithVerificationRes, VerifyOAuthReq, VerifyOtpReq, } from './type' const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined' export type { SignInRes, GetUserRes, CommonRes, SignInWithOtpRes, SignInOAuthRes, GetClaimsRes, ResetPasswordForEmailRes, GetUserIdentitiesRes, LinkIdentityRes, ReauthenticateRes, ResendRes, UpdateUserWithVerificationRes, OnAuthStateChangeCallback, SignInWithPasswordReq, SignInWithIdTokenReq, SignInWithOAuthReq, VerifyOAuthReq, VerifyOtpReq, LinkIdentityReq, UnlinkIdentityReq, UpdateUserReq, SignInWithOtpReq, ResetPasswordForOldReq, ResendReq, SetSessionReq, DeleteMeReq, SignUpRes, } from './type' declare const cloudbase: ICloudbase declare const wx: any const COMPONENT_NAME = 'auth' interface UserInfo { uid?: string gender?: string picture?: string email?: string email_verified?: boolean phone_number?: string username?: string name?: string birthdate?: string zoneinfo?: string locale?: string sub?: string created_from?: string } const onCredentialsError = eventBus => (params) => { eventBus.fire(EVENTS.LOGIN_STATE_CHANGED, { ...params, eventType: LOGIN_STATE_CHANGED_TYPE.CREDENTIALS_ERROR }) } interface IUserOptions { cache: ICloudbaseCache oauthInstance: CloudbaseOAuth } export class User implements IUser { public uid?: string public gender?: string public picture?: string public email?: string public emailVerified?: boolean public phoneNumber?: string public username?: string // 用户名称,长度 5-24 位,支持字符中英文、数字、特殊字符(仅支持_-),不支持中文 public name?: string public providers?: { id?: string providerUserId?: string name?: string }[] public birthdate?: string public zoneinfo?: string public locale?: string public sub?: string public createdFrom?: string private cache: ICloudbaseCache private oauthInstance: CloudbaseOAuth // CloudbaseOAuth 类型 constructor(options: IUserOptions) { const { cache, oauthInstance } = options this.cache = cache this.oauthInstance = oauthInstance this.setUserInfo() } /** * 获取本地用户信息-同步 */ public async checkLocalInfo() { this.uid = this.getLocalUserInfo('uid') as string this.gender = this.getLocalUserInfo('gender') as string this.picture = this.getLocalUserInfo('picture') as string this.email = this.getLocalUserInfo('email') as string this.emailVerified = this.getLocalUserInfo('email_verified') as boolean this.phoneNumber = this.getLocalUserInfo('phone_number') as string this.username = this.getLocalUserInfo('username') as string this.name = this.getLocalUserInfo('name') as string this.birthdate = this.getLocalUserInfo('birthdate') as string this.zoneinfo = this.getLocalUserInfo('zoneinfo') as string this.locale = this.getLocalUserInfo('locale') as string this.sub = this.getLocalUserInfo('sub') as string this.createdFrom = this.getLocalUserInfo('created_from') as string this.providers = this.getLocalUserInfo('providers') as any } /** * 获取本地用户信息-异步 */ public async checkLocalInfoAsync() { this.uid = await this.getLocalUserInfoAsync('uid') this.gender = await this.getLocalUserInfoAsync('gender') this.picture = this.getLocalUserInfo('picture') as string this.email = await this.getLocalUserInfoAsync('email') this.emailVerified = this.getLocalUserInfo('email_verified') as boolean this.phoneNumber = this.getLocalUserInfo('phone_number') as string this.username = await this.getLocalUserInfoAsync('username') this.name = this.getLocalUserInfo('name') as string this.birthdate = this.getLocalUserInfo('birthdate') as string this.zoneinfo = this.getLocalUserInfo('zoneinfo') as string this.locale = this.getLocalUserInfo('locale') as string this.sub = this.getLocalUserInfo('sub') as string this.createdFrom = this.getLocalUserInfo('created_from') as string this.providers = this.getLocalUserInfo('providers') as any } /** * 更新用户信息 * @param userInfo */ @catchErrorsDecorator({ title: '更新用户信息失败', messages: [ '请确认以下各项:', ' 1 - 调用 User.update() 的语法或参数是否正确', ' 2 - 用户信息中是否包含非法值', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async update(userInfo: authModels.UserProfile): Promise { // const { name, gender, avatarUrl, province, country, city } = userInfo const newUserInfo = await this.oauthInstance.authApi.setUserProfile({ ...userInfo }) this.setLocalUserInfo(newUserInfo) } public async updateUserBasicInfo(params: authModels.ModifyUserBasicInfoRequest): Promise { await this.oauthInstance.authApi.updateUserBasicInfo({ ...params }) await this.refresh() } /** * 更新密码 * @param newPassword * @param oldPassword */ @catchErrorsDecorator({ title: '更新密码失败', messages: [ '请确认以下各项:', ' 1 - 调用 User.updatePassword() 的语法或参数是否正确', ' 3 - 新密码中是否包含非法字符', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public updatePassword(newPassword: string, oldPassword: string) { return this.oauthInstance.authApi.updatePasswordByOld({ old_password: oldPassword, new_password: newPassword, }) } /** * 更新用户名 * @param username */ @catchErrorsDecorator({ title: '更新用户名失败', messages: [ '请确认以下各项:', ' 1 - 调用 User.updateUsername() 的语法或参数是否正确', ' 2 - 当前环境是否开通了用户名密码登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public updateUsername(username: string) { if (typeof username !== 'string') { throwError(ERRORS.INVALID_PARAMS, 'username must be a string') } return this.update({ username, }) } /** * 刷新本地用户信息。当用户在其他客户端更新用户信息之后,可以调用此接口同步更新之后的信息。 */ @catchErrorsDecorator({ title: '刷新本地用户信息失败', messages: [ '请确认以下各项:', ' 1 - 调用 User.refresh() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async refresh(params?: { version?: string; query?: any }): Promise { const newUserInfo = await this.oauthInstance.authApi.getUserInfo(params) if ((newUserInfo as any).code === 'INVALID_ACCESS_TOKEN') { this.setLocalUserInfo({}) throw newUserInfo } this.setLocalUserInfo(newUserInfo) return newUserInfo } public getLocalUserInfo(key?: string): string | boolean | Record { const { userInfoKey } = this.cache.keys const userInfo = this.cache.getStore(userInfoKey) if (!key) return userInfo || {} return userInfo[key] } public setLocalUserInfo(userInfo: any) { const { userInfoKey } = this.cache.keys this.cache.setStore(userInfoKey, userInfo) this.setUserInfo() } private async getLocalUserInfoAsync(key: string): Promise { const { userInfoKey } = this.cache.keys const userInfo = await this.cache.getStoreAsync(userInfoKey) return userInfo[key] } private setUserInfo() { const { userInfoKey } = this.cache.keys const userInfo = this.cache.getStore(userInfoKey) ;[ 'uid', 'email', 'name', 'gender', 'picture', 'email_verified', 'phone_number', 'birthdate', 'zoneinfo', 'locale', 'sub', 'created_from', 'providers', 'username', 'created_at', ].forEach((infoKey) => { this[infoKey] = userInfo[infoKey] }) } } interface ILoginStateOptions extends IUserOptions { envId: string } export class LoginState implements ILoginState { public user: User public oauthLoginState: any private oauthInstance: CloudbaseOAuth private cache: ICloudbaseCache constructor(options: ILoginStateOptions) { const { envId, cache, oauthInstance } = options if (!envId) { throwError(ERRORS.INVALID_PARAMS, 'envId is not defined') } this.cache = cache this.oauthInstance = oauthInstance this.user = new User({ cache: this.cache, oauthInstance, }) } public checkLocalState() { this.oauthLoginState = this.oauthInstance?.authApi.hasLoginStateSync() this.user.checkLocalInfo() } public async checkLocalStateAsync() { await this.oauthInstance?.authApi.getLoginState() await this.user.checkLocalInfoAsync() } } class Auth { readonly config: ICloudbaseAuthConfig oauthInstance: CloudbaseOAuth readonly cache: ICloudbaseCache private listeners: Map> = new Map() private hasListenerSetUp = false constructor(config: ICloudbaseAuthConfig & { cache: ICloudbaseCache request?: ICloudbaseRequest runtime?: string },) { this.config = config this.oauthInstance = config.oauthInstance this.cache = config.cache this.init() this.setAccessKey() } /** * 绑定手机号 * @param phoneNumber * @param phoneCode */ @catchErrorsDecorator({ title: '绑定手机号失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().bindPhoneNumber() 的语法或参数是否正确', ' 2 - 当前环境是否开通了短信验证码登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async bindPhoneNumber(params: authModels.BindPhoneRequest) { return this.oauthInstance.authApi.editContact(params) } /** * 解绑三方绑定 * @param loginType */ @catchErrorsDecorator({ title: '解除三方绑定失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().unbindProvider() 的语法或参数是否正确', ' 2 - 当前账户是否已经与此登录方式解绑', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async unbindProvider(params: authModels.UnbindProviderRequest): Promise { return this.oauthInstance.authApi.unbindProvider(params) } /** * 更新邮箱地址 * @param email * @param sudo_token * @param verification_token */ @catchErrorsDecorator({ title: '绑定邮箱地址失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().bindEmail() 的语法或参数是否正确', ' 2 - 当前环境是否开通了邮箱密码登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public bindEmail(params: authModels.BindEmailRequest) { return this.oauthInstance.authApi.editContact(params) } /** * verify * @param {authModels.VerifyRequest} params * @returns {Promise} * @memberof User */ @catchErrorsDecorator({ title: '验证码验证失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().verify() 的语法或参数是否正确', ' 2 - 当前环境是否开通了手机验证码/邮箱登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async verify(params: authModels.VerifyRequest): Promise { return this.oauthInstance.authApi.verify(params) } /** * 获取验证码 * @param {authModels.GetVerificationRequest} params * @returns {Promise} * @memberof User */ @catchErrorsDecorator({ title: '获取验证码失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().getVerification() 的语法或参数是否正确', ' 2 - 当前环境是否开通了手机验证码/邮箱登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getVerification( params: authModels.GetVerificationRequest, options?: { withCaptcha: boolean }, ): Promise { return this.oauthInstance.authApi.getVerification(params, options) } /** * 获取当前登录的用户信息-同步 */ get currentUser() { if (this.cache.mode === 'async') { // async storage的平台调用此API提示 printWarn( ERRORS.INVALID_OPERATION, 'current platform\'s storage is asynchronous, please use getCurrentUser instead', ) return } const loginState = this.hasLoginState() if (loginState) { return loginState.user || null } return null } /** * 获取当前登录的用户信息-异步 */ @catchErrorsDecorator({ title: '获取用户信息失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().getCurrentUser() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getCurrentUser(): Promise<(authModels.UserInfo & Partial) | null> { const loginState = await this.getLoginState() if (loginState) { const userInfo = loginState.user.getLocalUserInfo() as authModels.UserInfo await loginState.user.checkLocalInfoAsync() return { ...loginState.user, ...userInfo } as unknown as authModels.UserInfo & Partial } return null } // /** // * 匿名登录 // * @returns {Promise} // * @memberof Auth // */ // @catchErrorsDecorator({ // title: '匿名登录失败', // messages: [ // '请确认以下各项:', // ' 1 - 当前环境是否开启了匿名登录', // ' 2 - 调用 auth().signInAnonymously() 的语法或参数是否正确', // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, // ], // }) // public async signInAnonymously(data: { // provider_token?: string // } = {},): Promise { // await this.oauthInstance.authApi.signInAnonymously(data) // return this.createLoginState() // } /** * 匿名登录 * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '小程序匿名登录失败', messages: [ '请确认以下各项:', ' 1 - 当前环境是否开启了匿名登录', ' 2 - 调用 auth().signInAnonymouslyInWx() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async signInAnonymouslyInWx({ useWxCloud, }: { useWxCloud?: boolean } = {}): Promise { if (!adapterForWxMp.isMatch()) { throw Error('wx api undefined') } const wxInfo = wx.getAccountInfoSync().miniProgram const mainFunc = async (code) => { let result: authModels.GrantProviderTokenResponse | undefined = undefined let credentials: Credentials | undefined = undefined try { result = await this.oauthInstance.authApi.grantProviderToken( { provider_id: wxInfo?.appId, provider_code: code, provider_params: { provider_code_type: 'open_id', appid: wxInfo?.appId, }, }, useWxCloud, ) if ((result as any)?.error_code || !result.provider_token) { throw result } credentials = await this.oauthInstance.authApi.signInAnonymously( { provider_token: result.provider_token }, useWxCloud, ) if ((credentials as any)?.error_code) { throw credentials } } catch (error) { throw error } } await new Promise((resolve, reject) => { wx.login({ success: async (res: { code: string }) => { try { await mainFunc(res.code) resolve(true) } catch (error) { reject(error) } }, fail: (res: any) => { const error = new Error(res?.errMsg) ;(error as any).code = res?.errno reject(error) }, }) }) return this.createLoginState(undefined, { asyncRefreshUser: true }) } /** * 小程序绑定OpenID * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '小程序绑定OpenID失败', messages: [ '请确认以下各项:', ' 1 - 当前环境是否开启了小程序openId静默登录', ' 2 - 调用 auth().bindOpenId() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async bindOpenId(): Promise { if (!adapterForWxMp.isMatch()) { throw Error('wx api undefined') } const wxInfo = wx.getAccountInfoSync().miniProgram const mainFunc = async (code) => { let result: authModels.GrantProviderTokenResponse | undefined = undefined try { result = await this.oauthInstance.authApi.grantProviderToken({ provider_id: wxInfo?.appId, provider_code: code, provider_params: { provider_code_type: 'open_id', appid: wxInfo?.appId, }, }) if ((result as any)?.error_code || !result.provider_token) { throw result } await this.oauthInstance.authApi.bindWithProvider({ provider_token: result.provider_token }) } catch (error) { throw error } } await new Promise((resolve, reject) => { wx.login({ success: async (res: { code: string }) => { try { await mainFunc(res.code) resolve(true) } catch (error) { reject(error) } }, fail: (res: any) => { const error = new Error(res?.errMsg) ;(error as any).code = res?.errno reject(error) }, }) }) return } /** * 小程序unionId静默登录 * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '小程序unionId静默登录失败', messages: [ '请确认以下各项:', ' 1 - 当前环境是否开启了小程序unionId静默登录', ' 2 - 调用 auth().signInWithUnionId() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async signInWithUnionId(): Promise { if (!adapterForWxMp.isMatch()) { throw Error('wx api undefined') } try { await new Promise((resolve, reject) => { const wxInfo = wx.getAccountInfoSync().miniProgram wx.login({ success: async (res: { code: string }) => { const providerId = wxInfo?.appId try { const result = await this.oauthInstance.authApi.grantProviderToken({ provider_code: res.code, provider_id: providerId, provider_params: { provider_code_type: 'union_id', appid: wxInfo?.appId, }, }) const { provider_token: providerToken } = result if (!providerToken) { reject(result) return } const signInRes = await this.oauthInstance.authApi.signInWithProvider({ provider_id: providerId, provider_token: providerToken, }) if ((signInRes as any)?.error_code) { reject(signInRes) return } resolve(true) } catch (error) { reject(error) } }, fail: (res: any) => { const error = new Error(res?.errMsg) ;(error as any).code = res?.errno reject(error) }, }) }) } catch (error) { throw error } return this.createLoginState() } /** * 小程序手机号授权登录,目前只支持全托管手机号授权登录 * @returns {Promise} * @memberof Auth */ // @catchErrorsDecorator({ // title: '小程序手机号授权登录失败', // messages: [ // '请确认以下各项:', // ' 1 - 当前环境是否开启了小程序手机号授权登录', // ' 2 - 调用 auth().signInWithPhoneAuth() 的语法或参数是否正确', // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, // ], // }) // public async signInWithPhoneAuth({ phoneCode = '' }): Promise { // if (!adapterForWxMp.isMatch()) { // throw Error('wx api undefined') // } // const wxInfo = wx.getAccountInfoSync().miniProgram // const providerInfo = { // provider_params: { provider_code_type: 'phone' }, // provider_id: wxInfo.appId, // } // const { code } = await wx.login() // ;(providerInfo as any).provider_code = code // try { // let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo) // if (providerToken.error_code) { // throw providerToken // } // providerToken = await this.oauthInstance.authApi.patchProviderToken({ // provider_token: providerToken.provider_token, // provider_id: wxInfo.appId, // provider_params: { // code: phoneCode, // provider_code_type: 'phone', // }, // }) // if (providerToken.error_code) { // throw providerToken // } // const signInRes = await this.oauthInstance.authApi.signInWithProvider({ // provider_token: providerToken.provider_token, // }) // if ((signInRes as any)?.error_code) { // throw signInRes // } // } catch (error) { // throw error // } // return this.createLoginState() // } /** * 小程序短信验证码登陆 * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '短信验证码登陆', messages: [ '请确认以下各项:', ' 1 - 当前环境是否开启了小程序短信验证码登陆', ' 2 - 调用 auth().signInWithSms() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async signInWithSms({ verificationInfo = { verification_id: '', is_user: false }, verificationCode = '', phoneNum = '', bindInfo = undefined, }): Promise { try { return this.signInWithUsername({ verificationInfo, verificationCode, bindInfo, username: phoneNum, loginType: 'sms', }) } catch (error) { throw error } } /** * 邮箱验证码登陆 * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '邮箱验证码登陆', messages: [ '请确认以下各项:', ' 1 - 当前环境是否开启了邮箱登陆', ' 2 - 调用 auth().signInWithEmail() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async signInWithEmail({ verificationInfo = { verification_id: '', is_user: false }, verificationCode = '', bindInfo = undefined, email = '', }): Promise { try { return this.signInWithUsername({ verificationInfo, verificationCode, bindInfo, username: email, loginType: 'email', }) } catch (error) { throw error } } /** * 设置获取自定义登录 ticket 函数 * @param {authModels.GetCustomSignTicketFn} getTickFn * @memberof Auth */ public setCustomSignFunc(getTickFn: authModels.GetCustomSignTicketFn): void { this.oauthInstance.authApi.setCustomSignFunc(getTickFn) } /** * * @returns {Promise} * @memberof Auth */ // @catchErrorsDecorator({ // title: '自定义登录失败', // messages: [ // '请确认以下各项:', // ' 1 - 当前环境是否开启了自定义登录', // ' 2 - 调用 auth().signInWithCustomTicket() 的语法或参数是否正确', // ' 3 - ticket 是否归属于当前环境', // ' 4 - 创建 ticket 的自定义登录私钥是否过期', // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, // ], // }) // public async signInWithCustomTicket(): Promise { // await this.oauthInstance.authApi.signInWithCustomTicket() // return this.createLoginState() // } /** * * @param {authModels.SignInRequest} params * @returns {Promise} * @memberof Auth */ public async signIn(params: authModels.SignInRequest): Promise { await this.oauthInstance.authApi.signIn(params) return this.createLoginState(params) } // /** // * // * @param {authModels.SignUpRequest} params // * @returns {Promise} // * @memberof Auth // */ // @catchErrorsDecorator({ // title: '注册失败', // messages: [ // '请确认以下各项:', // ' 1 - 当前环境是否开启了指定登录方式', // ' 2 - 调用 auth().signUp() 的语法或参数是否正确', // `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, // ], // }) // public async signUp(params: authModels.SignUpRequest): Promise { // await this.oauthInstance.authApi.signUp(params) // return this.createLoginState() // } /** * 设置密码 * @param {authModels.SetPasswordRequest} params * @returns {Promise} * @memberof Auth */ public async setPassword(params: authModels.SetPasswordRequest): Promise { return this.oauthInstance.authApi.setPassword(params) } /** * 检测用户名是否已经占用 * @param username */ @catchErrorsDecorator({ title: '获取用户是否被占用失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().isUsernameRegistered() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async isUsernameRegistered(username: string): Promise { if (typeof username !== 'string') { throwError(ERRORS.INVALID_PARAMS, 'username must be a string') } const { exist } = await this.oauthInstance.authApi.checkIfUserExist({ username }) return exist } /** * 获取本地登录态-同步 */ public hasLoginState(): LoginState | null { if (this.cache.mode === 'async') { // async storage的平台调用此API提示 printWarn( ERRORS.INVALID_OPERATION, 'current platform\'s storage is asynchronous, please use getLoginState instead', ) return } const oauthLoginState = this.oauthInstance?.authApi.hasLoginStateSync() if (oauthLoginState) { const loginState = new LoginState({ envId: this.config.env, cache: this.cache, oauthInstance: this.oauthInstance, }) return loginState } return null } /** * 获取本地登录态-异步 * 此API为兼容异步storage的平台 */ @catchErrorsDecorator({ title: '获取本地登录态失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().getLoginState() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getLoginState() { let oauthLoginState = null try { oauthLoginState = await this.oauthInstance.authApi.getLoginState() } catch (error) { return null } if (oauthLoginState) { const loginState = new LoginState({ envId: this.config.env, cache: this.cache, oauthInstance: this.oauthInstance, }) return loginState } return null } @catchErrorsDecorator({ title: '获取用户信息失败', messages: [ '请确认以下各项:', ' 1 - 是否已登录', ' 2 - 调用 auth().getUserInfo() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getUserInfo(): Promise<(authModels.UserInfo & Partial) | null> { return this.getCurrentUser() } @catchErrorsDecorator({ title: '获取微搭插件用户信息失败', messages: [ '请确认以下各项:', ' 1 - 是否已登录', ' 2 - 调用 auth().getWedaUserInfo() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getWedaUserInfo(): Promise { return this.oauthInstance.authApi.getWedaUserInfo() } public async updateUserBasicInfo(params: authModels.ModifyUserBasicInfoRequest) { const loginState = await this.getLoginState() if (loginState) { await (loginState.user as User).updateUserBasicInfo(params) } return } /** * getAuthHeader 兼容处理 * 返回空对象 */ public getAuthHeader(): {} { console.error('Auth.getAuthHeader API 已废弃') return {} } /** * 为已有账户绑第三方账户 * @param {authModels.BindWithProviderRequest} params * @returns {Promise} * @memberof Auth */ @catchErrorsDecorator({ title: '绑定第三方登录方式失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().bindWithProvider() 的语法或参数是否正确', ' 2 - 此账户是否已经绑定此第三方', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async bindWithProvider(params: authModels.BindWithProviderRequest): Promise { return this.oauthInstance.authApi.bindWithProvider(params) } /** * 查询用户 * @param {authModels.QueryUserProfileRequest} appended_params * @returns {Promise} * @memberof Auth */ public async queryUser(queryObj: authModels.QueryUserProfileRequest): Promise { return this.oauthInstance.authApi.queryUserProfile(queryObj) } public async getAccessToken() { const oauthAccessTokenRes = await this.oauthInstance.oauth2client.getAccessToken() return { accessToken: oauthAccessTokenRes, env: this.config.env, } } public async grantProviderToken(params: authModels.GrantProviderTokenRequest,): Promise { return this.oauthInstance.authApi.grantProviderToken(params) } public async patchProviderToken(params: authModels.PatchProviderTokenRequest,): Promise { return this.oauthInstance.authApi.patchProviderToken(params) } public async signInWithProvider(params: authModels.SignInWithProviderRequest): Promise { await this.oauthInstance.authApi.signInWithProvider(params) return this.createLoginState(params) } public async signInWithWechat(params: any = {}) { await this.oauthInstance.authApi.signInWithWechat(params) return this.createLoginState(params) } public async grantToken(params: authModels.GrantTokenRequest): Promise { await this.oauthInstance.authApi.grantToken(params) return this.createLoginState() } public async genProviderRedirectUri(params: authModels.GenProviderRedirectUriRequest,): Promise { return this.oauthInstance.authApi.genProviderRedirectUri(params) } public async resetPassword(params: authModels.ResetPasswordRequest): Promise { return this.oauthInstance.authApi.resetPassword(params) } public async deviceAuthorize(params: authModels.DeviceAuthorizeRequest): Promise { return this.oauthInstance.authApi.deviceAuthorize(params) } public async sudo(params: authModels.SudoRequest): Promise { return this.oauthInstance.authApi.sudo(params) } public async deleteMe(params: authModels.WithSudoRequest): Promise { return this.oauthInstance.authApi.deleteMe(params) } public async getProviders(): Promise { return this.oauthInstance.authApi.getProviders() } public async loginScope(): Promise { return this.oauthInstance.authApi.loginScope() } public async loginGroups(): Promise { return this.oauthInstance.authApi.loginGroups() } public async onLoginStateChanged(callback: Function) { this.config.eventBus?.on(EVENTS.LOGIN_STATE_CHANGED, async (params) => { // getLoginState会重复触发getCredentials,导致死循环,所以getCredentials出错不再出发getLoginState const loginState = params?.data?.eventType !== LOGIN_STATE_CHANGED_TYPE.CREDENTIALS_ERROR ? await this.getLoginState() : {} callback.call(this, { ...params, ...loginState }) }) // 立刻执行一次回调 const loginState = await this.getLoginState() callback.call(this, loginState) } /** * 强制刷新token * @param params * @returns */ public async refreshTokenForce(params: { version?: string }): Promise { return this.oauthInstance.authApi.refreshTokenForce(params) } /** * 获取身份信息 * @returns */ public async getCredentials(): Promise { return this.oauthInstance.authApi.getCredentials() } /** * 写入身份信息 */ public async setCredentials(credentials: Credentials) { await this.oauthInstance.oauth2client.setCredentials(credentials) } @catchErrorsDecorator({ title: '获取身份源类型', messages: [ '请确认以下各项:', ' 1 - 调用 auth().getProviderSubType() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getProviderSubType(): Promise { return this.oauthInstance.authApi.getProviderSubType() } public async createCaptchaData(params: { state: string; redirect_uri?: string }) { return this.oauthInstance.authApi.createCaptchaData(params) } public async verifyCaptchaData(params: { token: string; key: string }) { return this.oauthInstance.authApi.verifyCaptchaData(params) } public async getMiniProgramQrCode(params: authModels.GetMiniProgramQrCodeRequest,): Promise { return this.oauthInstance.authApi.getMiniProgramCode(params) } public async getMiniProgramQrCodeStatus(params: authModels.GetMiniProgramQrCodeStatusRequest,): Promise { return this.oauthInstance.authApi.getMiniProgramQrCodeStatus(params) } public async modifyPassword(params: authModels.ModifyUserBasicInfoRequest): Promise { return this.oauthInstance.authApi.modifyPassword(params) } public async modifyPasswordWithoutLogin(params: authModels.ModifyPasswordWithoutLoginRequest): Promise { return this.oauthInstance.authApi.modifyPasswordWithoutLogin(params) } public async getUserBehaviorLog(params: authModels.GetUserBehaviorLog): Promise { return this.oauthInstance.authApi.getUserBehaviorLog(params) } /** * sms/email 验证码登录/注册,逻辑一致收敛 */ public async signInWithUsername({ verificationInfo = { verification_id: '', is_user: false }, verificationCode = '', username: rawUsername = '', bindInfo = undefined, loginType = '', }: { verificationInfo?: authModels.GetVerificationResponse verificationCode?: string username?: string bindInfo?: any loginType?: string }): Promise { try { // 1. 验证验证码 const verifyRes = await this.oauthInstance.authApi.verify({ verification_id: verificationInfo.verification_id, verification_code: verificationCode, }) if ((verifyRes as any)?.error_code) { throw verifyRes } // eslint-disable-next-line @typescript-eslint/naming-convention const { verification_token } = verifyRes // 手机登录参数 let username = /^\+\d{1,3}\s+/.test(rawUsername) ? rawUsername : `+86 ${rawUsername}` let signUpParam: any = { phone_number: username } // 邮箱登录参数 if (loginType === 'email') { username = rawUsername signUpParam = { email: username } } // 2. 根据是否已经是用户,分别走登录或注册逻辑 if (verificationInfo.is_user) { // 私有化环境或者自定义应用走v1版本的老逻辑 const signInRes = await this.oauthInstance.authApi.signIn({ username, verification_token, }) if ((signInRes as any)?.error_code) { throw signInRes } if (bindInfo) { const bindRes = await this.oauthInstance.authApi.bindWithProvider({ provider_token: (bindInfo as any)?.providerToken, }) if ((bindRes as any)?.error_code) { throw bindRes } } } else { // 自定义应用走signUp逻辑 const signUpRes = await this.oauthInstance.authApi.signUp({ ...signUpParam, verification_token, provider_token: (bindInfo as any)?.providerId, }) if ((signUpRes as any)?.error_code) { throw signUpRes } } return this.createLoginState() } catch (error) { throw error } } async createLoginState( params?: { version?: string; query?: any }, options?: { asyncRefreshUser?: boolean; userInfo?: any }, ): Promise { const loginState = new LoginState({ envId: this.config.env, cache: this.cache, oauthInstance: this.oauthInstance, }) await loginState.checkLocalStateAsync() if (options?.userInfo) { loginState.user.setLocalUserInfo(options.userInfo) } else { if (options?.asyncRefreshUser) { loginState.user.refresh(params) } else { await loginState.user.refresh(params) } } this.config.eventBus?.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_IN }) this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN }) return loginState } async setAccessKey() { if (this.config.accessKey) { try { this.oauthInstance.oauth2client.setAccessKeyCredentials({ access_token: this.config.accessKey, token_type: 'Bearer', scope: 'accessKey', expires_at: new Date(+new Date() + +new Date()), expires_in: +new Date() + +new Date(), }) } catch (error) { console.warn('accessKey error: ', error) } } } // ========== new auth api methods merged below ========== /** * https://supabase.com/docs/reference/javascript/auth-signinanonymously * Sign in a user anonymously. * const { data, error } = await auth.signInAnonymously(); * @param params * @returns Promise */ async signInAnonymously(params: SignInAnonymouslyReq): Promise { try { await this.oauthInstance.authApi.signInAnonymously(params) const loginState = await this.createLoginState() const { data: { session } = {} } = await this.getSession() // loginState返回是为了兼容v2版本 return { ...(loginState as any), data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-signup * 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. * @param params * @returns Promise */ async signUp(params: authModels.SignUpRequest & { phone?: string }): Promise { if (params.phone_number || params.verification_code || params.verification_token || params.provider_token) { await this.oauthInstance.authApi.signUp(params) return this.createLoginState() as any } try { // 参数校验:email或phone必填其一 this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number') // 第一步:发送验证码并存储 verificationInfo const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },) return { data: { // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件) verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise => { try { // 第三步:待用户输入完验证码之后,验证短信验证码 const verificationTokenRes = await this.verify({ verification_id: messageId || verificationInfo.verification_id, verification_code: token, }) // 第四步:注册并登录或直接登录 // 如果用户已经存在,直接登录 if (verificationInfo.is_user) { await this.signIn({ username: params.email || this.formatPhone(params.phone), verification_token: verificationTokenRes.verification_token, }) } else { // 如果用户不存在,注册用户 const data = JSON.parse(JSON.stringify(params)) delete data.email delete data.phone await this.oauthInstance.authApi.signUp({ ...data, ...(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) }), verification_token: verificationTokenRes.verification_token, verification_code: token, }) await this.createLoginState() } const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } }, }, error: null, } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-signout * const result = await auth.signOut(); * * @param params */ async signOut(params?: authModels.SignoutRequest,): Promise { try { const { userInfoKey } = this.cache.keys const res = await this.oauthInstance.authApi.signOut(params) await this.cache.removeStoreAsync(userInfoKey) this.setAccessKey() this.config.eventBus?.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_OUT }) this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_OUT }) // res返回是为了兼容v2版本 return { ...res, data: {}, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-onauthstatechange * Receive a notification every time an auth event happens. * @param callback * @returns Promise<{ data: { subscription: Subscription }, error: Error | null }> */ onAuthStateChange(callback: OnAuthStateChangeCallback) { if (!this.hasListenerSetUp) { this.setupListeners() this.hasListenerSetUp = true } const id = Math.random().toString(36) if (!this.listeners.has(id)) { this.listeners.set(id, new Set()) } this.listeners.get(id)!.add(callback) // 返回 Subscription 对象 const subscription = { id, callback, unsubscribe: () => { const callbacks = this.listeners.get(id) if (callbacks) { callbacks.delete(callback) if (callbacks.size === 0) { this.listeners.delete(id) } } }, } return { data: { subscription }, } } /** * https://supabase.com/docs/reference/javascript/auth-signinwithpassword * Log in an existing user with an email and password or phone and password or username and password. * @param params * @returns Promise */ async signInWithPassword(params: SignInWithPasswordReq): Promise { try { // 参数校验:username/email/phone三选一,password必填 this.validateAtLeastOne( params, [['username'], ['email'], ['phone']], 'You must provide either username, email, or phone', ) this.validateParams(params, { password: { required: true, message: 'Password is required' }, }) await this.signIn({ username: params.username || params.email || this.formatPhone(params.phone), password: params.password, ...(params.is_encrypt ? { isEncrypt: true, version: 'v2' } : {}), }) const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-signinwithidtoken * 第三方平台登录。如果用户不存在,会根据云开发平台-登录方式中对应身份源的登录模式配置,判断是否自动注册 * @param params * @returns Promise */ async signInWithIdToken(params: SignInWithIdTokenReq): Promise { try { // 参数校验:token必填 this.validateParams(params, { token: { required: true, message: 'Token is required' }, }) await this.signInWithProvider({ provider_token: params.token, }) const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-signinwithotp * Log in a user using a one-time password (OTP). * @param params * @returns Promise */ async signInWithOtp(params: SignInWithOtpReq): Promise { try { // 参数校验:email或phone必填其一 this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number') // 第一步:发送验证码并存储 verificationInfo const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },) return { data: { user: null, session: null, // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件) verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise => this.verifyOtp({ email: params.email, phone: params.phone, token, messageId, }), }, error: null, } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 校验第三方平台授权登录回调 * @param params * @returns Promise */ async verifyOAuth(params?: VerifyOAuthReq): Promise { const data: any = {} try { // 回调至 provider_redirect_uri 地址(url query中携带 授权code,state等参数),此时检查 state 是否符合预期(如 自己设置的 wx_open) const code = params?.code || utils.getQuery('code') const state = params?.state || utils.getQuery('state') // 参数校验:code和state必填 if (!code) { return { data: {}, error: new AuthError({ message: 'Code is required' }) } } if (!state) { return { data: {}, error: new AuthError({ message: 'State is required' }) } } const cacheData = getBrowserSession(state) data.type = cacheData?.type const provider = params?.provider || cacheData?.provider || utils.getQuery('provider') if (!provider) { return { data, error: new AuthError({ message: 'Provider is required' }) } } // state符合预期,则获取该三方平台token const { provider_token: token } = await this.grantProviderToken({ provider_id: provider, provider_redirect_uri: location.origin + location.pathname, // 指定三方平台跳回的 url 地址 provider_code: code, // 第三方平台跳转回页面时,url param 中携带的 code 参数 }) let res: SignInRes | LinkIdentityRes if (cacheData.type === OAUTH_TYPE.BIND_IDENTITY) { res = await this.oauthInstance.authApi.toBindIdentity({ provider_token: token, provider, fireEvent: true }) } else { // 通过 provider_token 仅登录或登录并注册(与云开发平台-登录方式-身份源登录模式配置有关) res = await this.signInWithIdToken({ token, }) res.data = { ...data, ...res.data } } const localSearch = new URLSearchParams(location?.search) localSearch.delete('code') localSearch.delete('state') addUrlSearch( cacheData?.search === undefined ? `?${localSearch.toString()}` : cacheData?.search, cacheData?.hash || location.hash, ) removeBrowserSession(state) return res } catch (error) { return { data, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-signinwithoauth * 生成第三方平台授权 Uri (如微信二维码扫码授权网页) * @param params * @returns Promise */ async signInWithOAuth(params: SignInWithOAuthReq): Promise { try { // 参数校验:provider必填 this.validateParams(params, { provider: { required: true, message: 'Provider is required' }, }) const href = params.options?.redirectTo || location.href const urlObject = new URL(href) const provider_redirect_uri = urlObject.origin + urlObject.pathname const state = params.options?.state || `prd-${params.provider}-${Math.random().toString(36) .slice(2)}` const { uri } = await this.genProviderRedirectUri({ provider_id: params.provider, provider_redirect_uri, state, }) // 对 URL 进行解码 const decodedUri = decodeURIComponent(uri) // 合并额外的查询参数 let finalUri = decodedUri if (params.options?.queryParams) { const url = new URL(decodedUri) Object.entries(params.options.queryParams).forEach(([key, value]) => { url.searchParams.set(key, value) }) finalUri = url.toString() } saveToBrowserSession(state, { provider: params.provider, search: urlObject.search, hash: urlObject.hash, type: params.options?.type || OAUTH_TYPE.SIGN_IN, }) if (isBrowser() && !params.options?.skipBrowserRedirect) { window.location.assign(finalUri) } return { data: { url: finalUri, provider: params.provider }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } // https://supabase.com/docs/reference/javascript/auth-getclaims async getClaims(): Promise { try { const { accessToken } = await this.getAccessToken() const parsedToken = weAppJwtDecodeAll(accessToken) return { data: parsedToken, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail * 通过 email 或手机号重置密码 * @param emailOrPhone 邮箱或手机号 * @returns Promise */ async resetPasswordForEmail( emailOrPhone: string, options?: { redirectTo?: string }, ): Promise { try { // 参数校验:emailOrPhone必填 this.validateParams( { emailOrPhone }, { emailOrPhone: { required: true, message: 'Email or phone is required' }, }, ) const { redirectTo } = options || {} // 判断是邮箱还是手机号 const isEmail = emailOrPhone.includes('@') let verificationParams: { email?: string; phone_number?: string } if (isEmail) { verificationParams = { email: emailOrPhone } } else { // 正规化手机号 const formattedPhone = this.formatPhone(emailOrPhone) verificationParams = { phone_number: formattedPhone } } // 第一步:发送验证码并存储 verificationInfo const verificationInfo = await this.getVerification(verificationParams) return { data: { // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件) updateUser: async (attributes: UpdateUserAttributes): Promise => { this.validateParams(attributes, { nonce: { required: true, message: 'Nonce is required' }, password: { required: true, message: 'Password is required' }, }) try { // 第三步:待用户输入完验证码之后,验证验证码 const verificationTokenRes = await this.verify({ verification_id: verificationInfo.verification_id, verification_code: attributes.nonce, }) await this.oauthInstance.authApi.resetPassword({ email: isEmail ? emailOrPhone : undefined, phone_number: !isEmail ? emailOrPhone : undefined, new_password: attributes.password, verification_token: verificationTokenRes.verification_token, }) this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.PASSWORD_RECOVERY, }) const res = await this.signInWithPassword({ email: isEmail ? emailOrPhone : undefined, phone: !isEmail ? emailOrPhone : undefined, password: attributes.password, }) if (redirectTo && isBrowser()) { window.location.assign(redirectTo) } return res } catch (error) { return { data: {}, error: new AuthError(error) } } }, }, error: null, } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 通过旧密码重置密码 * @param new_password * @param old_password * @returns */ async resetPasswordForOld(params: ResetPasswordForOldReq) { try { await this.oauthInstance.authApi.updatePasswordByOld({ old_password: params.old_password, new_password: params.new_password, }) const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-verifyotp * Log in a user given a User supplied OTP and verificationId received through mobile or email. * @param params * @returns Promise */ async verifyOtp(params: VerifyOtpReq): Promise { try { const { type } = params // 参数校验:token和verificationInfo必填 this.validateParams(params, { token: { required: true, message: 'Token is required' }, messageId: { required: true, message: 'messageId is required' }, }) if (['phone_change', 'email_change'].includes(type)) { await this.verify({ verification_id: params.messageId, verification_code: params.token, }) } else { await this.signInWithUsername({ verificationInfo: { verification_id: params.messageId, is_user: true }, verificationCode: params.token, username: params.email || this.formatPhone(params.phone) || '', loginType: params.email ? 'email' : 'phone', }) } const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-getSession * Returns the session, refreshing it if necessary. * @returns Promise */ async getSession(): Promise { try { const credentials: Credentials = await this.oauthInstance.oauth2client.getCredentials() if (!credentials || credentials.scope === 'accessKey') { return { data: { session: null }, error: null } } const { data: { user } = {} } = await this.getUser() return { data: { session: { ...credentials, user }, user }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-refreshsession * 无论过期状态如何,都返回一个新的会话 * @param refresh_token * @returns Promise */ async refreshSession(refresh_token?: string): Promise { try { const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials() credentials.refresh_token = refresh_token || credentials.refresh_token const newTokens = await this.oauthInstance.oauth2client.refreshToken(credentials) const { data: { user } = {} } = await this.getUser() return { data: { user, session: { ...newTokens, user } }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-getuser * 如果存在现有会话,则获取当前用户详细信息 * @returns Promise */ async getUser(): Promise { try { const user = this.convertToUser(await this.getUserInfo()) return { data: { user }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 刷新用户信息 * @returns Promise */ async refreshUser(): Promise { try { await this.currentUser.refresh() const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-updateuser * 更新用户信息 * @param params * @returns Promise */ async updateUser(params: UpdateUserReq): Promise { try { // 参数校验:至少有一个更新字段被提供 const hasValue = Object.keys(params).some(key => params[key] !== undefined && params[key] !== null && params[key] !== '',) if (!hasValue) { throw new AuthError({ message: 'At least one field must be provided for update' }) } const { email, phone, ...restParams } = params // 检查是否需要更新 email 或 phone const needsEmailVerification = email !== undefined const needsPhoneVerification = phone !== undefined let extraRes = {} if (needsEmailVerification || needsPhoneVerification) { // 需要发送验证码 let verificationParams: { email?: string; phone_number?: string } let verificationType: 'email_change' | 'phone_change' if (needsEmailVerification) { verificationParams = { email: params.email } verificationType = 'email_change' } else { // 正规化手机号 const formattedPhone = this.formatPhone(params.phone) verificationParams = { phone_number: formattedPhone } verificationType = 'phone_change' } // 发送验证码 const verificationInfo = await this.getVerification(verificationParams) Object.keys(restParams).length > 0 && (await this.updateUserBasicInfo(restParams)) extraRes = { messageId: verificationInfo.verification_id, verifyOtp: async (verifyParams: { email?: string; phone?: string; token: string }): Promise => { try { if (verifyParams.email && params.email === verifyParams.email) { // 验证码验证 await this.verifyOtp({ type: 'email_change', email: params.email, token: verifyParams.token, messageId: verificationInfo.verification_id, }) await this.updateUserBasicInfo({ email: params.email }) } else if (verifyParams.phone && params.phone === verifyParams.phone) { // 验证码验证 await this.verifyOtp({ type: 'phone_change', phone: params.phone, token: verifyParams.token, messageId: verificationInfo.verification_id, }) await this.updateUserBasicInfo({ phone: this.formatPhone(params.phone) }) } else { await this.verifyOtp({ type: verificationType, email: needsEmailVerification ? params.email : undefined, phone: !needsEmailVerification ? params.phone : undefined, token: verifyParams.token, messageId: verificationInfo.verification_id, }) // 验证成功后更新用户信息 await this.updateUserBasicInfo(params) } const { data: { user }, } = await this.getUser() this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED }) return { data: { user }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } }, } } else { // 不需要验证,直接更新 await this.updateUserBasicInfo(params) } const { data: { user }, } = await this.getUser() this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED }) return { data: { user, ...extraRes }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-getuseridentities * 获取所有身份源 * @returns Promise */ async getUserIdentities(): Promise { try { const providers = await this.oauthInstance.authApi.getProviders() return { data: { identities: providers?.data?.filter(v => !!v.bind) as unknown as GetUserIdentitiesRes['data']['identities'] }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-linkidentity * 绑定身份源到当前用户 * @param params * @returns Promise */ async linkIdentity(params: LinkIdentityReq): Promise { try { // 参数校验:provider必填 this.validateParams(params, { provider: { required: true, message: 'Provider is required' }, }) await this.signInWithOAuth({ provider: params.provider, options: { type: OAUTH_TYPE.BIND_IDENTITY, }, }) return { data: { provider: params.provider }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-unlinkidentity * 解绑身份源 * @param params * @returns Promise */ async unlinkIdentity(params: UnlinkIdentityReq): Promise { try { // 参数校验:provider必填 this.validateParams(params, { provider: { required: true, message: 'Provider is required' }, }) await this.oauthInstance.authApi.unbindProvider({ provider_id: params.provider }) return { data: {}, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-reauthentication * 重新认证 * @returns Promise */ async reauthenticate(): Promise { try { const { data: { user }, } = await this.getUser() this.validateAtLeastOne(user, [['email', 'phone']], 'You must provide either an email or phone number') const userInfo = user.email ? { email: user.email } : { phone_number: this.formatPhone(user.phone) } // 第一步:发送验证码并存储 verificationInfo const verificationInfo = await this.getVerification(userInfo) return { data: { // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件) updateUser: async (attributes: UpdateUserAttributes): Promise => { this.validateParams(attributes, { nonce: { required: true, message: 'Nonce is required' }, }) try { if (attributes.password) { // 第三步:待用户输入完验证码之后,验证验证码 const verificationTokenRes = await this.verify({ verification_id: verificationInfo.verification_id, verification_code: attributes.nonce, }) // 第四步:获取 sudo_token const sudoRes = await this.oauthInstance.authApi.sudo({ verification_token: verificationTokenRes.verification_token, }) await this.oauthInstance.authApi.setPassword({ new_password: attributes.password, sudo_token: sudoRes.sudo_token, }) } else { await this.signInWithUsername({ verificationInfo, verificationCode: attributes.nonce, ...userInfo, loginType: userInfo.email ? 'email' : 'phone', }) } const { data: { session } = {} } = await this.getSession() return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } }, }, error: null, } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * https://supabase.com/docs/reference/javascript/auth-resend * 重新发送验证码 * @param params * @returns Promise */ async resend(params: ResendReq): Promise { try { // 参数校验:email或phone必填其一 this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number') const target = params.type === 'signup' ? 'ANY' : 'USER' const data: { email?: string; phone_number?: string; target: 'USER' | 'ANY' } = { target } if ('email' in params) { data.email = params.email } if ('phone' in params) { data.phone_number = this.formatPhone(params.phone) } // 重新发送验证码 const { verification_id: verificationId } = await this.oauthInstance.authApi.getVerification(data) return { data: { messageId: verificationId }, error: null, } } catch (error: any) { return { data: {}, error: new AuthError(error), } } } /** * https://supabase.com/docs/reference/javascript/auth-setsession * 使用access_token和refresh_token来设置会话 * @param params * @returns Promise */ async setSession(params: SetSessionReq): Promise { try { this.validateParams(params, { access_token: { required: true, message: 'Access token is required' }, refresh_token: { required: true, message: 'Refresh token is required' }, }) await this.oauthInstance.oauth2client.refreshToken(params, { throwError: true }) const { data: { session } = {} } = await this.getSession() this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN }) return { data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } // https://supabase.com/docs/reference/javascript/auth-exchangecodeforsession async exchangeCodeForSession() { // } /** * 删除当前用户 * @param params * @returns */ async deleteUser(params: DeleteMeReq): Promise { try { this.validateParams(params, { password: { required: true, message: 'Password is required' }, }) const { sudo_token } = await this.oauthInstance.authApi.sudo(params) await this.oauthInstance.authApi.deleteMe({ sudo_token }) return { data: {}, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 跳转系统默认登录页 * @returns {Promise} * @memberof Auth */ async toDefaultLoginPage(params: authModels.ToDefaultLoginPage = {}): Promise { try { const configVersion = params.config_version || 'env' const query = Object.keys(params.query || {}) .map(key => `${key}=${params.query[key]}`) .join('&') if (adapterForWxMp.isMatch()) { wx.navigateTo({ url: `/packages/$wd_system/pages/login/index${query ? `?${query}` : ''}` }) } else { const redirectUri = params.redirect_uri || window.location.href const urlObj = new URL(redirectUri) const loginPage = `${urlObj.origin}/__auth/?app_id=${params.app_id || ''}&env_id=${this.config.env}&client_id=${ this.config.clientId || this.config.env }&config_version=${configVersion}&redirect_uri=${encodeURIComponent(redirectUri)}${query ? `&${query}` : ''}` window.location.href = loginPage } return { data: {}, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 自定义登录 * @param getTickFn () => Promise, 获取自定义登录 ticket 的函数 * @returns */ async signInWithCustomTicket(getTickFn?: authModels.GetCustomSignTicketFn): Promise { if (getTickFn) { this.setCustomSignFunc(getTickFn) } try { await this.oauthInstance.authApi.signInWithCustomTicket() const loginState = await this.createLoginState() const { data: { session } = {} } = await this.getSession() // loginState返回是为了兼容v2版本 return { ...(loginState as any), data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 小程序openId静默登录 * @param params * @returns Promise */ async signInWithOpenId({ useWxCloud = true } = {}): Promise { if (!adapterForWxMp.isMatch()) { throw Error('wx api undefined') } const wxInfo = wx.getAccountInfoSync().miniProgram const mainFunc = async (code) => { let result: authModels.GrantProviderTokenResponse | undefined = undefined let credentials: Credentials | undefined = undefined try { result = await this.oauthInstance.authApi.grantProviderToken( { provider_id: wxInfo?.appId, provider_code: code, provider_params: { provider_code_type: 'open_id', appid: wxInfo?.appId, }, }, useWxCloud, ) if ((result as any)?.error_code || !result.provider_token) { throw result } credentials = await this.oauthInstance.authApi.signInWithProvider( { provider_token: result.provider_token }, useWxCloud, ) if ((credentials as any)?.error_code) { throw credentials } } catch (error) { throw error } await this.oauthInstance.oauth2client.setCredentials(credentials as Credentials) } try { await new Promise((resolve, reject) => { wx.login({ success: async (res: { code: string }) => { try { await mainFunc(res.code) resolve(true) } catch (error) { reject(error) } }, fail: (res: any) => { const error = new Error(res?.errMsg) ;(error as any).code = res?.errno reject(error) }, }) }) const loginState = await this.createLoginState() const { data: { session } = {} } = await this.getSession() // loginState返回是为了兼容v2版本 return { ...(loginState as any), data: { user: session.user, session }, error: null } } catch (error) { return { data: {}, error: new AuthError(error) } } } /** * 小程序手机号授权登录,目前只支持全托管手机号授权登录 * @param params * @returns Promise */ async signInWithPhoneAuth({ phoneCode = '' }): Promise { if (!adapterForWxMp.isMatch()) { return { data: {}, error: new AuthError({ message: 'wx api undefined' }) } } const wxInfo = wx.getAccountInfoSync().miniProgram const providerInfo = { provider_params: { provider_code_type: 'phone' }, provider_id: wxInfo.appId, } const { code } = await wx.login() ;(providerInfo as any).provider_code = code try { let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo) if (providerToken.error_code) { throw providerToken } providerToken = await this.oauthInstance.authApi.patchProviderToken({ provider_token: providerToken.provider_token, provider_id: wxInfo.appId, provider_params: { code: phoneCode, provider_code_type: 'phone', }, }) if (providerToken.error_code) { throw providerToken } const signInRes = await this.oauthInstance.authApi.signInWithProvider({ provider_token: providerToken.provider_token, }) if ((signInRes as any)?.error_code) { throw signInRes } } catch (error) { return { data: {}, error: new AuthError(error) } } const loginState = await this.createLoginState() const { data: { session } = {} } = await this.getSession() // loginState返回是为了兼容v2版本 return { ...(loginState as any), data: { user: session.user, session }, error: null } } private formatPhone(phone: string) { if (!/\s+/.test(phone) && /^\+\d{1,3}\d+/.test(phone)) { return phone.replace(/^(\+\d{1,2})(\d+)$/, '$1 $2') } return /^\+\d{1,3}\s+/.test(phone) ? phone : `+86 ${phone}` } private notifyListeners(event, session, info): OnAuthStateChangeCallback { this.listeners.forEach((callbacks) => { callbacks.forEach((callback) => { try { callback(event, session, info) } catch (error) { console.error('Error in auth state change callback:', error) } }) }) return } private setupListeners() { this.config.eventBus?.on(EVENTS.AUTH_STATE_CHANGED, async (params) => { const event = params?.data?.event const info = params?.data?.info const { data: { session }, } = await this.getSession() this.notifyListeners(event, session, info) }) } private convertToUser(userInfo: authModels.UserInfo & Partial) { if (!userInfo) return null // 优先使用 userInfo 中的数据(V3 API) const email = userInfo?.email || '' const phone = userInfo?.phone_number || '' const userId = userInfo?.sub || userInfo?.uid || '' return { id: userId, aud: 'authenticated', role: userInfo.groups?.map?.(group => (typeof group === 'string' ? group : group.id)), email: email || '', email_confirmed_at: userInfo?.email_verified ? userInfo.created_at : userInfo.created_at, phone, phone_confirmed_at: phone ? userInfo.created_at : undefined, confirmed_at: userInfo.created_at, last_sign_in_at: (userInfo as any).last_sign_in_at, app_metadata: { provider: userInfo.loginType?.toLowerCase() || 'cloudbase', providers: [userInfo.loginType?.toLowerCase() || 'cloudbase'], }, user_metadata: { // V3 API 用户信息 name: userInfo?.name, picture: userInfo?.picture, username: userInfo?.username, // 用户名称,长度 5-24 位,支持字符中英文、数字、特殊字符(仅支持_-),不支持中文 gender: userInfo?.gender, locale: userInfo?.locale, // V2 API 兼容(使用 any 避免类型错误) uid: userInfo.uid, nickName: userInfo.nickName || userInfo?.name, avatarUrl: userInfo.avatarUrl || userInfo.picture, location: userInfo.location, hasPassword: userInfo.hasPassword, }, identities: userInfo?.providers?.map(p => ({ id: p.id || '', identity_id: p.id || '', user_id: userId, identity_data: { provider_id: p.id, provider_user_id: p.provider_user_id, name: p.name, }, provider: p.id || 'cloudbase', created_at: userInfo.created_at, updated_at: userInfo.updated_at, last_sign_in_at: (userInfo as any).last_sign_in_at, })) || [], created_at: userInfo.created_at, updated_at: userInfo.updated_at, is_anonymous: userInfo.name === 'anonymous', } } /** * 参数校验辅助方法 */ private validateParams(params: any, rules: { [key: string]: { required?: boolean; message: string } }): void { for (const [key, rule] of Object.entries(rules)) { if (rule.required && (params?.[key] === undefined || params?.[key] === null || params?.[key] === '')) { throw new AuthError({ message: rule.message }) } } } /** * 校验必填参数组(至少有一个参数必须有值) */ private validateAtLeastOne(params: any, fieldGroups: string[][], message: string): void { const hasValue = fieldGroups.some(group => group.some(field => params?.[field] !== undefined && params?.[field] !== null && params?.[field] !== ''),) if (!hasValue) { throw new AuthError({ message }) } } private async init(): Promise<{ error: Error | null }> { try { const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials() if (credentials) { this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.INITIAL_SESSION }) } } catch (error) { // Ignore errors when checking for existing credentials } return { error: null } } } type TInitAuthOptions = Pick & Partial & { detectSessionInUrl?: boolean } export function generateAuthInstance( config: TInitAuthOptions & { sdkVersion?: string }, options?: { clientId: ICloudbaseConfig['clientId'] env: ICloudbaseConfig['env'] apiOrigin: string cache?: ICloudbaseCache platform?: ICloudbase['platform'] app?: ICloudbase debug?: ICloudbaseAuthConfig['debug'] detectSessionInUrl?: ICloudbaseAuthConfig['detectSessionInUrl'] }, ) { const { region = 'ap-shanghai', i18n, accessKey, useWxCloud } = config const platform = options?.platform || (useDefaultAdapter.bind(options)() as ICloudbasePlatformInfo) const { runtime, adapter } = platform const { env, clientId, debug, cache, app: cloudbase } = options || {} let { apiOrigin } = options || {} if (!apiOrigin) { apiOrigin = `https://${env}.${region}.tcb-api.tencentcloudapi.com` } const commonOpts = { env, clientId, i18n, accessKey, useWxCloud, eventBus: new CloudbaseEventEmitter(), } const oauthInstance = new CloudbaseOAuth(useAuthAdapter({ ...commonOpts, apiOrigin, apiPath: config?.apiPath || AUTH_API_PREFIX, // @todo 以下最好走adaptor处理,目前oauth模块没按adaptor接口设计 storage: config?.storage, baseRequest: config?.baseRequest, request: config?.request, anonymousSignInFunc: config?.anonymousSignInFunc, captchaOptions: config?.captchaOptions, wxCloud: config?.wxCloud, adapter, onCredentialsError: onCredentialsError(commonOpts.eventBus), headers: { 'X-SDK-Version': `@cloudbase/js-sdk/${config.sdkVersion}`, ...(config.headers || {}) }, detectSessionInUrl: config.detectSessionInUrl, debug, }),) const authInstance = new Auth({ ...commonOpts, region, persistence: config.persistence, debug, cache: cache || new CloudbaseCache({ persistence: config.persistence, keys: { userInfoKey: `user_info_${env}` }, platformInfo: platform, }), runtime: runtime || 'web', _fromApp: cloudbase, oauthInstance, }) // Initialize session with user info callback // This handles OAuth callback URL detection and creates login state atomically oauthInstance.initializeSession(async (data, error) => { if (!data) return if (data.type === OAUTH_TYPE.SIGN_IN) { if (error) { commonOpts.eventBus.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN, info: { ...data, error }, }) } else if (data.user) { // 使用已获取的 user 信息创建 LoginState,复用 createLoginState 逻辑 // 但跳过 refresh() 避免再次请求 API 导致死锁 authInstance.createLoginState({}, { userInfo: data.user }) } } else if (data.type === OAUTH_TYPE.BIND_IDENTITY) { commonOpts.eventBus.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.BIND_IDENTITY, info: { ...data, error }, }) } }) return { authInstance, oauthInstance } } const NAMESPACE = 'auth' const component: ICloudbaseComponent = { name: COMPONENT_NAME, namespace: NAMESPACE, entity(config?: TInitAuthOptions) { const auth = function (config?: TInitAuthOptions) { if (this.authInstance && !config) { // printWarn(ERRORS.INVALID_OPERATION, 'every cloudbase instance should has only one auth object') return this.authInstance } config = config || { region: '', persistence: 'local', apiPath: AUTH_API_PREFIX, } const { adapter } = this.platform // 如不明确指定persistence则优先取各平台adapter首选,其次localStorage const newPersistence = config.persistence || adapter.primaryStorage if (newPersistence && newPersistence !== this.config.persistence) { this.updateConfig({ persistence: newPersistence }) } const { authInstance, oauthInstance } = generateAuthInstance( { wxCloud: this.config.wxCloud, storage: this.config.storage, ...config, persistence: this.config.persistence, i18n: this.config.i18n, accessKey: this.config.accessKey, useWxCloud: this.config.useWxCloud, sdkVersion: this.version, detectSessionInUrl: this.config.auth?.detectSessionInUrl, }, { env: this.config.env, clientId: this.config.clientId, apiOrigin: this.request.getBaseEndPoint(this.config.endPointMode || 'CLOUD_API'), platform: this.platform, cache: this.cache, app: this, debug: this.config.debug, }, ) this.oauthInstance = oauthInstance this.authInstance = authInstance return this.authInstance } const authProto = auth.call(this, config) Object.assign(auth, authProto) Object.setPrototypeOf(auth, Object.getPrototypeOf(authProto)) this[NAMESPACE] = auth return auth }, } try { // 尝试自动注册至全局变量cloudbase // 此行为只在浏览器环境下有效 cloudbase.registerComponent(component) } catch (e) {} export { UserInfo, Auth } /** * @api 手动注册至cloudbase app */ export function registerAuth(app: Pick) { try { app.registerComponent(component) } catch (e) { console.warn(e) } }