oak-general-business/es/aspects/token.js

2600 lines
87 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { composeFileUrl } from '../utils/cos/index.backend';
import { OakChangeLoginWayException, OakDistinguishUserException, 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';
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
*/
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,
});
}
}
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 () => {
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('验证码已经过期');
}
// 到这里说明验证码已经通过
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, context) {
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, 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');
if (accountType === 'email') {
// const application = context.getApplication();
// const { system } = application!;
// 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: 'email'
// },
// }
// },
// {
// dontCollect: true,
// }
// );
// assert(applicationPassport?.passport);
// const config = applicationPassport.passport.config as EmailConfig;
// const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
// assert(emailConfig);
// const emailSuffixes = config.emailSuffixes;
// // 检查邮箱后缀是否满足配置
// if (emailSuffixes?.length! > 0) {
// let isValid = false;
// for (const suffix of emailSuffixes!) {
// if (account.endsWith(suffix)) {
// isValid = true;
// break;
// }
// }
// if (!isValid) {
// throw new OakUserException('error::user.emailSuffixIsInvalid');
// }
// }
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,
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;
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, 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,
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;
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, 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,
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;
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('不支持的登录方式');
}
}
}
};
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: password ? {
password,
verifyPasswordAt: Date.now(),
} : {
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 application = context.getApplication();
const { system } = application;
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: 'email'
},
}
}, {
dontCollect: true,
});
assert(applicationPassport?.passport);
const config = applicationPassport.passport.config;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
assert(emailConfig);
const emailSuffixes = config.emailSuffixes;
// 检查邮箱后缀是否满足配置
if (emailSuffixes?.length > 0) {
let isValid = false;
for (const suffix of emailSuffixes) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
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('验证码已经过期');
}
// 到这里说明验证码已经通过
// 检查当前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 application = context.getApplication();
const { system } = application;
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: 'email'
},
}
}, {
dontCollect: true,
});
assert(applicationPassport?.passport);
const config = applicationPassport.passport.config;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
assert(emailConfig);
const emailSuffixes = config.emailSuffixes;
// 检查邮箱后缀是否满足配置
if (emailSuffixes?.length > 0) {
let isValid = false;
for (const suffix of emailSuffixes) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
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 ||
composeFileUrl(application, extraFile$entity[0]) !== 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,
},
}, {});
}
}
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,
},
}, { dontCollect: true });
accessToken = at2;
}
const { nickname, gender, avatar } = await wechatInstance.getUserInfo(accessToken, openId);
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, wechatLoginId) => {
const wechatUserCreateData = {
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) => {
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
.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;
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,
});
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, wechatLoginId);
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对象了
return (await createWechatUserAndReturnTokenId()).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);
}
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, }, context) {
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, }, 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,
});
assert(applicationPassport?.passport);
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: {
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';
}
}
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,
},
}, {
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$$ < captchaDuration) {
closeRootMode();
throw new OakUserException('您的操作太迅捷啦,请稍候再点吧');
}
}
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, }, context) {
const { type } = env;
let visitorId = email;
if (type === 'web' || type === 'native') {
visitorId = env.visitorId;
}
const application = context.getApplication();
const { system } = application;
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: 'email'
},
}
}, {
dontCollect: true,
});
assert(applicationPassport?.passport);
const config = applicationPassport.passport.config;
const emailConfig = system?.config.Emails?.find((ele) => ele.account === config.account);
assert(emailConfig);
const duration = config.codeDuration || 5;
const digit = config.digit || 4;
const mockSend = config.mockSend;
const emailSuffixes = config.emailSuffixes;
// 检查邮箱后缀是否满足配置
if (emailSuffixes?.length > 0) {
let isValid = false;
for (const suffix of emailSuffixes) {
if (email.endsWith(suffix)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new OakUserException('邮箱后缀不符合要求');
}
}
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: {
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 = Math.floor(Math.random() * Math.random() * Math.pow(10, digit)).toString();
while (code.length < digit) {
code += '0';
}
const id = await generateNewIdAsync();
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,
},
}, {
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$$ < captchaDuration) {
closeRootMode();
throw new OakUserException('您的操作太迅捷啦,请稍候再点吧');
}
}
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 }, context) {
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 }, 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,
// },
},
],
},
}, {});
assert(token, `tokenValue: ${tokenValue}`);
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;
}