oak-general-business/src/aspects/token.ts

1949 lines
58 KiB
TypeScript
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 { EntityDict } from '../oak-app-domain';
import {
WechatMpInstance,
WechatPublicInstance,
WechatSDK,
} from 'oak-external-sdk';
import { assert } from 'oak-domain/lib/utils/assert';
import {
WechatMpConfig,
WechatPublicConfig,
WebConfig,
} 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 {
OakRowInconsistencyException,
OakUnloggedInException,
OakUserException,
OakUserUnpermittedException,
} from 'oak-domain/lib/types';
import { composeFileUrl } from '../utils/cos';
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';
async function makeDistinguishException<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(userId: string, context: Cxt, 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,
},
},
},
filter: {
id: userId,
},
},
{
dontCollect: true,
}
);
assert(user);
const { password, passwordSha1, idState, wechatUser$user, email$user } =
user;
return new OakDistinguishUserException(
userId,
!!(password || passwordSha1),
idState === 'verified',
(<any[]>wechatUser$user).length > 0,
(<any[]>email$user).length > 0,
message
);
}
async function tryMakeChangeLoginWay<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(userId: string, context: Cxt) {
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,
},
},
},
filter: {
id: userId,
},
},
{
dontCollect: true,
}
);
assert(user);
const { idState, wechatUser$user, email$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)
);
}
}
async function dealWithUserState(
user: Partial<EntityDict['user']['Schema']>,
context: BackendRuntimeContext<EntityDict>,
tokenData: EntityDict['token']['CreateSingle']['data']
): Promise<Partial<EntityDict['token']['CreateSingle']['data']>> {
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<ED extends EntityDict, Cxt extends BackendRuntimeContext<ED>>(context: Cxt) {
const { system } = context.getApplication()!;
return !!system!.config!.App.mergeUserDirectly;
}
/**
* 根据user的不同情况完成登录动作
* @param env
* @param context
* @param user
* @return tokenValue
*/
async function setUpTokenAndUser<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
env: WebEnv | WechatMpEnv | NativeEnv,
context: Cxt,
entity: string, // 支持更多的登录渠道使用此函数创建token
entityId?: string, // 如果是现有对象传id如果没有对象传createData
createData?: any,
user?: Partial<ED['user']['Schema']>
): Promise<string> {
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<ED, Cxt>(context);
if (autoMerge) {
await mergeUser<ED, Cxt>(
{ from: user.id!, to: currentToken.userId!, mergeMobile: true, mergeWechatUser: true, mergeEmail: true },
context,
true
);
return currentToken.value!;
}
throw await makeDistinguishException<ED, Cxt>(user.id!, context);
}
case 'shadow': {
assert(currentToken.userId !== user.id);
const autoMerge = autoMergeUser<ED, Cxt>(context);
if (autoMerge) {
await mergeUser<ED, Cxt>(
{ from: user.id!, to: currentToken.userId!, mergeMobile: true, mergeWechatUser: true, mergeEmail: true },
context,
true
);
return currentToken.value!;
}
throw await makeDistinguishException<ED, Cxt>(user.id!, context);
}
case 'disabled': {
throw new OakUserDisabledException();
}
case 'merged': {
assert(user.refId);
if (user.refId === currentToken.userId) {
return currentToken.value!;
}
const autoMerge = autoMergeUser<ED, Cxt>(context);
if (autoMerge) {
// 说明一个用户被其他用户merge了现在还是暂时先merge后面再说
console.warn(
`用户${user.id}已经是merged状态「${user.refId}再次被merged到「${currentToken.userId}]」`
);
await mergeUser<ED, Cxt>(
{ from: user.id!, to: currentToken.userId!, mergeMobile: true, mergeWechatUser: true, mergeEmail: true },
context,
true
);
return currentToken.value!;
}
throw await makeDistinguishException<ED, Cxt>(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']['CreateSingle']['data'] = {
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']['CreateSingle']['data'] = {
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;
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<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(mobile: string, env: WebEnv | WechatMpEnv | NativeEnv, context: Cxt) {
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<ED, Cxt>(
env,
context,
'mobile',
mobileRow.id,
undefined,
ref as Partial<ED['user']['Schema']>
);
}
return await setUpTokenAndUser<ED, Cxt>(
env,
context,
'mobile',
mobileRow.id,
undefined,
user as Partial<ED['user']['Schema']>
);
} else {
//此手机号不存在
return await setUpTokenAndUser<ED, Cxt>(
env,
context,
'mobile',
undefined,
{
id: await generateNewIdAsync(),
mobile,
}
);
}
}
async function loadTokenInfo<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(tokenValue: string, context: Cxt) {
return await context.select(
'token',
{
data: cloneDeep(tokenProjection),
filter: {
value: tokenValue,
},
},
{}
);
}
export async function loginByMobile<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
params: {
captcha?: string;
password?: string;
mobile: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: Cxt
): Promise<string> {
const { mobile, captcha, password, env, disableRegister } = params;
const loginLogic = async () => {
const systemId = context.getSystemId();
if (captcha) {
const result = await context.select(
'captcha',
{
data: {
id: 1,
expired: 1,
},
filter: {
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<ED, Cxt>(mobile, env, context);
} else {
throw new OakUserException('验证码无效');
}
} else {
assert(password);
const result = await context.select(
'mobile',
{
data: {
id: 1,
userId: 1,
ableState: 1,
},
filter: {
mobile: mobile,
user: {
$or: [
{
password,
},
{
passwordSha1: encryptPasswordSha1(password),
},
],
},
},
},
{
dontCollect: true,
}
);
switch (result.length) {
case 0: {
throw new OakUserException('用户名与密码不匹配');
}
case 1: {
const [mobileRow] = result;
const { ableState, userId } = mobileRow;
if (ableState === 'disabled') {
// 虽然密码和手机号匹配,但手机号已经禁用了,在可能的情况下提醒用户使用其它方法登录
const exception = await tryMakeChangeLoginWay<ED, Cxt>(
userId as string,
context
);
if (exception) {
throw exception;
}
}
return await setupMobile<ED, Cxt>(mobile, env, context);
}
default: {
throw new Error(
`手机号和密码匹配出现雷同mobile id是[${result
.map((ele) => ele.id)
.join(',')}], mobile是${mobile}`
);
}
}
}
};
const closeRootMode = context.openRootMode();
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<ED, Cxt>(tokenValue, context);
closeRootMode();
return tokenValue;
}
async function setUserInfoFromWechat<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
user: Partial<ED['user']['Schema']>,
userInfo: {
nickname?: string;
avatar?: string;
gender?: 'male' | 'female';
},
context: Cxt
) {
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<ED, Cxt, any>(extraFile$entity![0], context) !== 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<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(wechatUserId: string, context: Cxt) {
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, dontCreateModi: true, dontCreateOper: true }
);
accessToken = at2;
}
const { nickname, gender, avatar } = await wechatInstance.getUserInfo(
accessToken!,
openId!
);
await setUserInfoFromWechat<ED, Cxt>(
user!,
{ nickname, gender: gender as 'male', avatar },
context
);
}
export async function refreshWechatPublicUserInfo<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>({ }, context: Cxt) {
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<ED, Cxt>(
token.entityId,
context
);
}
// 用户在微信端授权登录后在web端触发该方法
export async function loginByWechat<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
params: {
wechatLoginId: string;
env: WebEnv | WechatMpEnv;
},
context: Cxt
): Promise<string> {
const { wechatLoginId, env } = params;
const closeRootMode = context.openRootMode();
const [wechatLoginData] = await context.select(
'wechatLogin',
{
data: {
id: 1,
userId: 1,
type: 1,
},
filter: {
id: wechatLoginId,
},
},
{
dontCollect: true,
}
);
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,
}
);
const tokenValue = await setUpTokenAndUser<ED, Cxt>(
env,
context,
'wechatUser',
wechatUserLogin.id,
undefined,
(wechatUserLogin as EntityDict['wechatUser']['Schema']).user!
);
await loadTokenInfo<ED, Cxt>(tokenValue, context);
closeRootMode();
return tokenValue;
}
async function loginFromWechatEnv<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
code: string,
env: WebEnv | WechatMpEnv,
context: Cxt,
wechatLoginId?: string
): Promise<string> {
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 {
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 = {
web: 'web',
wechatPublic: 'public',
wechatMp: 'mp',
native: 'native',
};
const createWechatUserAndReturnTokenId = async (
user?: EntityDict['user']['Schema']
) => {
const wechatUserCreateData: CreateWechatUser = {
id: await generateNewIdAsync(),
unionId,
origin: OriginMap[type] as 'mp',
openId,
applicationId: application!.id!,
...wechatUserData,
};
const tokenValue = await setUpTokenAndUser<ED, Cxt>(
env,
context,
'wechatUser',
undefined,
wechatUserCreateData,
user
);
return tokenValue;
};
// 扫码者
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]! as 'web',
},
},
{
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<ED, Cxt>(
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) {
const tokenValue = await setUpTokenAndUser<ED, Cxt>(
env,
context,
'wechatUser',
wechatUser.id,
undefined,
wechatUser.user!
);
await updateWechatLogin({ 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 = await createWechatUserAndReturnTokenId(
userData as EntityDict['user']['Schema']
);
await updateWechatLogin({ userId, successed: true });
return tokenValue;
}
}
} else {
if (wechatUser) {
const tokenValue = await setUpTokenAndUser<ED, Cxt>(
env,
context,
'wechatUser',
wechatUser.id,
undefined,
wechatUser.user as undefined
);
const wechatUserUpdateData = wechatUserData;
if (unionId !== (wechatUser.unionId as any)) {
Object.assign(wechatUserUpdateData, {
unionId,
});
}
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] as 'mp',
openId,
applicationId: application!.id!,
...wechatUserData,
};
const tokenValue = await setUpTokenAndUser<ED, Cxt>(
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();
}
/**
* 公众号授权登录
* @param param0
* @param context
*/
export async function loginWechat<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
{
code,
env,
wechatLoginId,
}: {
code: string;
env: WebEnv;
wechatLoginId?: string;
},
context: Cxt
): Promise<string> {
const closeRootMode = context.openRootMode();
const tokenValue = await loginFromWechatEnv<ED, Cxt>(
code,
env,
context,
wechatLoginId
);
const [tokenInfo] = await loadTokenInfo<ED, Cxt>(tokenValue, context);
assert(tokenInfo.entity === 'wechatUser');
await context.setTokenValue(tokenValue);
await tryRefreshWechatPublicUserInfo<ED, Cxt>(tokenInfo.entityId!, context);
closeRootMode();
return tokenValue;
}
/**
* 小程序授权登录
* @param param0
* @param context
* @returns
*/
export async function loginWechatMp<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
{
code,
env,
}: {
code: string;
env: WechatMpEnv;
},
context: Cxt
): Promise<string> {
const closeRootMode = context.openRootMode();
const tokenValue = await loginFromWechatEnv<ED, Cxt>(code, env, context);
await loadTokenInfo<ED, Cxt>(tokenValue, context);
closeRootMode();
return tokenValue;
}
/**
* 同步从wx.getUserProfile拿到的用户信息
* @param param0
* @param context
*/
export async function syncUserInfoWechatMp<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
{
nickname,
avatarUrl,
encryptedData,
iv,
signature,
}: {
nickname: string;
avatarUrl: string;
encryptedData: string;
iv: string;
signature: string;
},
context: Cxt
) {
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<ED, Cxt>(
user!,
{ nickname, avatar: avatarUrl },
context
);
}
export async function sendCaptcha<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
{
mobile,
env,
type: type2,
}: {
mobile: string;
env: WechatMpEnv | WebEnv | NativeEnv;
type: 'login' | 'changePassword' | 'confirm';
},
context: Cxt
): Promise<string> {
const { type } = env;
assert(type === 'web' || type === 'native');
let { visitorId } = env;
const application = context.getApplication();
const { system } = application!;
const mockSend = system?.config?.Sms?.mockSend;
const now = Date.now();
const duration = 1; // 多少分钟内有效
const closeRootMode = context.openRootMode();
if (process.env.NODE_ENV !== 'development' && !mockSend) {
const [count1, count2] = await Promise.all([
context.count(
'captcha',
{
filter: {
visitorId,
$$createAt$$: {
$gt: now - 3600 * 1000,
},
type: type2,
},
},
{
dontCollect: true,
}
),
context.count(
'captcha',
{
filter: {
mobile,
$$createAt$$: {
$gt: now - 3600 * 1000,
},
type: type2,
},
},
{
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: {
mobile,
$$createAt$$: {
$gt: now - 60 * 1000,
},
expired: false,
type: type2,
},
},
{
dontCollect: true,
}
);
if (captcha) {
const code = captcha.code!;
if (process.env.NODE_ENV !== 'production' || mockSend) {
closeRootMode();
return `验证码[${code}]已创建`;
} else if ((captcha.$$createAt$$! as number) - now < 60000) {
closeRootMode();
throw new OakUserException('您的操作太迅捷啦,请稍等再点吧');
} else {
// todo 再次发送
const result = await sendSms<ED, Cxt>(
{
origin: 'tencent',
templateName: '登录',
mobile,
templateParam: { code, duration: duration.toString() },
},
context
);
closeRootMode();
if (result.success) {
return '验证码已发送';
}
return '验证码发送失败';
}
} else {
let code: string;
if (process.env.NODE_ENV === 'development' || mockSend) {
code = mobile.substring(7);
} else {
code = Math.floor(Math.random() * 10000).toString();
while (code.length < 4) {
code += '0';
}
}
const id = await generateNewIdAsync();
await context.operate(
'captcha',
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id,
mobile,
code,
visitorId,
env,
expired: false,
expiresAt: now + 660 * 1000,
type: type2,
},
},
{
dontCollect: true,
}
);
if (process.env.NODE_ENV === 'development' || mockSend) {
closeRootMode();
return `验证码[${code}]已创建`;
} else {
//发送短信
const result = await sendSms<ED, Cxt>(
{
origin: 'tencent',
templateName: '登录',
mobile,
templateParam: { code, duration: duration.toString() },
},
context
);
closeRootMode();
if (result.success) {
return '验证码已发送';
}
return '验证码发送失败';
}
}
}
export async function switchTo<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>({ userId }: { userId: string }, context: Cxt) {
const reallyRoot = context.isReallyRoot();
if (!reallyRoot) {
throw new OakUserUnpermittedException('user', { id: 'switchTo', action: 'switch', data: {}, filter: { id: userId } });
}
const currentUserId = context.getCurrentUserId();
if (currentUserId === userId) {
throw new OakRowInconsistencyException(undefined, '您已经是当前用户');
}
const token = context.getToken()!;
await context.operate(
'token',
{
id: await generateNewIdAsync(),
action: 'update',
data: {
userId,
},
filter: {
id: token.id,
},
},
{}
);
}
export async function getWechatMpUserPhoneNumber<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>({ code, env }: { code: string; env: WechatMpEnv }, context: Cxt) {
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<ED, Cxt>(phoneNumber, env, context);
closeRootMode()
return reuslt;
}
export async function logout<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(params: { tokenValue: string }, context: Cxt) {
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<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
params: {
id: string;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: Cxt
) {
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) {
throw new OakRowInconsistencyException(
{
a: 's',
d: {
parasite: {
[id]: pick(parasite, ['id', 'expired']),
},
},
},
'数据已经过期'
);
}
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,
tokenValue,
applicationId: context.getApplicationId(),
},
},
{ dontCollect: true }
);
await loadTokenInfo<ED, Cxt>(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 === (<WebEnv>env2).visitorId;
}
case 'wechatMp': {
return true;
}
case 'native': {
return env1.visitorId === (<NativeEnv>env2).visitorId;
}
default: {
return false;
}
}
return true;
}
export async function refreshToken<
ED extends EntityDict,
Cxt extends BackendRuntimeContext<ED>
>(
params: {
env: WebEnv | WechatMpEnv | NativeEnv;
tokenValue: string;
},
context: Cxt
) {
const { env, tokenValue } = params;
const fn = context.openRootMode();
let [token] = await context.select('token', {
data: Object.assign({
env: 1,
...tokenProjection,
}),
filter: {
value: tokenValue,
},
}, {});
assert(token);
const now = Date.now();
if (!checkTokenEnvConsistency(env, token.env as WebEnv)) {
await context.operate('token', {
id: await generateNewIdAsync(),
action: 'disable',
data: {
},
filter: {
id: token.id,
}
}, { dontCollect: true });
fn();
return '';
}
else if (now - (token.refreshedAt as number) > 600 * 1000) {
const newValue = await generateNewIdAsync();
await context.operate('token', {
id: await generateNewIdAsync(),
action: 'update',
data: {
value: newValue,
refreshedAt: now,
},
filter: {
id: token.id,
}
}, {});
fn();
return newValue;
}
else {
fn();
}
return tokenValue;
}