From 32f3d7d9f83d1ac8f1917f9702861c277701133d Mon Sep 17 00:00:00 2001 From: "Xc@centOs" Date: Sat, 9 Jul 2022 22:36:22 +0800 Subject: [PATCH] =?UTF-8?q?loginByMobile=E4=B8=AD=E7=9A=84=E9=83=A8?= =?UTF-8?q?=E5=88=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/mobile/login/index.ts | 35 ++++- app/pages/mobile/login/index.tsx | 22 ++- app/pages/token/me/index.ts | 7 +- lib/RuntimeContext.d.ts | 2 +- lib/RuntimeContext.js | 2 +- lib/aspects/AspectDict.d.ts | 6 +- lib/aspects/index.d.ts | 4 +- lib/aspects/index.js | 2 +- lib/aspects/token.d.ts | 6 +- lib/aspects/token.js | 221 ++++++++++++++++++++++++---- lib/entities/Mobile.js | 13 ++ lib/entities/User.d.ts | 1 + lib/entities/User.js | 1 + lib/entities/WechatUser.d.ts | 4 +- lib/entities/WechatUser.js | 2 + lib/features/token.d.ts | 4 +- lib/features/token.js | 12 +- lib/types/Exceptions.d.ts | 18 +++ lib/types/Exceptions.js | 66 ++++++++- package.json | 3 +- src/RuntimeContext.ts | 2 +- src/aspects/AspectDict.ts | 2 +- src/aspects/index.ts | 4 +- src/aspects/token.ts | 245 ++++++++++++++++++++++++++----- src/entities/Mobile.ts | 19 ++- src/entities/User.ts | 2 + src/entities/WechatUser.ts | 4 + src/features/token.ts | 8 +- src/types/Exceptions.ts | 75 +++++++++- test/test.ts | 25 +--- 30 files changed, 690 insertions(+), 127 deletions(-) diff --git a/app/pages/mobile/login/index.ts b/app/pages/mobile/login/index.ts index b2c66307a..95039422e 100644 --- a/app/pages/mobile/login/index.ts +++ b/app/pages/mobile/login/index.ts @@ -1,5 +1,6 @@ import { composeFileUrl } from '../../../../src/utils/extraFile'; +const SEND_KEY = 'captcha:sendAt'; export default OakPage({ path: 'mobile:me', entity: 'mobile', @@ -12,24 +13,52 @@ export default OakPage({ mobile: '', password: '', captcha: '', + counter: 0, + }, + async formData({ features }) { + const lastSendAt = features.localStorage.load(SEND_KEY); + const now = Date.now(); + let counter = 0; + if (typeof lastSendAt === 'number') { + counter = Math.max(60 - Math.ceil((now - lastSendAt) / 1000), 0); + if (counter > 0) { + this.counterHandler = setTimeout(() => this.reRender(), 1000); + } + else if (this.counterHandler) { + clearTimeout(this.couuterHandler); + this.counterHandler = undefined; + } + } + return { + counter, + }; }, methods: { - onInput(e) { + onInput(e: any) { const { dataset, value } = this.resolveInput(e); const{ attr } = dataset; this.setState({ [attr]: value, }); }, - async sendCaptcha(type: 'web') { + async sendCaptcha() { const { mobile } = this.state; - const result = await this.features.token.sendCaptcha(mobile, type); + const result = await this.features.token.sendCaptcha(mobile); + // 显示返回消息 this.setState({ oakError: { type: 'success', msg: result, } }); + this.save(SEND_KEY, Date.now()); + this.reRender(); + }, + async loginByMobile() { + const { eventLoggedIn } = this.props; + const { mobile, password, captcha } = this.state; + await this.features.token.loginByMobile(mobile, password, captcha); + this.pub(eventLoggedIn); } }, }); \ No newline at end of file diff --git a/app/pages/mobile/login/index.tsx b/app/pages/mobile/login/index.tsx index 986773e30..06872e578 100644 --- a/app/pages/mobile/login/index.tsx +++ b/app/pages/mobile/login/index.tsx @@ -7,10 +7,7 @@ import { isMobile, isPassword, isCaptcha } from 'oak-domain/lib/utils/validator' const { TabPane } = Tabs; export default function render() { - const { mobile, captcha, password } = this.state; - const onFinish = (values: any) => { - console.log('Received values of form: ', values); - }; + const { mobile, captcha, password, counter} = this.state; const validMobile = isMobile(mobile); const validCaptcha = isCaptcha(captcha); const validPassword = isPassword(password); @@ -28,11 +25,9 @@ export default function render() { name="normal_login" className="login-form" initialValues={{ remember: true }} - onFinish={onFinish} > this.loginByMobile()} > Log in @@ -82,11 +77,9 @@ export default function render() { name="normal_login" className="login-form" initialValues={{ remember: true }} - onFinish={onFinish} > this.loginByMobile()} > Log in diff --git a/app/pages/token/me/index.ts b/app/pages/token/me/index.ts index c9abad4f6..b645b3f0c 100644 --- a/app/pages/token/me/index.ts +++ b/app/pages/token/me/index.ts @@ -118,8 +118,13 @@ export default OakPage({ break; } case 'web': { + const eventLoggedIn = `token:me:login:${Date.now()}`; + this.sub(eventLoggedIn, () => { + this.navigateBack(); + }) this.navigateTo({ - url: '/mobile/login' + url: '/mobile/login', + eventLoggedIn, }); break; } diff --git a/lib/RuntimeContext.d.ts b/lib/RuntimeContext.d.ts index 40fc0c81e..bd4382791 100644 --- a/lib/RuntimeContext.d.ts +++ b/lib/RuntimeContext.d.ts @@ -27,7 +27,7 @@ export declare abstract class GeneralRuntimeContext exten }> | undefined>; getTokenValue(): string | undefined; toString(): Promise; - protected static destructString(strCxt: string): { + protected static fromString(strCxt: string): { applicationId: any; scene: any; token: any; diff --git a/lib/RuntimeContext.js b/lib/RuntimeContext.js index 4431ee3ec..6e118c0c0 100644 --- a/lib/RuntimeContext.js +++ b/lib/RuntimeContext.js @@ -78,7 +78,7 @@ class GeneralRuntimeContext extends UniversalContext_1.UniversalContext { } return JSON.stringify(data); } - static destructString(strCxt) { + static fromString(strCxt) { const { applicationId, scene, token, } = JSON.parse(strCxt); return { applicationId, diff --git a/lib/aspects/AspectDict.d.ts b/lib/aspects/AspectDict.d.ts index 9941309b8..e751c91b7 100644 --- a/lib/aspects/AspectDict.d.ts +++ b/lib/aspects/AspectDict.d.ts @@ -3,9 +3,11 @@ import { EntityDict } from "general-app-domain"; import { QiniuUploadInfo } from "oak-frontend-base/src/types/Upload"; import { GeneralRuntimeContext } from ".."; declare type GeneralAspectDict> = { - loginByPassword: (params: { - password: string; + loginByMobile: (params: { + captcha?: string; + password?: string; mobile: string; + env: WebEnv | WechatMpEnv; }, context: Cxt) => Promise; loginMp: (params: { code: string; diff --git a/lib/aspects/index.d.ts b/lib/aspects/index.d.ts index 948e0fc0e..bc29b17dc 100644 --- a/lib/aspects/index.d.ts +++ b/lib/aspects/index.d.ts @@ -1,7 +1,7 @@ -import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token'; +import { loginByMobile, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token'; import { getUploadInfo } from './extraFile'; export declare const aspectDict: { - loginByPassword: typeof loginByPassword; + loginByMobile: typeof loginByMobile; loginMp: typeof loginMp; loginWechatMp: typeof loginWechatMp; syncUserInfoWechatMp: typeof syncUserInfoWechatMp; diff --git a/lib/aspects/index.js b/lib/aspects/index.js index 99f3aadfe..aa435d766 100644 --- a/lib/aspects/index.js +++ b/lib/aspects/index.js @@ -6,7 +6,7 @@ const extraFile_1 = require("./extraFile"); // import commonAspectDict from 'oak-common-aspect'; const lodash_1 = require("lodash"); exports.aspectDict = (0, lodash_1.assign)({ - loginByPassword: token_1.loginByPassword, + loginByMobile: token_1.loginByMobile, loginMp: token_1.loginMp, loginWechatMp: token_1.loginWechatMp, syncUserInfoWechatMp: token_1.syncUserInfoWechatMp, diff --git a/lib/aspects/token.d.ts b/lib/aspects/token.d.ts index b579039b7..5ca79148c 100644 --- a/lib/aspects/token.d.ts +++ b/lib/aspects/token.d.ts @@ -5,9 +5,11 @@ import { WebEnv, WechatMpEnv } from 'general-app-domain/Token/Schema'; export declare function loginMp>(params: { code: string; }, context: Cxt): Promise; -export declare function loginByPassword>(params: { - password: string; +export declare function loginByMobile>(params: { + captcha?: string; + password?: string; mobile: string; + env: WebEnv | WechatMpEnv; }, context: Cxt): Promise; export declare function loginWechatMp>({ code, env }: { code: string; diff --git a/lib/aspects/token.js b/lib/aspects/token.js index 191e9d570..459410ea3 100644 --- a/lib/aspects/token.js +++ b/lib/aspects/token.js @@ -3,29 +3,188 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendCaptcha = exports.syncUserInfoWechatMp = exports.loginWechatMp = exports.loginByPassword = exports.loginMp = void 0; +exports.sendCaptcha = exports.syncUserInfoWechatMp = exports.loginWechatMp = exports.loginByMobile = exports.loginMp = void 0; const oak_external_sdk_1 = require("oak-external-sdk"); const assert_1 = __importDefault(require("assert")); const lodash_1 = require("lodash"); const types_1 = require("oak-domain/lib/types"); const extraFile_1 = require("../utils/extraFile"); +const Exceptions_1 = require("../types/Exceptions"); async function loginMp(params, context) { const { rowStore } = context; throw new Error('method not implemented!'); } exports.loginMp = loginMp; -async function loginByPassword(params, context) { +async function setupMobile(mobile, env, context) { const { rowStore } = context; - const { result: [mobile] } = await rowStore.select('mobile', { + const currentToken = await context.getToken(); + const applicationId = context.getApplicationId(); + const { result: result2 } = await rowStore.select('mobile', { data: { id: 1, mobile: 1, userId: 1, + ableState: 1, + user: { + id: 1, + userState: 1, + wechatUser$user: { + $entity: 'wechatUser', + data: { + id: 1, + }, + }, + }, }, - }, context); - throw new Error('method not implemented!'); + filter: { + mobile, + ableState: 'enabled', + } + }, context, { notCollect: true }); + if (result2.length > 0) { + // 此手机号已经存在 + (0, assert_1.default)(result2.length === 1); + const [mobileRow] = result2; + if (currentToken) { + if (currentToken.userId === mobileRow.userId) { + return currentToken.id; + } + else { + // 此时可能要合并用户,如果用户有wechatUser信息,则抛出OakDistinguishUserByWechatUser异常,否则抛出 + const { user } = mobileRow; + const { wechatUser$user } = user; + if (wechatUser$user.length > 0) { + throw new Exceptions_1.OakDistinguishUserByWechatUserException(mobileRow.userId); + } + else { + throw new Exceptions_1.OakDistinguishUserByBusinessException(mobileRow.userId); + } + } + } + else { + // 此时以该手机号登录 todo根据环境来判断,用户也有可能是新获得此手机号,未来再进一步处理 + const tokenData = { + id: await generateNewId(), + applicationId, + playerId: mobileRow.userId, + env, + }; + const { user } = mobileRow; + const { userState } = user; + switch (userState) { + case 'disabled': { + throw new Exceptions_1.OakUserDisabledException(); + } + case 'shadow': { + (0, lodash_1.assign)(tokenData, { + userId: mobileRow.userId, + user: { + action: 'activate', + } + }); + break; + } + default: { + (0, assert_1.default)(userState === 'normal'); + (0, lodash_1.assign)(tokenData, { + userId: mobileRow.id, + }); + } + } + await rowStore.operate('token', { + data: tokenData, + action: 'create', + }, context); + return tokenData.id; + } + } + else { + //此手机号不存在 + if (currentToken) { + // 创建手机号并与之关联即可 + const mobileData = { + id: await generateNewId(), + mobile, + userId: currentToken.userId, + }; + await rowStore.operate('mobile', { + action: 'create', + data: mobileData + }, context); + return currentToken.id; + } + else { + // 创建token, mobile, user + const userData = { + id: await generateNewId(), + userState: 'normal', + }; + await rowStore.operate('user', { + action: 'create', + data: userData, + }, context); + const tokenData = { + id: await generateNewId(), + userId: userData.id, + playerId: userData.id, + env, + mobile: { + action: 'create', + data: { + id: await generateNewId(), + mobile, + userId: userData.id, + } + } + }; + await rowStore.operate('token', { + action: 'create', + data: tokenData, + }, context); + return tokenData.id; + } + } } -exports.loginByPassword = loginByPassword; +async function loginByMobile(params, context) { + const { mobile, captcha, password, env } = params; + const { rowStore } = context; + if (captcha) { + const { result } = await rowStore.select('captcha', { + data: { + id: 1, + expired: 1, + }, + filter: { + mobile, + code: captcha, + }, + sorter: [{ + $attr: { + $$createAt$$: 1, + }, + $direction: 'desc', + }], + indexFrom: 0, + count: 1, + }, context, { notCollect: true }); + if (result.length > 0) { + const [captchaRow] = result; + if (captchaRow.expired) { + throw new types_1.OakUserException('验证码已经过期'); + } + // 到这里说明验证码已经通过 + return await setupMobile(mobile, env, context); + } + else { + throw new types_1.OakUserException('验证码无效'); + } + } + else { + (0, assert_1.default)(password); + throw new Error('method not implemented!'); + } +} +exports.loginByMobile = loginByMobile; async function loginWechatMp({ code, env }, context) { const { rowStore } = context; const application = (await context.getApplication()); @@ -246,6 +405,8 @@ async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData, iv, si data: { id: 1, sessionKey: 1, + nickname: 1, + avatar: 1, user: { id: 1, nickname: 1, @@ -322,6 +483,7 @@ async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData, iv, si } }, context); } + // todo update nickname/avatar in wechatUser } exports.syncUserInfoWechatMp = syncUserInfoWechatMp; async function sendCaptcha({ mobile, env }, context) { @@ -330,26 +492,28 @@ async function sendCaptcha({ mobile, env }, context) { let { visitorId } = env; const { rowStore } = context; const now = Date.now(); - const [count1, count2] = await Promise.all([ - rowStore.count('captcha', { - filter: { - visitorId, - $$createAt$$: { - $gt: now - 3600 * 1000, + if (process.env.NODE_ENV !== 'development') { + const [count1, count2] = await Promise.all([ + rowStore.count('captcha', { + filter: { + visitorId, + $$createAt$$: { + $gt: now - 3600 * 1000, + }, }, - }, - }, context), - rowStore.count('captcha', { - filter: { - mobile, - $$createAt$$: { - $gt: now - 3600 * 1000, - }, - } - }, context) - ]); - if (count1 > 5 || count2 > 5) { - throw new types_1.OakUserException('您已发送很多次短信,请休息会再发吧'); + }, context), + rowStore.count('captcha', { + filter: { + mobile, + $$createAt$$: { + $gt: now - 3600 * 1000, + }, + } + }, context) + ]); + if (count1 > 5 || count2 > 5) { + throw new types_1.OakUserException('您已发送很多次短信,请休息会再发吧'); + } } const { result: [captcha] } = await rowStore.select('captcha', { data: { @@ -360,7 +524,7 @@ async function sendCaptcha({ mobile, env }, context) { filter: { mobile, $$createAt$$: { - $gt: now - 600 * 1000, + $gt: now - 60 * 1000, }, expired: false, } @@ -389,11 +553,12 @@ async function sendCaptcha({ mobile, env }, context) { code += '0'; } } - const { v1 } = require('uuid'); + const id = await generateNewId(); + console.log('captcha created', id); await rowStore.operate('captcha', { action: 'create', data: { - id: v1(), + id, mobile, code, visitorId, diff --git a/lib/entities/Mobile.js b/lib/entities/Mobile.js index cbda49b9a..5ed2ad340 100644 --- a/lib/entities/Mobile.js +++ b/lib/entities/Mobile.js @@ -1,12 +1,25 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +const action_1 = require("oak-domain/lib/actions/action"); ; +const AbleActionDef = (0, action_1.makeAbleActionDef)('enabled'); const locale = { zh_CN: { attr: { + ableState: '是否可用', mobile: '手机号', user: '关联用户', tokens: '相关令牌', }, + action: { + enable: '启用', + disable: '禁用', + }, + v: { + ableState: { + enabled: '可用的', + disabled: '禁用的', + } + } }, }; diff --git a/lib/entities/User.d.ts b/lib/entities/User.d.ts index 90972de5f..d5cc6aeb3 100644 --- a/lib/entities/User.d.ts +++ b/lib/entities/User.d.ts @@ -5,6 +5,7 @@ export interface Schema extends EntityShape { name?: String<16>; nickname?: String<64>; password?: Text; + passwordOrigin?: Text; birth?: Datetime; gender?: 'male' | 'female'; avatar?: Image; diff --git a/lib/entities/User.js b/lib/entities/User.js index 9b3f373f9..ec8cb9f2e 100644 --- a/lib/entities/User.js +++ b/lib/entities/User.js @@ -51,6 +51,7 @@ const locale = { nickname: '昵称', birth: '生日', password: '密码', + passwordOrigin: '明文密码', gender: '性别', avatar: '头像', idCardType: '证件类型', diff --git a/lib/entities/WechatUser.d.ts b/lib/entities/WechatUser.d.ts index 8c2b2cdb1..6165979ae 100644 --- a/lib/entities/WechatUser.d.ts +++ b/lib/entities/WechatUser.d.ts @@ -1,4 +1,4 @@ -import { String, Datetime, Boolean } from 'oak-domain/lib/types/DataType'; +import { String, Datetime, Image, Boolean } from 'oak-domain/lib/types/DataType'; import { Schema as User } from './User'; import { Schema as Application } from './Application'; import { Schema as Token } from './Token'; @@ -15,4 +15,6 @@ export interface Schema extends EntityShape { user?: User; application: Application; tokens: Array; + nickname?: String<128>; + avatar?: Image; } diff --git a/lib/entities/WechatUser.js b/lib/entities/WechatUser.js index edab668b7..cd7c69b30 100644 --- a/lib/entities/WechatUser.js +++ b/lib/entities/WechatUser.js @@ -15,6 +15,8 @@ const locale = { user: '用户', tokens: '相关令牌', application: '应用', + nickname: '昵称', + avatar: '头像', }, v: { origin: { diff --git a/lib/features/token.d.ts b/lib/features/token.d.ts index 1c19e7fb7..b3d6c85e5 100644 --- a/lib/features/token.d.ts +++ b/lib/features/token.d.ts @@ -11,12 +11,12 @@ export declare class Token>, cache: Cache>, context: Cxt); - loginByPassword(mobile: string, password: string): Promise; + loginByMobile(mobile: string, password?: string, captcha?: string): Promise; loginWechatMp(): Promise; syncUserInfoWechatMp(): Promise; logout(): Promise; getToken(): Promise; getUserId(): Promise; isRoot(): Promise; - sendCaptcha(mobile: string, type: 'web'): Promise; + sendCaptcha(mobile: string): Promise; } diff --git a/lib/features/token.js b/lib/features/token.js index fc943283b..b3c438978 100644 --- a/lib/features/token.js +++ b/lib/features/token.js @@ -22,10 +22,11 @@ class Token extends oak_frontend_base_1.Feature { this.cache = cache; this.context = context; } - async loginByPassword(mobile, password) { + async loginByMobile(mobile, password, captcha) { + const env = await (0, env_1.getEnv)(); await this.rwLock.acquire('X'); try { - const { result } = await this.getAspectWrapper().exec('loginByPassword', { password, mobile }); + const { result } = await this.getAspectWrapper().exec('loginByMobile', { password, mobile, captcha, env }); this.token = result; this.rwLock.release(); this.context.setToken(result); @@ -125,7 +126,7 @@ class Token extends oak_frontend_base_1.Feature { })); return tokenValue?.player?.userRole$user.length > 0 ? tokenValue?.player?.userRole$user[0]?.roleId === constants_1.ROOT_ROLE_ID : false; } - async sendCaptcha(mobile, type) { + async sendCaptcha(mobile) { const env = await (0, env_1.getEnv)(); const { result } = await this.getAspectWrapper().exec('sendCaptcha', { mobile, @@ -136,7 +137,7 @@ class Token extends oak_frontend_base_1.Feature { } __decorate([ oak_frontend_base_1.Action -], Token.prototype, "loginByPassword", null); +], Token.prototype, "loginByMobile", null); __decorate([ oak_frontend_base_1.Action ], Token.prototype, "loginWechatMp", null); @@ -146,4 +147,7 @@ __decorate([ __decorate([ oak_frontend_base_1.Action ], Token.prototype, "logout", null); +__decorate([ + oak_frontend_base_1.Action +], Token.prototype, "sendCaptcha", null); exports.Token = Token; diff --git a/lib/types/Exceptions.d.ts b/lib/types/Exceptions.d.ts index 82c7b75d5..01fb6079c 100644 --- a/lib/types/Exceptions.d.ts +++ b/lib/types/Exceptions.d.ts @@ -5,3 +5,21 @@ export declare class OakUnloggedInException extends OakUserException { export declare class OakNotEnoughMoneyException extends OakUserException { constructor(message?: string); } +export declare class OakDistinguishUserByWechatUserException extends OakUserException { + userId: string; + constructor(userId: string, message?: string); + toString(): string; +} +export declare class OakDistinguishUserByBusinessException extends OakUserException { + userId: string; + constructor(userId: string, message?: string); + toString(): string; +} +export declare class OakUserDisabledException extends OakUserException { + constructor(message?: string); +} +export declare function makeException(data: { + name: string; + message?: string; + [A: string]: any; +}): import("oak-domain/lib/types").OakException | undefined; diff --git a/lib/types/Exceptions.js b/lib/types/Exceptions.js index 2ef6cb9ce..5ec3d0a4d 100644 --- a/lib/types/Exceptions.js +++ b/lib/types/Exceptions.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.OakNotEnoughMoneyException = exports.OakUnloggedInException = void 0; +exports.makeException = exports.OakUserDisabledException = exports.OakDistinguishUserByBusinessException = exports.OakDistinguishUserByWechatUserException = exports.OakNotEnoughMoneyException = exports.OakUnloggedInException = void 0; const types_1 = require("oak-domain/lib/types"); class OakUnloggedInException extends types_1.OakUserException { constructor(message) { @@ -16,3 +16,67 @@ class OakNotEnoughMoneyException extends types_1.OakUserException { } exports.OakNotEnoughMoneyException = OakNotEnoughMoneyException; ; +class OakDistinguishUserByWechatUserException extends types_1.OakUserException { + userId; + constructor(userId, message) { + super(message || '系统中发现可能属于您的另一帐户'); + this.userId = userId; + } + toString() { + return JSON.stringify({ + name: this.name, + message: this.message, + userId: this.userId, + }); + } +} +exports.OakDistinguishUserByWechatUserException = OakDistinguishUserByWechatUserException; +class OakDistinguishUserByBusinessException extends types_1.OakUserException { + userId; + constructor(userId, message) { + super(message || '系统中发现可能属于您的另一帐户'); + this.userId = userId; + } + toString() { + return JSON.stringify({ + name: this.name, + message: this.message, + userId: this.userId, + }); + } +} +exports.OakDistinguishUserByBusinessException = OakDistinguishUserByBusinessException; +class OakUserDisabledException extends types_1.OakUserException { + constructor(message) { + super(message || '您的帐户已被禁用,请联系系统管理员'); + } +} +exports.OakUserDisabledException = OakUserDisabledException; +function makeException(data) { + const exception = (0, types_1.makeException)(data); + if (exception) { + return exception; + } + const { name, message } = data; + switch (name) { + case OakUnloggedInException.name: { + return new OakUnloggedInException(message); + } + case OakNotEnoughMoneyException.name: { + return new OakNotEnoughMoneyException(message); + } + case OakDistinguishUserByWechatUserException.name: { + return new OakDistinguishUserByWechatUserException(data.userId, message); + } + case OakDistinguishUserByBusinessException.name: { + return new OakDistinguishUserByBusinessException(data.userId, message); + } + case OakUserDisabledException.name: { + return new OakUserDisabledException(message); + } + default: { + return; + } + } +} +exports.makeException = makeException; diff --git a/package.json b/package.json index f0eab84ea..f9d998eb8 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "build": "tsc", "get:area": "ts-node ./scripts/getAmapArea.ts", "clean:dir": "ts-node ./scripts/cleanDtsAndJs", - "postinstall": "npm run prebuild" + "postinstall": "npm run prebuild", + "test": "ts-node ./test/test.ts" }, "main": "src/index" } diff --git a/src/RuntimeContext.ts b/src/RuntimeContext.ts index d989832e2..bab9f2285 100644 --- a/src/RuntimeContext.ts +++ b/src/RuntimeContext.ts @@ -103,7 +103,7 @@ export abstract class GeneralRuntimeContext extends Unive return JSON.stringify(data); } - protected static destructString(strCxt: string) { + protected static fromString(strCxt: string) { const { applicationId, scene, diff --git a/src/aspects/AspectDict.ts b/src/aspects/AspectDict.ts index 158a678ab..8a496849e 100644 --- a/src/aspects/AspectDict.ts +++ b/src/aspects/AspectDict.ts @@ -5,7 +5,7 @@ import { QiniuUploadInfo } from "oak-frontend-base/src/types/Upload"; import { GeneralRuntimeContext } from ".."; type GeneralAspectDict> = { - loginByPassword: (params: { password: string, mobile: string }, context: Cxt) => Promise, + loginByMobile: (params: { captcha?: string, password?: string, mobile: string, env: WebEnv | WechatMpEnv }, context: Cxt) => Promise, loginMp: (params: { code: string }, context: Cxt) => Promise, loginWechatMp: ({ code, env }: { code: string; diff --git a/src/aspects/index.ts b/src/aspects/index.ts index 54571881a..ad4143ef2 100644 --- a/src/aspects/index.ts +++ b/src/aspects/index.ts @@ -1,9 +1,9 @@ -import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token'; +import { loginByMobile, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token'; import { getUploadInfo } from './extraFile'; // import commonAspectDict from 'oak-common-aspect'; import { assign } from 'lodash'; export const aspectDict = assign({ - loginByPassword, + loginByMobile, loginMp, loginWechatMp, syncUserInfoWechatMp, diff --git a/src/aspects/token.ts b/src/aspects/token.ts index 2f12d3cc4..ca6fb79ea 100644 --- a/src/aspects/token.ts +++ b/src/aspects/token.ts @@ -10,24 +10,194 @@ import { Operation as ExtraFileOperation } from 'general-app-domain/ExtraFile/Sc import { assign, isEqual, keys } from 'lodash'; import { OakUserException, SelectRowShape } from 'oak-domain/lib/types'; import { composeFileUrl, decomposeFileUrl } from '../utils/extraFile'; +import { OakDistinguishUserByBusinessException, OakDistinguishUserByWechatUserException, OakUserDisabledException } from '../types/Exceptions'; export async function loginMp>(params: { code: string }, context: Cxt): Promise { const { rowStore } = context; throw new Error('method not implemented!'); } -export async function loginByPassword>(params: { password: string, mobile: string }, context: Cxt): Promise { +async function setupMobile>(mobile: string, env: WebEnv | WechatMpEnv, context: Cxt) { const { rowStore } = context; + const currentToken = await context.getToken(); + const applicationId = context.getApplicationId(); - const { result: [mobile] } = await rowStore.select('mobile', { + const { result: result2 } = await rowStore.select('mobile', { data: { id: 1, mobile: 1, userId: 1, + ableState: 1, + user: { + id: 1, + userState: 1, + wechatUser$user: { + $entity: 'wechatUser', + data: { + id: 1, + }, + }, + }, }, - }, context); + filter: { + mobile, + ableState: 'enabled', + } + }, context, { notCollect: true }); + if (result2.length > 0) { + // 此手机号已经存在 + assert(result2.length === 1); + const [ mobileRow ] = result2; + if (currentToken) { + if (currentToken.userId === mobileRow.userId) { + return currentToken.id; + } + else { + // 此时可能要合并用户,如果用户有wechatUser信息,则抛出OakDistinguishUserByWechatUser异常,否则抛出 + const { user } = mobileRow; + const { wechatUser$user } = user as { + wechatUser$user: any[]; + }; + if (wechatUser$user.length > 0) { + throw new OakDistinguishUserByWechatUserException(mobileRow.userId as string); + } + else { + throw new OakDistinguishUserByBusinessException(mobileRow.userId as string); + } + } + } + else { + // 此时以该手机号登录 todo根据环境来判断,用户也有可能是新获得此手机号,未来再进一步处理 + const tokenData: EntityDict['token']['CreateSingle']['data'] = { + id: await generateNewId(), + applicationId, + playerId: mobileRow.userId as string, + env, + }; + const { user } = mobileRow; + const { userState } = user as SelectRowShape; + switch (userState) { + case 'disabled': { + throw new OakUserDisabledException(); + } + case 'shadow': { + assign(tokenData, { + userId: mobileRow.userId, + user: { + action: 'activate', + } + }); + break; + } + default: { + assert(userState === 'normal'); + assign(tokenData, { + userId: mobileRow.id, + }); + } + } - throw new Error('method not implemented!'); + await rowStore.operate('token', { + data: tokenData, + action: 'create', + }, context); + + return tokenData.id; + } + } + else { + //此手机号不存在 + if (currentToken) { + // 创建手机号并与之关联即可 + const mobileData: EntityDict['mobile']['CreateSingle']['data'] = { + id: await generateNewId(), + mobile, + userId: currentToken.userId!, + }; + await rowStore.operate('mobile', { + action: 'create', + data: mobileData + }, context); + return currentToken.id; + } + else { + // 创建token, mobile, user + const userData: EntityDict['user']['CreateSingle']['data'] = { + id: await generateNewId(), + userState: 'normal', + }; + await rowStore.operate('user', { + action: 'create', + data: userData, + }, context); + const tokenData: EntityDict['token']['CreateSingle']['data'] = { + id: await generateNewId(), + userId: userData.id, + playerId: userData.id, + env, + mobile: { + action: 'create', + data: { + id: await generateNewId(), + mobile, + userId: userData.id, + } + } + }; + await rowStore.operate('token', { + action: 'create', + data: tokenData, + }, context); + + return tokenData.id; + } + } +} + +export async function loginByMobile>( + params: { captcha?: string, password?: string, mobile: string, env: WebEnv | WechatMpEnv }, + context: Cxt): Promise { + const { mobile, captcha, password, env } = params; + const { rowStore } = context; + if (captcha) { + const { result } = await rowStore.select('captcha', { + data: { + id: 1, + expired: 1, + }, + filter: { + mobile, + code: captcha, + }, + sorter: [{ + $attr: { + $$createAt$$: 1, + }, + $direction: 'desc', + }], + indexFrom: 0, + count: 1, + }, context, { notCollect: true }); + if (result.length > 0) { + const [captchaRow] = result; + if (captchaRow.expired) { + throw new OakUserException('验证码已经过期'); + } + + // 到这里说明验证码已经通过 + return await setupMobile(mobile, env, context); + } + else { + throw new OakUserException('验证码无效'); + } + } + else { + assert(password); + throw new Error('method not implemented!'); + } } export async function loginWechatMp>({ code, env }: { @@ -180,7 +350,7 @@ export async function loginWechatMp>({ nickname, avatarUrl, encryptedData, iv, signature -}: {nickname: string, avatarUrl: string, encryptedData: string, iv: string, signature: string}, context: Cxt) { +}: { nickname: string, avatarUrl: string, encryptedData: string, iv: string, signature: string }, context: Cxt) { const { rowStore } = context; const { userId } = (await context.getToken())!; const application = (await context.getApplication())!; - const { result: [{ sessionKey, user }]} = await rowStore.select('wechatUser', { + const { result: [{ sessionKey, user }] } = await rowStore.select('wechatUser', { data: { id: 1, sessionKey: 1, + nickname: 1, + avatar:1, user: { id: 1, nickname: 1, @@ -358,6 +530,8 @@ export async function syncUserInfoWechatMp 5 || count2 > 5) { - throw new OakUserException('您已发送很多次短信,请休息会再发吧'); + }, context), + rowStore.count('captcha', { + filter: { + mobile, + $$createAt$$: { + $gt: now - 3600 * 1000, + }, + } + }, context) + ] + ); + if (count1 > 5 || count2 > 5) { + throw new OakUserException('您已发送很多次短信,请休息会再发吧'); + } } const { result: [captcha] } = await rowStore.select('captcha', { data: { @@ -404,7 +580,7 @@ export async function sendCaptcha; @@ -10,12 +11,28 @@ export interface Schema extends EntityShape { tokens: Array; }; -const locale: LocaleDef = { +type Action = AbleAction; +const AbleActionDef = makeAbleActionDef('enabled'); + +const locale: LocaleDef = { zh_CN: { attr: { + ableState: '是否可用', mobile: '手机号', user: '关联用户', tokens: '相关令牌', }, + action: { + enable: '启用', + disable: '禁用', + }, + v: { + ableState: { + enabled: '可用的', + disabled: '禁用的', + } + } }, }; diff --git a/src/entities/User.ts b/src/entities/User.ts index dc9d0577e..f298363c6 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -9,6 +9,7 @@ export interface Schema extends EntityShape { name?: String<16>; nickname?: String<64>; password?: Text; + passwordOrigin?: Text; birth?: Datetime; gender?: 'male' | 'female'; avatar?: Image; @@ -84,6 +85,7 @@ const locale: LocaleDef; + nickname?: String<128>; + avatar?: Image; }; const locale: LocaleDef, } @Action - async loginByPassword(mobile: string, password: string) { + async loginByMobile(mobile: string, password?: string, captcha?: string) { + const env = await getEnv(); await this.rwLock.acquire('X'); try { - const { result } = await this.getAspectWrapper().exec('loginByPassword', { password, mobile }); + const { result } = await this.getAspectWrapper().exec('loginByMobile', { password, mobile, captcha, env }); this.token = result; this.rwLock.release(); this.context.setToken(result); @@ -154,7 +155,8 @@ export class Token, return (tokenValue?.player?.userRole$user as any).length > 0 ? (tokenValue?.player?.userRole$user as any)[0]?.roleId === ROOT_ROLE_ID : false; } - async sendCaptcha(mobile: string, type: 'web') { + @Action + async sendCaptcha(mobile: string) { const env = await getEnv(); const { result } = await this.getAspectWrapper().exec('sendCaptcha', { mobile, diff --git a/src/types/Exceptions.ts b/src/types/Exceptions.ts index d834a5586..5c9f59243 100644 --- a/src/types/Exceptions.ts +++ b/src/types/Exceptions.ts @@ -1,4 +1,4 @@ -import { OakUserException } from "oak-domain/lib/types"; +import { OakUserException, makeException as makeException2 } from "oak-domain/lib/types"; export class OakUnloggedInException extends OakUserException { constructor(message?: string) { @@ -9,4 +9,75 @@ export class OakNotEnoughMoneyException extends OakUserException { constructor(message?: string) { super(message || '您的余额不足'); } -}; \ No newline at end of file +}; + +export class OakDistinguishUserByWechatUserException extends OakUserException { + userId: string; + constructor(userId: string, message?: string) { + super(message || '系统中发现可能属于您的另一帐户'); + this.userId = userId; + } + + toString() { + return JSON.stringify({ + name: this.name, + message: this.message, + userId: this.userId, + }); + } +} + +export class OakDistinguishUserByBusinessException extends OakUserException { + userId: string; + constructor(userId: string, message?: string) { + super(message || '系统中发现可能属于您的另一帐户'); + this.userId = userId; + } + + toString() { + return JSON.stringify({ + name: this.name, + message: this.message, + userId: this.userId, + }); + } +} + +export class OakUserDisabledException extends OakUserException { + constructor(message?: string) { + super(message || '您的帐户已被禁用,请联系系统管理员'); + } +} + +export function makeException(data: { + name: string; + message?: string; + [A: string]: any; +}) { + const exception = makeException2(data); + if (exception) { + return exception; + } + + const { name, message } = data; + switch (name) { + case OakUnloggedInException.name: { + return new OakUnloggedInException(message); + } + case OakNotEnoughMoneyException.name: { + return new OakNotEnoughMoneyException(message); + } + case OakDistinguishUserByWechatUserException.name: { + return new OakDistinguishUserByWechatUserException(data.userId, message); + } + case OakDistinguishUserByBusinessException.name: { + return new OakDistinguishUserByBusinessException(data.userId, message); + } + case OakUserDisabledException.name: { + return new OakUserDisabledException(message); + } + default: { + return; + } + } +} \ No newline at end of file diff --git a/test/test.ts b/test/test.ts index d2c2b5971..2ed3b8ab2 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,22 +1,7 @@ -import { EntityDict } from 'oak-app-domain'; -import { SelectRowShape } from 'oak-domain/lib/types/Entity'; +import { v1 } from 'uuid'; -function select(entity: T, proj: P): SelectRowShape { - throw new Error('method not implemented'); -} - -const r = select('address', { - id: 1, - name: 1, - detail: 1, - area: { - id: 1, - name: 1, - }, - $expr10: { - $abs: 10, - }, -}); - -r.area.name +let iter = 0; +while( iter ++ < 20) { + console.log(v1()); +} \ No newline at end of file