From aa1177b8026209ae50b22e6432c08e110e5c29f0 Mon Sep 17 00:00:00 2001 From: zhl Date: Tue, 4 Apr 2023 18:50:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=82=AE=E4=BB=B6=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E7=99=BB=E5=BD=95=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 +- package.json | 2 + src/api.server.ts | 14 ++- src/controllers/facebook.controller.ts | 4 +- src/controllers/mail.controller.ts | 163 +++++++++++++++++++++++++ src/controllers/verify.controller.ts | 10 ++ src/modules/Account.ts | 48 +++++++- src/modules/CodeRecord.ts | 80 ++++++++++++ src/schedule/codetask.schedule.ts | 19 +++ src/service/email.svr.ts | 33 +++-- src/utils/security.util.ts | 19 ++- yarn.lock | 38 ++++++ 12 files changed, 415 insertions(+), 17 deletions(-) create mode 100644 src/controllers/mail.controller.ts create mode 100644 src/modules/CodeRecord.ts create mode 100644 src/schedule/codetask.schedule.ts diff --git a/.env.development b/.env.development index d28dbef..d6fae8d 100644 --- a/.env.development +++ b/.env.development @@ -9,7 +9,7 @@ GOOGLE_OAUTH_CLIENT2="53206975661-ih3r0ubph3rqejdq97b029difbrk2bqj.apps.googleus GOOGLE_OAUTH_CLIENT_IOS="53206975661-qan0rnefniegjv53ohild375pv0p7ekd.apps.googleusercontent.com" DB_MAIN=mongodb://localhost/wallet-development -EMAIL_VERIFY_URL="http://127.0.0.1:3007" +EMAIL_VERIFY_URL="https://wallet.cebggame.com" ALCHEMY_APPID="f83Is2y7L425rxl8" ALCHEMY_APP_SECRET="4Yn8RkxDXN71Q3p0" diff --git a/package.json b/package.json index 8016387..d4dbbb9 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,13 @@ "mongoose": "^6.6.5", "mongoose-findorcreate": "^3.0.0", "nanoid": "^3.1.23", + "node-schedule": "^2.1.1", "tracer": "^1.1.6", "verify-apple-id-token": "^3.0.0" }, "devDependencies": { "@typegoose/typegoose": "^9.12.1", + "@types/node-schedule": "^2.1.0", "@types/dotenv": "^8.2.0", "@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.40.1", diff --git a/src/api.server.ts b/src/api.server.ts index 7f372c1..4d6bf55 100644 --- a/src/api.server.ts +++ b/src/api.server.ts @@ -6,6 +6,7 @@ import { mongoose } from '@typegoose/typegoose' import logger from 'logger/logger' import config from 'config/config' import { ConnectOptions } from 'mongoose' +import CodeTaskSchedule from 'schedule/codetask.schedule' const zReqParserPlugin = require('plugins/zReqParser') @@ -30,11 +31,11 @@ export class ApiServer { this.server.register(zReqParserPlugin) this.server.register(helmet, { hidePoweredBy: false }) this.server.register(zTokenParserPlugin) - this.server.register(require("@fastify/view"), { + this.server.register(require('@fastify/view'), { engine: { - ejs: require("ejs"), + ejs: require('ejs'), }, - }); + }) this.server.register(apiAuthPlugin, { secret: config.api.token_secret, @@ -83,6 +84,11 @@ export class ApiServer { return require(join(controllers, file)) }) } + + initSchedules() { + new CodeTaskSchedule().scheduleAll() + } + async connectDB() { const options: ConnectOptions = { minPoolSize: 5, @@ -152,7 +158,7 @@ export class ApiServer { self.registerRouter() self.setErrHandler() self.setFormatSend() - // new Schedule().start(); + self.initSchedules() this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => { if (err) { logger.log(err) diff --git a/src/controllers/facebook.controller.ts b/src/controllers/facebook.controller.ts index 35beff7..bdf1b09 100644 --- a/src/controllers/facebook.controller.ts +++ b/src/controllers/facebook.controller.ts @@ -36,8 +36,8 @@ class FacebookController extends BaseController { user.accessToken = code user.accessTokenExpire = result.data['expires_at'] user.scope = data['scopes'] - user.nickname = infoRes['name'] - user.email = infoRes['email'] + if (infoRes['name']) user.nickname = infoRes['name'] + if (infoRes['email']) user.email = infoRes['email'] let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, user) const ztoken = await res.jwtSign({ id: account.id }) return { token: ztoken } diff --git a/src/controllers/mail.controller.ts b/src/controllers/mail.controller.ts new file mode 100644 index 0000000..5df985a --- /dev/null +++ b/src/controllers/mail.controller.ts @@ -0,0 +1,163 @@ +import BaseController, { ROLE_ANON } from 'common/base.controller' +import { ZError } from 'common/ZError' +import { role, router } from 'decorators/router' +import logger from 'logger/logger' +import { Account, PlatEnum } from 'modules/Account' +import { CodeRecord, CodeStatus, CodeType, DEFAULT_CODE, DEFAULT_EXPIRE_TIME } from 'modules/CodeRecord' +import { + DEFAULT_REGIST_HTML, + DEFAULT_REGIST_SUBJECT, + DEFAULT_RESET_HTML, + DEFAULT_RESET_SUBJECT, + EmailSvr, +} from 'service/email.svr' +import { uuid } from 'utils/security.util' + +const PASSWORD_SALT = '^6AssF(n,/]2>Iv, email) { + return this.findOne({ email, plat: PlatEnum.EMAIL }).exec() + } + + public updatePassword(password: string) { + if (password) { + let passData = saltHashPassword(password) + this.password = passData.passwordHash + this.salt = passData.salt + } + } + + public verifyPassword(password: string) { + return verifyPass(password, this.password, this.salt) + } } export const Account = getModelForClass(AccountClass, { existingConnection: AccountClass.db }) diff --git a/src/modules/CodeRecord.ts b/src/modules/CodeRecord.ts new file mode 100644 index 0000000..3b1b6bc --- /dev/null +++ b/src/modules/CodeRecord.ts @@ -0,0 +1,80 @@ +import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + +import { customAlphabet } from 'nanoid' + +const nanoid = customAlphabet('1234567890', 6) + +export const DEFAULT_CODE = '000000' +export const DEFAULT_EXPIRE_TIME = 5 * 60 * 1000 + +export enum CodeType { + REGIST = 1, // 注册 + RESET = 2, // 重置密码 +} + +export enum CodeStatus { + PENDING = 1, + SUCCESS = 2, + FAIL = 3, + EXPIRED = 4, +} + +/** + * 邮件验证码发送记录 + */ +@dbconn() +@index({ email: 1, type: 1 }, { unique: true, partialFilterExpression: { status: 1 } }) +@index({ code: 1 }, { partialFilterExpression: { status: 1 } }) +@modelOptions({ + schemaOptions: { collection: 'code_send_record', timestamps: true }, +}) +@pre('save', async function () { + if (this.code === DEFAULT_CODE) { + let exists = false + while (!exists) { + const code = nanoid() + const record = await CodeRecord.findByCode(code, this.type) + if (!record) { + exists = true + this.code = code + } + } + } +}) +class CodeRecordClass extends BaseModule { + @prop({ required: true }) + public email!: string + + @prop() + public account?: string + + @prop({ required: true }) + public code!: string + + @prop({ default: Date.now() + DEFAULT_EXPIRE_TIME }) + public expiredAt?: number + + @prop({ required: true, default: CodeType.REGIST }) + public type: CodeType + + @prop({ required: true, default: CodeStatus.PENDING }) + public status: CodeStatus + + @prop({ default: false }) + public mailSend: boolean + + @prop() + public emailId?: string + + public static async findByCode(this: ReturnModelType, code: string, type: CodeType) { + return this.findOne({ code, type, status: CodeStatus.PENDING }).exec() + } + + public static async findByEmail(this: ReturnModelType, email: string, type: CodeType) { + return this.findOne({ email, type, status: CodeStatus.PENDING }).exec() + } +} + +export const CodeRecord = getModelForClass(CodeRecordClass, { existingConnection: CodeRecordClass.db }) diff --git a/src/schedule/codetask.schedule.ts b/src/schedule/codetask.schedule.ts new file mode 100644 index 0000000..bb536bc --- /dev/null +++ b/src/schedule/codetask.schedule.ts @@ -0,0 +1,19 @@ +import { singleton } from 'decorators/singleton' +import { CodeRecord, CodeStatus } from 'modules/CodeRecord' +import * as schedule from 'node-schedule' + +/** + * 定时更新发送邮件验证码的过期状态 + */ +@singleton +export default class CodeTaskSchedule { + async parseAllRecord() { + let now = Date.now() + await CodeRecord.updateMany({ expiredAt: { $lt: now }, status: CodeStatus.PENDING }, { status: CodeStatus.EXPIRED }) + } + scheduleAll() { + const job = schedule.scheduleJob('*/1 * * * *', async () => { + await this.parseAllRecord() + }) + } +} diff --git a/src/service/email.svr.ts b/src/service/email.svr.ts index d2dd6a7..9c18dfc 100644 --- a/src/service/email.svr.ts +++ b/src/service/email.svr.ts @@ -1,13 +1,31 @@ -import {singleton} from "decorators/singleton"; -import { NetClient} from "net/NetClient"; +import { singleton } from 'decorators/singleton' +import { NetClient } from 'net/NetClient' -export const DEFAULT_VERIFY_HTML = -` +export const DEFAULT_VERIFY_HTML = `

Email Verification

CEBG needs to confirm your email address is still valid. Please click the link below to confirm you received this mail.

Verify Email

If you're worried about this email being legitimate, you can visit CEBG directly to confirm your email needs verifying. After doing so, please don't forget to click the link above.

` + +export const DEFAULT_REGIST_SUBJECT = 'CEBG regist code' +export const DEFAULT_REGIST_HTML = ` +

Your CEBG regist code is

+

{{ocde}}

+

{{time}}

+

This is your one time code that expires in 5 minutes.

+

Use it to login CEBG. Never share this code with anyone.

+` + +export const DEFAULT_RESET_SUBJECT = 'CEBG reset password code' +export const DEFAULT_RESET_HTML = ` +

Your CEBG reset password code is

+

{{ocde}}

+

{{time}}

+

This is your one time code that expires in 5 minutes.

+

Use it to login CEBG. Never share this code with anyone.

+` + export interface IMailData { from?: string to: string @@ -19,7 +37,7 @@ export interface IMailData { const DEFAULT_MSG_DATA: IMailData = { from: 'CEBG ', to: '', - subject: 'Please verify your email address' + subject: 'Please verify your email address', } const MAIL_SVR = 'http://127.0.0.1:3087' @@ -28,10 +46,9 @@ export class EmailSvr { public sendMail(msg: IMailData) { Object(DEFAULT_MSG_DATA).zssign(msg) let reqData = { - url: MAIL_SVR+'/mail/send', - data: JSON.stringify({message: msg}) + url: MAIL_SVR + '/mail/send', + data: JSON.stringify({ message: msg }), } return new NetClient().httpPost(reqData) } } - diff --git a/src/utils/security.util.ts b/src/utils/security.util.ts index 444d6d4..2444c12 100644 --- a/src/utils/security.util.ts +++ b/src/utils/security.util.ts @@ -1,5 +1,5 @@ import crypto from 'crypto' -import {compressUuid} from './string.util' +import { compressUuid } from './string.util' const ENCODER = 'base64' const REG_KEY = /^[0-9a-fA-F]{63,64}$/ @@ -24,6 +24,23 @@ export function aesDecrypt(encryptedText: string, password: string, iv: string) return decrypted + decipher.final('utf8') } +export function sha512(password: string, salt: string) { + let hash = crypto.createHmac('sha512', salt) + hash.update(password) + let value = hash.digest('hex') + return { + salt: salt, + passwordHash: value, + } +} + +export function genRandomString(length: number) { + return crypto + .randomBytes(Math.ceil(length / 2)) + .toString('hex') + .slice(0, length) +} + export function uuid() { return crypto.randomUUID() } diff --git a/yarn.lock b/yarn.lock index 1c37292..6945586 100644 --- a/yarn.lock +++ b/yarn.lock @@ -250,6 +250,13 @@ resolved "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/node-schedule@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-2.1.0.tgz#60375640c0509bab963573def9d1f417f438c290" + integrity sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg== + dependencies: + "@types/node" "*" + "@types/node@*": version "18.11.0" resolved "https://registry.npmmirror.com/@types/node/-/node-18.11.0.tgz#f38c7139247a1d619f6cc6f27b072606af7c289d" @@ -679,6 +686,13 @@ create-require@^1.1.0: resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron-parser@^4.2.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.8.1.tgz#47062ea63d21d78c10ddedb08ea4c5b6fc2750fb" + integrity sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ== + dependencies: + luxon "^3.2.1" + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1559,6 +1573,11 @@ loglevel@^1.8.0: resolved "https://registry.npmmirror.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1582,6 +1601,11 @@ lru-memoizer@^2.1.4: lodash.clonedeep "^4.5.0" lru-cache "~4.0.0" +luxon@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" + integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -1742,6 +1766,15 @@ node-forge@^1.3.1: resolved "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-schedule@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-2.1.1.tgz#6958b2c5af8834954f69bb0a7a97c62b97185de3" + integrity sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ== + dependencies: + cron-parser "^4.2.0" + long-timeout "0.1.1" + sorted-array-functions "^1.3.0" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -2097,6 +2130,11 @@ sonic-boom@^3.1.0: dependencies: atomic-sleep "^1.0.0" +sorted-array-functions@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" + integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== + source-map-support@^0.5.12: version "0.5.21" resolved "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"