mobile-login的部分业务逻辑

This commit is contained in:
Xu Chang 2022-07-08 20:15:47 +08:00
parent c20791a4ed
commit 17a2024233
35 changed files with 739 additions and 62 deletions

View File

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "手机号登录",
"usingComponents": {
"l-button": "../../../lin-ui/button/index"
}
}

View File

@ -0,0 +1,31 @@
/** index.wxss **/
@import "../../../config/styles/_base.less";
@import "../../../config/styles/_mixins.less";
.page-body {
height: 100vh;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
background-color: @background-color-base;
.safe-area-inset-bottom();
}
.card {
width: 90%;
align-self: center;
}
.login-form-button {
width: 100%;
}
.ant-tabs-nav-list {
width: 100%;
.ant-tabs-tab {
width: 50%;
}
}

View File

@ -0,0 +1,35 @@
import { composeFileUrl } from '../../../../src/utils/extraFile';
export default OakPage({
path: 'mobile:me',
entity: 'mobile',
projection: {
id: 1,
mobile: 1,
userId: 1,
},
data: {
mobile: '',
password: '',
captcha: '',
},
methods: {
onInput(e) {
const { dataset, value } = this.resolveInput(e);
const{ attr } = dataset;
this.setState({
[attr]: value,
});
},
async sendCaptcha(type: 'web') {
const { mobile } = this.state;
const result = await this.features.token.sendCaptcha(mobile, type);
this.setState({
oakError: {
type: 'success',
msg: result,
}
});
}
},
});

View File

@ -0,0 +1,152 @@
import React, { Component } from 'react';
import { Card, Form, Input, Checkbox, Button, Tabs } from 'antd';
import { LockOutlined, FieldNumberOutlined, MobileOutlined } from '@ant-design/icons';
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 validMobile = isMobile(mobile);
const validCaptcha = isCaptcha(captcha);
const validPassword = isPassword(password);
const allowSubmit = validMobile && (validCaptcha|| validPassword);
return (
<div className='page-body'>
<div style={{
flex: 2,
}} />
<Card className="card">
<Tabs defaultActiveKey="1" size="large" tabBarStyle={{ width: '100%' }}>
<TabPane tab="in Password" key="1">
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
onFinish={onFinish}
>
<Form.Item
name="mobile"
rules={[{ required: true, message: 'Please input your Mobile!' }]}
>
<Input
allowClear
value={mobile}
type="tel"
data-attr="mobile"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input
allowClear
value={password}
data-attr="password"
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
>
Log in
</Button>
</Form.Item>
</Form>
</TabPane>
<TabPane tab="in Captcha" key="2">
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
onFinish={onFinish}
>
<Form.Item
name="mobile"
rules={[{ required: true, message: 'Please input your Mobile!' }]}
>
<Input.Group compact>
<Input
allowClear
value={mobile}
data-attr="mobile"
type="tel"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
style={{ width: 'calc(100% - 65px)' }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
<Button
type="primary"
disabled={!validMobile}
onClick={() => this.sendCaptcha('web')}
>
Send
</Button>
</Input.Group>
</Form.Item>
<Form.Item
name="captcha"
rules={[{ required: true, message: 'Please input the captcha received!' }]}
>
<Input
allowClear
value={captcha}
data-attr="captcha"
prefix={<FieldNumberOutlined className="site-form-item-icon" />}
type="number"
maxLength={4}
placeholder="Captcha"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
>
Log in
</Button>
</Form.Item>
</Form>
</TabPane>
</Tabs>
</Card>
<div style={{
flex: 3,
}} />
</div>
);
}

View File

@ -0,0 +1,17 @@
<!-- index.wxml -->
<view class="page-body">
<block wx:if="{{mobiles && mobiles.length > 0}}">
<view wx:for="{{mobiles}}" wx:key="index" class="card">
<text>{{item.mobile}}</text>
</view>
</block>
<block wx:else>
<view style="flex:1; display:flex; align-items:center;justify-content:center">尚未授权手机号</view>
</block>
<view style="display: flex; flex: 1"></view>
<view class="btn-box">
<l-button open-type="getPhoneNumber" type="default" size="large" bindgetphonenumber="onRefreshMobile">
授权手机号
</l-button>
</view>
</view>

View File

@ -119,7 +119,7 @@ export default OakPage({
}
case 'web': {
this.navigateTo({
url: '/mobile/me'
url: '/mobile/login'
});
break;
}

View File

@ -1,5 +1,5 @@
import { WebEnv, WechatMpEnv } from "general-app-domain/Token/Schema";
import { EntityDict } from "general-app-domain";
import { WechatMpEnv } from "general-app-domain/Token/Schema";
import { QiniuUploadInfo } from "oak-frontend-base/src/types/Upload";
import { GeneralRuntimeContext } from "..";
declare type GeneralAspectDict<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>> = {
@ -25,6 +25,10 @@ declare type GeneralAspectDict<ED extends EntityDict, Cxt extends GeneralRuntime
origin: string;
fileName: string;
}, context: Cxt) => Promise<QiniuUploadInfo>;
sendCaptcha: (params: {
mobile: string;
env: WechatMpEnv | WebEnv;
}) => Promise<string>;
};
export declare type AspectDict<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>> = GeneralAspectDict<ED, Cxt>;
export {};

View File

@ -1,4 +1,4 @@
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp } from './token';
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token';
import { getUploadInfo } from './extraFile';
export declare const aspectDict: {
loginByPassword: typeof loginByPassword;
@ -6,4 +6,5 @@ export declare const aspectDict: {
loginWechatMp: typeof loginWechatMp;
syncUserInfoWechatMp: typeof syncUserInfoWechatMp;
getUploadInfo: typeof getUploadInfo;
sendCaptcha: typeof sendCaptcha;
};

View File

@ -11,5 +11,6 @@ exports.aspectDict = (0, lodash_1.assign)({
loginWechatMp: token_1.loginWechatMp,
syncUserInfoWechatMp: token_1.syncUserInfoWechatMp,
getUploadInfo: extraFile_1.getUploadInfo,
sendCaptcha: token_1.sendCaptcha,
} /* , commonAspectDict */);
// export type AspectDict<ED extends EntityDict & BaseEntityDict> = TokenAD<ED> & CrudAD<ED>;

View File

@ -1,6 +1,7 @@
import { GeneralRuntimeContext } from '../RuntimeContext';
import { EntityDict } from 'general-app-domain';
import { WechatMpEnv } from 'general-app-domain/Token/Schema';
import { WechatMpConfig } from 'general-app-domain/Application/Schema';
import { WebEnv, WechatMpEnv } from 'general-app-domain/Token/Schema';
export declare function loginMp<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>(params: {
code: string;
}, context: Cxt): Promise<string>;
@ -24,3 +25,7 @@ export declare function syncUserInfoWechatMp<ED extends EntityDict, Cxt extends
iv: string;
signature: string;
}, context: Cxt): Promise<void>;
export declare function sendCaptcha<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>({ mobile, env }: {
mobile: string;
env: WechatMpConfig | WebEnv;
}, context: Cxt): Promise<string>;

View File

@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.syncUserInfoWechatMp = exports.loginWechatMp = exports.loginByPassword = exports.loginMp = void 0;
exports.sendCaptcha = exports.syncUserInfoWechatMp = exports.loginWechatMp = exports.loginByPassword = 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");
async function loginMp(params, context) {
const { rowStore } = context;
@ -323,8 +324,90 @@ async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData, iv, si
}
}
exports.syncUserInfoWechatMp = syncUserInfoWechatMp;
/* export type AspectDict<ED extends EntityDict> = {
loginMp: (params: { code: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
loginByPassword: (params: { password: string, mobile: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
};
*/
async function sendCaptcha({ mobile, env }, context) {
const { type } = env;
(0, assert_1.default)(type === 'web');
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,
},
},
}, 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: {
id: 1,
code: 1,
$$createAt$$: 1,
},
filter: {
mobile,
$$createAt$$: {
$gt: now - 600 * 1000,
},
expired: false,
}
}, context);
if (captcha) {
if (process.env.NODE_ENV === 'development') {
const { code } = captcha;
return `验证码[${code}]已创建`;
}
else if (captcha.$$createAt$$ - now < 60000) {
throw new types_1.OakUserException('您的操作太迅捷啦,请稍等再点吧');
}
else {
// todo 再次发送
return '验证码已发送';
}
}
else {
let code;
if (process.env.NODE_ENV === 'development') {
code = mobile.substring(7);
}
else {
code = Math.floor(Math.random() * 10000).toString();
while (code.length < 4) {
code += '0';
}
}
const { v1 } = require('uuid');
await rowStore.operate('captcha', {
action: 'create',
data: {
id: v1(),
mobile,
code,
visitorId,
env,
expired: false,
expiresAt: now + 660 * 1000,
}
}, context);
if (process.env.NODE_ENV === 'development') {
return `验证码[${code}]已创建`;
}
else {
return '验证码已创建';
}
}
}
exports.sendCaptcha = sendCaptcha;

View File

@ -9,7 +9,7 @@ export declare function createWechatQrCode<ED extends EntityDict, T extends keyo
lifetimeLength?: number;
permanent?: boolean;
props: WechatQrCodeProps;
}, context: Cxt): Promise<Omit<Omit<import("general-app-domain/WechatQrCode/Schema").OpSchema, "entity" | "entityId" | "applicationId">, import("oak-domain/lib/types").InstinctiveAttributes> & {
}, context: Cxt): Promise<Omit<Omit<import("general-app-domain/WechatQrCode/Schema").OpSchema, "applicationId" | "entity" | "entityId">, import("oak-domain/lib/types").InstinctiveAttributes> & {
id: string;
} & {
applicationId: string;

11
lib/entities/Captcha.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { String, Text, Boolean, Datetime } from 'oak-domain/lib/types/DataType';
export interface Schema extends EntityShape {
mobile: String<11>;
code: String<4>;
visitorId: Text;
reason?: Text;
env: Object;
expired: Boolean;
expiresAt: Datetime;
}

38
lib/entities/Captcha.js Normal file
View File

@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
;
const IActionDef = {
stm: {
send: ['unsent', 'sending'],
success: ['sending', 'sent'],
fail: ['sending', 'failure'],
},
is: 'unsent',
};
const locale = {
zh_CN: {
attr: {
mobile: '手机号',
code: '验证码',
visitorId: '用户标识',
reason: '失败原因',
env: '用户环境',
expired: '是否过期',
expiresAt: '过期时间',
iState: '状态',
},
action: {
send: '发送',
fail: '失败',
success: '成功',
},
v: {
iState: {
unsent: '未发送',
sending: '发送中',
sent: '已发送',
failure: '已失败',
}
},
},
};

View File

@ -21,6 +21,19 @@ export declare type WechatMpEnv = {
};
export declare type WebEnv = {
type: 'web';
visitorId: string;
platform: {
value: string;
};
timezone: {
value: string;
};
vendor: {
value: string;
};
vendorFlavors: {
value: string[];
};
};
export declare type ServerEnv = {
type: 'server';

View File

@ -18,4 +18,5 @@ export declare class Token<ED extends EntityDict, Cxt extends GeneralRuntimeCont
getToken(): Promise<string | undefined>;
getUserId(): Promise<string | undefined>;
isRoot(): Promise<boolean>;
sendCaptcha(mobile: string, type: 'web'): Promise<string>;
}

View File

@ -7,9 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Token = void 0;
const lodash_1 = require("lodash");
const oak_frontend_base_1 = require("oak-frontend-base");
const concurrent_1 = require("oak-domain/lib/utils/concurrent");
const env_1 = require("../utils/env");
const constants_1 = require("../constants");
class Token extends oak_frontend_base_1.Feature {
token;
@ -39,26 +39,10 @@ class Token extends oak_frontend_base_1.Feature {
await this.rwLock.acquire('X');
try {
const { code } = await wx.login();
const env = await wx.getSystemInfo();
const env2 = (0, lodash_1.pick)(env, [
'brand',
'model',
'pixelRatio',
'screenWidth',
'screenHeight',
'windowWidth',
'windowHeight',
'statusBarHeight',
'language',
'version',
'system',
'platform',
'fontSizeSetting',
'SDKVersion'
]);
const env = await (0, env_1.getEnv)();
const { result } = await this.getAspectWrapper().exec('loginWechatMp', {
code,
env: Object.assign(env2, { type: 'wechatMp' }),
env: env,
});
this.token = result;
this.context.setToken(result);
@ -141,6 +125,14 @@ 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) {
const env = await (0, env_1.getEnv)();
const { result } = await this.getAspectWrapper().exec('sendCaptcha', {
mobile,
env: env,
});
return result;
}
}
__decorate([
oak_frontend_base_1.Action

2
lib/utils/env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { getEnv } from './env.web';
export { getEnv, };

6
lib/utils/env.js Normal file
View File

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEnv = void 0;
const env_web_1 = require("./env.web");
Object.defineProperty(exports, "getEnv", { enumerable: true, get: function () { return env_web_1.getEnv; } });
console.log('不应该走到这里');

5
lib/utils/env.web.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/**
* fingerprintJs当中的一些敏感项
* @returns
*/
export declare function getEnv(): Promise<any>;

27
lib/utils/env.web.js Normal file
View File

@ -0,0 +1,27 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEnv = void 0;
const fingerprintjs_1 = __importDefault(require("@fingerprintjs/fingerprintjs"));
const lodash_1 = require("lodash");
/**
* fingerprintJs当中的一些敏感项
* @returns
*/
async function getEnv() {
const fp = await fingerprintjs_1.default.load();
const result = await fp.get();
const { visitorId, components } = result;
return (0, lodash_1.assign)((0, lodash_1.pick)(components, [
'platform',
'timezone',
'vendor',
'vendorFlavors'
]), {
type: 'web',
visitorId,
});
}
exports.getEnv = getEnv;

1
lib/utils/env.wechatMp.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare function getEnv(): Promise<any>;

27
lib/utils/env.wechatMp.js Normal file
View File

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEnv = void 0;
const lodash_1 = require("lodash");
async function getEnv() {
const env = await wx.getSystemInfo();
const env2 = (0, lodash_1.pick)(env, [
'brand',
'model',
'pixelRatio',
'screenWidth',
'screenHeight',
'windowWidth',
'windowHeight',
'statusBarHeight',
'language',
'version',
'system',
'platform',
'fontSizeSetting',
'SDKVersion'
]);
return (0, lodash_1.assign)(env2, {
type: 'wechatMp',
});
}
exports.getEnv = getEnv;

View File

@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "oak框架中公共业务逻辑的实现",
"dependencies": {
"@fingerprintjs/fingerprintjs": "^3.3.3",
"lodash": "^4.17.21",
"oak-common-aspect": "file:../oak-common-aspect",
"oak-domain": "file:../oak-domain",
@ -40,7 +41,8 @@
"prebuild": "ts-node ./scripts/make.ts",
"build": "tsc",
"get:area": "ts-node ./scripts/getAmapArea.ts",
"clean:dir": "ts-node ./scripts/cleanDtsAndJs"
"clean:dir": "ts-node ./scripts/cleanDtsAndJs",
"postinstall": "npm run prebuild"
},
"main": "src/index"
}

View File

@ -1,5 +1,5 @@
import { WebEnv, WechatMpEnv } from "general-app-domain/Token/Schema";
import { EntityDict } from "general-app-domain";
import { WechatMpEnv } from "general-app-domain/Token/Schema";
import { QiniuUploadInfo } from "oak-frontend-base/src/types/Upload";
// import { AspectDict as CommonAspectDict } from 'oak-common-aspect/src/aspectDict';
import { GeneralRuntimeContext } from "..";
@ -15,6 +15,10 @@ type GeneralAspectDict<ED extends EntityDict, Cxt extends GeneralRuntimeContext<
nickname, avatarUrl, encryptedData, iv, signature
}: {nickname: string, avatarUrl: string, encryptedData: string, iv: string, signature: string}, context: Cxt) => Promise<void>,
getUploadInfo: (params: { origin: string, fileName: string }, context: Cxt) => Promise<QiniuUploadInfo>,
sendCaptcha: (params: {
mobile: string;
env: WechatMpEnv | WebEnv;
}) => Promise<string>,
};
export type AspectDict<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>> = GeneralAspectDict<ED, Cxt>;

View File

@ -1,4 +1,4 @@
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp } from './token';
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp, sendCaptcha } from './token';
import { getUploadInfo } from './extraFile';
// import commonAspectDict from 'oak-common-aspect';
import { assign } from 'lodash';
@ -8,6 +8,7 @@ export const aspectDict = assign({
loginWechatMp,
syncUserInfoWechatMp,
getUploadInfo,
sendCaptcha,
}/* , commonAspectDict */);
// export type AspectDict<ED extends EntityDict & BaseEntityDict> = TokenAD<ED> & CrudAD<ED>;

View File

@ -3,12 +3,12 @@ import { EntityDict } from 'general-app-domain';
import { WechatSDK } from 'oak-external-sdk';
import assert from 'assert';
import { WechatMpConfig } from 'general-app-domain/Application/Schema';
import { CreateOperationData as CreateToken, WechatMpEnv } from 'general-app-domain/Token/Schema';
import { CreateOperationData as CreateToken, WebEnv, WechatMpEnv } from 'general-app-domain/Token/Schema';
import { CreateOperationData as CreateWechatUser } from 'general-app-domain/WechatUser/Schema';
import { CreateOperationData as CreateUser, Schema as User } from 'general-app-domain/User/Schema';
import { Operation as ExtraFileOperation } from 'general-app-domain/ExtraFile/Schema';
import { assign, isEqual, keys } from 'lodash';
import { SelectRowShape } from 'oak-domain/lib/types';
import { OakUserException, SelectRowShape } from 'oak-domain/lib/types';
import { composeFileUrl, decomposeFileUrl } from '../utils/extraFile';
export async function loginMp<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>(params: { code: string }, context: Cxt): Promise<string> {
@ -360,8 +360,99 @@ export async function syncUserInfoWechatMp<ED extends EntityDict, Cxt extends Ge
}
}
/* export type AspectDict<ED extends EntityDict> = {
loginMp: (params: { code: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
loginByPassword: (params: { password: string, mobile: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
};
*/
export async function sendCaptcha<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>({ mobile, env }: {
mobile: string;
env: WechatMpConfig | WebEnv
}, context: Cxt): Promise<string> {
const { type } = env;
assert(type === 'web');
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,
},
},
}, 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: {
id: 1,
code: 1,
$$createAt$$: 1,
},
filter: {
mobile,
$$createAt$$: {
$gt: now - 600 * 1000,
},
expired: false,
}
}, context);
if (captcha) {
if (process.env.NODE_ENV === 'development') {
const { code } = captcha;
return `验证码[${code}]已创建`;
}
else if (captcha.$$createAt$$! as number - now < 60000) {
throw new OakUserException('您的操作太迅捷啦,请稍等再点吧');
}
else {
// todo 再次发送
return '验证码已发送';
}
}
else {
let code: string;
if (process.env.NODE_ENV === 'development') {
code = mobile.substring(7);
}
else {
code = Math.floor(Math.random() * 10000).toString();
while (code.length < 4) {
code += '0';
}
}
const { v1 } = require('uuid');
await rowStore.operate('captcha', {
action: 'create',
data: {
id: v1(),
mobile,
code,
visitorId,
env,
expired: false,
expiresAt: now + 660 * 1000,
}
}, context);
if (process.env.NODE_ENV === 'development') {
return `验证码[${code}]已创建`;
}
else {
return '验证码已创建';
}
}
}

58
src/entities/Captcha.ts Normal file
View File

@ -0,0 +1,58 @@
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { String, Text, Boolean, Datetime } from 'oak-domain/lib/types/DataType';
import { LocaleDef } from 'oak-domain/lib/types/Locale';
import { ActionDef } from 'oak-domain/lib/types';
export interface Schema extends EntityShape {
mobile: String<11>;
code: String<4>;
visitorId: Text;
reason?: Text;
env: Object;
expired: Boolean;
expiresAt: Datetime;
};
type IState = 'unsent' | 'sending' | 'sent' | 'failure';
type IAction = 'send' | 'success' | 'fail';
const IActionDef: ActionDef<IAction, IState> = {
stm: {
send: ['unsent', 'sending'],
success: ['sending', 'sent'],
fail: ['sending', 'failure'],
},
is: 'unsent',
};
type Action = IAction;
const locale: LocaleDef<Schema, Action, '', {
iState: IState,
}> = {
zh_CN: {
attr: {
mobile: '手机号',
code: '验证码',
visitorId: '用户标识',
reason: '失败原因',
env: '用户环境',
expired: '是否过期',
expiresAt: '过期时间',
iState: '状态',
},
action: {
send: '发送',
fail: '失败',
success: '成功',
},
v: {
iState: {
unsent: '未发送',
sending: '发送中',
sent: '已发送',
failure: '已失败',
}
},
},
};

View File

@ -26,6 +26,19 @@ export type WechatMpEnv = {
export type WebEnv = {
type: 'web',
visitorId: string;
platform: {
value: string;
};
timezone: {
value: string;
};
vendor: {
value: string;
};
vendorFlavors: {
value: string[];
};
};
export type ServerEnv = {

View File

@ -91,7 +91,7 @@ const locale: LocaleDef<Schema, Action, '', {
ref: '介绍人',
files: '相关文件',
userState: '用户状态',
idState: '身份验证状态',
idState: '身份验证状态',
},
action: {
activate: '激活',

View File

@ -1,10 +1,10 @@
import { pick } from 'lodash';
import { EntityDict } from 'general-app-domain';
import { Action, Feature } from 'oak-frontend-base';
import { RWLock } from 'oak-domain/lib/utils/concurrent';
import { WechatMpEnv } from 'general-app-domain/Token/Schema';
import { WebEnv, WechatMpEnv } from 'general-app-domain/Token/Schema';
import { Cache } from 'oak-frontend-base';
import { CommonAspectDict } from 'oak-common-aspect';
import { getEnv } from '../utils/env';
import { AspectDict } from '../aspects/AspectDict';
import { GeneralRuntimeContext } from '..';
import { AspectWrapper, SelectRowShape } from 'oak-domain/lib/types';
@ -43,26 +43,11 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
await this.rwLock.acquire('X');
try {
const { code } = await wx.login();
const env = await wx.getSystemInfo();
const env2 = pick(env, [
'brand',
'model',
'pixelRatio',
'screenWidth',
'screenHeight',
'windowWidth',
'windowHeight',
'statusBarHeight',
'language',
'version',
'system',
'platform',
'fontSizeSetting',
'SDKVersion'
]);
const env = await getEnv();
const { result } = await this.getAspectWrapper().exec('loginWechatMp', {
code,
env: Object.assign(env2, { type: 'wechatMp' }) as WechatMpEnv,
env: env as WechatMpEnv,
});
this.token = result;
this.context.setToken(result);
@ -168,4 +153,13 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
}>[];
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') {
const env = await getEnv();
const { result } = await this.getAspectWrapper().exec('sendCaptcha', {
mobile,
env: env as WebEnv,
});
return result as string;
}
}

View File

@ -16,7 +16,7 @@ async function checkIsRoot<ED extends EntityDict, Cxt extends GeneralRuntimeCont
userId: playerId!,
roleId: ROOT_ROLE_ID,
},
} as any, context);
}, context);
if (count === 0) {
// 只有root允许扮演其他用户身份
return false;

7
src/utils/env.ts Normal file
View File

@ -0,0 +1,7 @@
import { getEnv } from './env.web';
export {
getEnv,
};
console.log('不应该走到这里');

24
src/utils/env.web.ts Normal file
View File

@ -0,0 +1,24 @@
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { WebEnv } from 'general-app-domain/Token/Schema';
import { assign, pick } from 'lodash';
/**
* fingerprintJs当中的一些敏感项
* @returns
*/
export async function getEnv() {
const fp = await FingerprintJS.load();
const result = await fp.get();
const { visitorId, components } = result;
return assign(
pick(components, [
'platform',
'timezone',
'vendor',
'vendorFlavors'
]), {
type: 'web',
visitorId,
}) as any;
}

25
src/utils/env.wechatMp.ts Normal file
View File

@ -0,0 +1,25 @@
import { WechatMpEnv } from "general-app-domain/Token/Schema";
import { pick, assign } from "lodash";
export async function getEnv() {
const env = await wx.getSystemInfo();
const env2 = pick(env, [
'brand',
'model',
'pixelRatio',
'screenWidth',
'screenHeight',
'windowWidth',
'windowHeight',
'statusBarHeight',
'language',
'version',
'system',
'platform',
'fontSizeSetting',
'SDKVersion'
]);
return assign(env2, {
type: 'wechatMp',
}) as any;
}