import { Feature } from 'oak-frontend-base/es/types/Feature'; import { isOakException, OakUnloggedInException, OakNetworkException, OakServerProxyException, OakPreConditionUnsetException, OakRequestTimeoutException, OakClockDriftException } from 'oak-domain/lib/types/Exception'; import { tokenProjection } from '../types/Projection'; import { OakPasswordUnset, OakUserInfoLoadingException } from '../types/Exception'; import { LOCAL_STORAGE_KEYS } from '../config/constants'; import { cloneDeep } from 'oak-domain/lib/utils/lodash'; export class Token extends Feature { tokenValue; environment; cache; storage; application; ignoreExceptionList = [OakNetworkException, OakServerProxyException, OakRequestTimeoutException, OakClockDriftException]; async loadSavedToken() { this.tokenValue = await this.storage.load(LOCAL_STORAGE_KEYS.token); await this.refreshTokenData(this.tokenValue); this.publish(); } constructor(cache, storage, environment, application) { super(); this.cache = cache; this.storage = storage; this.environment = environment; this.application = application; this.tokenValue = ''; // 置个空字符串代表还在load storage缓存的数据 // this.loadSavedToken(); application.subscribe(() => this.loadSavedToken()); if (process.env.OAK_PLATFORM === 'web' && (process.env.NODE_ENV !== 'development' || process.env.OAK_DEV_MODE === 'server')) { // 纯前台模式 多窗口时不监听storage // 在web下可能多窗口,一个窗口更新了token,其它窗口应跟着变 window.addEventListener('storage', async (e) => { if (e.key === LOCAL_STORAGE_KEYS.token) { this.tokenValue = e.newValue ? JSON.parse(e.newValue) : undefined; await this.refreshTokenData(this.tokenValue); this.publish(); } }); } } checkNeedSetPassword() { const user = this.getUserInfo(); const { system } = this.application.getApplication(); const { Security } = system.config; if (Security?.level === 'strong' && user && !user.hasPassword) { return Promise.reject(new OakPasswordUnset()); } } async refreshTokenData(tokenValue) { if (!tokenValue) { this.tokenValue = undefined; return; } const env = await this.environment.getEnv(); try { const applicationId = this.application.getApplicationId(); const { result } = await this.cache.exec('refreshToken', { tokenValue, env, applicationId, }, undefined, true, true); if (tokenValue !== result) { // 如果返回空字符串,token被disabled,tokenValue置为undefined if (result) { this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.checkNeedSetPassword(); } else { this.removeToken(true); } } else { this.checkNeedSetPassword(); } } catch (err) { // refresh出了任何错都无视(排除网络异常),直接放弃此token console.warn(err); // if ( // err instanceof OakNetworkException || // err instanceof OakServerProxyException || // err instanceof OakRequestTimeoutException || // err instanceof OakClockDriftException // ) { // return; // } if (this.ignoreExceptionList.some((clazz) => { return isOakException(err, clazz); })) { return; } this.removeToken(true); } } async loginByMobile(mobile, captcha, disableRegister) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginByMobile', { mobile, captcha, disableRegister, env, }, undefined, true); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginByEmail(email, captcha, disableRegister) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginByEmail', { email, captcha, disableRegister, env, }, undefined, true); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginByAccount(account, password) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginByAccount', { account, password, env, }, undefined, true); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async bindByMobile(mobile, captcha) { const env = await this.environment.getEnv(); await this.cache.exec('bindByMobile', { mobile, captcha, env, }, undefined, true); this.publish(); } async bindByEmail(email, captcha) { const env = await this.environment.getEnv(); await this.cache.exec('bindByEmail', { email, captcha, env, }, undefined, true); this.publish(); } async loginWebByMpToken(mpToken) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWebByMpToken', { mpToken, env, }, undefined, true); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginByWechatInWebEnv(wechatLoginId) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginByWechat', { env: env, wechatLoginId, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginByOAuth(code, state) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginByOauth', { env: env, code, state, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginWechat(code, params) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechat', { code, env: env, wechatLoginId: params?.wechatLoginId, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginWechatMp(params) { const { code } = await wx.login(); const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechatMp', { code, env: env, wechatLoginId: params?.wechatLoginId, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async loginWechatNative(code) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('loginWechatNative', { code, env: env, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); this.checkNeedSetPassword(); } async syncUserInfoWechatMp() { // 2.27.1及以上版本不再支持getUserProfile(返回头像为【灰色头像】,昵称为【微信用户】) const info = await wx.getUserProfile({ desc: '同步微信昵称和头像信息', }); const { userInfo: { nickName: nickname, avatarUrl }, encryptedData, signature, iv, } = info; await this.cache.exec('syncUserInfoWechatMp', { nickname, avatarUrl, encryptedData, signature, iv, }); this.publish(); } async logout(dontPublish) { await this.cache.exec('logout', { tokenValue: this.tokenValue, }, undefined, undefined, true); this.removeToken(dontPublish); } removeToken(dontPublish) { this.tokenValue = undefined; this.storage.remove(LOCAL_STORAGE_KEYS.token); if (!dontPublish) { this.publish(); } } getTokenValue() { if (this.tokenValue === '') { throw new OakUserInfoLoadingException(); } return this.tokenValue; } getToken(allowUnloggedIn) { if (this.tokenValue === '') { throw new OakUserInfoLoadingException(); } if (this.tokenValue) { const token = this.cache.get('token', { data: cloneDeep(tokenProjection), filter: { value: this.tokenValue, }, })[0]; if (!token) { if (allowUnloggedIn) { return undefined; } throw new OakUserInfoLoadingException(); } return token; } if (allowUnloggedIn) { return undefined; } throw new OakUnloggedInException(); } getUserId(allowUnloggedIn) { const token = this.getToken(allowUnloggedIn); if (token?.userId) { return token.userId; } } // getUserInfo 不要求登录 getUserInfo() { const token = this.getToken(true); if (token?.user) { return token.user; } } isRoot() { const token = this.getToken(true); return !!token?.user?.isRoot; } /** * 这个是指token的player到底是不是root * @returns */ isReallyRoot() { const token = this.getToken(true); return !!token?.player?.isRoot; } isSelf() { const token = this.getToken(); return token?.playerId === token?.userId; } async sendCaptcha(origin, content, type) { const env = await this.environment.getEnv(); if (origin === 'mobile') { const { result } = await this.cache.exec('sendCaptchaByMobile', { mobile: content, env: env, type, }); return result; } else { const { result } = await this.cache.exec('sendCaptchaByEmail', { email: content, env: env, type, }); return result; } } async switchTo(userId) { const currentUserId = this.getUserId(); if (currentUserId === userId) { throw new OakPreConditionUnsetException('您已经是当前用户'); } await this.cache.exec('switchTo', { userId, }); this.publish(); } async refreshWechatPublicUserInfo() { await this.cache.exec('refreshWechatPublicUserInfo', {}); } async getWechatMpUserPhoneNumber(code) { const env = await this.environment.getEnv(); await this.cache.exec('getWechatMpUserPhoneNumber', { code, env: env, }); } async wakeupParasite(id) { const env = await this.environment.getEnv(); const { result } = await this.cache.exec('wakeupParasite', { id, env: env, }); this.tokenValue = result; await this.storage.save(LOCAL_STORAGE_KEYS.token, result); this.publish(); } needVerifyPassword() { const user = this.getUserInfo(); const { system } = this.application.getApplication(); const { Security } = system.config; if (Security && ['strong', 'medium'].includes(Security.level)) { // 对于安全要求中高的系统,需要检查其验证密码时间 const stamp = Date.now() - Security.passwordVerifyGap; return user?.hasPassword && user.verifyPasswordAt < stamp; } } async verifyPassword(password) { const env = this.environment.getEnv(); await this.cache.exec('verifyPassword', { password, env, }); } /** * 添加一个异常到忽略列表,如果refreshToken时出现这个异常,不会强制用户登出 */ addIgnoreException(clazz) { if (!this.ignoreExceptionList.includes(clazz)) { this.ignoreExceptionList.push(clazz); } } removeIgnoreException(clazz) { this.ignoreExceptionList = this.ignoreExceptionList.filter((c) => c !== clazz); } }