import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import WechatSDK from 'oak-external-sdk/lib/WechatSDK'; import { assert } from 'oak-domain/lib/utils/assert'; import { OakPreConditionUnsetException, OakRowInconsistencyException, OakUnloggedInException, OakUserException, OakOperationUnpermittedException, } from 'oak-domain/lib/types'; import { composeFileUrlBackend } from '../utils/cos/index.backend'; import { OakChangeLoginWayException, OakDistinguishUserException, OakIncompleteConfig, OakUserDisabledException, } from '../types/Exception'; import { encryptPasswordSha1 } from '../utils/password'; import { tokenProjection } from '../types/Projection'; import { sendSms } from '../utils/sms'; import { mergeUser } from './user'; import { cloneDeep } from 'oak-domain/lib/utils/lodash'; import { sendEmail } from '../utils/email'; import { isEmail, isMobile } from 'oak-domain/lib/utils/validator'; import { getAndCheckPassportByEmail } from '../utils/passport'; async function makeDistinguishException(userId, context, message) { const [user] = await context.select('user', { data: { id: 1, password: 1, passwordSha1: 1, idState: 1, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, email$user: { $entity: 'email', data: { id: 1, email: 1, }, }, mobile$user: { $entity: 'mobile', data: { id: 1, mobile: 1, }, }, }, filter: { id: userId, }, }, { dontCollect: true, }); assert(user); const { password, passwordSha1, idState, wechatUser$user, email$user, mobile$user } = user; return new OakDistinguishUserException(userId, !!(password || passwordSha1), idState === 'verified', !!wechatUser$user?.length, !!email$user?.length, !!mobile$user?.length, message); } async function tryMakeChangeLoginWay(userId, context) { const [user] = await context.select('user', { data: { id: 1, idState: 1, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, email$user: { $entity: 'email', data: { id: 1, email: 1, }, }, mobile$user: { $entity: 'mobile', data: { id: 1, mobile: 1, }, }, }, filter: { id: userId, }, }, { dontCollect: true, }); assert(user); const { idState, wechatUser$user, email$user, mobile$user } = user; if (idState === 'verified' || (wechatUser$user && wechatUser$user.length > 0) || (email$user && email$user.length > 0)) { return new OakChangeLoginWayException(userId, idState === 'verified', !!(wechatUser$user && wechatUser$user.length > 0), !!(email$user && email$user.length > 0), !!(mobile$user && mobile$user.length > 0)); } } async function dealWithUserState(user, context, tokenData) { switch (user.userState) { case 'disabled': { throw new OakUserDisabledException(); } case 'shadow': { return { userId: user.id, user: { id: await generateNewIdAsync(), action: 'activate', data: {}, }, }; } case 'merged': { assert(user?.refId); const [user2] = await context.select('user', { data: { id: 1, userState: 1, refId: 1, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, userSystem$user: { $entity: 'userSystem', data: { id: 1, systemId: 1, }, }, }, filter: { id: user.refId, }, }, { dontCollect: true, }); return await dealWithUserState(user2, context, tokenData); } default: { assert(user.userState === 'normal'); return { userId: user.id, }; } } } function autoMergeUser(context) { const { system } = context.getApplication(); return !!system.config.App.mergeUserDirectly; } /** * 根据user的不同情况,完成登录动作 * @param env * @param context * @param user * @return tokenValue */ export async function setUpTokenAndUser(env, context, entity, // 支持更多的登录渠道使用此函数创建token entityId, // 如果是现有对象传id,如果没有对象传createData createData, user) { const currentToken = context.getToken(true); const schema = context.getSchema(); assert(schema.hasOwnProperty(entity), `${entity}必须是有效的对象名 `); assert(schema.token.attributes.entity.ref.includes(entity), `${entity}必须是token的有效关联对象`); assert(schema[entity].attributes.hasOwnProperty('userId') && schema[entity].attributes.userId.ref === 'user', `${entity}必须有指向user的userId属性`); if (currentToken) { assert(currentToken.id); assert(currentToken.userId); if (user) { // 有用户,和当前用户进行合并 const { userState } = user; switch (userState) { case 'normal': { if (currentToken.userId === user.id) { return currentToken.value; } const autoMerge = autoMergeUser(context); if (autoMerge) { await mergeUser({ from: user.id, to: currentToken.userId, mergeMobile: true, mergeWechatUser: true, mergeEmail: true }, context, true); return currentToken.value; } throw await makeDistinguishException(user.id, context); } case 'shadow': { assert(currentToken.userId !== user.id); const autoMerge = autoMergeUser(context); if (autoMerge) { await mergeUser({ from: user.id, to: currentToken.userId, mergeMobile: true, mergeWechatUser: true, mergeEmail: true }, context, true); return currentToken.value; } throw await makeDistinguishException(user.id, context); } case 'disabled': { throw new OakUserDisabledException(); } case 'merged': { assert(user.refId); if (user.refId === currentToken.userId) { return currentToken.value; } const autoMerge = autoMergeUser(context); if (autoMerge) { // 说明一个用户被其他用户merge了,现在还是暂时先merge,后面再说 console.warn(`用户${user.id}已经是merged状态「${user.refId}」,再次被merged到「${currentToken.userId}]」`); await mergeUser({ from: user.id, to: currentToken.userId, mergeMobile: true, mergeWechatUser: true, mergeEmail: true }, context, true); return currentToken.value; } throw await makeDistinguishException(user.id, context); } default: { assert(false, `不能理解的user状态「${userState}」`); } } } else { // 没用户,指向当前用户 assert(createData && !entityId); if (createData) { await context.operate(entity, { id: await generateNewIdAsync(), action: 'create', data: Object.assign(createData, { userId: currentToken.userId, }), }, { dontCollect: entity !== 'mobile' }); } else { assert(entityId); await context.operate(entity, { id: await generateNewIdAsync(), action: 'update', data: { userId: currentToken.userId, }, filter: { id: entityId, }, }, { dontCollect: entity !== 'mobile' }); } return currentToken.value; } } else { /* if (entityId) { // 已经有相应对象,判定一下能否重用上一次的token // 不再重用了 const application = context.getApplication(); const [originToken] = await context.select( 'token', { data: { id: 1, value: 1, }, filter: { applicationId: application!.id, ableState: 'enabled', entity, entityId: entityId, }, }, { dontCollect: true } ); if (originToken) { return originToken.value!; } } */ const tokenData = { id: await generateNewIdAsync(), env, refreshedAt: Date.now(), value: await generateNewIdAsync(), }; if (user) { // 根据此用户状态进行处理 const { userState } = user; let userId = user.id; switch (userState) { case 'normal': { break; } case 'merged': { userId = user.refId; break; } case 'disabled': { throw new OakUserDisabledException(); } case 'shadow': { await context.operate('user', { id: await generateNewIdAsync(), action: 'activate', data: {}, filter: { id: userId, }, }, { dontCollect: true }); break; } default: { assert(false, `不能理解的user状态「${userState}」`); } } tokenData.userId = userId; tokenData.applicationId = context.getApplicationId(); tokenData.playerId = userId; if (entityId) { tokenData.entity = entity; tokenData.entityId = entityId; } else { assert(createData); Object.assign(tokenData, { [entity]: { id: await generateNewIdAsync(), action: 'create', data: Object.assign(createData, { userId, }), }, }); } await context.operate('token', { id: await generateNewIdAsync(), action: 'create', data: tokenData, }, { dontCollect: true }); return tokenData.value; } else { // 创建新用户 // 要先create token,再create entity。不然可能权限会被挡住 const userData = { id: await generateNewIdAsync(), userState: 'normal', }; await context.operate('user', { id: await generateNewIdAsync(), action: 'create', data: userData, }, {}); assert(entityId || createData.id, 'entityId和createData必须存在一项'); tokenData.userId = userData.id; tokenData.playerId = userData.id; tokenData.entity = entity; tokenData.entityId = entityId || createData.id; tokenData.applicationId = context.getApplicationId(); await context.operate('token', { id: await generateNewIdAsync(), action: 'create', data: tokenData, }, { dontCollect: true }); await context.setTokenValue(tokenData.value); if (createData) { await context.operate(entity, { id: await generateNewIdAsync(), action: 'create', data: Object.assign(createData, { userId: userData.id, }), }, { dontCollect: true }); } else { assert(entityId); await context.operate(entity, { id: await generateNewIdAsync(), action: 'update', data: { userId: userData.id, }, filter: { id: entityId, }, }, { dontCollect: true }); } return tokenData.value; } } } async function setupMobile(mobile, env, context) { const result2 = await context.select('mobile', { data: { id: 1, mobile: 1, userId: 1, ableState: 1, user: { id: 1, userState: 1, refId: 1, ref: { id: 1, userState: 1, refId: 1, }, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, userSystem$user: { $entity: 'userSystem', data: { id: 1, systemId: 1, }, }, }, }, filter: { mobile, ableState: 'enabled', }, }, { dontCollect: true }); if (result2.length > 0) { // 此手机号已经存在 assert(result2.length === 1); const [mobileRow] = result2; const { user } = mobileRow; const { userState, ref } = user; if (userState === 'merged') { return await setUpTokenAndUser(env, context, 'mobile', mobileRow.id, undefined, ref); } return await setUpTokenAndUser(env, context, 'mobile', mobileRow.id, undefined, user); } else { //此手机号不存在 return await setUpTokenAndUser(env, context, 'mobile', undefined, { id: await generateNewIdAsync(), mobile, }); } } export async function loadTokenInfo(tokenValue, context) { return await context.select('token', { data: cloneDeep(tokenProjection), filter: { value: tokenValue, }, }, {}); } export async function loginByMobile(params, context) { const { mobile, captcha, env, disableRegister } = params; const loginLogic = async (isRoot) => { const systemId = context.getSystemId(); if (isRoot) { return await setupMobile(mobile, env, context); } const result = await context.select('captcha', { data: { id: 1, expired: 1, }, filter: { origin: 'mobile', content: mobile, code: captcha, }, sorter: [ { $attr: { $$createAt$$: 1, }, $direction: 'desc', }, ], indexFrom: 0, count: 1, }, { dontCollect: true }); if (result.length > 0) { const [captchaRow] = result; if (captchaRow.expired) { throw new OakUserException('验证码已经过期'); } await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaRow.id } }, {}); // 到这里说明验证码已经通过 return await setupMobile(mobile, env, context); } else { throw new OakUserException('验证码无效'); } }; const isRoot = context.isRoot(); const closeRootMode = context.openRootMode(); const application = context.getApplication(); const [applicationPassport] = await context.select('applicationPassport', { data: { id: 1, passportId: 1, passport: { id: 1, config: 1, type: 1, }, applicationId: 1, }, filter: { applicationId: application?.id, passport: { type: 'sms' }, } }, { dontCollect: true, }); assert(applicationPassport?.passport); if (disableRegister) { const [existMobile] = await context.select('mobile', { data: { id: 1, mobile: 1, }, filter: { mobile: mobile, ableState: 'enabled', }, }, { dontCollect: true }); if (!existMobile) { closeRootMode(); throw new OakUserException('账号不存在'); } } const tokenValue = await loginLogic(isRoot); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } export async function verifyPassword(params, context) { const { password } = params; const { system } = context.getApplication(); const pwdConfig = system?.config.Password; const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}; if (pwdMode === 'all') { pwdFilter = { $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ] }; } else if (pwdMode === 'plain') { pwdFilter = { password, }; } else if (pwdMode === 'sha1') { pwdFilter = { passwordSha1: password, }; } const userId = context.getCurrentUserId(); const [user] = await context.select('user', { data: {}, filter: { id: userId, ...pwdFilter, } }, {}); if (!user) { throw new OakUserException('error::user.passwordUnmatch'); } await context.operate('user', { id: await generateNewIdAsync(), action: 'update', data: { verifyPasswordAt: Date.now(), }, filter: { id: userId, } }, {}); } export async function loginByAccount(params, context) { const { account, password, env } = params; let needUpdatePassword = false; const loginLogic = async () => { const systemId = context.getSystemId(); const applicationId = context.getApplicationId(); assert(password); assert(account); const accountType = isEmail(account) ? 'email' : (isMobile(account) ? 'mobile' : 'loginName'); const applicationPassports = await context.select('applicationPassport', { data: { id: 1, applicationId: 1, passportId: 1, passport: { id: 1, type: 1, systemId: 1, }, allowPwd: 1, }, filter: { passport: { systemId, }, applicationId, allowPwd: true, } }, { dontCollect: true, }); const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); if (accountType === 'email') { const { config, emailConfig } = await getAndCheckPassportByEmail(context, account); if (!allowEmail) { throw new OakUserException('暂不支持邮箱登录'); } const existEmail = await context.select('email', { data: { id: 1, email: 1, }, filter: { email: account, }, }, { dontCollect: true, }); if (!(existEmail && existEmail.length > 0)) { throw new OakUserException('error::user.emailUnexists'); } const result = await context.select('user', { data: { id: 1, password: 1, passwordSha1: 1, email$user: { $entity: 'email', data: { id: 1, email: 1, ableState: 1, } }, }, filter: { $and: [ { email$user: { email: account, } }, { ...pwdFilter, } ], }, }, { dontCollect: true }); switch (result.length) { case 0: { throw new OakUserException('error::user.passwordUnmath'); } case 1: { const [userRow] = result; const { email$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); const email = email$user?.find(ele => ele.email.toLowerCase() === account.toLowerCase()); if (email) { const ableState = email.ableState; if (ableState === 'disabled') { // 虽然密码和邮箱匹配,但邮箱已经禁用了,在可能的情况下提醒用户使用其它方法登录 const exception = await tryMakeChangeLoginWay(userId, context); if (exception) { throw exception; } } return await setupEmail(account, env, context); } else { throw new OakUserException('error::user.emailUnexists'); } } default: { throw new OakUserException('error::user.loginWayDisabled'); } } } else if (accountType === 'mobile') { if (!allowSms) { throw new OakUserException('暂不支持手机号登录'); } const existMobile = await context.select('mobile', { data: { id: 1, mobile: 1, }, filter: { mobile: account, }, }, { dontCollect: true, }); if (!(existMobile && existMobile.length > 0)) { throw new OakUserException('手机号未注册'); } const result = await context.select('user', { data: { id: 1, password: 1, passwordSha1: 1, mobile$user: { $entity: 'mobile', data: { id: 1, mobile: 1, ableState: 1, }, }, }, filter: { $and: [ { mobile$user: { mobile: account, } }, { ...pwdFilter, } ], }, }, { dontCollect: true }); switch (result.length) { case 0: { throw new OakUserException('手机号与密码不匹配'); } case 1: { const [userRow] = result; const { mobile$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); const mobile = mobile$user?.find(ele => ele.mobile === account); if (mobile) { const ableState = mobile.ableState; if (ableState === 'disabled') { // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 const exception = await tryMakeChangeLoginWay(userId, context); if (exception) { throw exception; } } return await setupMobile(account, env, context); } else { throw new OakUserException('手机号未注册'); } } default: { throw new OakUserException('error::user.loginWayDisabled'); } } } else { const existLoginName = await context.select('loginName', { data: { id: 1, name: 1, }, filter: { name: account, }, }, { dontCollect: true, }); if (!(existLoginName && existLoginName.length > 0)) { throw new OakUserException('账号未注册'); } const result = await context.select('user', { data: { id: 1, password: 1, passwordSha1: 1, loginName$user: { $entity: 'loginName', data: { id: 1, name: 1, ableState: 1, } } }, filter: { $and: [ { loginName$user: { name: account, } }, { ...pwdFilter, } ], }, }, { dontCollect: true }); switch (result.length) { case 0: { throw new OakUserException('账号与密码不匹配'); } case 1: { const [userRow] = result; needUpdatePassword = !(userRow.password || userRow.passwordSha1); const { loginName$user, id: userId, } = userRow; const loginName = loginName$user?.find((ele) => ele.name.toLowerCase() === account.toLowerCase()); if (loginName) { const ableState = loginName.ableState; if (ableState === 'disabled') { // 虽然密码和账号匹配,但账号已经禁用了,在可能的情况下提醒用户使用其它方法登录 const exception = await tryMakeChangeLoginWay(userId, context); if (exception) { throw exception; } } return await setupLoginName(account, env, context); } else { throw new OakUserException('账号不存在'); } } default: { throw new OakUserException('error::user.loginWayDisabled'); } } } }; const closeRootMode = context.openRootMode(); const { system } = context.getApplication(); const pwdConfig = system?.config.Password; const pwdMode = pwdConfig?.mode ?? 'all'; let pwdFilter = {}, updateData = {}; if (pwdMode === 'all') { pwdFilter = { $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ], }; updateData = { password, passwordSha1: encryptPasswordSha1(password), }; } else if (pwdMode === 'plain') { pwdFilter = { password, }; updateData = { password, }; } else if (pwdMode === 'sha1') { pwdFilter = { passwordSha1: password, }; updateData = { passwordSha1: password, }; } const tokenValue = await loginLogic(); const [tokenInfo] = await loadTokenInfo(tokenValue, context); const { userId } = tokenInfo; await context.operate('user', { id: await generateNewIdAsync(), action: 'update', data: needUpdatePassword ? { verifyPasswordAt: Date.now(), ...updateData, } : { verifyPasswordAt: Date.now(), }, filter: { id: userId, } }, {}); closeRootMode(); return tokenValue; } export async function loginByEmail(params, context) { const { email, captcha, env, disableRegister } = params; const loginLogic = async () => { const systemId = context.getSystemId(); const result = await context.select('captcha', { data: { id: 1, expired: 1, }, filter: { origin: 'email', content: email, code: captcha, }, sorter: [ { $attr: { $$createAt$$: 1, }, $direction: 'desc', }, ], indexFrom: 0, count: 1, }, { dontCollect: true }); if (result.length > 0) { const [captchaRow] = result; if (captchaRow.expired) { throw new OakUserException('验证码已经过期'); } // 到这里说明验证码已经通过,可以让验证码失效 await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaRow.id } }, {}); return await setupEmail(email, env, context); } else { throw new OakUserException('验证码无效'); } }; const closeRootMode = context.openRootMode(); const { config, emailConfig } = await getAndCheckPassportByEmail(context, email); if (disableRegister) { const [existEmail] = await context.select('email', { data: { id: 1, email: 1, }, filter: { email: email, ableState: 'enabled', }, }, { dontCollect: true }); if (!existEmail) { closeRootMode(); throw new OakUserException('账号不存在'); } } const tokenValue = await loginLogic(); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } export async function bindByMobile(params, context) { const { mobile, captcha, env, } = params; const userId = context.getCurrentUserId(); const bindLogic = async () => { const systemId = context.getSystemId(); const result = await context.select('captcha', { data: { id: 1, expired: 1, }, filter: { origin: 'mobile', content: mobile, code: captcha, }, sorter: [ { $attr: { $$createAt$$: 1, }, $direction: 'desc', }, ], indexFrom: 0, count: 1, }, { dontCollect: true }); if (result.length > 0) { const [captchaRow] = result; if (captchaRow.expired) { throw new OakUserException('验证码已经过期'); } // 到这里说明验证码已经通过 await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaRow.id } }, {}); // 检查当前user是否已绑定mobile const [boundMobile] = await context.select('mobile', { data: { id: 1, mobile: 1, }, filter: { ableState: 'enabled', userId: userId, }, }, { dontCollect: true, forUpdate: true, }); if (boundMobile) { //用户已绑定的mobile与当前输入的mobile一致 if (boundMobile.mobile === mobile) { throw new OakUserException('已绑定该手机号,无需重复绑定'); } //更新mobile await context.operate('mobile', { id: await generateNewIdAsync(), action: 'update', data: { mobile, }, filter: { id: boundMobile.id, }, }, {}); } else { //创建mobile await context.operate('mobile', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), mobile, userId, }, }, {}); } } else { throw new OakUserException('验证码无效'); } }; const closeRootMode = context.openRootMode(); const [otherUserMobile] = await context.select('mobile', { data: { id: 1, mobile: 1, }, filter: { mobile: mobile, ableState: 'enabled', userId: { $ne: userId, } }, }, { dontCollect: true }); if (otherUserMobile) { closeRootMode(); throw new OakUserException('该手机号已绑定其他用户,请检查'); } await bindLogic(); closeRootMode(); } export async function bindByEmail(params, context) { const { email, captcha, env, } = params; const userId = context.getCurrentUserId(); const bindLogic = async () => { const systemId = context.getSystemId(); const result = await context.select('captcha', { data: { id: 1, expired: 1, }, filter: { origin: 'email', content: email, code: captcha, }, sorter: [ { $attr: { $$createAt$$: 1, }, $direction: 'desc', }, ], indexFrom: 0, count: 1, }, { dontCollect: true }); if (result.length > 0) { const [captchaRow] = result; if (captchaRow.expired) { throw new OakUserException('验证码已经过期'); } // 到这里说明验证码已经通过,可以让验证码失效 await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaRow.id } }, {}); //检查当前user是否已绑定email const [boundEmail] = await context.select('email', { data: { id: 1, email: 1, }, filter: { ableState: 'enabled', userId: userId, }, }, { dontCollect: true, forUpdate: true, }); if (boundEmail) { //用户已绑定的email与当前输入的email一致 if (boundEmail.email === email) { throw new OakUserException('已绑定该邮箱,无需重复绑定'); } //更新email await context.operate('email', { id: await generateNewIdAsync(), action: 'update', data: { email, }, filter: { id: boundEmail.id, }, }, {}); } else { //创建email await context.operate('email', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), email, userId, }, }, {}); } } else { throw new OakUserException('验证码无效'); } }; const closeRootMode = context.openRootMode(); const { config, emailConfig } = await getAndCheckPassportByEmail(context, email); const [otherUserEmail] = await context.select('email', { data: { id: 1, email: 1, }, filter: { email: email, ableState: 'enabled', userId: { $ne: userId, } }, }, { dontCollect: true }); if (otherUserEmail) { closeRootMode(); throw new OakUserException('该邮箱已绑定其他用户,请检查'); } await bindLogic(); closeRootMode(); } async function setupLoginName(name, env, context) { const result2 = await context.select('loginName', { data: { id: 1, name: 1, userId: 1, ableState: 1, user: { id: 1, userState: 1, refId: 1, ref: { id: 1, userState: 1, refId: 1, }, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, userSystem$user: { $entity: 'userSystem', data: { id: 1, systemId: 1, }, }, }, }, filter: { name, ableState: 'enabled', }, }, { dontCollect: true }); assert(result2.length === 1); const [loginNameRow] = result2; const { user } = loginNameRow; const { userState, ref } = user; if (userState === 'merged') { return await setUpTokenAndUser(env, context, 'loginName', loginNameRow.id, undefined, ref); } return await setUpTokenAndUser(env, context, 'loginName', loginNameRow.id, undefined, user); } async function setupEmail(email, env, context) { const result2 = await context.select('email', { data: { id: 1, email: 1, userId: 1, ableState: 1, user: { id: 1, userState: 1, refId: 1, ref: { id: 1, userState: 1, refId: 1, }, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, userSystem$user: { $entity: 'userSystem', data: { id: 1, systemId: 1, }, }, }, }, filter: { email, ableState: 'enabled', }, }, { dontCollect: true }); if (result2.length > 0) { // 此邮箱已经存在 assert(result2.length === 1); const [emailRow] = result2; const { user } = emailRow; const { userState, ref } = user; if (userState === 'merged') { return await setUpTokenAndUser(env, context, 'email', emailRow.id, undefined, ref); } return await setUpTokenAndUser(env, context, 'email', emailRow.id, undefined, user); } else { //此邮箱不存在 return await setUpTokenAndUser(env, context, 'email', undefined, { id: await generateNewIdAsync(), email, }); } } async function setUserInfoFromWechat(user, userInfo, context) { const application = context.getApplication(); const applicationId = context.getApplicationId(); const config = application?.system?.config || application?.system?.platform?.config; const { nickname, gender, avatar } = userInfo; const { nickname: originalNickname, gender: originalGender, extraFile$entity, } = user; const updateData = {}; if (nickname && nickname !== originalNickname) { Object.assign(updateData, { nickname, }); } if (gender && gender !== originalGender) { Object.assign(updateData, { gender, }); } if (avatar && (extraFile$entity?.length === 0 || await composeFileUrlBackend(application, extraFile$entity[0], context) !== avatar)) { // 需要更新新的avatar extra file const extraFileOperations = [ { id: await generateNewIdAsync(), action: 'create', data: Object.assign({ id: await generateNewIdAsync(), tag1: 'avatar', entity: 'user', entityId: user.id, objectId: await generateNewIdAsync(), origin: 'unknown', extra1: avatar, type: 'image', filename: '', bucket: '', applicationId: applicationId, }), }, ]; if (extraFile$entity.length > 0) { extraFileOperations.push({ id: await generateNewIdAsync(), action: 'remove', data: {}, filter: { id: extraFile$entity[0].id, }, }); } Object.assign(updateData, { extraFile$entity: extraFileOperations, }); } if (Object.keys(updateData).length > 0) { await context.operate('user', { id: await generateNewIdAsync(), action: 'update', data: updateData, filter: { id: user.id, }, }, {}); } } //将获取的微信头像作为用户头像 export async function setUserAvatarFromWechat(params, context) { const { avatar } = params; const application = context.getApplication(); const { type, config } = application; assert(type === 'wechatMp' || config.type === 'wechatMp'); const applicationId = context.getApplicationId(); const userId = context.getCurrentUserId(); const extraFiles = await context.select('extraFile', { data: { id: 1, tag1: 1, origin: 1, bucket: 1, objectId: 1, filename: 1, extra1: 1, entity: 1, entityId: 1, }, filter: { entity: 'user', entityId: userId, tag1: 'avatar', } }, { forUpdate: true }); const updateData = {}; if (avatar && (extraFiles?.length === 0 || await composeFileUrlBackend(application, extraFiles[0], context) !== avatar)) { // 需要更新新的avatar extra file const extraFileOperations = [ { id: await generateNewIdAsync(), action: 'create', data: Object.assign({ id: await generateNewIdAsync(), tag1: 'avatar', entity: 'user', entityId: userId, objectId: await generateNewIdAsync(), origin: 'unknown', extra1: avatar, type: 'image', filename: '', bucket: '', applicationId: applicationId, }), }, ]; if (extraFiles.length > 0) { extraFileOperations.push({ id: await generateNewIdAsync(), action: 'remove', data: {}, filter: { id: extraFiles[0].id, }, }); } Object.assign(updateData, { extraFile$entity: extraFileOperations, }); } if (Object.keys(updateData).length > 0) { await context.operate('user', { id: await generateNewIdAsync(), action: 'update', data: updateData, filter: { id: userId, }, }, {}); } } async function tryRefreshWechatPublicUserInfo(wechatUserId, context) { const [wechatUser] = await context.select('wechatUser', { data: { id: 1, accessToken: 1, refreshToken: 1, atExpiredAt: 1, rtExpiredAt: 1, scope: 1, openId: 1, user: { id: 1, nickname: 1, gender: 1, extraFile$entity: { $entity: 'extraFile', data: { id: 1, tag1: 1, origin: 1, bucket: 1, objectId: 1, filename: 1, extra1: 1, entity: 1, entityId: 1, }, filter: { tag1: 'avatar', }, }, }, }, filter: { id: wechatUserId, }, }, { dontCollect: true }); const application = context.getApplication(); const { type, config } = application; assert(type !== 'wechatMp' && type !== 'native'); if (type === 'web') { return; } let appId, appSecret; const config2 = config; appId = config2.appId; appSecret = config2.appSecret; const wechatInstance = WechatSDK.getInstance(appId, type, appSecret); let { accessToken, refreshToken, atExpiredAt, rtExpiredAt, scope, openId, user, } = wechatUser; const now = Date.now(); assert(scope.toLowerCase().includes('userinfo')); if (rtExpiredAt < now) { // refreshToken过期,直接返回未登录异常,使用户去重新登录 throw new OakUnloggedInException(); } if (atExpiredAt < now) { // 刷新accessToken const { accessToken: at2, atExpiredAt: ate2, scope: s2, } = await wechatInstance.refreshUserAccessToken(refreshToken); await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: { accessToken: at2, atExpiredAt: ate2, scope: s2, }, filter: { id: wechatUserId, } }, { dontCollect: true }); accessToken = at2; } const { nickname, gender, avatar } = await wechatInstance.getUserInfo(accessToken, openId); await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: { nickname, avatar, }, filter: { id: wechatUserId, } }, { dontCollect: true }); await setUserInfoFromWechat(user, { nickname, gender: gender, avatar }, context); } export async function refreshWechatPublicUserInfo({}, context) { const tokenValue = context.getTokenValue(); const [token] = await context.select('token', { data: { id: 1, entity: 1, entityId: 1, }, filter: { id: tokenValue, }, }, { dontCollect: true }); assert(token.entity === 'wechatUser'); assert(token.entityId); return await tryRefreshWechatPublicUserInfo(token.entityId, context); } // 用户在微信端授权登录后,在web端触发该方法 export async function loginByWechat(params, context) { const { wechatLoginId, env } = params; const closeRootMode = context.openRootMode(); const [wechatLoginData] = await context.select('wechatLogin', { data: { id: 1, userId: 1, user: { id: 1, name: 1, nickname: 1, userState: 1, refId: 1, isRoot: 1, }, type: 1, }, filter: { id: wechatLoginId, }, }, { dontCollect: true, }); const tokenValue = await setUpTokenAndUser(env, context, 'wechatLogin', wechatLoginId, undefined, wechatLoginData.user); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } async function loginFromWechatEnv(code, env, context, wechatLoginId) { const application = context.getApplication(); const { type, config, systemId } = application; let appId, appSecret; if (type === 'wechatPublic') { const config2 = config; appId = config2.appId; appSecret = config2.appSecret; } else if (type === 'wechatMp') { const config2 = config; appId = config2.appId; appSecret = config2.appSecret; } else if (type === 'native') { const config2 = config; assert(config2.wechatNative); appId = config2.wechatNative.appId; appSecret = config2.wechatNative.appSecret; } else { assert(type === 'web'); const config2 = config; assert(config2.wechat); appId = config2.wechat.appId; appSecret = config2.wechat.appSecret; } const wechatInstance = WechatSDK.getInstance(appId, type, appSecret); const { isSnapshotUser, openId, unionId, ...wechatUserData } = await wechatInstance.code2Session(code); if (isSnapshotUser) { throw new OakUserException('请使用完整服务后再进行登录操作'); } const OriginMap = { web: 'web', wechatPublic: 'public', wechatMp: 'mp', native: 'native', }; const createWechatUserAndReturnTokenId = async (user) => { const wechatUserCreateData = { id: await generateNewIdAsync(), unionId, origin: OriginMap[type], openId, applicationId: application.id, ...wechatUserData, }; const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', undefined, wechatUserCreateData, user); return { tokenValue, wechatUserId: wechatUserCreateData.id }; }; // 扫码者 const [wechatUser] = await context.select('wechatUser', { data: { id: 1, userId: 1, unionId: 1, user: { id: 1, name: 1, nickname: 1, userState: 1, refId: 1, isRoot: 1, }, }, filter: { applicationId: application.id, openId, origin: OriginMap[type], }, }, { dontCollect: true, }); if (wechatLoginId) { const updateWechatLogin = async (updateData) => { await context.operate('wechatLogin', { id: await generateNewIdAsync(), action: 'update', data: updateData, filter: { id: wechatLoginId, }, }, { dontCollect: true }); }; // 扫码产生的实体wechaLogin const [wechatLoginData] = await context.select('wechatLogin', { data: { id: 1, userId: 1, type: 1, user: { id: 1, name: 1, nickname: 1, userState: 1, refId: 1, isRoot: 1, }, }, filter: { id: wechatLoginId, }, }, { dontCollect: true, }); assert(wechatLoginData, 'wechatLogin data not found'); // 用户已登录,通过扫描二维码绑定 if (wechatLoginData.type === 'bind') { // 首先通过wechaLogin.userId查询是否存在wechatUser 判断是否绑定 // 登录者 const [wechatUserLogin] = await context.select('wechatUser', { data: { id: 1, userId: 1, user: { id: 1, name: 1, nickname: 1, userState: 1, refId: 1, isRoot: 1, }, }, filter: { userId: wechatLoginData.userId, applicationId: application.id, origin: OriginMap[type], }, }, { dontCollect: true, }); // 已绑定 if (wechatUserLogin) { throw new OakUserException('当前登录者已绑定微信'); } // 未绑定的情况,就要先看扫码者是否绑定了公众号 // 扫码者已绑定, 将扫码者的userId替换成登录者的userId if (wechatUser) { await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: { userId: wechatLoginData.userId, ...wechatUserData }, filter: { id: wechatUser.id, }, }, { dontCollect: true }); const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', wechatUser.id, undefined, wechatLoginData.user); await updateWechatLogin({ successed: true, wechatUserId: wechatUser.id }); return tokenValue; } else { const { tokenValue, wechatUserId } = await createWechatUserAndReturnTokenId(wechatLoginData.user); await updateWechatLogin({ successed: true, wechatUserId }); return tokenValue; } } // 用户未登录情况下 else if (wechatLoginData.type === 'login') { // wechatUser存在直接登录 if (wechatUser) { // 微信公众号关注后,会创建一个没有userId的wechatUser let user2 = wechatUser.user; if (!user2 && unionId) { // 如果有unionId,查找同一个system下有相同的unionId的user const [user] = await context.select('user', { data: { id: 1, userState: 1, refId: 1, }, filter: { wechatUser$user: { application: { systemId: application.systemId, }, unionId, }, }, }, { dontCollect: true, }); user2 = user; } const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', wechatUser.id, undefined, user2); const wechatUserUpdateData = wechatUserData; if (unionId !== wechatUser.unionId) { Object.assign(wechatUserUpdateData, { unionId, }); } if (user2 && !wechatUser.userId) { Object.assign(wechatUserUpdateData, { userId: user2.id, }); } await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: wechatUserUpdateData, filter: { id: wechatUser.id, }, }, { dontCollect: true }); await updateWechatLogin({ userId: wechatUser.userId, successed: true, wechatUserId: wechatUser.id, }); return tokenValue; } else { // 创建user和wechatUser(绑定并登录) const userId = await generateNewIdAsync(); const userData = { id: userId, userState: 'normal', }; await context.operate('user', { id: await generateNewIdAsync(), action: 'create', data: userData, }, {}); const { tokenValue, wechatUserId } = await createWechatUserAndReturnTokenId(userData); await updateWechatLogin({ userId, wechatUserId, successed: true }); return tokenValue; } } } else { if (wechatUser) { // 微信公众号关注后,会创建一个没有userId的wechatUser let user2 = wechatUser.user; if (!user2 && unionId) { // 如果有unionId,查找同一个system下有相同的unionId的user const [user] = await context.select('user', { data: { id: 1, userState: 1, refId: 1, }, filter: { wechatUser$user: { application: { systemId: application.systemId, }, unionId, } }, }, { dontCollect: true, }); user2 = user; } const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', wechatUser.id, undefined, user2); const wechatUserUpdateData = wechatUserData; if (unionId !== wechatUser.unionId) { Object.assign(wechatUserUpdateData, { unionId, }); } if (user2 && !wechatUser.userId) { Object.assign(wechatUserUpdateData, { userId: user2.id, }); } await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: wechatUserUpdateData, filter: { id: wechatUser.id, }, }, { dontCollect: true }); return tokenValue; } else if (unionId) { // 如果有unionId,查找同一个system下有没有相同的unionId const [wechatUser3] = await context.select('wechatUser', { data: { id: 1, userId: 1, unionId: 1, user: { id: 1, userState: 1, refId: 1, }, }, filter: { application: { systemId: application.systemId, }, unionId, }, }, { dontCollect: true, }); if (wechatUser3) { const wechatUserCreateData = { id: await generateNewIdAsync(), unionId, origin: OriginMap[type], openId, applicationId: application.id, ...wechatUserData, }; const tokenValue = await setUpTokenAndUser(env, context, 'wechatUser', undefined, wechatUserCreateData, wechatUser3.user); if (!wechatUser3.userId) { // 这里顺便帮其它wechatUser数据也补上相应的userId await context.operate('wechatUser', { id: await generateNewIdAsync(), action: 'update', data: { userId: wechatUserCreateData.userId, // 在setUpTokenAndUser内赋上值 }, filter: { id: wechatUser3.id, }, }, { dontCollect: true }); } return tokenValue; } } } // 到这里都是要同时创建wechatUser和user对象了 const { tokenValue } = await createWechatUserAndReturnTokenId(); return tokenValue; } /** * 微信App授权登录 * @param param0 * @param context * @returns */ export async function loginWechatNative({ code, env, }, context) { const closeRootMode = context.openRootMode(); const tokenValue = await loginFromWechatEnv(code, env, context); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } /** * 公众号授权登录 * @param param0 * @param context */ export async function loginWechat({ code, env, wechatLoginId, }, context) { const closeRootMode = context.openRootMode(); const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId); const [tokenInfo] = await loadTokenInfo(tokenValue, context); assert(tokenInfo.entity === 'wechatUser' || tokenInfo.entity === 'wechatLogin'); await context.setTokenValue(tokenValue); if (tokenInfo.entity === 'wechatUser') { await tryRefreshWechatPublicUserInfo(tokenInfo.entityId, context); } closeRootMode(); return tokenValue; } /** * 小程序授权登录 * @param param0 * @param context * @returns */ export async function loginWechatMp({ code, env, wechatLoginId, }, context) { const closeRootMode = context.openRootMode(); const tokenValue = await loginFromWechatEnv(code, env, context, wechatLoginId); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } /** * 同步从wx.getUserProfile拿到的用户信息 * @param param0 * @param context */ export async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData, iv, signature, }, context) { const { userId } = context.getToken(); const application = context.getApplication(); const config = application?.system?.config || application?.system?.platform?.config; const [{ sessionKey, user }] = await context.select('wechatUser', { data: { id: 1, sessionKey: 1, nickname: 1, avatar: 1, user: { id: 1, nickname: 1, gender: 1, extraFile$entity: { $entity: 'extraFile', data: { id: 1, tag1: 1, origin: 1, bucket: 1, objectId: 1, filename: 1, extra1: 1, entity: 1, entityId: 1, }, filter: { tag1: 'avatar', }, }, }, }, filter: { userId: userId, applicationId: application.id, }, }, { dontCollect: true, }); const { type, config: config2 } = application; assert(type === 'wechatMp' || config2.type === 'wechatMp'); const { appId, appSecret } = config2; const wechatInstance = WechatSDK.getInstance(appId, 'wechatMp', appSecret); const result = wechatInstance.decryptData(sessionKey, encryptedData, iv, signature); // 实测发现解密出来的和userInfo完全一致…… await setUserInfoFromWechat(user, { nickname, avatar: avatarUrl }, context); } export async function sendCaptchaByMobile({ mobile, env, type: captchaType, }, context) { const { type } = env; let visitorId = mobile; if (type === 'web' || type === 'native') { visitorId = env.visitorId; } const application = context.getApplication(); const [applicationPassport] = await context.select('applicationPassport', { data: { id: 1, passportId: 1, passport: { id: 1, config: 1, type: 1, }, applicationId: 1, }, filter: { applicationId: application?.id, passport: { type: 'sms' }, } }, { dontCollect: true, }); if (!applicationPassport?.passport) { throw new OakIncompleteConfig(application.id); } const config = applicationPassport.passport.config; const mockSend = config.mockSend; const codeTemplateName = config.templateName; const origin = config.defaultOrigin; const duration = config.codeDuration || 1; const digit = config.digit || 4; const now = Date.now(); const closeRootMode = context.openRootMode(); if (!mockSend) { const [count1, count2] = await Promise.all([ context.count('captcha', { filter: { origin: 'mobile', visitorId, $$createAt$$: { $gt: now - 3600 * 1000, }, type: captchaType, }, }, { dontCollect: true, }), context.count('captcha', { filter: { origin: 'mobile', content: mobile, $$createAt$$: { $gt: now - 3600 * 1000, }, type: captchaType, }, }, { dontCollect: true, }), ]); if (count1 > 5 || count2 > 5) { closeRootMode(); throw new OakUserException('您已发送很多次短信,请休息会再发吧'); } } const [captcha] = await context.select('captcha', { data: { id: 1, code: 1, $$createAt$$: 1, }, filter: { origin: 'mobile', content: mobile, $$createAt$$: { $gt: now - duration * 60 * 1000, }, expired: false, type: captchaType, }, }, { dontCollect: true, }); const getCode = async () => { let code; if (mockSend) { code = mobile.substring(11 - digit); } else { // code = Math.floor(Math.random() * Math.pow(10, digit)).toString(); // while (code.length < digit) { // code += '0'; // } code = Array.from({ length: digit }, () => Math.floor(Math.random() * 10)).join(''); } const id = await generateNewIdAsync(); await context.operate('captcha', { id: await generateNewIdAsync(), action: 'create', data: { id, origin: 'mobile', content: mobile, code, visitorId, env, expired: false, expiresAt: now + duration * 60 * 1000, type: captchaType, applicationId: application?.id, }, }, { dontCollect: true, }); return { code, captchaId: id }; }; if (captcha) { const captchaDuration = process.env.NODE_ENV === 'development' ? 10 * 1000 : 60 * 1000; if (now - captcha.$$createAt$$ < captchaDuration) { closeRootMode(); throw new OakUserException('您的操作太迅捷啦,请稍候再点吧'); } await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captcha.id } }, {}); } const { code, captchaId } = await getCode(); if (mockSend) { closeRootMode(); return `验证码[${code}]已创建`; } else { assert(origin, '必须设置短信渠道'); //发送短信 const result = await sendSms({ origin: origin, templateName: codeTemplateName, mobile, templateParam: { code, duration: duration.toString() }, }, context); closeRootMode(); if (result.success) { return '验证码已发送'; } await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaId } }, {}); console.error('短信发送失败,原因:\n', result?.res); return '验证码发送失败'; } } export async function sendCaptchaByEmail({ email, env, type: captchaType, }, context) { const { type } = env; let visitorId = email; if (type === 'web' || type === 'native') { visitorId = env.visitorId; } const { config, emailConfig } = await getAndCheckPassportByEmail(context, email); const duration = config.codeDuration || 5; const digit = config.digit || 4; const mockSend = config.mockSend; let emailOptions = { // host: emailConfig.host, // port: emailConfig.port, // secure: emailConfig.secure, // account: emailConfig.account, // password: emailConfig.password, from: emailConfig.name ? `"${emailConfig.name}" <${emailConfig.account}>` : emailConfig.account, subject: config.subject, to: email, text: config.text, html: config.html, }; const now = Date.now(); const closeRootMode = context.openRootMode(); if (!mockSend) { const [count1, count2] = await Promise.all([ context.count('captcha', { filter: { origin: 'email', visitorId, $$createAt$$: { $gt: now - 3600 * 1000, }, type: captchaType, }, }, { dontCollect: true, }), context.count('captcha', { filter: { origin: 'email', content: email, $$createAt$$: { $gt: now - 3600 * 1000, }, type: captchaType, }, }, { dontCollect: true, }), ]); if (count1 > 5 || count2 > 5) { closeRootMode(); throw new OakUserException('您已发送很多次邮件,请休息会再发吧'); } } const [captcha] = await context.select('captcha', { data: { id: 1, code: 1, $$createAt$$: 1, }, filter: { origin: 'email', content: email, $$createAt$$: { $gt: now - duration * 60 * 1000, }, expired: false, type: captchaType, }, }, { dontCollect: true, }); const getCode = async () => { let code; code = Array.from({ length: digit }, () => Math.floor(Math.random() * 10)).join(''); const id = await generateNewIdAsync(); const applicationId = context.getApplication()?.id; await context.operate('captcha', { id: await generateNewIdAsync(), action: 'create', data: { id, origin: 'email', content: email, code, visitorId, env, expired: false, expiresAt: now + duration * 60 * 1000, type: captchaType, applicationId, }, }, { dontCollect: true, }); return { code, captchaId: id }; }; if (captcha) { const captchaDuration = process.env.NODE_ENV === 'development' ? 10 * 1000 : 60 * 1000; if (now - captcha.$$createAt$$ < captchaDuration) { closeRootMode(); throw new OakUserException('您的操作太迅捷啦,请稍候再点吧'); } await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captcha.id } }, {}); } const { code, captchaId } = await getCode(); if (mockSend) { closeRootMode(); return `验证码[${code}]已创建`; } else { assert(config.account, '必须设置邮箱'); //发送邮件 const subject = config.subject?.replace('${code}', code); const text = config.text?.replace('${duration}', duration.toString()).replace('${code}', code); const html = config.html?.replace('${duration}', duration.toString()).replace('${code}', code); emailOptions.subject = subject; emailOptions.text = text; emailOptions.html = html; const result = await sendEmail(emailOptions, context); closeRootMode(); if (result.success) { return '验证码已发送'; } await context.operate('captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captchaId } }, {}); console.error('邮件发送失败,原因:\n', result?.error); return '验证码发送失败'; } } export async function switchTo({ userId }, context) { const reallyRoot = context.isReallyRoot(); if (!reallyRoot) { throw new OakOperationUnpermittedException('user', { id: 'switchTo', action: 'switch', data: {}, filter: { id: userId } }, context.getCurrentUserId()); } const currentUserId = context.getCurrentUserId(); if (currentUserId === userId) { throw new OakPreConditionUnsetException('您已经是当前用户'); } const token = context.getToken(); await context.operate('token', { id: await generateNewIdAsync(), action: 'update', data: { userId, }, filter: { id: token.id, }, }, {}); } export async function getWechatMpUserPhoneNumber({ code, env }, context) { const application = context.getApplication(); const { type, config, systemId } = application; assert(type === 'wechatMp' && config.type === 'wechatMp'); const config2 = config; const { appId, appSecret } = config2; const wechatInstance = WechatSDK.getInstance(appId, 'wechatMp', appSecret); const result = await wechatInstance.getUserPhoneNumber(code); const closeRootMode = context.openRootMode(); //获取 绑定的手机号码 const phoneNumber = result?.phoneNumber; const reuslt = await setupMobile(phoneNumber, env, context); closeRootMode(); return reuslt; } export async function logout(params, context) { const { tokenValue } = params; if (tokenValue) { const closeRootMode = context.openRootMode(); try { await context.operate('token', { id: await generateNewIdAsync(), action: 'disable', data: {}, filter: { value: tokenValue, ableState: 'enabled', }, }, { dontCollect: true }); } catch (err) { closeRootMode(); throw err; } closeRootMode(); } } /** * 创建一个当前parasite上的token * @param params * @param context * @returns */ export async function wakeupParasite(params, context) { const { id, env } = params; const [parasite] = await context.select('parasite', { data: { id: 1, expired: 1, multiple: 1, userId: 1, tokenLifeLength: 1, user: { id: 1, userState: 1, }, }, filter: { id, }, }, { dontCollect: true }); if (parasite.expired) { const e = new OakRowInconsistencyException('数据已经过期'); e.addData('parasite', [parasite], context.getSchema()); throw e; } if (parasite.user?.userState !== 'shadow') { throw new OakUserException('此用户已经登录过系统,不允许借用身份'); } const closeRootMode = context.openRootMode(); if (!parasite.multiple) { await context.operate('parasite', { id: await generateNewIdAsync(), action: 'wakeup', data: { expired: true, }, filter: { id, }, }, { dontCollect: true }); } const tokenValue = await generateNewIdAsync(); await context.operate('token', { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), entity: 'parasite', entityId: id, userId: parasite.userId, playerId: parasite.userId, disablesAt: Date.now() + parasite.tokenLifeLength, env, refreshedAt: Date.now(), value: tokenValue, applicationId: context.getApplicationId(), }, }, { dontCollect: true }); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } /** * todo 检查登录环境一致性,同一个token不能跨越不同设备 * @param env1 * @param env2 * @returns */ function checkTokenEnvConsistency(env1, env2) { if (env1.type !== env2.type) { return false; } switch (env1.type) { case 'web': { return env1.visitorId === env2.visitorId; } case 'wechatMp': { return true; } case 'native': { return env1.visitorId === env2.visitorId; } default: { return false; } } } export async function refreshToken(params, context) { const { env, tokenValue, applicationId: appId } = params; const closeRootMode = context.openRootMode(); let [token] = await context.select('token', { data: Object.assign({ env: 1, ...tokenProjection, }), filter: { $or: [ { value: tokenValue, }, { oldValue: tokenValue, // refreshedAt: { // $gte: Date.now() - 300 * 1000, // }, }, ], }, }, {}); if (!token) { throw new OakUnloggedInException("Token令牌已失效,请重新登录"); } const now = Date.now(); if (!checkTokenEnvConsistency(env, token.env)) { console.log('####### refreshToken 环境改变 start #######\n'); console.log(env); console.log('---------------------\n'); console.log(token.env); console.log('####### refreshToken 环境改变 end #######\n'); await context.operate('token', { id: await generateNewIdAsync(), action: 'disable', data: {}, filter: { id: token.id, } }, { dontCollect: true }); closeRootMode(); return ''; } if (process.env.OAK_PLATFORM === 'server') { // 只有server模式去刷新token // 'development' | 'production' | 'staging' const intervals = { development: 7200 * 1000, // 2小时 staging: 600 * 1000, // 十分钟 production: 600 * 1000, // 十分钟 }; let applicationId = token.applicationId; // TODO 修复token上缺失applicationId, 原因是创建user时,创建的token上未附上applicationId,后面数据处理好了再移除代码 by wkj if (!applicationId) { assert(appId, '从上下文中获取applicationId为空'); const [application] = await context.select('application', { data: { id: 1, }, filter: { id: appId, }, }, { dontCollect: true }); assert(application, 'application找不到'); await context.operate('token', { id: await generateNewIdAsync(), action: 'update', data: { applicationId: application.id, }, filter: { id: token.id, } }, { dontCollect: true }); applicationId = application.id; } //assert(applicationId, 'token上applicationId必须存在,请检查token数据'); const [application] = await context.select('application', { data: { id: 1, systemId: 1, system: { config: 1, }, }, filter: { id: applicationId, }, }, { dontCollect: true }); const system = application?.system; const tokenRefreshTime = system?.config?.App?.tokenRefreshTime; // 系统设置刷新间隔 毫秒 const interval = tokenRefreshTime || intervals[process.env.NODE_ENV]; if (tokenRefreshTime !== 0 && now - token.refreshedAt > interval) { const newValue = await generateNewIdAsync(); console.log('####### refreshToken token update start #######\n'); console.log('刷新前tokenId:', token?.id); console.log('刷新前tokenValue:', tokenValue); console.log('---------------------\n'); await context.operate('token', { id: await generateNewIdAsync(), action: 'update', data: { value: newValue, refreshedAt: now, oldValue: tokenValue, }, filter: { id: token.id, }, }, {}); console.log('刷新后tokenValue:', newValue); console.log('####### refreshToken token update end #######\n'); closeRootMode(); return newValue; } } closeRootMode(); return tokenValue; } /** * 使用微信小程序中的token登录web * @param tokenValue * @param env * @param context * @returns */ async function setupWechatMp(mpToken, env, context) { const [token] = await context.select('token', { data: { id: 1, entity: 1, entityId: 1, ableState: 1, userId: 1, user: { id: 1, userState: 1, refId: 1, ref: { id: 1, userState: 1, refId: 1, }, wechatUser$user: { $entity: 'wechatUser', data: { id: 1, }, }, userSystem$user: { $entity: 'userSystem', data: { id: 1, systemId: 1, }, }, }, }, filter: { $or: [{ value: mpToken, }, { oldValue: mpToken, }] // ableState: 'enabled', }, }, { dontCollect: true }); assert(token); const { user } = token; return await setUpTokenAndUser(env, context, token.entity, token.entityId, undefined, user); } /** * 小程序web-view处理token * @param mpToken * @param env * @param context * @returns */ export async function loginWebByMpToken(params, context) { const { mpToken, env } = params; const closeRootMode = context.openRootMode(); const tokenValue = await setupWechatMp(mpToken, env, context); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; }