增加绑定游戏邮件帐号功能
This commit is contained in:
parent
06bfb2f6de
commit
a1f4920ebd
249
docs/uaw.md
249
docs/uaw.md
@ -64,12 +64,9 @@
|
||||
1. 增加验证google的access token(32)
|
||||
2. 增加两种任务类型: GoogleConnect, GameAchievement
|
||||
|
||||
#### 20240508
|
||||
1. 用户状态接口(10) 增加字段, gameScore, gameTicket, googleId, rankGame
|
||||
1. 增加接口: 游戏任务列表(33), 领取游戏任务奖励(34), 大转盘抽奖(35), 大转盘抽奖记录(36), 游戏积分详情列表(37), 游戏积分排行榜(38)
|
||||
1. 社交任务活动信息(3), 增加字段drawTime, 表示转盘开始时间
|
||||
1. 增加接口: 转盘配置(39), 用户NFT列表(40)
|
||||
|
||||
#### 20240514
|
||||
1. 用户状态(10) 增加返回emailId, email
|
||||
2. 增加接口: 发送邮件验证码(33), 验证邮件地址(34)
|
||||
|
||||
### 1. 钱包预登录
|
||||
|
||||
@ -171,7 +168,6 @@ SiweMessage的nonce说明(具体参考例子):
|
||||
"autoclaim": false // 任务完成后是否自动获取奖励
|
||||
}
|
||||
],
|
||||
"drawTime": 1702628292366, // 抽奖活动开始时间
|
||||
"startTime": 1702628292366, // 活动开始时间
|
||||
"endTime": 1705220292366 // 活动结束时间
|
||||
}
|
||||
@ -194,7 +190,7 @@ SiweMessage的nonce说明(具体参考例子):
|
||||
```js
|
||||
[
|
||||
{
|
||||
"status": 2, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取, 9: 失败
|
||||
"status": 2, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 9: 失败
|
||||
"id": "TwitterConnect", // 任务id
|
||||
"timeStart": 1703150269527, // 任务开始时间
|
||||
"data": { // 当前任务带的额外信息, 比如twitter的id和昵称等
|
||||
@ -365,14 +361,10 @@ body:
|
||||
"twitterName": "",
|
||||
"twitterAvatar": "", // twitter头像
|
||||
"discordId": "",
|
||||
"googleId": "",
|
||||
"discordName": "",
|
||||
"scoreToday": 100, // 今日获得积分
|
||||
"scoreTotal": 200, // 总积分
|
||||
"gameScore": 100, // 游戏内积分
|
||||
"gameTicket": 1, // 游戏内可抽奖次数
|
||||
"rankTotal": "-",
|
||||
"rankGame": "", // 游戏内积分排行榜
|
||||
"invite": "邀请人address",
|
||||
"inviteCount": 0, // 我邀请的用户总数
|
||||
"inviteScore": 0, // 我邀请用户总数获得的分数
|
||||
@ -789,7 +781,7 @@ body:
|
||||
```
|
||||
|
||||
|
||||
### 26. 宝箱助力状态查询
|
||||
### 26.\ 宝箱助力状态查询
|
||||
|
||||
#### Request
|
||||
|
||||
@ -1005,40 +997,11 @@ body:
|
||||
}
|
||||
```
|
||||
|
||||
### 33. 游戏任务列表
|
||||
### 33.\* 发送邮件验证码
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/tasks`
|
||||
- 方法:`GET`
|
||||
|
||||
#### Response
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"id": "任务id",
|
||||
"task": "任务类型",
|
||||
"title": "任务名",
|
||||
"desc": "任务描述",
|
||||
"show": 1, // 是否显示, 0: 不显示, 1: 显示
|
||||
"type": 1, //任务类型, 1: 一次性任务, 2: 日常任务
|
||||
"pretasks": ["task id 1"], //前置任务
|
||||
"score": 0, // 完成任务可获得的积分
|
||||
"ticket": 0, // 完成任务可获得抽奖次数
|
||||
"cfg": {}, // 其他一些任务相关配置参数, 比如icon, 或者其他未考虑的参数
|
||||
"end": false, // 是否已经结束
|
||||
"status": 1,// 任务状态, -1:不可用 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取, 9: 失败
|
||||
"autoclaim": false // 任务完成后是否自动获取奖励
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 34.\* 领取游戏任务奖励
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/claim`
|
||||
- URL:`/api/email/send_code`
|
||||
- 方法:POST
|
||||
- 头部:
|
||||
- Authorization: Bearer JWT_token
|
||||
@ -1048,26 +1011,30 @@ body:
|
||||
|
||||
```js
|
||||
{
|
||||
"task": "g31x9wzja7eg18t3vtm" // 任务id
|
||||
"email": "email"
|
||||
}
|
||||
|
||||
```
|
||||
> 验证email的正则
|
||||
```js
|
||||
export const isEmail = (email) => {
|
||||
const reg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
|
||||
return reg.test(email)
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 1, // 任务状态, 0: 未开始, 1: 进行中, 2: 成功, 3: 已领取 9: 失败
|
||||
"score": 1,
|
||||
"ticket": 3, // 获得的ticket, 可能没这个字段
|
||||
}
|
||||
```js
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
### 35.\* 大转盘抽奖
|
||||
### 34.\* 验证邮件地址
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/draw`
|
||||
- URL:`/api/user/verify_email`
|
||||
- 方法:POST
|
||||
- 头部:
|
||||
- Authorization: Bearer JWT_token
|
||||
@ -1077,175 +1044,21 @@ body:
|
||||
|
||||
```js
|
||||
{
|
||||
"step": 1
|
||||
"email": "email",
|
||||
"code": "123221"
|
||||
}
|
||||
|
||||
```
|
||||
> 验证code的正则
|
||||
```js
|
||||
export const isValiedCode = (code) => {
|
||||
return /^\d{6}$/.test(code)
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
score: 100, // 获得的积分
|
||||
items: [
|
||||
{
|
||||
id: "001", // 物品id
|
||||
type: 1, // 1白单, 2: nft
|
||||
name: "", // 物品名
|
||||
desc: "", // 描述
|
||||
amount: 1 // 数量
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 36.\* 大转盘抽奖记录
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/draw_history`
|
||||
- 方法:GET
|
||||
- 头部:
|
||||
- Authorization: Bearer JWT_token
|
||||
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
[{
|
||||
time: 12123, // 抽奖时间
|
||||
score: 100, // 获得的积分
|
||||
items: [
|
||||
{
|
||||
id: "001", // 物品id
|
||||
type: 1, // 1白单, 2: nft
|
||||
name: "", // 物品名
|
||||
desc: "", // 描述
|
||||
amount: 1 // 数量
|
||||
}
|
||||
],
|
||||
position: [1, 2] //每次转动的位置
|
||||
}]
|
||||
```
|
||||
|
||||
### 37.\* 游戏积分详情列表
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/score_list`
|
||||
- 方法:`GET`
|
||||
- 头部:
|
||||
- Authorization: Bearer JWT_token
|
||||
|
||||
#### Response
|
||||
|
||||
```js
|
||||
[{
|
||||
score: 100, // 获得的积分
|
||||
type: '', // 获取原因
|
||||
time: 111 // 开启时间
|
||||
}]
|
||||
```
|
||||
|
||||
### 38. 游戏 积分排行榜
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/leaderboard/:page`
|
||||
- 方法:`GET`
|
||||
- 参数:
|
||||
- `page` (返回数据的分页序号, 0 开始)
|
||||
|
||||
> 默认返回100条记录, 如果要返回不同数量, query param传 limit
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"rank": 1, // 排名, 从1开始
|
||||
"level": 1, // 段位
|
||||
"nickname": "昵称",
|
||||
"score": 1 //获得的积分
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 39. 转盘配置
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/draw_cfg`
|
||||
- 方法:`GET`
|
||||
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"position": 1, // 位置, 从1开始
|
||||
"type": 0, // 类型, 0: 积分, 1: 白单, 2: nft
|
||||
"amount": 1, // 数量
|
||||
"name": "", // 没有的话, 就是score
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 40. \* 用户NFT列表
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/ingame/nft_list`
|
||||
- 方法:`GET`
|
||||
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"tokenId": "1", // tokenID
|
||||
"rarity": "", // Common, Rare, Legendary
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 41.\* 积分详情列表(分页)
|
||||
|
||||
#### Request
|
||||
|
||||
- URL:`/api/activity/user_score_list`
|
||||
- 方法:`POST`
|
||||
- 头部:
|
||||
- Authorization: Bearer JWT_token
|
||||
|
||||
body:
|
||||
|
||||
```js
|
||||
{
|
||||
"page": 0, // 第几页
|
||||
"limit": 8, // 每页数据数量
|
||||
{
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
####
|
||||
|
||||
#### Response
|
||||
|
||||
```js
|
||||
{
|
||||
"page": 0,
|
||||
"limit": 8,
|
||||
"total": 1000,
|
||||
"records": [{
|
||||
"score": 100, // 获得的积分
|
||||
"type": "", // 获取原因
|
||||
"time": 111 // 开启时间
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
###
|
||||
```
|
@ -10,6 +10,7 @@ import NonceRecordSchedule from 'schedule/noncerecord.schedule'
|
||||
import { RouterMap, ZRedisClient } from 'zutils'
|
||||
import { SyncLocker } from 'common/SyncLocker'
|
||||
import CacheSchedule from 'schedule/cache.schedule'
|
||||
import CodeTaskSchedule from 'schedule/codetask.schedule'
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
@ -118,6 +119,7 @@ export class ApiServer {
|
||||
}
|
||||
private initSchedules() {
|
||||
new CacheSchedule().scheduleAll()
|
||||
new CodeTaskSchedule().scheduleAll()
|
||||
new NonceRecordSchedule().scheduleAll()
|
||||
}
|
||||
|
||||
|
@ -68,10 +68,15 @@ class InGameController extends BaseController {
|
||||
}
|
||||
}
|
||||
if (user) {
|
||||
if (user.googleId) {
|
||||
if (user.gameAccountBinded()) {
|
||||
try {
|
||||
let res = await queryInGameInfo(user.googleId, '0')
|
||||
Object.assign(gameData, res)
|
||||
if (user.googleId) {
|
||||
let res = await queryInGameInfo(user.googleId, '0')
|
||||
Object.assign(gameData, res)
|
||||
} else if (user.emailId) {
|
||||
let res = await queryInGameInfo(user.emailId, '6')
|
||||
Object.assign(gameData, res)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info('queryInGameInfo with err: ', e.message || e)
|
||||
}
|
||||
@ -108,7 +113,7 @@ class InGameController extends BaseController {
|
||||
logger.db('claim_ingame', req)
|
||||
const { task } = req.params
|
||||
const user = req.user
|
||||
if (!user.googleId) {
|
||||
if (!user.gameAccountBinded()) {
|
||||
throw new ZError(10, 'need connect game account first')
|
||||
}
|
||||
if (!taskMap.has(task)) {
|
||||
@ -145,7 +150,7 @@ class InGameController extends BaseController {
|
||||
logger.db('draw_ingame', req)
|
||||
const user = req.user
|
||||
const { step } = req.params
|
||||
if (!user.googleId) {
|
||||
if (!user.gameAccountBinded()) {
|
||||
throw new ZError(10, 'need connect game account first')
|
||||
}
|
||||
const ingameStat = await InGameStats.insertOrUpdate({ user: user.id }, {})
|
||||
|
126
src/controllers/mail.controller.ts
Normal file
126
src/controllers/mail.controller.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import logger from 'logger/logger'
|
||||
import { ActivityUser } from 'models/ActivityUser'
|
||||
import {
|
||||
CodeRecord,
|
||||
CodeStatus,
|
||||
CodeType,
|
||||
DEFAULT_CODE,
|
||||
DEFAULT_EXPIRE_TIME,
|
||||
isEmail,
|
||||
isValiedCode,
|
||||
} from 'models/CodeRecord'
|
||||
import { DEFAULT_LOGIN_MAIL_HTML, DEFAULT_LOGIN_MAIL_SUBJECT, EmailSvr } from 'services/email.svr'
|
||||
import { BaseController, router, ZError } from 'zutils'
|
||||
import { sha1 } from 'zutils/utils/security.util'
|
||||
import { SyncLocker } from 'common/SyncLocker'
|
||||
|
||||
class MailController extends BaseController {
|
||||
/**
|
||||
* 通过邮件, 密码形式的登录
|
||||
*/
|
||||
@router('post /api/user/verify_email')
|
||||
async loginWithEmail(req, res) {
|
||||
await new SyncLocker().checkLock(req)
|
||||
logger.db('verify_email', req)
|
||||
let user = req.user
|
||||
const { email, code } = req.params
|
||||
if (!email || !code) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
throw new ZError(11, 'Invalid email')
|
||||
}
|
||||
if (!isValiedCode(code)) {
|
||||
throw new ZError(11, 'code error')
|
||||
}
|
||||
if (user.gameAccountBinded()) {
|
||||
throw new ZError(12, 'already bind game account')
|
||||
}
|
||||
let openId = sha1(email)
|
||||
let userCheck = await ActivityUser.findOne({ emailId: openId })
|
||||
if (userCheck && userCheck.id !== user.id) {
|
||||
throw new ZError(13, 'Email already binded to another account')
|
||||
}
|
||||
let recordCode = await CodeRecord.findByEmail(email, CodeType.LOGIN)
|
||||
if (!recordCode) {
|
||||
throw new ZError(14, 'code expired')
|
||||
}
|
||||
if (recordCode.status !== CodeStatus.PENDING) {
|
||||
throw new ZError(15, 'code expired')
|
||||
}
|
||||
if (recordCode.code !== code) {
|
||||
throw new ZError(16, 'code error')
|
||||
}
|
||||
user.emailId = openId
|
||||
user.email = email
|
||||
recordCode.status = CodeStatus.SUCCESS
|
||||
await recordCode.save()
|
||||
await user.save()
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
@router('post /api/email/send_code')
|
||||
async sendVerifyCode(req, res) {
|
||||
await new SyncLocker().checkLock(req)
|
||||
logger.db('send_mail_code', req)
|
||||
let user = req.user
|
||||
let { email, type } = req.params
|
||||
type = type || CodeType.LOGIN
|
||||
if (!email) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
throw new ZError(11, 'Invalid email')
|
||||
}
|
||||
if (user.gameAccountBinded()) {
|
||||
throw new ZError(12, 'already bind game account')
|
||||
}
|
||||
let openId = sha1(email)
|
||||
let userCheck = await ActivityUser.findOne({ emailId: openId })
|
||||
if (userCheck && userCheck.id !== user.id) {
|
||||
throw new ZError(13, 'Email already binded to another account')
|
||||
}
|
||||
type = parseInt(type)
|
||||
let record = await CodeRecord.findByEmail(email, type)
|
||||
if (!record || record.status === CodeStatus.EXPIRED || record.status === CodeStatus.FAIL) {
|
||||
record = new CodeRecord({ email, type, code: DEFAULT_CODE, user: user.id })
|
||||
await record.save()
|
||||
}
|
||||
let html, subject
|
||||
switch (type) {
|
||||
case CodeType.LOGIN:
|
||||
html = DEFAULT_LOGIN_MAIL_HTML
|
||||
subject = DEFAULT_LOGIN_MAIL_SUBJECT
|
||||
}
|
||||
if (!html || !subject) {
|
||||
throw new ZError(15, 'type error')
|
||||
}
|
||||
|
||||
subject = record.code + ' ' + subject
|
||||
html = html.replace('{{ocde}}', record.code)
|
||||
html = html.replace('{{time}}', new Date().format('yyyy-MM-dd hh:mm:ss'))
|
||||
let msgData = {
|
||||
to: email,
|
||||
html,
|
||||
subject,
|
||||
}
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
let result = await new EmailSvr().sendMail(msgData)
|
||||
record.mailSend = true
|
||||
record.emailId = result.messageId
|
||||
record.expiredAt = Date.now() + DEFAULT_EXPIRE_TIME
|
||||
await record.save()
|
||||
} catch (err) {
|
||||
logger.info(`error send mail:: email: ${email}, type: ${type}`)
|
||||
logger.error(err)
|
||||
record.status = CodeStatus.FAIL
|
||||
await record.save()
|
||||
}
|
||||
})
|
||||
return {}
|
||||
}
|
||||
}
|
@ -167,6 +167,8 @@ class SignController extends BaseController {
|
||||
googleMail: user.googleEmail,
|
||||
gameScore: ingameStat.score,
|
||||
gameTicket: ingameStat.ticket,
|
||||
emailId: user.emailId,
|
||||
email: user.email,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -49,7 +49,8 @@ export interface ActivityUserClass extends Base, TimeStamps {}
|
||||
@index({ inviteCode: 1, activity: 1 }, { unique: true, partialFilterExpression: { inviteCode: { $exists: true } } })
|
||||
@index({ inviteUser: 1, activity: 1 }, { unique: false })
|
||||
@index({ twitterId: 1 }, { unique: true, partialFilterExpression: { twitterId: { $exists: true } } })
|
||||
@index({ googleId: 1 }, { unique: true, partialFilterExpression: { twitterId: { $exists: true } } })
|
||||
@index({ googleId: 1 }, { unique: true, partialFilterExpression: { googleId: { $exists: true } } })
|
||||
@index({ emailId: 1 }, { unique: true, partialFilterExpression: { emailId: { $exists: true } } })
|
||||
@index({ discordId: 1 }, { unique: true, partialFilterExpression: { discordId: { $exists: true } } })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'activity_user', timestamps: true },
|
||||
@ -111,6 +112,11 @@ export class ActivityUserClass extends BaseModule {
|
||||
@prop()
|
||||
public googleEmail?: string
|
||||
|
||||
@prop()
|
||||
public emailId?: string
|
||||
@prop()
|
||||
public email?: string
|
||||
|
||||
@prop({ default: false })
|
||||
public inWhiteList: boolean
|
||||
|
||||
@ -142,6 +148,10 @@ export class ActivityUserClass extends BaseModule {
|
||||
return task.status !== TaskStatusEnum.NOT_START && task.status !== TaskStatusEnum.RUNNING
|
||||
})
|
||||
}
|
||||
|
||||
public gameAccountBinded() {
|
||||
return this.googleId || this.emailId
|
||||
}
|
||||
}
|
||||
|
||||
export const ActivityUser = getModelForClass(ActivityUserClass, { existingConnection: ActivityUserClass.db })
|
||||
|
90
src/models/CodeRecord.ts
Normal file
90
src/models/CodeRecord.ts
Normal file
@ -0,0 +1,90 @@
|
||||
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, // 重置密码
|
||||
VERIFY = 3, // 验证邮箱
|
||||
LOGIN = 4, // 验证码登录
|
||||
}
|
||||
|
||||
export enum CodeStatus {
|
||||
PENDING = 1,
|
||||
SUCCESS = 2,
|
||||
FAIL = 3,
|
||||
EXPIRED = 4,
|
||||
}
|
||||
export const isEmail = (email: string) => {
|
||||
const reg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
|
||||
return reg.test(email)
|
||||
}
|
||||
|
||||
export const isValiedCode = (code: string) => {
|
||||
return /^\d{6}$/.test(code)
|
||||
}
|
||||
/**
|
||||
* 邮件验证码发送记录
|
||||
*/
|
||||
@dbconn()
|
||||
@index({ email: 1, type: 1, status: 1 }, { unique: true, partialFilterExpression: { status: 1 } })
|
||||
@index({ code: 1 }, { unique: true })
|
||||
@index({ expiredAt: 1 }, { unique: false })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'code_send_record', timestamps: true },
|
||||
})
|
||||
@pre<CodeRecordClass>('save', async function () {
|
||||
if (this.code === DEFAULT_CODE) {
|
||||
let exists = false
|
||||
while (!exists) {
|
||||
const code = nanoid()
|
||||
const record = await CodeRecord.findByCode(code)
|
||||
if (!record) {
|
||||
exists = true
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
class CodeRecordClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public email!: string
|
||||
|
||||
@prop()
|
||||
public user?: 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<typeof CodeRecordClass>, code: string) {
|
||||
return this.findOne({ code }).exec()
|
||||
}
|
||||
|
||||
public static async findByEmail(this: ReturnModelType<typeof CodeRecordClass>, email: string, type: CodeType) {
|
||||
return this.findOne({ email, type, status: CodeStatus.PENDING }).exec()
|
||||
}
|
||||
}
|
||||
|
||||
export const CodeRecord = getModelForClass(CodeRecordClass, { existingConnection: CodeRecordClass.db })
|
19
src/schedule/codetask.schedule.ts
Normal file
19
src/schedule/codetask.schedule.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { singleton } from 'zutils'
|
||||
import { CodeRecord, CodeStatus } from 'models/CodeRecord'
|
||||
import * as schedule from 'node-schedule'
|
||||
|
||||
/**
|
||||
* 定时更新发送邮件验证码的过期状态
|
||||
*/
|
||||
@singleton
|
||||
export default class CodeTaskSchedule {
|
||||
async parseAllRecord() {
|
||||
let now = Date.now()
|
||||
await CodeRecord.deleteMany({ expiredAt: { $lt: now } })
|
||||
}
|
||||
scheduleAll() {
|
||||
const job = schedule.scheduleJob('*/1 * * * *', async () => {
|
||||
await this.parseAllRecord()
|
||||
})
|
||||
}
|
||||
}
|
76
src/services/email.svr.ts
Normal file
76
src/services/email.svr.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { singleton } from 'zutils'
|
||||
import { timeoutFetch } from 'zutils/utils/net.util'
|
||||
|
||||
export const DEFAULT_VERIFY_HTML = `
|
||||
<h1>Email Verification</h1>
|
||||
<p>CEBG needs to confirm your email address is still valid. Please click the link below to confirm you received this mail.</p>
|
||||
<p><a href="{{href}}" target="_blank">Verify Email</a></p>
|
||||
<p>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.</p>
|
||||
`
|
||||
|
||||
export const DEFAULT_REGIST_SUBJECT = 'CEBG regist code'
|
||||
export const DEFAULT_REGIST_HTML = `
|
||||
<h1>Your CEBG regist code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login CEBG. Never share this code with anyone.</p>
|
||||
`
|
||||
|
||||
export const DEFAULT_RESET_SUBJECT = 'CEBG reset password code'
|
||||
export const DEFAULT_RESET_HTML = `
|
||||
<h1>Your CEBG reset password code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login CEBG. Never share this code with anyone.</p>
|
||||
`
|
||||
export const DEFAULT_VERIFY_MAIL_SUBJECT = 'CEBG verify email code'
|
||||
export const DEFAULT_VERIFY_MAIL_HTML = `
|
||||
<h1>Your CEBG verify email code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login CEBG. Never share this code with anyone.</p>
|
||||
`
|
||||
|
||||
export const DEFAULT_LOGIN_MAIL_SUBJECT = 'Counter Fire email login code'
|
||||
export const DEFAULT_LOGIN_MAIL_HTML = `
|
||||
<h1>Your Counter Fire email login code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login Counter Fire. Never share this code with anyone.</p>
|
||||
`
|
||||
|
||||
export interface IMailData {
|
||||
from?: string
|
||||
to: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
const DEFAULT_MSG_DATA: IMailData = {
|
||||
from: 'CEBG <noreply@cebg.games>',
|
||||
to: '',
|
||||
subject: 'Please verify your email address',
|
||||
}
|
||||
const MAIL_SVR = process.env.EMAIL_SERVER
|
||||
|
||||
@singleton
|
||||
export class EmailSvr {
|
||||
public sendMail(msg: IMailData) {
|
||||
let url = MAIL_SVR + '/mail/send'
|
||||
Object(DEFAULT_MSG_DATA).zssign(msg)
|
||||
const options: any = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: JSON.stringify({ message: msg }),
|
||||
}
|
||||
|
||||
return timeoutFetch(url, options, 10000).then(res => res.json())
|
||||
}
|
||||
}
|
@ -9,13 +9,13 @@ export default class GoogleConnect extends ITask {
|
||||
}
|
||||
|
||||
public async claimReward(task: any) {
|
||||
if (!this.user.googleId) {
|
||||
throw new ZError(100, 'google account not binded')
|
||||
if (!this.user.gameAccountBinded()) {
|
||||
throw new ZError(100, 'game account not binded')
|
||||
}
|
||||
return { score: 0, ticket: 0 }
|
||||
}
|
||||
|
||||
public async check(cfg: any, gameData: any) {
|
||||
return !!this.user.googleId
|
||||
return !!this.user.gameAccountBinded()
|
||||
}
|
||||
}
|
||||
|
@ -8,21 +8,21 @@ export default class GoogleConnect extends ITask {
|
||||
static show: boolean = true
|
||||
|
||||
async execute(data: any) {
|
||||
if (!this.user.googleId) {
|
||||
throw new ZError(100, 'google account already binded')
|
||||
if (!this.user.gameAccountBinded()) {
|
||||
throw new ZError(100, 'game account already binded')
|
||||
}
|
||||
const { task } = data
|
||||
if (task.status === TaskStatusEnum.RUNNING) {
|
||||
task.status = TaskStatusEnum.SUCCESS
|
||||
task.timeFinish = Date.now()
|
||||
task.data = {
|
||||
userid: this.user.googleId,
|
||||
email: this.user.googleEmail,
|
||||
userid: this.user.googleId || this.user.emailId,
|
||||
email: this.user.googleEmail || this.user.email,
|
||||
}
|
||||
try {
|
||||
await this.user.save()
|
||||
} catch (err) {
|
||||
throw new ZError(100, 'google account already binded')
|
||||
throw new ZError(100, 'game account already binded')
|
||||
}
|
||||
let cfg = this.activity.tasks.find((t: TaskCfg) => t.id === task.id)
|
||||
if (cfg.autoclaim) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user