import { ICloudbase } from '@cloudbase/types' import { utils, constants, helpers, events } from '@cloudbase/utilities' import { ICloudbaseCache } from '@cloudbase/types/cache' import { ICloudbaseRequest } from '@cloudbase/types/request' import { ICloudbaseAuthConfig, IUser, IUserInfo, ILoginState } from '@cloudbase/types/auth' import { ICloudbaseComponent } from '@cloudbase/types/component' import { authModels, CloudbaseOAuth, AuthOptions } from '@cloudbase/oauth' declare const cloudbase: ICloudbase const { printWarn, throwError } = utils const { ERRORS, COMMUNITY_SITE_URL } = constants const { catchErrorsDecorator } = helpers const { CloudbaseEventEmitter } = events const COMPONENT_NAME = 'auth' const EVENTS = { // 登录态改变后触发 LOGIN_STATE_CHANGED: 'loginStateChanged', } 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 eventBus = new CloudbaseEventEmitter() interface IUserOptions { cache: ICloudbaseCache // request: ICloudbaseRequest oauthInstance: CloudbaseOAuth } 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; 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) } /** * 更新密码 * @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(): Promise { const newUserInfo = await this.oauthInstance.authApi.getUserInfo() this.setLocalUserInfo(newUserInfo) return newUserInfo } private getLocalUserInfo(key: string): string | boolean { const { userInfoKey } = this.cache.keys const userInfo = this.cache.getStore(userInfoKey) return userInfo[key] } 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', ].forEach((infoKey) => { this[infoKey] = userInfo[infoKey] }) } private setLocalUserInfo(userInfo: any) { const { userInfoKey } = this.cache.keys this.cache.setStore(userInfoKey, userInfo) this.setUserInfo() } } interface ILoginStateOptions extends IUserOptions { envId: string } export class LoginState implements ILoginState { public user: IUser; 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 { private readonly config: ICloudbaseAuthConfig; private readonly cache: ICloudbaseCache; private oauthInstance: CloudbaseOAuth; constructor(config: ICloudbaseAuthConfig & { cache: ICloudbaseCache, request?: ICloudbaseRequest, runtime?: string }) { this.config = config this.cache = config.cache this.oauthInstance = config.oauthInstance } /** * 绑定手机号 * @param phoneNumber * @param phoneCode */ @catchErrorsDecorator({ title: '绑定手机号失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().bindPhoneNumber() 的语法或参数是否正确', ' 2 - 当前环境是否开通了短信验证码登录', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async bindPhoneNumber(params: authModels.BindPhoneRequest) { return this.oauthInstance.authApi.bindPhone(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.bindEmail(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): Promise { return this.oauthInstance.authApi.getVerification(params) } /** * 获取当前登录的用户信息-同步 */ get currentUser() { if (this.cache.mode === 'async') { // async storage的平台调用此API提示 printWarn( ERRORS.INVALID_OPERATION, 'current platform\'s storage is asynchronous, please use getCurrentUser insteed' ) return } const loginState = this.hasLoginState() if (loginState) { return loginState.user || null } return null } /** * 获取当前登录的用户信息-异步 */ @catchErrorsDecorator({ title: '获取用户信息失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().getCurrenUser() 的语法或参数是否正确', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getCurrentUser() { const loginState = await this.getLoginState() if (loginState) { await loginState.user.checkLocalInfoAsync() return loginState.user || null } 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() } /** * 设置获取自定义登录 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() } /** * * @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 } /** * 登出 */ @catchErrorsDecorator({ title: '用户登出失败', messages: [ '请确认以下各项:', ' 1 - 调用 auth().signOut() 的语法或参数是否正确', ' 2 - 当前用户是否为匿名登录(匿名登录不支持signOut)', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async signOut() { const { userInfoKey } = this.cache.keys await this.oauthInstance.authApi.signOut() await this.cache.removeStoreAsync(userInfoKey) eventBus.fire(EVENTS.LOGIN_STATE_CHANGED) } /** * 获取本地登录态-同步 */ 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 insteed' ) 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() { const oauthLoginState = await this.oauthInstance.authApi.getLoginState() 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 { return this.oauthInstance.authApi.getUserInfo() } /** * 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() } 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) { eventBus.on(EVENTS.LOGIN_STATE_CHANGED, async () => { const loginState = await this.getLoginState() callback.call(this, loginState) }) // 立刻执行一次回调 const loginState = await this.getLoginState() callback.call(this, loginState) } private async createLoginState(): Promise { const loginState = new LoginState({ envId: this.config.env, cache: this.cache, oauthInstance: this.oauthInstance, }) await loginState.checkLocalStateAsync() await loginState.user.refresh() eventBus.fire(EVENTS.LOGIN_STATE_CHANGED) return loginState } } const component: ICloudbaseComponent = { name: COMPONENT_NAME, namespace: 'auth', entity(config: Pick & Partial = { region: '', persistence: 'local' }) { if (this.authInstance) { printWarn(ERRORS.INVALID_OPERATION, 'every cloudbase instance should has only one auth object') return this.authInstance } const { adapter, runtime } = this.platform // 如不明确指定persistence则优先取各平台adapter首选,其次localStorage const newPersistence = config.persistence || adapter.primaryStorage if (newPersistence && (newPersistence !== this.config.persistence)) { this.updateConfig({ persistence: newPersistence }) } const { env, persistence, debug, clientId, storage } = this.config const oauthInstance = new CloudbaseOAuth({ clientId, apiOrigin: this.request.getBaseEndPoint(), // @todo 以下最好走adaptor处理,目前oauth模块没按adaptor接口设计 storage: config?.storage || storage, request: config?.request, anonymousSignInFunc: config?.anonymousSignInFunc, }) this.oauthInstance = oauthInstance this.authInstance = new Auth({ env, region: config.region, persistence, debug, cache: this.cache, // request: this.request, runtime, _fromApp: this, // oauthInstance: this.oauthInstance || (this as any).oauth() oauthInstance, }) return this.authInstance }, } 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) } }