import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { EntityDict } from '../oak-app-domain'; import WechatSDK, { WechatMpInstance, WechatPublicInstance, } from 'oak-external-sdk/lib/WechatSDK'; import { assert } from 'oak-domain/lib/utils/assert'; import { WechatMpConfig, WechatPublicConfig, WebConfig, NativeConfig, } from '../oak-app-domain/Application/Schema'; import { NativeEnv, WebEnv, WechatMpEnv, } from 'oak-domain/lib/types/Environment'; import { CreateOperationData as CreateWechatUser } from '../oak-app-domain/WechatUser/Schema'; import { UpdateOperationData as UpdateWechatLoginData } from '../oak-app-domain/WechatLogin/Schema'; import { Operation as ExtraFileOperation } from '../oak-app-domain/ExtraFile/Schema'; import { OakPreConditionUnsetException, OakRowInconsistencyException, OakUnloggedInException, OakUserException, OakOperationUnpermittedException, } from 'oak-domain/lib/types'; import { composeFileUrl } from '../utils/cos/index.backend'; import { OakChangeLoginWayException, OakDistinguishUserException, OakUserDisabledException, } from '../types/Exception'; import { encryptPasswordSha1 } from '../utils/password'; import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; import { tokenProjection } from '../types/Projection'; import { sendSms } from '../utils/sms'; import { mergeUser } from './user'; import { cloneDeep, pick } from 'oak-domain/lib/utils/lodash'; import { BRC } from '../types/RuntimeCxt'; import { SmsConfig } from '../entities/Passport'; import { sendEmail } from '../utils/email'; import { EmailConfig } from '../oak-app-domain/Passport/Schema'; import { isEmail, isMobile } from 'oak-domain/lib/utils/validator'; import { EmailOptions } from '../types/Email'; import { getAndCheckPassportByEmail } from '../utils/passport'; async function makeDistinguishException(userId: string, context: BRC, message?: string) { 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: string, context: BRC) { 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: Partial, context: BackendRuntimeContext, tokenData: EntityDict['token']['CreateOperationData'] ): Promise> { 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: BRC) { const { system } = context.getApplication()!; return !!system!.config!.App.mergeUserDirectly; } /** * 根据user的不同情况,完成登录动作 * @param env * @param context * @param user * @return tokenValue */ async function setUpTokenAndUser( env: WebEnv | WechatMpEnv | NativeEnv, context: BRC, entity: string, // 支持更多的登录渠道使用此函数创建token entityId?: string, // 如果是现有对象传id,如果没有对象传createData createData?: any, user?: Partial ): Promise { 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 as keyof ED].attributes.hasOwnProperty('userId') && (schema[entity as keyof ED].attributes as any).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 as keyof ED, { id: await generateNewIdAsync(), action: 'create', data: Object.assign(createData, { userId: currentToken.userId, }), } as any, { dontCollect: entity !== 'mobile' } ); } else { assert(entityId); await context.operate( entity as keyof ED, { id: await generateNewIdAsync(), action: 'update', data: { userId: currentToken.userId, }, filter: { id: entityId, }, } as any, { 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: EntityDict['token']['CreateOperationData'] = { id: await generateNewIdAsync(), env, refreshedAt: Date.now(), value: await generateNewIdAsync(), }; if (user) { // 根据此用户状态进行处理 const { userState } = user; let userId: string = 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: EntityDict['user']['CreateOperationData'] = { 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 as keyof ED, { id: await generateNewIdAsync(), action: 'create', data: Object.assign(createData, { userId: userData.id, }), } as any, { dontCollect: true } ); } else { assert(entityId); await context.operate( entity as keyof ED, { id: await generateNewIdAsync(), action: 'update', data: { userId: userData.id, }, filter: { id: entityId, }, } as any, { dontCollect: true } ); } return tokenData.value!; } } } async function setupMobile(mobile: string, env: WebEnv | WechatMpEnv | NativeEnv, context: BRC) { 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 as Partial ); } return await setUpTokenAndUser( env, context, 'mobile', mobileRow.id, undefined, user as Partial ); } else { //此手机号不存在 return await setUpTokenAndUser( env, context, 'mobile', undefined, { id: await generateNewIdAsync(), mobile, } ); } } async function loadTokenInfo(tokenValue: string, context: BRC) { return await context.select( 'token', { data: cloneDeep(tokenProjection), filter: { value: tokenValue, }, }, {} ); } export async function loginByMobile( params: { mobile: string; captcha: string; disableRegister?: boolean; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ): Promise { const { mobile, captcha, env, disableRegister } = params; const loginLogic = 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! } }, {} ); // 到这里说明验证码已经通过 return await setupMobile(mobile, env, context); } else { throw new OakUserException('验证码无效'); } }; 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(); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } export async function verifyPassword( params: { password: string; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ) { const { password } = params; const userId = context.getCurrentUserId(); const [user] = await context.select('user', { data: {}, filter: { id: userId, $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ], } }, {}); 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: { account: string; password: string; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ): Promise { 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'); if (accountType === 'email') { const { config, emailConfig } = await getAndCheckPassportByEmail(context, account); 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, } }, { $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ], } ], }, }, { dontCollect: true } ); switch (result.length) { case 0: { throw new OakUserException('error::user.passwordUnmath'); } case 1: { const applicationPassports = await context.select( 'applicationPassport', { data: { id: 1, applicationId: 1, passportId: 1, passport: { id: 1, type: 1, systemId: 1, } }, filter: { passport: { systemId, }, applicationId, } }, { dontCollect: true, } ); const allowEmail = !!applicationPassports.find((ele) => ele.passport?.type === 'email'); const [userRow] = result; const { email$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); if (allowEmail) { 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 as string, context ); if (exception) { throw exception; } } return await setupEmail(account, env, context); } else { throw new OakUserException('error::user.emailUnexists'); } } else { throw new OakUserException('error::user.loginWayDisabled'); } } default: { throw new OakUserException('error::user.loginWayDisabled'); } } } else if (accountType === 'mobile') { 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, } }, { $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ], } ], }, }, { dontCollect: true } ); switch (result.length) { case 0: { throw new OakUserException('手机号与密码不匹配'); } case 1: { const applicationPassports = await context.select( 'applicationPassport', { data: { id: 1, applicationId: 1, passportId: 1, passport: { id: 1, type: 1, systemId: 1, } }, filter: { passport: { systemId, }, applicationId, } }, { dontCollect: true, } ); const [userRow] = result; const { mobile$user, id: userId, } = userRow; needUpdatePassword = !(userRow.password || userRow.passwordSha1); const allowSms = !!applicationPassports.find((ele) => ele.passport?.type === 'sms'); if (allowSms) { const mobile = mobile$user?.find(ele => ele.mobile === account); if (mobile) { const ableState = mobile.ableState; if (ableState === 'disabled') { // 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录 const exception = await tryMakeChangeLoginWay( userId as string, context ); if (exception) { throw exception; } } return await setupMobile(account, env, context); } else { throw new OakUserException('手机号未注册'); } } else { throw new OakUserException('暂不支持手机号登录'); } } default: { throw new OakUserException('不支持的登录方式'); } } } 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, } }, { $or: [ { password, }, { passwordSha1: encryptPasswordSha1(password), }, ], } ], }, }, { 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 as string, context ); if (exception) { throw exception; } } return await setupLoginName(account, env, context); } else { throw new OakUserException('账号不存在'); } } default: { throw new OakUserException('不支持的登录方式'); } } } }; 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: 'password' }, } }, { dontCollect: true, } ); assert(applicationPassport?.passport); const tokenValue = await loginLogic(); const [tokenInfo] = await loadTokenInfo(tokenValue, context); const { userId } = tokenInfo; await context.operate('user', { id: await generateNewIdAsync(), action: 'update', data: needUpdatePassword ? { password, passwordSha1: encryptPasswordSha1(password), verifyPasswordAt: Date.now(), } : { verifyPasswordAt: Date.now(), }, filter: { id: userId!, } }, {}); closeRootMode(); return tokenValue; } export async function loginByEmail( params: { email: string; captcha: string; disableRegister?: boolean; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ): Promise { 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: { mobile: string; captcha: string; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ) { 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: { email: string; captcha: string; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ) { 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: string, env: WebEnv | WechatMpEnv | NativeEnv, context: BRC) { 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 as Partial ); } return await setUpTokenAndUser( env, context, 'loginName', loginNameRow.id, undefined, user as Partial ); } async function setupEmail(email: string, env: WebEnv | WechatMpEnv | NativeEnv, context: BRC) { 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 as Partial ); } return await setUpTokenAndUser( env, context, 'email', emailRow.id, undefined, user as Partial ); } else { //此邮箱不存在 return await setUpTokenAndUser( env, context, 'email', undefined, { id: await generateNewIdAsync(), email, } ); } } async function setUserInfoFromWechat( user: Partial, userInfo: { nickname?: string; avatar?: string; gender?: 'male' | 'female'; }, context: BRC ) { 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 || composeFileUrl(application as EntityDict['application']['Schema'], extraFile$entity![0] as EntityDict['extraFile']['Schema']) !== avatar) ) { // 需要更新新的avatar extra file const extraFileOperations: ExtraFileOperation['data'][] = [ { 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!, }, }, {} ); } } async function tryRefreshWechatPublicUserInfo(wechatUserId: string, context: BRC) { 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: string, appSecret: string; const config2 = config as WechatPublicConfig; appId = config2.appId; appSecret = config2.appSecret; const wechatInstance = WechatSDK.getInstance( appId!, type!, appSecret! ) as WechatPublicInstance; let { accessToken, refreshToken, atExpiredAt, rtExpiredAt, scope, openId, user, } = wechatUser; const now = Date.now(); assert(scope!.toLowerCase().includes('userinfo')); if ((rtExpiredAt as number) < now) { // refreshToken过期,直接返回未登录异常,使用户去重新登录 throw new OakUnloggedInException(); } if ((atExpiredAt as number) < 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, }, }, { dontCollect: true } ); accessToken = at2; } const { nickname, gender, avatar } = await wechatInstance.getUserInfo( accessToken!, openId! ); await setUserInfoFromWechat( user!, { nickname, gender: gender as 'male', avatar }, context ); } export async function refreshWechatPublicUserInfo({ }, context: BRC) { 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: { wechatLoginId: string; env: WebEnv | WechatMpEnv; }, context: BRC ): Promise { 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: string, env: WebEnv | WechatMpEnv | NativeEnv, context: BRC, wechatLoginId?: string ): Promise { const application = context.getApplication(); const { type, config, systemId } = application!; let appId: string, appSecret: string; if (type === 'wechatPublic') { const config2 = config as WechatPublicConfig; appId = config2.appId; appSecret = config2.appSecret; } else if (type === 'wechatMp') { const config2 = config as WechatMpConfig; appId = config2.appId; appSecret = config2.appSecret; } else if (type === 'native') { const config2 = config as NativeConfig; assert(config2.wechatNative); appId = config2.wechatNative.appId; appSecret = config2.wechatNative.appSecret; } else { assert(type === 'web'); const config2 = config as WebConfig; assert(config2.wechat); appId = config2.wechat.appId; appSecret = config2.wechat.appSecret; } const wechatInstance = WechatSDK.getInstance( appId!, type!, appSecret! ) as WechatPublicInstance; const { isSnapshotUser, openId, unionId, ...wechatUserData } = await wechatInstance.code2Session(code); if (isSnapshotUser) { throw new OakUserException('请使用完整服务后再进行登录操作'); } const OriginMap: Record = { web: 'web', wechatPublic: 'public', wechatMp: 'mp', native: 'native', }; const createWechatUserAndReturnTokenId = async ( user?: EntityDict['user']['Schema'], wechatLoginId?: string, ) => { const wechatUserCreateData: CreateWechatUser = { id: await generateNewIdAsync(), unionId, origin: OriginMap[type], openId, applicationId: application!.id!, ...wechatUserData, }; let tokenValue; if (wechatLoginId) { tokenValue = await setUpTokenAndUser( env, context, 'wechatLogin', wechatLoginId, undefined, user ); } else { 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: UpdateWechatLoginData) => { 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, } ); // 用户已登录,通过扫描二维码绑定 if (wechatLoginData && 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!, }, }, { dontCollect: true, } ); // 已绑定 assert(!wechatUserLogin, '登录者已经绑定微信公众号'); // 未绑定的情况,就要先看扫码者是否绑定了公众号 // 扫码者已绑定, 将扫码者的userId替换成登录者的userId if (wechatUser) { await context.operate( 'wechatUser', { id: await generateNewIdAsync(), action: 'update', data: { userId: wechatLoginData.userId!, }, filter: { id: wechatUser.id, }, }, { dontCollect: true } ); const tokenValue = await setUpTokenAndUser( env, context, 'wechatUser', wechatUser.id, undefined, (wechatUserLogin as EntityDict['wechatUser']['Schema']) .user! ); await updateWechatLogin({ successed: true }); return tokenValue; } else { const { tokenValue } = await createWechatUserAndReturnTokenId( wechatLoginData.user!, ); await updateWechatLogin({ successed: true }); return tokenValue; } } // 用户未登录情况下 else if (wechatLoginData.type === 'login') { // wechatUser存在直接登录 if (wechatUser) { // 微信公众号关注后,会创建一个没有userId的wechatUser let user2 = wechatUser.user as EntityDict['user']['Schema']; 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 as EntityDict['user']['Schema']; } 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, }); 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 as EntityDict['user']['Schema'], wechatLoginId, ); await updateWechatLogin({ userId, wechatUserId, successed: true }); return tokenValue; } } } else { if (wechatUser) { // 微信公众号关注后,会创建一个没有userId的wechatUser let user2 = wechatUser.user as EntityDict['user']['Schema']; 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 as EntityDict['user']['Schema']; } 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: CreateWechatUser = { 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对象了 return (await createWechatUserAndReturnTokenId()).tokenValue; } /** * 微信App授权登录 * @param param0 * @param context * @returns */ export async function loginWechatNative( { code, env, }: { code: string; env: NativeEnv; }, context: BRC ): Promise { 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, }: { code: string; env: WebEnv; wechatLoginId?: string; }, context: BRC ): Promise { 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); } else if (tokenInfo.entity === 'wechatLogin') { const [wechatLogin] = await context.select('wechatLogin', { data: { id: 1, wechatUserId: 1, }, filter: { id: tokenInfo.entityId, }, }, {} ) assert(wechatLogin?.wechatUserId); await tryRefreshWechatPublicUserInfo(wechatLogin.wechatUserId, context); } closeRootMode(); return tokenValue; } /** * 小程序授权登录 * @param param0 * @param context * @returns */ export async function loginWechatMp( { code, env, }: { code: string; env: WechatMpEnv; }, context: BRC ): Promise { const closeRootMode = context.openRootMode(); const tokenValue = await loginFromWechatEnv(code, env, context); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } /** * 同步从wx.getUserProfile拿到的用户信息 * @param param0 * @param context */ export async function syncUserInfoWechatMp( { nickname, avatarUrl, encryptedData, iv, signature, }: { nickname: string; avatarUrl: string; encryptedData: string; iv: string; signature: string; }, context: BRC ) { 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 as Partial< EntityDict['application']['Schema'] >; assert(type === 'wechatMp' || config2!.type === 'wechatMp'); const { appId, appSecret } = config2 as WechatMpConfig; const wechatInstance = WechatSDK.getInstance(appId, 'wechatMp', appSecret); const result = wechatInstance.decryptData( sessionKey as string, encryptedData, iv, signature ); // 实测发现解密出来的和userInfo完全一致…… await setUserInfoFromWechat( user!, { nickname, avatar: avatarUrl }, context ); } export async function sendCaptchaByMobile( { mobile, env, type: captchaType, }: { mobile: string; env: WechatMpEnv | WebEnv | NativeEnv; type: 'login' | 'changePassword' | 'confirm'; }, context: BRC ): Promise { 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, } ); assert(applicationPassport?.passport); const config = applicationPassport.passport.config as SmsConfig; 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: string; 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; } let code = captcha?.code!; if (captcha) { const captchaDuration = process.env.NODE_ENV === 'development' ? 10 * 1000 : 60 * 1000; if (now - (captcha.$$createAt$$! as number) < captchaDuration) { closeRootMode(); throw new OakUserException('您的操作太迅捷啦,请稍候再点吧'); } await context.operate( 'captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captcha.id! } }, {} ); code = await getCode(); } else { code = 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 '验证码已发送'; } console.error('短信发送失败,原因:\n', result?.res); return '验证码发送失败'; } } export async function sendCaptchaByEmail( { email, env, type: captchaType, }: { email: string; env: WechatMpEnv | WebEnv | NativeEnv; type: 'login' | 'changePassword' | 'confirm'; }, context: BRC ): Promise { 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: 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: string; // code = Math.floor(Math.random() * 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(); 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; } let code = captcha?.code!; if (captcha) { const captchaDuration = process.env.NODE_ENV === 'development' ? 10 * 1000 : 60 * 1000; if (now - (captcha.$$createAt$$! as number) < captchaDuration) { closeRootMode(); throw new OakUserException('您的操作太迅捷啦,请稍候再点吧'); } await context.operate( 'captcha', { id: await generateNewIdAsync(), action: 'update', data: { expired: true }, filter: { id: captcha.id! } }, {} ); code = await getCode(); } else { code = 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 '验证码已发送'; } console.error('邮件发送失败,原因:\n', result?.error); return '验证码发送失败'; } } export async function switchTo({ userId }: { userId: string }, context: BRC) { const reallyRoot = context.isReallyRoot(); if (!reallyRoot) { throw new OakOperationUnpermittedException('user', { id: 'switchTo', action: 'switch', data: {}, filter: { id: userId } }); } 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 }: { code: string; env: WechatMpEnv }, context: BRC ) { const application = context.getApplication(); const { type, config, systemId } = application!; assert(type === 'wechatMp' && config!.type === 'wechatMp'); const config2 = config as WechatMpConfig; const { appId, appSecret } = config2; const wechatInstance = WechatSDK.getInstance( appId, 'wechatMp', appSecret ) as WechatMpInstance; 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: { tokenValue: string }, context: BRC) { 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: { id: string; env: WebEnv | WechatMpEnv | NativeEnv; }, context: BRC ) { 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: WebEnv | WechatMpEnv | NativeEnv, env2: WebEnv | WechatMpEnv | NativeEnv) { 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: { env: WebEnv | WechatMpEnv | NativeEnv; tokenValue: string; applicationId: string; }, context: BRC ) { 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, // }, }, ], }, }, {} ); assert(token, `tokenValue: ${tokenValue}`); const now = Date.now(); if (!checkTokenEnvConsistency(env, token.env as WebEnv)) { 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 as number) > 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: string, env: WebEnv, context: BRC) { 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 as Partial ); } /** * 小程序web-view处理token * @param mpToken * @param env * @param context * @returns */ export async function loginWebByMpToken(params: { mpToken: string, env: WebEnv, }, context: BRC): Promise { const { mpToken, env } = params; const closeRootMode = context.openRootMode(); const tokenValue = await setupWechatMp(mpToken, env, context); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; }