Compare commits

..

No commits in common. "master" and "passhash" have entirely different histories.

80 changed files with 1420 additions and 4899 deletions

View File

@ -4,14 +4,10 @@ API_TOKEN_SECRET_PRIVATE=MC4CAQAwBQYDK2VwBCIEIKdK/eFQ2+Q/ml4ruDAItNIwGnQMQm76UX0
API_TOKEN_SECRET_PUBLIC=MCowBQYDK2VwAyEAySgE/YiiI2fzpXaco+OWeDAKymEoqqLYYb6RKOEU1n8=
API_TOKEN_EXPIRESIN=1d
REFRESH_TOKEN_SECRET_PRIVATE=MC4CAQAwBQYDK2VwBCIEIMNKHEo6d3B6O4SiB4a5cFgKNNCMGj0BaRhPx5wG3DrZ
REFRESH_TOKEN_SECRET_PUBLIC=MCowBQYDK2VwAyEAWFiOqbdxFu1XW5MoI3YeVRBZ4JoEWQMwXg49v1ssaXM=
GOOGLE_OAUTH_CLIENT="53206975661-asnf3qe4bg29p8h981pgf099osvrjbme.apps.googleusercontent.com"
GOOGLE_OAUTH_CLIENT2="53206975661-ih3r0ubph3rqejdq97b029difbrk2bqj.apps.googleusercontent.com"
GOOGLE_OAUTH_CLIENT_IOS="53206975661-qan0rnefniegjv53ohild375pv0p7ekd.apps.googleusercontent.com"
# DB_MAIN=mongodb://188.88.0.2/wallet-development
DB_MAIN=mongodb://192.168.100.22/wallet-development
EMAIL_VERIFY_URL="https://wallet.cebggame.com"
@ -49,19 +45,4 @@ HASH_SALT='iG4Rpsa)6U31$H#^T85$^^3'
GAME_PAY_CB_URL=https://game2006api-test.kingsome.cn/webapp/index.php?c=Shop&a=buyGoodsDirect
# client登录时,验证用户数据的private key
WALLET_CLIENT_SK='38d9baa24aaea6f87a1caa51f588b0c9578368a1cb00b1639eb9f450b6cada00'
# 检查guest能否绑定平台账号
GAME_CHECK_RELATION_URL='https://game2006api-test.kingsome.cn/webapp/index.php?c=AccountVerify&a=canBind'
OKX_API_KEY='5cda794d-b2af-479c-bd75-af1eb877d4ef'
OKX_PROJECT_ID='17c69bdda138a6342f9bece529030cbb'
OKX_PASS='7654321Cf_'
OKX_SECRET_KEY='AF7F4CEE2A10715F9709D38452CE0BFD'
DISCORD_CLIENT_ID='1199290913155981345'
DISCORD_CLIENT_SECRET='0-iIPG1waeQ7GpFV3e_dGH6kfjv1SVNS'
DISCORD_REDIRECT_URI='https://oauth-svr.cebggame.com/oauth/redirect'
REDIS=redis://192.168.100.22:6379/13
WALLET_CLIENT_SK='38d9baa24aaea6f87a1caa51f588b0c9578368a1cb00b1639eb9f450b6cada00'

View File

@ -4,9 +4,6 @@ API_TOKEN_SECRET_PRIVATE=MC4CAQAwBQYDK2VwBCIEIKdK/eFQ2+Q/ml4ruDAItNIwGnQMQm76UX0
API_TOKEN_SECRET_PUBLIC=MCowBQYDK2VwAyEAySgE/YiiI2fzpXaco+OWeDAKymEoqqLYYb6RKOEU1n8=
API_TOKEN_EXPIRESIN=1d
REFRESH_TOKEN_SECRET_PRIVATE=MC4CAQAwBQYDK2VwBCIEIMNKHEo6d3B6O4SiB4a5cFgKNNCMGj0BaRhPx5wG3DrZ
REFRESH_TOKEN_SECRET_PUBLIC=MCowBQYDK2VwAyEAWFiOqbdxFu1XW5MoI3YeVRBZ4JoEWQMwXg49v1ssaXM=
GOOGLE_OAUTH_CLIENT="53206975661-asnf3qe4bg29p8h981pgf099osvrjbme.apps.googleusercontent.com"
GOOGLE_OAUTH_CLIENT2="53206975661-ih3r0ubph3rqejdq97b029difbrk2bqj.apps.googleusercontent.com"

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "packages/zutils"]
path = packages/zutils
url = git@git.kingsome.cn:zhanghongliang/zutils.git

View File

@ -17,29 +17,22 @@
"@fastify/formbody": "^7.3.0",
"@fastify/helmet": "^10.0.1",
"@fastify/jwt": "^6.3.2",
"@fastify/rate-limit": "^9.1.0",
"@fastify/view": "^7.4.1",
"@metamask/eth-sig-util": "^4.0.1",
"axios": "^1.1.3",
"crypto": "^1.0.1",
"dotenv": "^16.0.3",
"ejs": "^3.1.9",
"ethers": "^5.6.8",
"fast-rbac": "^2.0.1",
"fastify": "^4.8.1",
"fastify-plugin": "^4.2.1",
"google-auth-library": "^8.5.2",
"ioredis": "^5.4.1",
"mongoose": "^6.6.5",
"mongoose-findorcreate": "^3.0.0",
"nanoid": "^3.1.23",
"node-schedule": "^2.1.1",
"redlock": "^5.0.0-beta.2",
"rustwallet": "file:./rustwallet",
"siwe": "^2.1.4",
"tracer": "^1.1.6",
"verify-apple-id-token": "^3.0.0",
"zutils": "link:packages/zutils"
"verify-apple-id-token": "^3.0.0"
},
"devDependencies": {
"@typegoose/typegoose": "^9.12.1",

@ -1 +0,0 @@
Subproject commit b97e33472f46eb8fb47a8cf3c3924c5d26af5eca

View File

@ -1,4 +0,0 @@
cd ..
tar zcvf wallet-svr.tar.gz ./wallet-svr
scp -P27256 ./wallet-svr.tar.gz root@45.78.31.162:./upload
cd wallet-svr

View File

@ -1,14 +1,13 @@
import fastify, { FastifyError, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import helmet from '@fastify/helmet'
import { IncomingMessage, Server, ServerResponse } from 'http'
import { RouterMap } from 'decorators/router'
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'
import { PriceSvr } from 'service/price.svr'
import NonceRecordSchedule from 'schedule/noncerecord.schedule'
import { RouterMap } from 'zutils'
const zReqParserPlugin = require('plugins/zReqParser')
@ -29,15 +28,6 @@ export class ApiServer {
this.registerPlugins()
}
private registerPlugins() {
// @ts-ignore
this.server.register(import('@fastify/rate-limit'), {
global: false,
max: 5,
timeWindow: '1 minute',
keyGenerator: (req: FastifyRequest) => {
return req.headers['x-real-ip'] || req.ip
},
})
this.server.register(require('@fastify/formbody'))
this.server.register(zReqParserPlugin)
this.server.register(helmet, { hidePoweredBy: false })
@ -53,7 +43,7 @@ export class ApiServer {
expiresIn: config.api.token_expiresIn,
})
if (process.env.NODE_ENV !== 'production') {
// mongoose.set('debug', true)
mongoose.set('debug', true)
this.server.register(require('@fastify/cors'), {})
}
}
@ -74,13 +64,6 @@ export class ApiServer {
data.path,
{
preValidation: async function (request: FastifyRequest, reply: FastifyReply) {
if (config.limit) {
if (!config.limitMethod) {
config.limitMethod = this.rateLimit(config.limit)
}
// @ts-ignore
await config.limitMethod(request, reply)
}
request.roles = config.roles
await this.apiAuth(request, reply)
},
@ -106,7 +89,6 @@ export class ApiServer {
initSchedules() {
new CodeTaskSchedule().scheduleAll()
new NonceRecordSchedule().scheduleAll()
new PriceSvr().scheduleAll()
}
@ -139,9 +121,9 @@ export class ApiServer {
if (statusCode >= 500) {
logger.error(error)
} else if (statusCode >= 400) {
logger.error(error)
logger.info(error)
} else {
logger.info(error?.message || error || 'unknown error')
logger.error(error)
}
reply.code(200).send({
errcode: statusCode,

View File

@ -1,4 +1,4 @@
import { singleton } from 'zutils'
import { singleton } from '../decorators/singleton'
import Clock from './ClockTimer'
import { Delayed } from './Delayed'

107
src/common/AsyncQueue.ts Normal file
View File

@ -0,0 +1,107 @@
type Callback<T> = () => Promise<T>
export type AsyncQueue<T = void> = {
push: (task: Callback<T>) => Promise<T>
flush: () => Promise<void>
size: number
}
/**
* Ensures that each callback pushed onto the queue is executed in series.
* Such a quetie 😻
* @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple
* tasks are pushed onto the queue while there is an active task, only the
* last one will be executed, once the active task has completed.
* e.g. in the below example, only 0 and 3 will be executed.
* ```
* const queue = createAsyncQueue({ dedupeConcurrent: true })
* queue.push(async () => console.log(0)) // returns 0
* queue.push(async () => console.log(1)) // returns 3
* queue.push(async () => console.log(2)) // returns 3
* queue.push(async () => console.log(3)) // returns 3
* ```
* */
export function createAsyncQueue<T = void>(opts = { dedupeConcurrent: false }): AsyncQueue<T> {
const { dedupeConcurrent } = opts
let queue: Callback<T>[] = []
let running: Promise<void> | undefined
let nextPromise = new DeferredPromise<T>()
const push = (task: Callback<T>) => {
let taskPromise = new DeferredPromise<T>()
if (dedupeConcurrent) {
queue = []
if (nextPromise.started) nextPromise = new DeferredPromise<T>()
taskPromise = nextPromise
}
queue.push(() => {
taskPromise.started = true
task().then(taskPromise.resolve).catch(taskPromise.reject)
return taskPromise.promise
})
if (!running) running = start()
return taskPromise.promise
}
const start = async () => {
while (queue.length) {
const task = queue.shift()!
await task().catch(() => {})
}
running = undefined
}
return {
push,
flush: () => running || Promise.resolve(),
get size() {
return queue.length
},
}
}
export const createAsyncQueues = <T = void>(opts = { dedupeConcurrent: false }) => {
const queues: { [queueId: string]: AsyncQueue<T> } = {}
const push = (queueId: string, task: Callback<T>) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].push(task)
}
const flush = (queueId: string) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].flush()
}
return { push, flush }
}
class DeferredPromise<T = void, E = any> {
started = false
resolve: (x: T | PromiseLike<T>) => void = () => {}
reject: (x: E) => void = () => {}
promise: Promise<T>
constructor() {
this.promise = new Promise<T>((res, rej) => {
this.resolve = res
this.reject = rej
})
}
}
// function main() {
// const queue = createAsyncQueue()
// queue.push(async () => {
// console.log(0)
// }) // returns 0
// queue.push(async () => {
// console.log(1)
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// console.log('12')
// resolve()
// }, 1000)
// })
// }) // returns 3
// queue.push(async () => console.log(2)) // returns 3
// queue.push(async () => console.log(3)) // returns 3
// console.log('hi')
// }
// main()

View File

@ -1,66 +0,0 @@
import { singleton } from 'zutils'
import Client from 'ioredis'
import Redlock, { Lock, ResourceLockedError } from 'redlock'
import logger from 'logger/logger'
interface IRequest {
method: string
url: string
user?: {
id: string
}
lock?: Lock
}
const redisA = new Client(process.env.REDIS)
const redlock = new Redlock(
// You should have one client for each independent redis node
// or cluster.
[redisA],
{
// The expected clock drift; for more details see:
// http://redis.io/topics/distlock
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
// The max number of times Redlock will attempt to lock a resource
// before erroring.
retryCount: 0,
// the time in ms between attempts
retryDelay: 200, // time in ms
// the max time in ms randomly added to retries
// to improve performance under high contention
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
retryJitter: 200, // time in ms
// The minimum remaining time on a lock before an extension is automatically
// attempted with the `using` API.
automaticExtensionThreshold: 500, // time in ms
},
)
redlock.on('error', error => {
// Ignore cases where a resource is explicitly marked as locked on a client.
if (error instanceof ResourceLockedError) {
return
}
// Log all other errors.
logger.error(error)
})
@singleton
export class SyncLocker {
public async unlock(req: IRequest) {
if (req.lock) {
await req.lock.release()
}
}
public async checkLock(req: IRequest, key?: string, lockTime: number = 60000) {
key = key || `${req.method}:${req.url}:${req.user?.id || ''}`
let lock = await redlock.acquire([key], lockTime)
req.lock = lock
return true
}
}

13
src/common/ZError.ts Normal file
View File

@ -0,0 +1,13 @@
import { FastifyError } from 'fastify'
export class ZError implements FastifyError {
code: string
statusCode?: number
message: string
name: string
constructor(statusCode: number, message: string) {
this.statusCode = statusCode
this.message = message
}
}

View File

@ -0,0 +1,7 @@
import fastify = require('fastify')
export const ROLE_ANON = 'anon'
class BaseController {
aotoRoute(req: fastify.FastifyRequest, res) {}
}
export default BaseController

View File

@ -0,0 +1,181 @@
import logger from 'logger/logger'
import BaseController from 'common/base.controller'
import { ZError } from 'common/ZError'
import { router } from 'decorators/router'
import { createOrder, createPageSign, queryFiat, queryPrice, refreshToken } from 'service/alchemy.svr'
import { generateKVStr } from 'utils/net.util'
import { PayRecord, PayStatus } from 'modules/PayRecord'
import { PriceSvr } from 'service/price.svr'
import { reportPayResult } from 'service/game.svr'
const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/out/alchemy/buycb`
class AlchemyController extends BaseController {
// @router('post /pay/alchemy/buy')
async beginApiPay(req, res) {
const user = req.user
const { network, crypto, address, fiat, fiatAmount, payWayCode, country, accountId, orderId, env } = req.params
let envStr = env || 'dev'
if (fiat || fiatAmount || country) {
if (!fiat || !fiatAmount || !country || !payWayCode) {
throw new ZError(11, 'fiat, fiatAmount payWayCode and country must be provided')
}
}
if (network || crypto) {
if (!network || !crypto) {
throw new ZError(12, 'network and crypto must be provided')
}
}
const tokenResult = await refreshToken(user.emailReal || user.email)
if (!tokenResult.success || tokenResult.returnCode !== '0000') {
logger.info(`fetch pay token error::code: ${tokenResult.returnCode} msg: ${tokenResult.returnMsg}`)
throw new ZError(10, 'fetch pay token error')
}
const { id, email, accessToken } = tokenResult.data
if (crypto.toLowerCase() === 'agor') {
let today = new Date()
today.setHours(0, 0, 0, 0)
let count = await PayRecord.countDocuments({
account: user.id,
crypto,
status: PayStatus.SUCCESS,
createdAt: { $gte: today },
})
if (count >= 3) {
throw new ZError(13, 'daily limit')
}
}
let record = new PayRecord({
account: user.id,
address,
network,
crypto,
env: envStr,
gameAccountId: accountId,
gameOrderId: orderId,
})
if (fiat) record.fiat = fiat
if (fiatAmount) record.fiatAmount = fiatAmount
if (country) record.country = country
await record.save()
let payData: any = {
side: 'BUY',
merchantOrderNo: record.id,
amount: record.fiatAmount,
fiatCurrency: record.fiat,
cryptoCurrency: record.crypto,
depositType: '2',
address: address,
network: record.network,
payWayCode,
alpha2: record.country,
callbackUrl: CALL_BACK_URL,
merchantName: 'CEBG',
}
logger.info(`create order data::${JSON.stringify(payData)}`)
let payRes = await createOrder(accessToken, payData)
logger.info(`create order result::${JSON.stringify(payRes)}`)
record.outData = payRes.data
if (payRes.success) {
record.outOrderId = payRes.data.orderNo
await record.save()
} else {
record.status = PayStatus.FAIL
await record.save()
setImmediate(() => {
reportPayResult(record)
})
throw new ZError(payRes.returnCode, payRes.returnMsg)
}
return { url: payRes.data.payUrl }
}
// @router('post /pay/alchemy/buypage')
async beginPagePay(req, res) {
const user = req.user
const { network, crypto, address, fiat, fiatAmount, country } = req.params
if (fiat || fiatAmount || country) {
if (!fiat || !fiatAmount || !country) {
throw new ZError(11, 'fiat, fiatAmount and country must be provided')
}
}
if (network || crypto) {
if (!network || !crypto) {
throw new ZError(12, 'network and crypto must be provided')
}
}
const tokenResult = await refreshToken(user.emailReal || user.email)
console.log(tokenResult)
if (!tokenResult.success || tokenResult.returnCode !== '0000') {
logger.info(`fetch pay token error::code: ${tokenResult.returnCode} msg: ${tokenResult.returnMsg}`)
throw new ZError(10, 'fetch pay token error')
}
const { id, email, accessToken } = tokenResult.data
let record = new PayRecord({ account: user.id, address, network, crypto })
if (fiat) record.fiat = fiat
if (fiatAmount) record.fiatAmount = fiatAmount
if (country) record.country = country
await record.save()
const merchantOrderNo = record.id
let dataOrign: any = {
token: accessToken,
email,
id,
showTable: 'buy',
merchantOrderNo,
}
if (network) dataOrign.network = network
if (crypto) dataOrign.crypto = crypto
if (fiat) dataOrign.fiat = fiat
if (fiatAmount) dataOrign.fiatAmount = fiatAmount
if (country) dataOrign.country = country
let dataSign: any = {
appId: process.env.ALCHEMY_APPID,
address,
callbackUrl: CALL_BACK_URL,
}
let signStr = generateKVStr({ data: dataSign, sort: true })
let sign = createPageSign(signStr)
dataOrign.sign = sign
Object.assign(dataOrign, dataSign)
const urlBase = process.env.ALCHEMY_PAGE_BASE
let url = `${urlBase}/`
url = generateKVStr({ data: dataOrign, encode: true, uri: url })
return { url }
}
// @router('post /pay/alchemy/crypto_price')
async queryCryptoPrice(req, res) {
let { token, chain, currency, env } = req.params
if (!token || !chain) {
throw new ZError(11, 'token or network not found')
}
if (
(chain.toLowerCase() === 'agor' || chain.toLowerCase() === 'eth') &&
(token.toLowerCase() === 'ceg' || token.toLowerCase() === 'cec')
) {
return { price: 1 }
}
if ((chain.toLowerCase() === 'agor' || chain.toLowerCase() === 'eth') && token.toLowerCase() === 'agor') {
token = 'ETH'
chain = 'ETH'
}
if (token.toLowerCase() === 'ceg') {
return { price: 1 }
}
let data = {
crypto: token,
network: chain,
fiat: currency || 'USD',
}
let result = await new PriceSvr().fetchPrice(data)
return { price: result }
}
// @router('get /pay/alchemy/fait_list')
async cryptoList(req, res) {
let result = await queryFiat()
if (!result.success) {
throw new ZError(10, result.returnMsg || 'fetch fiat list error')
}
return result.data
}
}

View File

@ -0,0 +1,158 @@
import logger from 'logger/logger'
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router'
import { checkPayResultSign, checkSha1Sign, checkSimpleSign } from 'service/alchemy.svr'
import { PayRecord, PayStatus } from 'modules/PayRecord'
import { TransferQueue } from 'queue/transfer.queue'
import { TransferRecord } from 'modules/TransferRecord'
import { reportPayResult } from 'service/game.svr'
let errorRes = function (msg: string) {
logger.info(`error res: ${msg}`)
return {
direct: 1,
data: null,
success: false,
returnCode: '9999',
returnMsg: msg,
}
}
/**
* for Alchemy call
*/
class AlchemyOutController extends BaseController {
// @role(ROLE_ANON)
// @router('post /pay/out/alchemy/buycb')
async alchemyCallback(req, res) {
let { orderNo, status, crypto, network, merchantOrderNo } = req.params
logger.info(`alchemy callback: ${orderNo}, ${status}, ${crypto}, ${network}, ${merchantOrderNo}`)
if (!merchantOrderNo) {
logger.info(`alchemy callback merchantOrderNo not found`)
throw new ZError(11, 'alchemy callback merchantOrderNo not found')
}
let record = await PayRecord.findById(merchantOrderNo)
if (!record) {
logger.info(`alchemy callback record not found`)
throw new ZError(12, 'alchemy callback record not found')
}
if (
record.status !== PayStatus.PENDING &&
record.status !== PayStatus.TRANSFERING &&
record.status !== PayStatus.TRANSFERED
) {
logger.info(`alchemy callback record status error`)
throw new ZError(13, 'alchemy callback record status error')
}
if (!checkPayResultSign(req.params)) {
logger.info(`alchemy callback sign error`)
record.status = PayStatus.FAIL
await record.save()
throw new ZError(14, 'alchemy callback sign error')
}
let transferRecord = await TransferRecord.findByRecordId(record.id)
if (transferRecord) {
transferRecord.status = 9
await transferRecord.save()
}
record.outOrderId = orderNo
record.network = network
record.crypto = crypto
record.outData = req.params
record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL
await record.save()
setImmediate(() => {
reportPayResult(record)
})
logger.info(`alchemy callback success, pay finished`)
return {}
}
/**
*
* TODO:: calc networkFee
*/
// @role(ROLE_ANON)
// @router('get /pay/out/alchemy/queryprice')
async queryToken(req, res) {
const { crypto } = req.params
logger.info(`alchemy query price: ${crypto}`)
let { appId, appid, timestamp, sign } = req.headers
logger.info(`alchemy query price headers: ${appid}, ${timestamp}, ${sign}`)
if (!crypto) {
return errorRes('params mismatch')
}
appId = appId || appid
if (!appId || !timestamp || !sign) {
return errorRes('headers mismatch')
}
if (!checkSha1Sign(req.headers)) {
return errorRes('sign error')
}
let result = {
direct: 1,
data: {
price: '1.0',
networkList: [
{
network: 'AGOR',
networkFee: '0.037',
},
],
},
success: true,
returnCode: '0000', // false: 9999
returnMsg: 'in amet',
}
return result
}
/**
*
*/
// @role(ROLE_ANON)
// @router('post /pay/out/alchemy/distribute')
async distributeToken(req, res) {
const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params
logger.info(
`alchemy distributeToken: orderNo: ${orderNo}, crypto: ${crypto}, network: ${network}, address: ${address}, cryptoAmount: ${cryptoAmount}, cryptoPrice: ${cryptoPrice}, usdtAmount: ${usdtAmount}`,
)
let { appId, appid, timestamp, sign } = req.headers
logger.info(`alchemy distributeToken: appId: ${appId || appid}, timestamp: ${timestamp}, sign: ${sign}`)
if (!orderNo || !crypto || !network || !address || !cryptoAmount || !cryptoPrice || !usdtAmount) {
return errorRes('params mismatch')
}
appId = appId || appid
if (!timestamp || !sign) {
return errorRes('headers mismatch')
}
// let signData = { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount }
if (!checkSha1Sign(req.headers)) {
return errorRes('sign error')
}
let record = await PayRecord.findByRecordId(orderNo)
if (!record) {
return errorRes('orderNo not found')
}
if (record.crypto != crypto || record.network != network || record.address != address) {
return errorRes('params mismatch')
}
record.cryptoAmount =
record.network.toLowerCase() === 'agor' && record.crypto.toLowerCase() === 'agor' ? '0.001' : cryptoAmount
record.cryptoPrice = cryptoPrice
record.usdtAdmount = usdtAmount
record.status = PayStatus.TRANSFERING
await record.save()
new TransferQueue().addTask(record)
let result = {
direct: 1,
data: null,
success: true,
returnCode: '0000', // false: 9999
returnMsg: 'in amet',
}
return result
}
}

View File

@ -1,11 +1,15 @@
import { Account } from 'modules/Account'
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { role, router } from 'decorators/router'
import verifyAppleToken from 'verify-apple-id-token'
import { Account, PlatEnum } from 'modules/Account'
import axios from 'axios'
import logger from 'logger/logger'
import { PlatApple } from 'plats/PlatApple'
import { IPlat } from 'plats/IPlat'
import { PlatEnum } from 'enums/PlatEnum'
import { BaseController, ROLE_ANON, role, router } from 'zutils'
var https = require('follow-redirects').https
const CLIENT_ID_DEBUG = 'com.jc.tebg'
const CLIENT_ID_RELEASE = 'com.cege.games.release'
const CLIEND_ID_ANDROID = 'wallet.cebggame.com'
const plat: IPlat = new PlatApple()
class AppleController extends BaseController {
@role(ROLE_ANON)
@router('post /apple/login-notify')
@ -16,9 +20,20 @@ class AppleController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/apple')
async checkAppleJwt(req, res) {
async checkGoogleJwt(req, res) {
const { token } = req.params
logger.db('login', req)
const { openId, data } = await plat.verifyToken(req)
const payload = await verifyAppleToken({
idToken: token,
clientId: [CLIENT_ID_DEBUG, CLIENT_ID_RELEASE, CLIEND_ID_ANDROID],
})
const openId = payload.sub
let data: any = {}
if (payload.email) data.email = payload.email
if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified
if (payload.locale) data.locale = payload.locale
if (payload.name) data.nickname = payload.name
if (payload.picture) data.avatar = payload.picture
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform

View File

@ -1,27 +1,41 @@
import { PlatEnum } from 'enums/PlatEnum'
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 } from 'modules/Account'
import { IPlat } from 'plats/IPlat'
import { PlatClient } from 'plats/PlatClient'
import { BaseController, ROLE_ANON, ZError, role, router } from 'zutils'
import { Account, PlatEnum } from 'modules/Account'
import * as wasm from 'rustwallet'
import { isUUID } from 'utils/string.util'
const plat: IPlat = new PlatClient()
const CLIENT_SUFFIX = '_clientid'
function checkClientId(clientId: string) {
if (!clientId) {
return false
}
if (!clientId.endsWith(CLIENT_SUFFIX)) {
return false
}
const id = clientId.slice(0, clientId.length - CLIENT_SUFFIX.length)
return isUUID(id)
}
class ClientController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/client')
async clientLogin(req, res) {
const { code } = req.params
const { api_platform } = req.headers
logger.db('login', req)
if (!code) {
throw new ZError(11, 'param missing')
}
const { openId, data } = await plat.verifyToken(req)
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
const sk = process.env.WALLET_CLIENT_SK
let codeDecrypto = wasm.rdecrypt(sk, code)
if (!checkClientId(codeDecrypto)) {
throw new ZError(12, 'param invalid')
}
const openId = codeDecrypto.slice(0, codeDecrypto.length - CLIENT_SUFFIX.length)
logger.info('clientLogin', openId)
let user = await Account.insertOrUpdate({ plat: PlatEnum.CLIENT, openId }, data)
let user = await Account.insertOrUpdate({ plat: PlatEnum.CLIENT, openId }, { platform: api_platform })
const ztoken = await res.jwtSign({
id: user.id,
openid: user.openId,

View File

@ -1,21 +0,0 @@
import logger from 'logger/logger'
import { IPlat } from 'plats/IPlat'
import { BaseController, ROLE_ANON, role, router } from 'zutils'
import { PlatDiscord } from 'plats/PlatDiscord'
const plat: IPlat = new PlatDiscord()
class DiscordController extends BaseController {
@role(ROLE_ANON)
@router('get /discord/oauth_redirect')
async appleWebLoginCb(req, res) {
const { code, state, error } = req.params
console.log(`code: ${code}, state: ${state}, error: ${error}`)
if (error) {
res.redirect(`cebgdiscordcb://discord_login_result?state=${state}&error=${JSON.stringify(error)}`)
} else {
res.redirect(`cebgdiscordcb://discord_login_result?token=${code}&state=${state}`)
}
}
}

View File

@ -1,11 +1,10 @@
import { PlatEnum } from 'enums/PlatEnum'
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 } from 'modules/Account'
import { IPlat } from 'plats/IPlat'
import { PlatFacebook } from 'plats/PlatFacebook'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
import { Account, PlatEnum } from 'modules/Account'
import { FACEBOOK_APP_ID, fetchUserInfo, verifyFbUserAccessToken } from 'providers/facebook.provider'
const plat: IPlat = new PlatFacebook()
class FacebookController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/facebook')
@ -15,16 +14,41 @@ class FacebookController extends BaseController {
if (!code) {
throw new ZError(10, 'params mismatch')
}
const { openId, data } = await plat.verifyToken(req)
const result = await verifyFbUserAccessToken(code)
if (!!result.error) {
throw new ZError(10, `${result.error?.message} (${result.error?.code})`)
}
const { data } = result
if (!data) {
throw new ZError(11, 'no data from facebook')
}
if (data.app_id !== FACEBOOK_APP_ID) {
throw new ZError(12, 'app id mismatch')
}
if (!data.is_valid) {
throw new ZError(13, 'access_token not valid')
}
const infoRes = await fetchUserInfo(code)
if (!!infoRes.error) {
throw new ZError(13, `${infoRes.error?.message} (${infoRes.error.code})`)
}
const openId = infoRes.id || data.user_id
let user: any = {}
let now = Date.now() / 1000
user.accessToken = code
user.accessTokenExpire = result.data['expires_at']
user.scope = data['scopes']
if (infoRes['name']) user.nickname = infoRes['name']
if (infoRes['email']) user.email = infoRes['email']
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
user.platform = api_platform
}
let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, data)
let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, user)
const ztoken = await res.jwtSign({
id: account.id,
openid: account.openId,
version: account.accountVersion || 0,
openid: user.openId,
version: user.accountVersion || 0,
plat: PlatEnum.FACEBOOK,
})
return { token: ztoken }

View File

@ -1,19 +1,54 @@
import { PlatEnum } from 'enums/PlatEnum'
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router'
import { OAuth2Client } from 'google-auth-library'
import logger from 'logger/logger'
import { Account } from 'modules/Account'
import { IPlat } from 'plats/IPlat'
import { PlatGoogle } from 'plats/PlatGoogle'
import { BaseController, role, ROLE_ANON, router } from 'zutils'
import { Account, PlatEnum } from 'modules/Account'
import { customAlphabet } from 'nanoid'
const plat: IPlat = new PlatGoogle()
const nanoid = customAlphabet('1234567890abcdef', 10)
const GOOGLE_OAUTH_ISS = 'https://accounts.google.com'
const GOOGLE_OAUTH_ISS1 = 'accounts.google.com'
const IOS_TEST = '53206975661-0d6q9pqljn84n9l63gm0to1ulap9cbk4.apps.googleusercontent.com'
class GoogleController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/google')
async checkGoogleJwt(req, res) {
const { token } = req.params
logger.db('login', req)
const { openId, data } = await plat.verifyToken(req)
const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT
const CLIENT_ID2 = process.env.GOOGLE_OAUTH_CLIENT2
const CLIENT_ID_IOS = process.env.GOOGLE_OAUTH_CLIENT_IOS
const client = new OAuth2Client(CLIENT_ID)
const ticket = await client.verifyIdToken({
idToken: token,
audience: [CLIENT_ID, CLIENT_ID2, CLIENT_ID_IOS, IOS_TEST], // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
})
const payload = ticket.getPayload()
if (!(payload.iss === GOOGLE_OAUTH_ISS || payload.iss === GOOGLE_OAUTH_ISS1)) {
throw new ZError(10, 'id token error')
}
if (
payload.aud !== CLIENT_ID &&
payload.aud !== CLIENT_ID2 &&
payload.aud !== CLIENT_ID_IOS &&
payload.aud !== IOS_TEST
) {
throw new ZError(11, 'client id mismatch')
}
const openId = payload.sub
let data: any = {}
if (payload.email) data.email = payload.email
if (process.env.NODE_ENV !== 'development') {
if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified
}
if (payload.locale) data.locale = payload.locale
if (payload.name) data.nickname = payload.name
if (payload.picture) data.avatar = payload.picture
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform

View File

@ -1,14 +1,16 @@
import { ZError } from 'common/ZError'
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { role, router } from 'decorators/router'
import logger from 'logger/logger'
import { PayRecord, PayRecordClass, PayStatus } from 'modules/PayRecord'
import { TransferRecord, TransferRecordClass } from 'modules/TransferRecord'
import { hmacSha256 } from 'zutils/utils/security.util'
import { hmacsha256 } from 'utils/security.util'
import { DocumentType } from '@typegoose/typegoose'
import { queryPrice, updateOrderStatus } from 'service/alchemy.svr'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
const calcHash = function (data: any) {
let signStr = JSON.stringify(data)
return hmacSha256(signStr, process.env.HASH_SALT).toLowerCase()
return hmacsha256(signStr, process.env.HASH_SALT)
}
const notify = async function (record: DocumentType<PayRecordClass>, subTask: DocumentType<TransferRecordClass>) {

View File

@ -1,179 +0,0 @@
import { PlatEnum } from 'enums/PlatEnum'
import logger from 'logger/logger'
import { Account } from 'modules/Account'
import { UnionAccount } from 'modules/UnionAccount'
import { Wallet } from 'modules/Wallet'
import { IPlat } from 'plats/IPlat'
import { PlatApple } from 'plats/PlatApple'
import { PlatClient } from 'plats/PlatClient'
import { PlatDiscord } from 'plats/PlatDiscord'
import { PlatEmail } from 'plats/PlatEmail'
import { PlatExternalWallet } from 'plats/PlatExternalWallet'
import { PlatFacebook } from 'plats/PlatFacebook'
import { PlatGoogle } from 'plats/PlatGoogle'
import { PlatTikTok } from 'plats/PlatTikTok'
import { checkReleation } from 'service/game.svr'
import { generateRefreshToken, verifyRefreshToken } from 'utils/jwt.utils'
import { ZError, BaseController, role, ROLE_ANON, router } from 'zutils'
import { uuid } from 'zutils/utils/security.util'
const plats: Map<PlatEnum, IPlat> = new Map([
[PlatEnum.GOOGLE, new PlatGoogle()],
[PlatEnum.APPLE, new PlatApple()],
[PlatEnum.FACEBOOK, new PlatFacebook()],
[PlatEnum.TIKTOK, new PlatTikTok()],
[PlatEnum.CLIENT, new PlatClient()],
[PlatEnum.WC, new PlatExternalWallet()],
[PlatEnum.EXTERNAL_WALLET, new PlatExternalWallet()],
[PlatEnum.RELAY_WALLET, new PlatExternalWallet()],
[PlatEnum.DISCORD, new PlatDiscord()],
[PlatEnum.EMAIL, new PlatEmail()],
])
// 如果客户端有传入account, 则说明该次登录是绑定账号
// 首先查找该账号是否已经绑定了其他账号
const parseBindAccount = async (account: string, channel: PlatEnum, user: any) => {
const uid = user.id
let unionAccount
const filterData: any = {}
filterData[`plats.${channel}`] = uid
if (account) {
// TODO:: check from game svr, verify account and check if plat account could bind
let checkResult: any = await checkReleation(account, channel, user.openId)
console.log(checkResult)
if (checkResult.errcode) {
throw new ZError(30, checkResult.errmsg)
}
unionAccount = await UnionAccount.findOne({ gameAccount: account })
if (unionAccount) {
let platInfo = unionAccount.plats.get(channel + '')
// 如果已经绑定, 且绑定的相同平台下不同的账号, 则抛出异常
// 如果未绑定, 那么查找平台账号是否已经绑定了其他账号
if (platInfo && platInfo !== uid) {
throw new ZError(21, 'account already bind')
} else if (!platInfo) {
// 检查pid是否已经绑定了其他账号
let unionAccount2 = await UnionAccount.findOne(filterData)
// 如果记录不存在, 那么将平台账号绑定至当前unionAccount
if (!unionAccount2) {
// 如果当前unionAccount已经设置了钱包账号, 且钱包账号不是当前账号, 那么检查当前账号是否开启了钱包
// 如果开启了钱包, 那么就不允许绑定
if (unionAccount.walletAccount && unionAccount.walletAccount !== uid) {
const wallet = await Wallet.findByAccount(uid)
if (wallet && wallet.address) {
throw new ZError(23, 'plat account already had wallet')
}
}
unionAccount.plats.set(channel + '', uid)
unionAccount.markModified('plats')
} else if (unionAccount2.gameAccount && unionAccount2.gameAccount === account) {
// 这种情况不用处理, 理论上是不可能出现的
} else {
throw new ZError(22, 'plat account already bind')
}
}
} else {
unionAccount = await UnionAccount.insertOrUpdate(filterData, {})
if (unionAccount.gameAccount && unionAccount.gameAccount !== account) {
throw new ZError(22, 'plat account already bind')
}
unionAccount.gameAccount = account
}
await unionAccount.save()
} else {
unionAccount = await UnionAccount.findOne(filterData)
}
let walletUser
// 如果统一账号存在, 但钱包账号不存在,那么就把当前绑定账号的信息写入钱包账号
if (unionAccount && !unionAccount.walletAccount) {
unionAccount.walletAccount = uid
walletUser = user
await unionAccount.save()
} else if (unionAccount && unionAccount.walletAccount) {
walletUser = await Account.findById(unionAccount.walletAccount)
} else {
walletUser = user
}
return { unionAccount, walletUser }
}
class LoginController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/general')
async generalLogin(req, res) {
// nb: 是否不返回unionAccount相关信息
const { code, channel, account, nb } = req.params
logger.db('login', req)
if (!code) {
throw new ZError(10, 'code not found')
}
const plat = plats.get(channel)
if (!plat) {
throw new ZError(11, 'plat not support: ' + channel)
}
const { openId, data } = await plat.verifyToken(req)
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
}
const user = await Account.insertOrUpdate({ plat: channel, openId }, data)
let unionAccount = {id: '', gameAccount: ''}
let walletUser = user
if (!nb) {
const res = await parseBindAccount(account, channel, user)
unionAccount = res.unionAccount
walletUser = res.walletUser
}
if (plat.afterLogin) {
await plat.afterLogin(user)
}
const ztoken = await res.jwtSign({
id: walletUser.id,
uid: unionAccount?.id || '',
gid: unionAccount?.gameAccount || '',
openid: walletUser.openId,
version: walletUser.accountVersion || 0,
plat: walletUser.plat,
})
const refreshTokenKey = uuid()
walletUser.refreshTime = Date.now()
walletUser.refreshTokenKey = refreshTokenKey
await walletUser.save()
const refreshToken1 = generateRefreshToken({ id: refreshTokenKey })
return { token: ztoken, refreshToken: refreshToken1 }
}
@role(ROLE_ANON)
@router('post /wallet/refresh_token')
async refreshToken(req, res) {
logger.db('refresh_token', req)
const { refreshToken } = req.params
if (!refreshToken) {
throw new ZError(10, 'no refresh token')
}
const tokenData = verifyRefreshToken(refreshToken)
if (!tokenData || !tokenData.id) {
throw new ZError(11, 'refresh token invalid')
}
const user = await Account.findByRefreshToken(tokenData.id)
if (!user) {
throw new ZError(12, 'account not found')
}
if (user.locked) {
throw new ZError(13, 'account locked')
}
const refreshTokenKey = uuid()
user.refreshTime = Date.now()
user.refreshTokenKey = refreshTokenKey
await user.save()
const refreshToken1 = generateRefreshToken({ id: refreshTokenKey })
const ztoken = await res.jwtSign({
id: user.id,
uid: '',
gid: '',
openid: user.openId,
version: user.accountVersion || 0,
plat: user.plat,
})
return { refreshToken: refreshToken1, token: ztoken }
}
}

View File

@ -1,10 +1,10 @@
import { PlatEnum } from 'enums/PlatEnum'
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 } from 'modules/Account'
import { Account, PlatEnum } from 'modules/Account'
import { CodeRecord, CodeStatus, CodeType, DEFAULT_CODE, DEFAULT_EXPIRE_TIME } from 'modules/CodeRecord'
import {
DEFAULT_LOGIN_MAIL_HTML,
DEFAULT_LOGIN_MAIL_SUBJECT,
DEFAULT_REGIST_HTML,
DEFAULT_REGIST_SUBJECT,
DEFAULT_RESET_HTML,
@ -13,14 +13,7 @@ import {
DEFAULT_VERIFY_MAIL_SUBJECT,
EmailSvr,
} from 'service/email.svr'
import { sha1, uuid } from 'zutils/utils/security.util'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
import { SyncLocker } from 'common/SyncLocker'
export const isEmail = (email: string) => {
const reg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
return reg.test(email)
}
import { uuid } from 'utils/security.util'
class MailController extends BaseController {
/**
@ -137,15 +130,7 @@ class MailController extends BaseController {
if (!email || !type) {
throw new ZError(10, 'params mismatch')
}
if (!isEmail(email)) {
throw new ZError(12, 'email error')
}
type = parseInt(type)
if (type !== CodeType.REGIST && type !== CodeType.RESET && type !== CodeType.VERIFY && type !== CodeType.LOGIN) {
throw new ZError(13, 'type error')
}
const lockKey = sha1(`${email}_${type}`)
await new SyncLocker().checkLock(req, lockKey, 55000)
if (type === CodeType.REGIST) {
let account = await Account.findByEmail(email)
if (account) {
@ -170,12 +155,6 @@ class MailController extends BaseController {
case CodeType.VERIFY:
html = DEFAULT_VERIFY_MAIL_HTML
subject = DEFAULT_VERIFY_MAIL_SUBJECT
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
@ -186,27 +165,19 @@ class MailController extends BaseController {
html,
subject,
}
setImmediate(async () => {
try {
let { errcode, errmsg, data } = await new EmailSvr().sendMail(msgData)
if (errcode) {
logger.info(`error send mail:: email: ${email}, type: ${type}, errcode: ${errcode}, errmsg: ${errmsg}`)
record.status = CodeStatus.FAIL
} else {
logger.info(`success send mail:: email: ${email}, type: ${type}, messageId: ${data.messageId}`)
record.mailSend = true
record.emailId = data.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()
throw new ZError(14, 'send mail error')
}
})
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()
throw new ZError(14, 'send mail error')
}
return {}
}
/**

View File

@ -1,6 +1,8 @@
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 { ShareCodeRecord, DEFAULT_SHARE_CODE, ShareCodeStatus, DEFAULT_EXPIRE_TIME } from 'modules/ShareCodeRecord'
import { BaseController, router } from 'zutils'
import { Account } from 'modules/Account'
class MainController extends BaseController {
@router('post /wallet/account/reset')
@ -10,25 +12,4 @@ class MainController extends BaseController {
await user.updateOne({ $inc: { accountVersion: 1 } })
return {}
}
@router('post /wallet/code/generate')
async shareCode(req, res) {
logger.db('code_generate', req)
let user = req.user
let { type } = req.body
let record = await ShareCodeRecord.findOne({ account: user.id, type, status: ShareCodeStatus.PENDING })
if (!record) {
record = new ShareCodeRecord({
account: user.id,
openId: user.openId,
plat: user.plat + '',
code: DEFAULT_SHARE_CODE,
email: user.email,
type,
})
}
record.expiredAt = Date.now() + DEFAULT_EXPIRE_TIME
await record.save()
return { code: record.code }
}
}

View File

@ -1,157 +0,0 @@
import logger from 'logger/logger'
import { TranRecord } from 'modules/TranRecord'
import { Wallet } from 'modules/Wallet'
import {
addCoin,
createWallet,
ensureTxhash,
getAddresses,
getGasPrice,
getSignInfo,
queryCoin,
queryTranDetail,
queryTranHistory,
sendTran,
} from 'service/okx.svr'
import { BaseController, router, ZError } from 'zutils'
const DEFAULT_CHAINID = 42161
class OkxController extends BaseController {
// @role(ROLE_ANON)
@router('post /wallet/okx/gasprice')
async getPrice(req, res) {
let { chain } = req.params
chain = chain || DEFAULT_CHAINID
let { data } = await getGasPrice(chain)
if (data.code) {
throw new ZError(data.code, data.message)
}
if (!data.data || data.data.length === 0) {
throw new ZError(100, 'no data')
}
return data.data[0]
}
// @role(ROLE_ANON)
// @router('post /wallet/okx/signinfo')
async signInfo(req, res) {
let { signData } = req.params
let { data } = await getSignInfo(signData)
return data
}
// @role(ROLE_ANON)
// @router('post /wallet/okx/addcoin')
async addCustomerCoin(req, res) {
let { chainId, address } = req.params
let reqData = {
coins: [
{
chainId: chainId,
tokenAddress: address,
},
],
}
let { data } = await addCoin(reqData)
return data
}
// @router('post /wallet/okx/bindwallet')
async bindWallet(req, res) {
let user = req.user
let { chainId, address, walletId } = req.params
chainId = chainId || DEFAULT_CHAINID
let data = {
addresses: [
{
chainId: chainId,
address: address,
},
],
walletId,
}
let rep = await createWallet(data)
return rep.data
}
@router('post /wallet/okx/walletaddress')
async walletAddresses(req, res) {
let { walletId } = req.params
let result = await getAddresses(walletId)
return result.data
}
@router('post /wallet/okx/walletassets')
async queryWalletCoin(req, res) {
let { coinIds, chainIds, walletId } = req.params
let reqData: any = {
walletId,
}
if (coinIds && coinIds.length > 0) {
reqData.coinIds = coinIds
}
chainIds = chainIds || [DEFAULT_CHAINID]
reqData.chainIds = chainIds
let result = await queryCoin(reqData)
return result.data
}
@router('post /wallet/okx/transhistory')
async queryWalletTransHistory(req, res) {
let { chainIds, walletId } = req.params
let reqData: any = {
walletId,
limit: 10,
}
chainIds = chainIds || [DEFAULT_CHAINID]
reqData.chainIds = chainIds
let result = await queryTranHistory(reqData)
return result.data
}
@router('post /wallet/okx/transinfo')
async queryWalletTransInfo(req, res) {
let { chainId, walletId, orderId } = req.params
chainId = chainId || DEFAULT_CHAINID
let result = await queryTranDetail({ walletId, orderId, chainId })
return result.data
}
@router('post /wallet/okx/sendtran')
async sendWalletTran(req, res) {
let { data } = req.params
let user = req.user
let record = await Wallet.findOne({ account: user.id })
if (!record || !record.address) {
throw new ZError(11, 'no wallet found')
}
data.chainId = data.chainId || DEFAULT_CHAINID
data.walletId = record.id
console.log('send trans: ' + JSON.stringify(data))
let result = await sendTran(data)
if (result.status !== 200) {
throw new ZError(result.status, result.statusText)
}
if (result.data.code) {
throw new ZError(result.data.code, result.data.msg)
}
if (result.data?.data) {
setImmediate(async () => {
try {
await TranRecord.insertOrUpdate(
{
transactionHash: data.txHash,
},
{
okxOrderId: result.data.data.orderId,
},
)
logger.log(`success update log: ${data.txHash}`)
} catch (err) {
logger.log(`error update log: ${data.txHash} with error: ${err}`)
}
})
}
return { txHash: data.txHash }
}
}

View File

@ -1,6 +1,8 @@
import BaseController from 'common/base.controller'
import { ZError } from 'common/ZError'
import { router } from 'decorators/router'
import logger from 'logger/logger'
import { TranRecord } from 'modules/TranRecord'
import { BaseController, router, ZError } from 'zutils'
class RecordController extends BaseController {
@router('post /trans/record')

View File

@ -1,84 +0,0 @@
import logger from 'logger/logger'
import { RelayRecord, RelayStatusEnum } from 'modules/RelayRecord'
import { RelaySession } from 'modules/RelaySession'
import { checkPersionalSign } from 'zutils/utils/chain.util'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
export const ROLE_SESSION = 'session'
class RelayController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/relay/prepare')
async prepareClient(req, res) {
let { msg, address, signature } = req.params
if (!msg || !address || !signature) {
throw new ZError(10, 'params mismatch')
}
// check signature
if (!checkPersionalSign(msg, address, signature)) {
throw new ZError(11, 'signature mismatch')
}
let session = new RelaySession({ msg, address, signature })
await session.save()
const ztoken = await res.jwtSign({
sid: session.id,
})
return { token: ztoken }
}
@role(ROLE_SESSION)
@router('post /wallet/relay/putdata')
async uploadData(req, res) {
let { data, type, session_id } = req.params
if (type == undefined || !data) {
throw new ZError(10, 'params mismatch')
}
type = parseInt(type)
let record = new RelayRecord({ sid: session_id, type, data })
await record.save()
return { id: record.id }
}
@role(ROLE_SESSION)
@router('post /wallet/relay/updata')
async updateData(req, res) {
let { data, id } = req.params
if (!id || !data) {
throw new ZError(10, 'params mismatch')
}
let record = await RelayRecord.findById(id)
record.status = RelayStatusEnum.RESOLVED
record.resp = data
await record.save()
return { id: record.id }
}
@role(ROLE_SESSION)
@router('post /wallet/relay/getlast')
async fetchLast(req, res) {
let { session_id, type } = req.params
if (type == undefined) {
throw new ZError(10, 'params mismatch')
}
type = parseInt(type)
let record = await RelayRecord.findLastRecord(session_id, type)
if (!record) {
throw new ZError(11, 'record not found')
}
return { id: record.id, data: record.data, status: record.status }
}
@role(ROLE_SESSION)
@router('post /wallet/relay/getdata')
async fetchData(req, res) {
let { id } = req.params
if (!id) {
throw new ZError(10, 'params mismatch')
}
let record = await RelayRecord.findById(id)
if (!record) {
throw new ZError(11, 'record not found')
}
return { id: record.id, data: record.data, resp: record.resp, status: record.status }
}
}

View File

@ -1,45 +0,0 @@
import logger from 'logger/logger'
import { DEFAULT_EXPIRED, NonceRecord } from 'modules/NonceRecord'
import { SiweMessage } from 'siwe'
import { checkParamsNeeded } from 'zutils/utils/net.util'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
import { checkNonce } from 'plats/PlatExternalWallet'
const LOGIN_TIP = 'This signature is just to verify your identity'
class SignController extends BaseController {
@role(ROLE_ANON)
@router('get /wallet/third/nonce')
async walletNonce(req, res) {
let record = new NonceRecord({ expired: Date.now() + DEFAULT_EXPIRED })
await record.save()
return { nonce: record.id, tips: LOGIN_TIP }
}
@role(ROLE_ANON)
@router('post /wallet/third/login')
async walletVerify(req, res) {
const { signature, message } = req.params
checkParamsNeeded(signature, message)
checkNonce(message.nonce)
if (message.nonce.length === 24) {
let record = await NonceRecord.findById(message.nonce)
if (!record || record.status !== 0) {
throw new ZError(12, 'nonce invalid')
}
if (record.expired < Date.now()) {
throw new ZError(13, 'nonce expired')
}
record.status = 1
await record.save()
}
const msgSign = new SiweMessage(message)
try {
await msgSign.verify({ signature, nonce: message.nonce })
} catch (e) {
throw new ZError(14, 'signature invalid')
}
return {}
}
}

View File

@ -1,29 +1,38 @@
import { PlatEnum } from 'enums/PlatEnum'
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 } from 'modules/Account'
import { IPlat } from 'plats/IPlat'
import { PlatTikTok } from 'plats/PlatTikTok'
import { Account, PlatEnum } from 'modules/Account'
import { fetchAccessToken, refreshAccessToken } from 'service/tiktok.svr'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
// 在tiktok的过期时间中, 减少一个小时
const EXPIRE_REDUCE_SECOND = 3600
const plat: IPlat = new PlatTikTok()
class TiktokController extends BaseController {
@role(ROLE_ANON)
@router('post /wallet/login/tiktok')
async checkTiktokCode(req, res) {
let { code } = req.params
logger.db('login', req)
const { openId, data } = await plat.verifyToken(req)
let result = await fetchAccessToken(code)
if (!(result.message === 'success' && result.data?.error_code === 0)) {
throw new ZError(10, `${result.message}: ${result.data?.description} (${result.data?.error_code})`)
}
const openId = result.data['open_id']
let user: any = {}
let now = Date.now() / 1000
user.accessToken = result.data['access_token']
user.refreshToken = result.data['refresh_token']
user.accessTokenExpire = now + result.data['expires_in'] - EXPIRE_REDUCE_SECOND
user.refreshTokenExpire = now + result.data['refresh_expires_in'] - EXPIRE_REDUCE_SECOND
user.scope = result.data['scope']
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
user.platform = api_platform
}
let account = await Account.insertOrUpdate({ plat: PlatEnum.TIKTOK, openId }, data)
let account = await Account.insertOrUpdate({ plat: PlatEnum.TIKTOK, openId }, user)
const ztoken = await res.jwtSign({
id: account.id,
openid: account.openId,
version: account.accountVersion || 0,
openid: user.openId,
version: user.accountVersion || 0,
plat: PlatEnum.TIKTOK,
})
return { token: ztoken }

View File

@ -1,8 +1,10 @@
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 } from 'modules/Account'
import { CodeRecord, CodeStatus, CodeType } from 'modules/CodeRecord'
import { DEFAULT_VERIFY_HTML, EmailSvr } from 'service/email.svr'
import { BaseController, router, ZError, role, ROLE_ANON } from 'zutils'
const TOKEN_PREFIX = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'

View File

@ -1,5 +1,7 @@
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router'
import { BridgeSvr } from 'service/bridge.svr'
import { BaseController, role, ROLE_ANON, router, ZError } from 'zutils'
const TOKEN_PREFIX = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'

View File

@ -1,30 +1,16 @@
import BaseController from 'common/base.controller'
import { ZError } from 'common/ZError'
import { router } from 'decorators/router'
import logger from 'logger/logger'
import { Wallet } from 'modules/Wallet'
import { WalletBackup } from 'modules/WalletBackup'
import { WalletExt } from 'modules/WalletExt'
import { customAlphabet } from 'nanoid'
import { createWallet } from 'service/okx.svr'
import { genRandomString, sha3_256, sha512 } from 'zutils/utils/security.util'
import { BaseController, router, ZError } from 'zutils'
import { genRandomString, sha3_256, sha512 } from 'utils/security.util'
const nanoid = customAlphabet('1234567890abcdef', 10)
const DEFAULT_CHAINID = 42161
const bindOkx = async (record: any) => {
console.log('bindOkx: ', record.address)
let res = await createWallet({
addresses: [{ chainId: DEFAULT_CHAINID, address: record.address }],
walletId: record.id,
})
console.log('bind result: ' + JSON.stringify(res.data))
if (!res.data.code || res.data.code == 81102) {
record.toOkx = true
await record.save()
}
}
class WalletController extends BaseController {
@router('get /wallet/info')
async getWalletInfo(req, res) {
@ -38,12 +24,6 @@ class WalletController extends BaseController {
record.nweRecord = false
await record.save()
}
// TODO:: 临时处理
// if (!record.toOkx && record.address) {
// setImmediate(async () => {
// await bindOkx(record)
// })
// }
Object.assign(data, record.toJson())
return data
}
@ -86,14 +66,6 @@ class WalletController extends BaseController {
}
record.address = address
await record.save()
// TODO:: 临时处理
// if (!record.toOkx) {
// if (!record.toOkx && record.address) {
// setImmediate(async () => {
// await bindOkx(record)
// })
// }
// }
return {}
}

View File

@ -1,5 +1,5 @@
import 'reflect-metadata'
import { singleton } from 'zutils'
import { singleton } from './singleton'
@singleton
export class NoJsonClass {

142
src/decorators/router.ts Normal file
View File

@ -0,0 +1,142 @@
import BaseController from '../common/base.controller'
export class RouterData {
target?: any
method?: string
path?: string
fun?: Function
}
export class RouterMap {
static decoratedRouters: Map<
Function,
{
roles?: string[]
permissions?: string[][]
data?: RouterData[]
depts?: string[]
}
> = new Map()
}
export function router(route?: string) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
if (!route) {
const controller = target.constructor.name
const controllerName = controller.toLowerCase().replace('.controller', '')
route = 'all ' + ['', controllerName, name].join('/')
}
const split = route.split(' ')
if (split.length > 2) {
throw new Error('路由中只允许一个空格')
}
const [method, path] = split
// @ts-ignore
const key = target[name]
let routerData = new RouterData()
routerData.target = target
routerData.method = method
routerData.path = path
// @ts-ignore
routerData.fun = target[name]
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
if (!objCurrent.data) {
objCurrent.data = [routerData]
} else {
objCurrent.data.push(routerData)
}
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
let routerObj = {
data: [routerData],
}
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], routerObj)
}
}
}
export function role(roles?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let roleList: string[] = []
if (roles) {
if (Array.isArray(roles)) {
roleList = roles
} else {
roleList = [roles]
}
}
// @ts-ignore
const key = target[name]
let roleObj = { roles: roleList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, roleObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], roleObj)
}
}
}
export function permission(permissions?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let permissionList: string[][] = [[]]
if (permissions) {
if (Array.isArray(permissions)) {
let arr = []
for (let sub of permissions) {
arr.push(sub.split(':'))
}
permissionList = arr
} else {
permissionList = [permissions.split(':')]
}
}
// @ts-ignore
const key = target[name]
let permissionObj = { permissions: permissionList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, permissionObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], permissionObj)
}
}
}
/**
* dept修饰器的, id是否存在
*/
export function dept(depts?: string | string[]) {
return (target: BaseController, name: string, value: PropertyDescriptor) => {
let deptList: string[] = []
if (depts) {
if (Array.isArray(depts)) {
deptList = depts
} else {
deptList = [depts]
}
}
// @ts-ignore
const key = target[name]
let deptObj = { depts: deptList }
if (RouterMap.decoratedRouters.has(key)) {
let objCurrent = RouterMap.decoratedRouters.get(key)
Object.assign(objCurrent, deptObj)
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], objCurrent)
} else {
// @ts-ignore
RouterMap.decoratedRouters.set(target[name], deptObj)
}
}
}

View File

@ -0,0 +1,29 @@
/**
* class
* 使:
* @singleton
* class Test {}
* new Test() === new Test() // returns `true`
* 使 decorator
* const TestSingleton = singleton(Test)
* new TestSingleton() === new TestSingleton() //returns 'true'
*/
export const SINGLETON_KEY = Symbol()
export type Singleton<T extends new (...args: any[]) => any> = T & {
[SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
}
export const singleton = <T extends new (...args: any[]) => any>(classTarget: T) =>
new Proxy(classTarget, {
construct(target: Singleton<T>, argumentsList, newTarget) {
// Skip proxy for children
if (target.prototype !== newTarget.prototype) {
return Reflect.construct(target, argumentsList, newTarget)
}
if (!target[SINGLETON_KEY]) {
target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)
}
return target[SINGLETON_KEY]
},
})

View File

@ -1,14 +0,0 @@
export enum PlatEnum {
GOOGLE = 0,
APPLE = 1,
TIKTOK = 2,
FACEBOOK = 3,
TWITTER = 4,
TELEGRAM = 5,
EMAIL = 6,
DISCORD = 7,
CLIENT = 10,
RELAY_WALLET = 11,
WC = 12,
EXTERNAL_WALLET = 13,
}

View File

@ -1,4 +0,0 @@
export enum RelayTypeEnum {
TO_WALLET = 0,
FROM_WALLET = 1,
}

View File

@ -2,8 +2,19 @@ import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType,
import { dbconn } from 'decorators/dbconn'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
import { genRandomString, sha512, uuid } from 'zutils/utils/security.util'
import { PlatEnum } from 'enums/PlatEnum'
import { genRandomString, sha512 } from 'utils/security.util'
export enum PlatEnum {
GOOGLE = 0,
APPLE = 1,
TIKTOK = 2,
FACEBOOK = 3,
TWITTER = 4,
TELEGRAM = 5,
EMAIL = 6,
DISCORD = 7,
CLIENT = 10,
}
/**
* salt和hash
@ -27,13 +38,11 @@ export function verifyPass(userpassword: string, passwordDb: string, salt: strin
return passwordData.passwordHash === passwordDb
}
export interface AccountClass extends Base, TimeStamps {}
interface AccountClass extends Base, TimeStamps {}
@dbconn()
@index({ plat: 1, openId: 1 }, { unique: true })
@index({ refreshTokenKey: 1 }, { unique: false })
@index({ plat: 1, email: 1 }, { collation: { locale: 'en', strength: 2 } })
@modelOptions({ schemaOptions: { collection: 'account', timestamps: true }, options: { allowMixed: Severity.ALLOW } })
export class AccountClass extends BaseModule {
class AccountClass extends BaseModule {
@prop({ enum: PlatEnum, default: PlatEnum.GOOGLE })
public plat!: PlatEnum
@prop({ required: true })
@ -101,12 +110,7 @@ export class AccountClass extends BaseModule {
@prop()
public platform: string
@prop()
public refreshTokenKey: string
@prop()
public refreshTime: number
public static async findByEmail(this: ReturnModelType<typeof AccountClass>, email: string) {
public static async findByEmail(this: ReturnModelType<typeof AccountClass>, email) {
return this.findOne({ email, plat: PlatEnum.EMAIL }).exec()
}
@ -118,11 +122,6 @@ export class AccountClass extends BaseModule {
}
}
public static async findByRefreshToken(this: ReturnModelType<typeof AccountClass>, refreshToken
: string) {
return this.findOne({ refreshTokenKey: refreshToken, deleted: false }).exec()
}
public verifyPassword(password: string) {
return verifyPass(password, this.password, this.salt)
}

View File

@ -6,8 +6,8 @@ import { plugin, prop, ReturnModelType } from '@typegoose/typegoose'
import findOrCreate from 'mongoose-findorcreate'
import { Connection } from 'mongoose'
import { ObjectId } from 'bson'
import { isTrue } from '../utils/string.util'
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types'
import { isTrue } from 'zutils/utils/string.util'
const jsonExcludeKeys = ['updatedAt', '__v']
const saveExcludeKeys = ['createdAt', 'updatedAt', '__v', '_id']

View File

@ -13,7 +13,6 @@ export enum CodeType {
REGIST = 1, // 注册
RESET = 2, // 重置密码
VERIFY = 3, // 验证邮箱
LOGIN = 4, // 验证码登录
}
export enum CodeStatus {
@ -28,7 +27,6 @@ export enum CodeStatus {
*/
@dbconn()
@index({ email: 1, type: 1 }, { unique: true, partialFilterExpression: { status: 1 } })
@index({ email: 1, type: 1, code: 1 }, { unique: false })
@index({ code: 1 }, { partialFilterExpression: { status: 1 } })
@modelOptions({
schemaOptions: { collection: 'code_send_record', timestamps: true },

View File

@ -1,20 +0,0 @@
import { getModelForClass, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export const DEFAULT_EXPIRED = 1000 * 60 * 5
@dbconn()
@modelOptions({ schemaOptions: { collection: 'nonce_record', timestamps: true } })
class NonceRecordClass extends BaseModule {
@prop({ required: true, default: 0 })
public status: number
@prop()
public expired: number
public static async removeExpired() {
await NonceRecord.deleteMany({ expired: { $lt: Date.now() } })
}
}
export const NonceRecord = getModelForClass(NonceRecordClass, { existingConnection: NonceRecordClass['db'] })

View File

@ -1,45 +0,0 @@
import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { RelayTypeEnum } from 'enums/RelayTypeEnum'
import { BaseModule } from './Base'
export const DEFAULT_EXPIRED = 1000 * 60 * 5
export enum RelayStatusEnum {
PENDING = 0,
RESOLVED = 1,
FINISHED = 2,
FAILED = 9,
}
@dbconn()
@index({ sid: 1, type: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'relay_data', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
class RelayRecordClass extends BaseModule {
@prop({ enum: RelayStatusEnum, default: RelayStatusEnum.PENDING })
public status: RelayStatusEnum
@prop({ required: true })
public sid: string
@prop({ enum: RelayTypeEnum, default: RelayTypeEnum.TO_WALLET })
public type: RelayTypeEnum
@prop({ required: true, type: mongoose.Schema.Types.Mixed })
public data: any
@prop({ type: mongoose.Schema.Types.Mixed })
public resp: any
public static async findLastRecord(sid: string, type: RelayTypeEnum) {
return RelayRecord.findOne({ sid, type }).sort({ _id: -1 })
}
public static async removeBySession(sid: string) {
await RelayRecord.deleteMany({ sid })
}
}
export const RelayRecord = getModelForClass(RelayRecordClass, { existingConnection: RelayRecordClass['db'] })

View File

@ -1,29 +0,0 @@
import { getModelForClass, modelOptions, prop } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export const DEFAULT_EXPIRED = 1000 * 60 * 60
@dbconn()
@modelOptions({
schemaOptions: { collection: 'relay_session', timestamps: true },
})
class RelaySessionClass extends BaseModule {
@prop({ default: 0 })
public status: number
@prop()
public address: string
@prop({ default: Date.now() + DEFAULT_EXPIRED })
public expired: number
public async refreshExpired() {
this.expired = Date.now() + DEFAULT_EXPIRED
}
public static async removeExpired() {
await RelaySession.deleteMany({ expired: { $lt: Date.now() } })
}
}
export const RelaySession = getModelForClass(RelaySessionClass, { existingConnection: RelaySessionClass['db'] })

View File

@ -1,80 +0,0 @@
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('23456789abcdefghjkmnpqrstuvwxy', 8)
export const DEFAULT_SHARE_CODE = '00000000'
export const DEFAULT_EXPIRE_TIME = 5 * 60 * 1000
export enum ShareCodeType {
BIND_UAW = 1, // uaw 绑定
}
export enum ShareCodeStatus {
PENDING = 1,
SUCCESS = 2,
FAIL = 3,
EXPIRED = 4,
}
/**
*
*/
@dbconn()
@index({ type: 1, code: 1 }, { unique: true, partialFilterExpression: { status: 1 } })
@index({ account: 1, type: 1, status: 1 }, { unique: true, partialFilterExpression: { status: 1 } })
@index({ expiredAt: 1, status: 1 }, { unique: false })
@modelOptions({
schemaOptions: { collection: 'share_code_record', timestamps: true },
})
@pre<ShareCodeRecordClass>('save', async function () {
if (this.code === DEFAULT_SHARE_CODE) {
let exists = false
while (!exists) {
const code = nanoid()
const record = await ShareCodeRecord.findByCode(code, this.type)
if (!record) {
exists = true
this.code = code
}
}
}
})
class ShareCodeRecordClass extends BaseModule {
@prop({ required: true })
public account: string
@prop({ required: true })
public openId: string
@prop()
public email?: string
@prop({ required: true })
public plat: string
@prop({ required: true })
public code!: string
@prop({ default: Date.now() + DEFAULT_EXPIRE_TIME })
public expiredAt?: number
@prop({ required: true, default: ShareCodeType.BIND_UAW })
public type: ShareCodeType
@prop({ required: true, default: ShareCodeStatus.PENDING })
public status: ShareCodeStatus
public static async findByCode(
this: ReturnModelType<typeof ShareCodeRecordClass>,
code: string,
type: ShareCodeType,
) {
return this.findOne({ code, type, status: ShareCodeStatus.PENDING }).exec()
}
}
export const ShareCodeRecord = getModelForClass(ShareCodeRecordClass, { existingConnection: ShareCodeRecordClass.db })

View File

@ -73,9 +73,6 @@ class TranRecordClass extends BaseModule {
@prop()
public confirmTime: number
@prop()
public okxOrderId?: string
public toJson() {
return {
transactionHash: this.transactionHash,

View File

@ -1,35 +0,0 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
import { BaseModule } from './Base'
import { PlatEnum } from 'enums/PlatEnum'
/**
*
*/
interface UnionAccountClass extends Base, TimeStamps {}
@dbconn()
@index({ gameAccount: 1 }, { unique: true, partialFilterExpression: { gameAccount: { $exists: true } } })
@index({ walletAccount: 1 }, { unique: true, partialFilterExpression: { walletAccount: { $exists: true } } })
@modelOptions({
schemaOptions: { collection: 'union_account', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
class UnionAccountClass extends BaseModule {
// 生成钱包所用账号
@prop()
public walletAccount?: string
// 绑定的guest账号
@prop()
public gameAccount?: string
@prop({ type: String })
public plats: Map<PlatEnum, string>
public static async findByPlat(this: ReturnModelType<typeof UnionAccountClass>, channel: PlatEnum, uid: string) {
const filterData: any = {}
filterData[`plats.${channel}`] = uid
return this.findOne(filterData).exec()
}
}
export const UnionAccount = getModelForClass(UnionAccountClass, { existingConnection: UnionAccountClass.db })

View File

@ -1,4 +1,4 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
@ -28,13 +28,6 @@ class WalletClass extends BaseModule {
@prop({ required: true, default: true })
public nweRecord: boolean
@prop({ default: false })
public toOkx: boolean
public static async findByAccount(this: ReturnModelType<typeof WalletClass>, account: string) {
return this.findOne({ account }).exec()
}
public toJson() {
return {
key: this.key,

View File

@ -29,7 +29,7 @@ export class NetClient {
headers: { 'Content-Type': 'application/json' },
}
Object.assign(defaultCfg, data)
// console.log(defaultCfg)
console.log(defaultCfg)
const res = await axios(defaultCfg)
return res.data
}

View File

@ -1,6 +0,0 @@
import { DocumentType } from '@typegoose/typegoose'
import { AccountClass } from 'modules/Account'
export interface IPlat {
verifyToken(req: any): Promise<any>
afterLogin?(user: DocumentType<AccountClass>): Promise<any>
}

View File

@ -1,30 +0,0 @@
import { IPlat } from './IPlat'
import verifyAppleToken from 'verify-apple-id-token'
const CLIENT_ID_DEBUG = 'com.jc.tebg'
const CLIENT_ID_RELEASE = 'com.cege.games.release'
const CLIEND_ID_ANDROID = 'wallet.counterfire.games'
export class PlatApple implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
const payload = await verifyAppleToken({
idToken: code,
clientId: [CLIENT_ID_DEBUG, CLIENT_ID_RELEASE, CLIEND_ID_ANDROID],
})
const openId = payload.sub
let data: any = {}
if (payload.email) data.email = payload.email
if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified
if (payload.locale) data.locale = payload.locale
if (payload.name) data.nickname = payload.name
if (payload.picture) data.avatar = payload.picture
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
}
return { openId, data }
}
}

View File

@ -1,31 +0,0 @@
import { IPlat } from './IPlat'
import verifyAppleToken from 'verify-apple-id-token'
import { ZError } from 'zutils'
import { isUUID } from 'zutils/utils/string.util'
import * as wasm from 'rustwallet'
const CLIENT_SUFFIX = '_clientid'
function checkClientId(clientId: string) {
if (!clientId) {
return false
}
if (!clientId.endsWith(CLIENT_SUFFIX)) {
return false
}
const id = clientId.slice(0, clientId.length - CLIENT_SUFFIX.length)
return isUUID(id)
}
export class PlatClient implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
const sk = process.env.WALLET_CLIENT_SK
let codeDecrypto = wasm.rdecrypt(sk, code)
if (!checkClientId(codeDecrypto)) {
throw new ZError(12, 'param invalid')
}
const openId = codeDecrypto.slice(0, codeDecrypto.length - CLIENT_SUFFIX.length)
return { openId, data: {} }
}
}

View File

@ -1,87 +0,0 @@
import { ZError } from 'zutils'
import { IPlat } from './IPlat'
import { handleFetch } from 'zutils/utils/net.util'
const EXPIRE_REDUCE_SECOND = 3600
export async function exchangeDiscrodCodeForToken(code: string) {
const clientId = process.env.DISCORD_CLIENT_ID
const clientSecret = process.env.DISCORD_CLIENT_SECRET
const redirectUri = process.env.DISCORD_REDIRECT_URI
const data = await handleFetch('https://discord.com/api/v10/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
code,
scope: 'identify email',
}),
})
return data
}
export async function refreshDiscordToken(refreshToken: string) {
const clientId = process.env.DISCORD_CLIENT_ID
const clientSecret = process.env.DISCORD_CLIENT_SECRET
const redirectUri = process.env.DISCORD_REDIRECT_URI
const data = await handleFetch('https://discord.com/api/v10/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
refresh_token: refreshToken,
scope: 'identify email',
}),
})
return data
}
export async function userInfo(token: string) {
const data = await handleFetch('https://discord.com/api/users/@me', {
headers: {
authorization: `Bearer ${token}`,
},
})
return data
}
export class PlatDiscord implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
let tokenResponse = await exchangeDiscrodCodeForToken(code)
if (!tokenResponse || tokenResponse.error) {
throw new ZError(60, tokenResponse.error_description)
}
let payload = await userInfo(tokenResponse.access_token)
const openId = payload.id
let data: any = {}
data.accessToken = tokenResponse['access_token']
data.refreshToken = tokenResponse['refresh_token']
data.accessTokenExpire = ((Date.now() / 1000) | 0) + tokenResponse['expires_in'] - EXPIRE_REDUCE_SECOND
if (payload.username) data.nickname = payload.username
if (payload.avatar) data.avatar = payload.avatar
if (payload.email) data.email = payload.email
if (payload.verified) data.emailVerified = payload.verified
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
}
return { openId, data }
}
}

View File

@ -1,52 +0,0 @@
import { ZError } from 'zutils'
import { IPlat } from './IPlat'
import { CodeRecord, CodeStatus, CodeType } from 'modules/CodeRecord'
import { sha1 } from 'zutils/utils/security.util'
import { Account } from 'modules/Account'
import { PlatEnum } from 'enums/PlatEnum'
const isEmail = (email: string) => {
const reg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
return reg.test(email)
}
const isCode = (code: string) => {
const reg = /^\d{6}$/
return reg.test(code)
}
export class PlatEmail implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
try {
JSON.parse(code)
} catch (err) {
throw new ZError(20, 'error parse token')
}
let payload = JSON.parse(code)
let { email, password } = payload
if (!email || !password || !isEmail(email) || !isCode(password)) {
throw new ZError(10, 'params mismatch')
}
let recordCode = await CodeRecord.findOne({ email, type: CodeType.LOGIN, code: password })
if (!recordCode) {
throw new ZError(11, 'code not exists')
}
if (recordCode.status !== CodeStatus.PENDING) {
throw new ZError(13, 'code expired')
}
email = email.toLowerCase()
let account = await Account.findOne({ plat: PlatEnum.EMAIL, email }).collation({ locale: 'en', strength: 2 })
const openId = account ? account.openId : sha1(email)
let data: any = { email, emailReal: email, emailVerified: true }
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
}
recordCode.status = CodeStatus.SUCCESS
await recordCode.save()
return { openId, data }
}
}

View File

@ -1,64 +0,0 @@
import { checkParamsNeeded } from 'zutils/utils/net.util'
import { IPlat } from './IPlat'
import { ZError } from 'zutils'
import { NonceRecord } from 'modules/NonceRecord'
import { SiweMessage } from 'siwe'
import { DocumentType } from '@typegoose/typegoose'
import { AccountClass } from 'modules/Account'
import { Wallet } from 'modules/Wallet'
// check if none is hex string with 24 length, or is timestamp within 5 minutes
export const checkNonce = (nonce: string) => {
if (!nonce) {
throw new ZError(11, 'Invalid nonce')
}
// use regex to check if nonce is 24 length hex string
if (nonce.length === 13) {
const timestamp = parseInt(nonce)
if (Date.now() - timestamp > 5 * 60 * 1000) {
throw new ZError(13, 'nonce expired')
}
} else {
if (!/^[0-9a-f]{24}$/.test(nonce)) {
throw new ZError(11, 'Invalid nonce.')
}
}
}
export class PlatExternalWallet implements IPlat {
async verifyToken(req: any): Promise<any> {
// here code is signature
let { code, message } = req.params
checkParamsNeeded(code, message)
checkNonce(message.nonce)
if (message.nonce.length === 24) {
let record = await NonceRecord.findById(message.nonce)
if (!record || record.status !== 0) {
throw new ZError(12, 'nonce invalid')
}
if (record.expired < Date.now()) {
throw new ZError(13, 'nonce expired')
}
record.status = 1
await record.save()
}
const msgSign = new SiweMessage(message)
try {
await msgSign.verify({ signature: code, nonce: message.nonce })
} catch (e) {
throw new ZError(14, 'signature invalid')
}
const openId = message.address
let data: any = {}
const { api_platform } = req.headers
if (api_platform) {
data.platform = api_platform
}
return { openId, data }
}
async afterLogin(user: DocumentType<AccountClass>) {
await Wallet.insertOrUpdate({ account: user.id }, { address: user.openId, nweRecord: false })
}
}

View File

@ -1,39 +0,0 @@
import { IPlat } from './IPlat'
import { ZError } from 'zutils'
import { FACEBOOK_APP_ID, fetchUserInfo, verifyFbUserAccessToken } from 'providers/facebook.provider'
export class PlatFacebook implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
const result = await verifyFbUserAccessToken(code)
if (!!result.error) {
throw new ZError(10, `${result.error?.message} (${result.error?.code})`)
}
const { data } = result
if (!data) {
throw new ZError(11, 'no data from facebook')
}
if (data.app_id !== FACEBOOK_APP_ID) {
throw new ZError(12, 'app id mismatch')
}
if (!data.is_valid) {
throw new ZError(13, 'access_token not valid')
}
const infoRes = await fetchUserInfo(code)
if (!!infoRes.error) {
throw new ZError(13, `${infoRes.error?.message} (${infoRes.error.code})`)
}
const openId = infoRes.id || data.user_id
let user: any = {}
user.accessToken = code
user.accessTokenExpire = result.data['expires_at']
user.scope = data['scopes']
if (infoRes['name']) user.nickname = infoRes['name']
if (infoRes['email']) user.email = infoRes['email']
return { openId, data: user }
}
}

View File

@ -1,67 +0,0 @@
import { OAuth2Client } from 'google-auth-library'
import { IPlat } from './IPlat'
import { ZError } from 'zutils'
import logger from 'logger/logger'
const GOOGLE_OAUTH_ISS = 'https://accounts.google.com'
const GOOGLE_OAUTH_ISS1 = 'accounts.google.com'
const IOS_TEST = '53206975661-0d6q9pqljn84n9l63gm0to1ulap9cbk4.apps.googleusercontent.com'
const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT
const CLIENT_ID2 = process.env.GOOGLE_OAUTH_CLIENT2
const CLIENT_ID_IOS = process.env.GOOGLE_OAUTH_CLIENT_IOS
const CLIENT_ID3 = '436789193812-5vh7ahctkaofjir9tnilfnvm19cf3vve.apps.googleusercontent.com'
const CLIENT_ID4 = '436789193812-9vubggj1op881elm41i7b9raeec9dgrj.apps.googleusercontent.com'
const CLIENT_ID5 = '436789193812-9vubggj1op881elm41i7b9raeec9dgrj.apps.googleusercontent.com'
const CLIENTS = [CLIENT_ID, CLIENT_ID2, CLIENT_ID3, CLIENT_ID4, CLIENT_ID_IOS, IOS_TEST, CLIENT_ID5]
export class PlatGoogle implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
let data: any = {}
let openId
const client = new OAuth2Client(CLIENT_ID)
try {
const ticket = await client.verifyIdToken({
idToken: code,
audience: CLIENTS, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
})
const payload = ticket.getPayload()
if (!(payload.iss === GOOGLE_OAUTH_ISS || payload.iss === GOOGLE_OAUTH_ISS1)) {
throw new ZError(10, 'id token error')
}
if (CLIENTS.indexOf(payload.aud) === -1) {
throw new ZError(11, 'client id mismatch')
}
if (payload.email) data.email = payload.email
if (process.env.NODE_ENV !== 'development') {
if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified
}
if (payload.locale) data.locale = payload.locale
if (payload.name) data.nickname = payload.name
if (payload.picture) data.avatar = payload.picture
openId = payload.sub
} catch (err) {
logger.log('error parse google id token', err)
try {
let info: any = await client.getTokenInfo(code)
console.log(info)
if (info.email) data.email = info.email
if (info.aud !== CLIENT_ID2) {
throw new ZError(11, 'client id mismatch')
}
if (process.env.NODE_ENV !== 'development') {
if (info.email_verified !== undefined) data.emailVerified = info.email_verified
}
if (info.name) data.nickname = info.name
openId = info.sub
} catch (e2) {
logger.log('error parse google access token', e2)
throw new ZError(10, 'id token error')
}
}
return { openId, data }
}
}

View File

@ -1,28 +0,0 @@
import { fetchAccessToken } from 'service/tiktok.svr'
import { IPlat } from './IPlat'
import { ZError } from 'zutils'
// 在tiktok的过期时间中, 减少一个小时
const EXPIRE_REDUCE_SECOND = 3600
export class PlatTikTok implements IPlat {
async verifyToken(req: any): Promise<any> {
let { code, token } = req.params
code = code || token
let result = await fetchAccessToken(code)
if (!(result.message === 'success' && result.data?.error_code === 0)) {
throw new ZError(10, `${result.message}: ${result.data?.description} (${result.data?.error_code})`)
}
const openId = result.data['open_id']
let user: any = {}
let now = Date.now() / 1000
user.accessToken = result.data['access_token']
user.refreshToken = result.data['refresh_token']
user.accessTokenExpire = now + result.data['expires_in'] - EXPIRE_REDUCE_SECOND
user.refreshTokenExpire = now + result.data['refresh_expires_in'] - EXPIRE_REDUCE_SECOND
user.scope = result.data['scope']
return { openId, data: user }
}
}

View File

@ -1,9 +1,6 @@
import { ROLE_SESSION } from 'controllers/relay.controller'
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import { Account } from 'modules/Account'
import { RelaySession } from 'modules/RelaySession'
import { ROLE_ANON } from 'zutils'
declare module 'fastify' {
interface FastifyRequest {
@ -31,13 +28,13 @@ const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastif
})
// 只有路由配置的role为anon才不需要过滤
fastify.decorate('apiAuth', async function (request: FastifyRequest, reply: FastifyReply) {
if (!(!!request.roles && (request.roles.indexOf(ROLE_ANON) > -1 || request.roles.indexOf(ROLE_SESSION) > -1))) {
if (!request.roles || request.roles.indexOf('anon') == -1) {
try {
if (!request.token) {
return reply.send({ errcode: 11, errmsg: 'need login' })
}
//@ts-ignore
const data = this.jwt.verify(request.token, { ignoreExpiration: true })
const data = this.jwt.verify(request.token)
if (!data || !data.id) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
@ -49,27 +46,6 @@ const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastif
} catch (err) {
return reply.send({ errcode: 401, errmsg: 'need auth' })
}
} else if (!!request.roles && request.roles.indexOf(ROLE_SESSION) != -1) {
try {
if (!request.token) {
return reply.send({ errcode: 11, errmsg: 'need login' })
}
//@ts-ignore
const data = this.jwt.verify(request.token)
if (!data || !data.sid) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
let session = await RelaySession.findById(data.sid)
if (!session) {
return reply.send({ errcode: 10, errmsg: 'need login' })
}
session.refreshExpired()
await session.save()
request.params['session_id'] = session.id
request.params['session_address'] = session.address
} catch (err) {
return reply.send({ errcode: 401, errmsg: 'need auth' })
}
}
})
}

View File

@ -1,26 +1,37 @@
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import {
FastifyInstance,
FastifyPluginAsync,
FastifyReply,
FastifyRequest,
} from "fastify";
import fastifyPlugin from "fastify-plugin";
/**
* post get req.params
*/
declare module 'fastify' {
declare module "fastify" {
interface FastifyInstance {
zReqParser: (request: FastifyRequest, reply: FastifyReply) => {}
zReqParser: (request: FastifyRequest, reply: FastifyReply) => {};
}
}
const zReqParserPlugin: FastifyPluginAsync = async function (fastify: FastifyInstance, options?: any) {
fastify.addHook('preValidation', async (request: FastifyRequest, reply: FastifyReply) => {
let params = request.params || {}
if (request.query) {
Object.assign(params, request.query)
const zReqParserPlugin: FastifyPluginAsync = async function (
fastify: FastifyInstance,
options?: any
) {
fastify.addHook(
"preValidation",
async (request: FastifyRequest, reply: FastifyReply) => {
let params = request.params || {};
if (request.query) {
Object.assign(params, request.query);
}
if (request.body) {
Object.assign(params, request.body);
}
request.params = params;
}
if (request.body) {
Object.assign(params, request.body)
}
request.params = params
})
return
}
);
return;
};
export default fastifyPlugin(zReqParserPlugin, '4.x')
export default fastifyPlugin(zReqParserPlugin, "4.x");

View File

@ -1,62 +1,73 @@
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fastifyPlugin from 'fastify-plugin'
import {
FastifyInstance,
FastifyPluginAsync,
FastifyReply,
FastifyRequest,
} from "fastify";
import fastifyPlugin from "fastify-plugin";
const getTokenFromHeader = function (request) {
let token: string | undefined
let token: string | undefined;
if (request.headers && request.headers.authorization) {
const parts = request.headers.authorization.split(' ')
const parts = request.headers.authorization.split(" ");
if (parts.length === 2) {
const scheme = parts[0]
const scheme = parts[0];
if (/^Bearer$/i.test(scheme)) {
token = parts[1]
token = parts[1];
}
}
}
return token
}
return token;
};
const getTokenFromCookie = function (request) {
let token: string | undefined
let token: string | undefined;
if (request.cookies) {
if (request.cookies['token']) {
token = request.cookies['token']
if (request.cookies["token"]) {
token = request.cookies["token"];
}
}
return token
}
return token;
};
const getTokenFromParams = function (request) {
let token: string | undefined
token = request.params && request.params.token
return token
}
let token: string | undefined;
token = request.params && request.params.token;
return token;
};
const getTokenFromQuery = function (request) {
let token: string | undefined
token = request.query && request.query.token
return token
}
let token: string | undefined;
token = request.query && request.query.token;
return token;
};
const getTokenFromBody = function (request) {
let token: string | undefined
token = request.body && request.body.token
return token
}
let token: string | undefined;
token = request.body && request.body.token;
return token;
};
const zTokenParserPlugin: FastifyPluginAsync = async function (fastify: FastifyInstance, options?: any) {
fastify.addHook('preValidation', async (request: FastifyRequest, reply: FastifyReply) => {
request['token'] =
getTokenFromHeader(request) ||
getTokenFromCookie(request) ||
getTokenFromParams(request) ||
getTokenFromQuery(request) ||
getTokenFromBody(request)
})
return
}
const zTokenParserPlugin: FastifyPluginAsync = async function (
fastify: FastifyInstance,
options?: any
) {
fastify.addHook(
"preValidation",
async (request: FastifyRequest, reply: FastifyReply) => {
request["token"] =
getTokenFromHeader(request) ||
getTokenFromCookie(request) ||
getTokenFromParams(request) ||
getTokenFromQuery(request) ||
getTokenFromBody(request);
}
);
return;
};
/**
* request的header, cookie, params, query和body中获取token, request.token中
* header中的字段key为authorization, Bearer xxxx
* key都为 token
*/
export default fastifyPlugin(zTokenParserPlugin, '4.x')
export default fastifyPlugin(zTokenParserPlugin, "4.x");

View File

@ -1,19 +1,19 @@
import { NetClient } from 'net/NetClient'
import {NetClient} from "net/NetClient";
const FACEBOOK_API_HOST = 'https://graph.facebook.com'
export const FACEBOOK_APP_ID = '1204701000119770'
const FACEBOOK_APP_SECRET = '5a1deba64b30c7326f497fc52691207f'
export const FACEBOOK_APP_ID = '1204701000119770';
const FACEBOOK_APP_SECRET = '5a1deba64b30c7326f497fc52691207f';
export async function getAppAccessToken() {
const url = `${FACEBOOK_API_HOST}/oauth/access_token?client_id=${FACEBOOK_APP_ID}&clent_secret=${FACEBOOK_APP_SECRET}&grant_type=client_credentials`
return new NetClient().httpGet(url)
}
export async function verifyFbUserAccessToken(accessToken: string) {
const url = `${FACEBOOK_API_HOST}/debug_token?input_token=${accessToken}&access_token=GG|${FACEBOOK_APP_ID}|${FACEBOOK_APP_SECRET}`
return new NetClient().httpGet(url)
const url = `${FACEBOOK_API_HOST}/oauth/access_token?client_id=${FACEBOOK_APP_ID}&clent_secret=${FACEBOOK_APP_SECRET}&grant_type=client_credentials`;
return new NetClient().httpGet(url);
}
export async function verifyFbUserAccessToken(accessToken: string){
const url = `${FACEBOOK_API_HOST}/debug_token?input_token=${accessToken}&access_token=GG|${FACEBOOK_APP_ID}|${FACEBOOK_APP_SECRET}`;
return new NetClient().httpGet(url);
}
export async function fetchUserInfo(accessToken: string) {
const url = `${FACEBOOK_API_HOST}/me?fields=["email","id", "name"]&access_token=${accessToken}`
return new NetClient().httpGet(url)
const url = `${FACEBOOK_API_HOST}/me?fields=["email","id", "name"]&access_token=${accessToken}`;
return new NetClient().httpGet(url);
}

View File

@ -1,4 +1,5 @@
import { AsyncQueue, createAsyncQueue, singleton } from 'zutils'
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
import { singleton } from 'decorators/singleton'
import logger from 'logger/logger'
import { UserLog } from 'modules/UserLog'

View File

@ -1,5 +1,5 @@
import { AsyncQueue, createAsyncQueue, singleton } from 'zutils'
import { singleton } from 'decorators/singleton'
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
import { DocumentType } from '@typegoose/typegoose'
import { PayRecordClass } from 'modules/PayRecord'
import logger from 'logger/logger'

View File

@ -1,7 +1,6 @@
import { singleton } from 'zutils'
import { singleton } from 'decorators/singleton'
import { CodeRecord, CodeStatus } from 'modules/CodeRecord'
import * as schedule from 'node-schedule'
import { ShareCodeRecord } from 'modules/ShareCodeRecord'
/**
*
@ -11,10 +10,6 @@ export default class CodeTaskSchedule {
async parseAllRecord() {
let now = Date.now()
await CodeRecord.updateMany({ expiredAt: { $lt: now }, status: CodeStatus.PENDING }, { status: CodeStatus.EXPIRED })
await ShareCodeRecord.updateMany(
{ expiredAt: { $lt: now }, status: CodeStatus.PENDING },
{ status: CodeStatus.EXPIRED },
)
}
scheduleAll() {
const job = schedule.scheduleJob('*/1 * * * *', async () => {

View File

@ -1,25 +0,0 @@
import { singleton } from 'zutils'
import { NonceRecord } from 'modules/NonceRecord'
import * as schedule from 'node-schedule'
/**
*
*/
@singleton
export default class NonceRecordSchedule {
async parseAllFinishedRecord() {
await NonceRecord.deleteMany({ status: 1 })
}
async parseAllExpiredRecord() {
let now = Date.now()
await NonceRecord.deleteMany({ expired: { $lt: now } })
}
scheduleAll() {
schedule.scheduleJob('*/1 * * * *', async () => {
await this.parseAllFinishedRecord()
})
schedule.scheduleJob('*/5 * * * *', async () => {
await this.parseAllExpiredRecord()
})
}
}

View File

@ -1,7 +1,7 @@
import axios from 'axios'
import { hmacSha256, sha1 } from 'zutils/utils/security.util'
import { hmacsha256, sha1 } from 'utils/security.util'
import crypto from 'crypto'
import { generateKVStr } from 'zutils/utils/net.util'
import { generateKVStr } from 'utils/net.util'
import logger from 'logger/logger'
export function createSimpleSign(data: any) {
@ -14,7 +14,7 @@ export function createSimpleSign(data: any) {
.sort()
.map(key => `${key}=${signData[key]}`)
.join('&')
let sign = hmacSha256(signStr, secret).toLowerCase()
let sign = hmacsha256(signStr, secret)
return {
appid,
timestamp,
@ -55,7 +55,7 @@ export function checkSimpleSign(headers: any, data: any) {
.sort()
.map(key => `${key}=${signData[key]}`)
.join('&')
const expectedSign = hmacSha256(signStr, process.env.ALCHEMY_APP_SECRET).toLowerCase()
const expectedSign = hmacsha256(signStr, process.env.ALCHEMY_APP_SECRET)
logger.info('compare sign: ', sign, expectedSign)
// const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp)
return sign === expectedSign

View File

@ -1,6 +1,6 @@
import { ZError } from 'zutils'
import { singleton } from 'zutils'
import { shortUuid } from 'zutils/utils/security.util'
import { ZError } from 'common/ZError'
import { singleton } from 'decorators/singleton'
import { shortUuid, uuid } from 'utils/security.util'
export interface IQrData {
clientId: string

View File

@ -1,4 +1,4 @@
import { singleton } from 'zutils'
import { singleton } from 'decorators/singleton'
import { NetClient } from 'net/NetClient'
export const DEFAULT_VERIFY_HTML = `
@ -34,15 +34,6 @@ export const DEFAULT_VERIFY_MAIL_HTML = `
<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
@ -52,7 +43,7 @@ export interface IMailData {
}
const DEFAULT_MSG_DATA: IMailData = {
from: 'Counter Fire <noreply@cebg.games>',
from: 'CEBG <noreply@cebg.games>',
to: '',
subject: 'Please verify your email address',
}

View File

@ -1,8 +1,7 @@
import axios from 'axios'
import { PayRecordClass } from 'modules/PayRecord'
import { DocumentType } from '@typegoose/typegoose'
import { hmacSha256 } from 'zutils/utils/security.util'
import { PlatEnum } from 'enums/PlatEnum'
import { hmacsha256 } from 'utils/security.util'
export async function reportPayResult(data: DocumentType<PayRecordClass>) {
let repData = {
@ -16,7 +15,7 @@ export async function reportPayResult(data: DocumentType<PayRecordClass>) {
.sort()
.map(key => `${key}=${encodeURIComponent(repData[key])}`)
.join('&')
const sign = hmacSha256(signStr, process.env.HASH_SALT).toLowerCase()
const sign = hmacsha256(signStr, process.env.HASH_SALT)
let url = `${process.env.GAME_PAY_CB_URL}&${signStr}&sign=${sign}`
let reqConfig: any = {
method: 'get',
@ -27,18 +26,3 @@ export async function reportPayResult(data: DocumentType<PayRecordClass>) {
}
return axios(reqConfig)
}
/**
* guest账号和平台账号能否绑定
*/
export async function checkReleation(gid: string, plat: PlatEnum, openId: string) {
let url = `${process.env.GAME_CHECK_RELATION_URL}&guest_account=${gid}&target_plat=${plat}&target_account=${openId}`
let reqConfig: any = {
method: 'get',
url,
headers: {
'Content-Type': 'application/json',
},
}
let res = await axios(reqConfig)
return res.data
}

View File

@ -1,316 +0,0 @@
import axios, { AxiosResponse } from 'axios'
import logger from 'logger/logger'
import { prepareOkxReqCfg } from 'utils/okx.utils'
export const OKX_BASE = 'https://www.okx.com/api/v5/waas'
export interface IOkxRes {
code: number
msg: string
data: any
}
// MARK: begin of tranactions
/**
* Get dynamic gas price
* https://www.okx.com/web3/build/docs/waas/api-transaction-get-gas-price
* @param chainId
* @returns
*/
export function getGasPrice(chainId: string | number) {
let config = {
method: 'get',
url: `${OKX_BASE}/transaction/get-gas-price?chainId=${chainId}`,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Get data required for signature
* https://www.okx.com/web3/build/docs/waas/api-transaction-get-sign-info
* @param data
* @returns
*/
export function getSignInfo(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/transaction/get-sign-info`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Send transaction
* https://www.okx.com/web3/build/docs/waas/api-transaction-send-transaction
* @param data
* @returns
*/
export function sendTran(data: any): Promise<AxiosResponse<IOkxRes>> {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/transaction/send-transaction`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Query transaction details
* https://www.okx.com/web3/build/docs/waas/api-transaction-get-transaction-detail
* @param data
* @returns
*/
export function queryTranDetail({
walletId,
orderId,
chainId,
}: {
walletId: string
orderId: string
chainId: string
}) {
let config = {
method: 'get',
url: `${OKX_BASE}/transaction/get-transaction-detail?walletId=${walletId}&orderId=${orderId}&chainId=${chainId}`,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
export async function ensureTxhash({
walletId,
orderId,
chainId,
}: {
walletId: string
orderId: string
chainId: string
}) {
return new Promise(async (resolve, reject) => {
let interReq = setInterval(async () => {
try {
let res = await queryTranDetail({ walletId, orderId, chainId })
let { code, data } = res.data
if (code === 0) {
let { txHash, txStatus } = data
if (txHash && txStatus === 4) {
clearInterval(interReq)
resolve && resolve(txHash)
} else if (txStatus == 3) {
clearInterval(interReq)
reject && reject('trade error')
}
}
} catch (err) {
logger.log('ensureTxhash err', err)
}
}, 1000)
})
}
/**
* Query transaction history
* https://www.okx.com/web3/build/docs/waas/api-transaction-get-transactions-history
* @param data
* @returns
*/
export function queryTranHistory(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/transaction/get-transactions`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
// end of tranactions
// begin of Wallet
/**
* Create wallet
* https://www.okx.com/api/v5/waas/wallet/create-wallet
* @param data
{
"addresses": [
{
"chainId": "1",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
},
{
"chainId": "56",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
},
{
"chainId": "66",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
}
],
"walletId": "13886e05-1265-4b79-8ac3-b7ab46211001"
}
* @returns
*/
export function createWallet(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/wallet/create-wallet`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Sync wallet other chain addresses
* https://www.okx.com/web3/build/docs/waas/api-wallet-synchronize-wallet-address
* @param data
{
"addresses": [
{
"chainId": "1",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
},
{
"chainId": "56",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
},
{
"chainId": "66",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
},
{
"chainId": "137",
"address": "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3"
}
],
"walletId": "13886e05-1265-4b79-8ac3-b7ab46211001"
}
* @returns
*/
export function syncAddress(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/wallet/sync-address`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Query current wallet address
* https://www.okx.com/web3/build/docs/waas/api-wallet-get-wallet-address
* @returns
*/
export function getAddresses(walletId: string) {
let config = {
method: 'get',
url: `${OKX_BASE}/wallet/get-addresses?walletId=${walletId}`,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
// end of Wallet
// begin of Assets
/**
* Query all supported coin information
* https://www.okx.com/web3/build/docs/waas/api-asset-get-all-coins
* @param type 0: Platform coin, 1: Custom token
*/
export function getAllCoins(type: string) {
let config = {
method: 'get',
url: `${OKX_BASE}/asset/get-all-coins?type=${type}`,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Add custom coins
* https://www.okx.com/web3/build/docs/waas/api-asset-add-coin
* @param data :
{
"coins":[
{
"chainId":1,
"tokenAddress":"0x4CEdA7906a5Ed2179785Cd3A40A69ee8bc99C466"
}
]
}
* @returns
*/
export function addCoin(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/asset/add-coin`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Delete custom coin
* https://www.okx.com/web3/build/docs/waas/api-asset-delete-coin
* @param data
{
"chainId": 1,
"tokenAddress": "0x4CEdA7906a5Ed2179785Cd3A40A69ee8bc99C466"
}
* @returns
*/
export function removeCoin(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/asset/del-coin`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* Query wallet token assets
* https://www.okx.com/web3/build/docs/waas/api-asset-get-wallet-asset
* @param data
{
"walletId": "13886e05-1265-4b79-8ac3-b7ab46211001",
"coinIds":["3"]
}
* @returns
*/
export function queryCoin(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/asset/get-assets`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
// end of assets

View File

@ -1,80 +0,0 @@
import axios from 'axios'
import logger from 'logger/logger'
import { generateKVStr } from 'zutils/utils/net.util'
import { prepareOkxReqCfg } from 'utils/okx.utils'
const OKX_BASE = 'https://www.okx.com/api/v5/mktplace'
/**
* https://www.okx.com/web3/build/docs/build-dapp/marketplace-create-a-listing
*/
export function beginSell(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/nft/markets/create-listing`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
export function submitOrder(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/nft/markets/submit-listing`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* https://www.okx.com/web3/build/docs/build-dapp/marketplace-buy-orders
*/
export function buyOrder(data: any) {
if (typeof data === 'object') {
data = JSON.stringify(data)
}
let config = {
method: 'post',
url: `${OKX_BASE}/nft/markets/buy`,
data,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* https://www.okx.com/web3/build/docs/build-dapp/marketplace-query-listing
*/
export function listings(data: any) {
let uri = `${OKX_BASE}/nft/markets/listings`
if (data) {
uri = generateKVStr({ data, uri })
}
let config = {
method: 'get',
url: uri,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}
/**
* https://www.okx.com/web3/build/docs/build-dapp/marketplace-query-offer
*/
export function offers(data: any) {
let uri = `${OKX_BASE}/nft/markets/offers`
if (data) {
uri = generateKVStr({ data, uri })
}
let config = {
method: 'get',
url: uri,
}
config = prepareOkxReqCfg(config)
return axios.request(config)
}

View File

@ -1,5 +1,5 @@
import axios from 'axios'
import { singleton } from 'zutils'
import { singleton } from 'decorators/singleton'
import { queryPrice } from './alchemy.svr'
import * as schedule from 'node-schedule'
import logger from 'logger/logger'

View File

@ -1,27 +0,0 @@
import { createSigner, createVerifier } from 'fast-jwt'
const privateKey = `-----BEGIN PRIVATE KEY-----
${process.env.REFRESH_TOKEN_SECRET_PRIVATE}
-----END PRIVATE KEY-----
`
const publicKey = `-----BEGIN PUBLIC KEY-----
${process.env.REFRESH_TOKEN_SECRET_PUBLIC}
-----END PUBLIC KEY-----
`
const REFRESH_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 * 1000
export const generateRefreshToken = (data: any) => {
const signSync = createSigner({
algorithm: 'EdDSA',
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
key: privateKey,
})
return signSync(data)
}
export const verifyRefreshToken = (token: string) => {
const verifier = createVerifier({
algorithms: ['EdDSA'],
key: publicKey,
})
return verifier(token)
}

186
src/utils/net.util.ts Normal file
View File

@ -0,0 +1,186 @@
const TIMEOUT_ERROR = new Error('timeout')
const hexRe = /^[0-9A-Fa-f]+$/gu
/**
* Execute fetch and verify that the response was successful.
*
* @param request - Request information.
* @param options - Fetch options.
* @returns The fetch response.
*/
export async function successfulFetch(request: string, options?: RequestInit) {
const response = await fetch(request, options)
if (!response.ok) {
throw new Error(`Fetch failed with status '${response.status}' for request '${request}'`)
}
return response
}
/**
* Execute fetch and return object response.
*
* @param request - The request information.
* @param options - The fetch options.
* @returns The fetch response JSON data.
*/
export async function handleFetch(request: string, options?: RequestInit) {
const response = await successfulFetch(request, options)
const object = await response.json()
return object
}
/**
* Execute fetch and return object response, log if known error thrown, otherwise rethrow error.
*
* @param request - the request options object
* @param request.url - The request url to query.
* @param request.options - The fetch options.
* @param request.timeout - Timeout to fail request
* @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context
* @returns The fetch response JSON data or undefined (if error occurs).
*/
export async function fetchWithErrorHandling({
url,
options,
timeout,
errorCodesToCatch,
}: {
url: string
options?: RequestInit
timeout?: number
errorCodesToCatch?: number[]
}) {
let result
try {
if (timeout) {
result = Promise.race([
await handleFetch(url, options),
new Promise<Response>((_, reject) =>
setTimeout(() => {
reject(TIMEOUT_ERROR)
}, timeout),
),
])
} else {
result = await handleFetch(url, options)
}
} catch (e) {
logOrRethrowError(e, errorCodesToCatch)
}
return result
}
/**
* Fetch that fails after timeout.
*
* @param url - Url to fetch.
* @param options - Options to send with the request.
* @param timeout - Timeout to fail request.
* @returns Promise resolving the request.
*/
export async function timeoutFetch(url: string, options?: RequestInit, timeout = 500): Promise<Response> {
return Promise.race([
successfulFetch(url, options),
new Promise<Response>((_, reject) =>
setTimeout(() => {
reject(TIMEOUT_ERROR)
}, timeout),
),
])
}
/**
* Utility method to log if error is a common fetch error and otherwise rethrow it.
*
* @param error - Caught error that we should either rethrow or log to console
* @param codesToCatch - array of error codes for errors we want to catch and log in a particular context
*/
function logOrRethrowError(error: any, codesToCatch: number[] = []) {
if (!error) {
return
}
const includesErrorCodeToCatch = codesToCatch.some(code =>
error.message.includes(`Fetch failed with status '${code}'`),
)
if (
error instanceof Error &&
(includesErrorCodeToCatch || error.message.includes('Failed to fetch') || error === TIMEOUT_ERROR)
) {
console.error(error)
} else {
throw error
}
}
/**
* key1=val1&key2=val2的字符串
* @param {object} data
* @param {boolean} sort key生序重排
* @param {boolean} ignoreNull (null值不参与拼接)
* @param splitChar , &
* @param equalChar =
*/
export function generateKVStr({
data = {},
sort = false,
encode = false,
ignoreNull = true,
splitChar = "&",
equalChar = "=",
uri = "",
}: {
data?: any;
sort?: boolean;
encode?: boolean;
ignoreNull?: boolean;
splitChar?: string;
equalChar?: string;
uri?: string;
}) {
const keys = Object.keys(data);
sort && keys.sort();
let result = "";
let i = 0;
for (let key of keys) {
if (ignoreNull && !data[key]) {
continue;
}
if (i++ > 0) result += splitChar;
if (encode) {
result += `${key}${equalChar}${encodeURIComponent(data[key])}`;
} else {
result += `${key}${equalChar}${data[key]}`;
}
}
if (uri) {
const joinChar = uri.search(/\?/) === -1 ? "?" : "&";
result = uri + joinChar + result;
}
return result;
}
/**
* key1=val&key2=val的字符串组装成对象
* @param str key1=val&key2=val的字符串
* @param splitChar , &
* @param equalChar =
*/
export function keyValToObject(
str: string,
splitChar: string = "&",
equalChar = "="
): {} {
let result: any = {};
if (!str) {
return result;
}
let arrs = str.split(splitChar);
for (let sub of arrs) {
let subArr = sub.split(equalChar);
result[subArr[0]] = subArr[1];
}
return result;
}

View File

@ -1,30 +0,0 @@
import crypto from 'crypto'
const apiKey = process.env.OKX_API_KEY
const projectId = process.env.OKX_PROJECT_ID
const pass = process.env.OKX_PASS
const secretKey = process.env.OKX_SECRET_KEY
export function prepareOkxReqCfg(config: any) {
let timestamp = new Date().toISOString()
let url = new URL(config.url)
let method = config.method.toUpperCase()
let signStr = timestamp + method + url.pathname
if (method === 'GET') {
signStr += url.search
} else if (method === 'POST') {
let bodyStr = JSON.stringify(JSON.parse(config.data))
signStr += bodyStr
}
const mac = crypto.createHmac('sha256', secretKey)
const sign = mac.update(signStr).digest('base64')
config.headers = {
'OK-ACCESS-PROJECT': projectId,
'OK-ACCESS-KEY': apiKey,
'OK-ACCESS-SIGN': sign,
'OK-ACCESS-TIMESTAMP': timestamp,
'OK-ACCESS-PASSPHRASE': pass,
'Content-Type': 'application/json',
}
return config
}

47
src/utils/promise.util.ts Normal file
View File

@ -0,0 +1,47 @@
/**
*
* @param {Function} cb
* @param {number} maxRetries
* @param {any[]} errorWhiteList
* @param {number} retries
* @return {Promise<T>}
*/
export function retry<T = any>(cb: Function, maxRetries: number = 3, errorWhiteList: any[] = [], retries: number = 0) {
return new Promise<T>((resolve, reject) => {
cb()
.then(resolve)
.catch(e => {
if (errorWhiteList.indexOf(e.constructor) !== -1 && retries++ < maxRetries) {
setTimeout(() => {
retry<T>(cb, maxRetries, errorWhiteList, retries)
.then(resolve)
.catch(e2 => reject(e2))
}, Math.floor(Math.random() * Math.pow(2, retries) * 400))
} else {
reject(e)
}
})
})
}
export class Deferred<T = any> {
public promise: Promise<T>
public resolve: Function
public reject: Function
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
public then(func: (value: T) => any) {
return this.promise.then.apply(this.promise, arguments)
}
public catch(func: (value: any) => any) {
return this.promise.catch(func)
}
}

View File

@ -0,0 +1,74 @@
import crypto from 'crypto'
import { compressUuid } from './string.util'
const ENCODER = 'base64'
const REG_KEY = /^[0-9a-fA-F]{63,64}$/
export function isEncrypt(msg: string) {
return !REG_KEY.test(msg)
}
export function aesEncrypt(text: string, password: string, iv: string) {
var md5 = crypto.createHash('md5')
const key = md5.update(password).digest('hex')
let cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
let encrypted = cipher.update(text, 'utf8', ENCODER)
encrypted += cipher.final(ENCODER)
return encrypted
}
export function aesDecrypt(encryptedText: string, password: string, iv: string) {
var md5 = crypto.createHash('md5')
const key = md5.update(password).digest('hex')
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let decrypted = decipher.update(encryptedText, ENCODER, 'utf8')
return decrypted + decipher.final('utf8')
}
export function hmacsha256(text: string, secret: string) {
const mac = crypto.createHmac('sha256', secret)
const data = mac.update(text).digest('hex').toLowerCase()
console.log(`HmacSHA256 rawContent is [${text}], key is [${secret}], hash result is [${data}]`)
return data
}
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 sha3_256(str: string) {
let hash = crypto.createHash('sha3-256')
hash.update(str)
return hash.digest('hex')
}
export function genRandomString(length: number) {
return crypto
.randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length)
}
export function uuid() {
return crypto.randomUUID()
}
export function shortUuid() {
let uid = uuid()
return compressUuid(uid)
}
export function md5(content: string) {
var md5 = crypto.createHash('md5')
return md5.update(content).digest('hex')
}
export function sha1(content: string) {
var md5 = crypto.createHash('sha1')
return md5.update(content).digest('hex')
}

141
src/utils/string.util.ts Normal file
View File

@ -0,0 +1,141 @@
/**
* key升序生成 key1=val1&key2=val2的字符串
* @param {object} data
* @param {boolean} ignoreNull (null值不参与拼接)
* @param splitChar , &
* @param equalChar =
*/
export function generateKeyValStr(data: Record<string, any>, ignoreNull = true, splitChar = '&', equalChar = '=') {
const keys = Object.keys(data)
keys.sort()
let result = ''
let i = 0
for (let key of keys) {
if (ignoreNull && !data[key]) {
return
}
if (i++ > 0) result += splitChar
result += `${key}${equalChar}${data[key]}`
}
return result
}
/**
* key1=val&key2=val的字符串组装成对象
* @param str key1=val&key2=val的字符串
* @param splitChar , &
* @param equalChar =
*/
export function keyValToObject(str: string, splitChar: string = '&', equalChar = '='): {} {
let result = {}
if (!str) {
return result
}
let arrs = str.split(splitChar)
for (let sub of arrs) {
let subArr = sub.split(equalChar)
result[subArr[0]] = subArr[1]
}
return result
}
/**
* true
* @param {Object} obj 'true','TRUE',1,'1','on','ON','YES','yes',true,false
* @return {boolean}
*/
export function isTrue(obj) {
return (
obj === 'true' ||
obj === 'TRUE' ||
obj === 'True' ||
obj === 'on' ||
obj === 'ON' ||
obj === true ||
obj === 1 ||
obj === '1' ||
obj === 'YES' ||
obj === 'yes'
)
}
/**
* ObjectId格式是否正确
* @param {string} id
* @return {boolean}
*/
export function isObjectId(id: string): boolean {
//mongoose.Types.ObjectId.isValid(id)
return /^[a-fA-F0-9]{24}$/.test(id)
}
/**
* 10 -> 62
* @param {string | number} number
* @return {string}
*/
export function string10to62(number: string | number) {
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('')
const radix = chars.length
let qutient = +number
const arr = []
do {
const mod = qutient % radix
qutient = (qutient - mod) / radix
arr.unshift(chars[mod])
} while (qutient)
return arr.join('')
}
/**
* 62 -> 10
* @param {string} numberCode
* @return {number}
*/
export function string62to10(numberCode: string) {
const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
const radix = chars.length
numberCode = numberCode + ''
const len = numberCode.length
let i = 0
let originNumber = 0
while (i < len) {
originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0)
}
return originNumber
}
const reNormalUUID = /^[0-9a-fA-F-]{36}$/
const reLongUUID = /^[0-9a-fA-F]{32}$/
const reShortUUID = /^[0-9a-zA-Z+/]{22,23}$/
const n = /-/g
export function compressUuid(e: string, t: boolean = false) {
if (reNormalUUID.test(e)) {
e = e.replace(n, '')
} else if (!reLongUUID.test(e)) {
return e
}
var r = !0 === t ? 2 : 5
return compressHex(e, r)
}
export function isUUID(uuid: string) {
return reNormalUUID.test(uuid)
}
const CHARS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
export function compressHex(e: string, r: number) {
var i,
n = e.length
i = void 0 !== r ? r : n % 3
for (var s = e.slice(0, i), o = []; i < n; ) {
var u = parseInt(e[i], 16),
a = parseInt(e[i + 1], 16),
c = parseInt(e[i + 2], 16)
o.push(CHARS_BASE64[(u << 2) | (a >> 2)])
o.push(CHARS_BASE64[((3 & a) << 4) | c])
i += 3
}
return s + o.join('')
}

View File

@ -4,7 +4,7 @@
"name": "wallet-svr",
"script": "npm",
"args": "run prod:api",
"cwd": "/home/kingsome/code/wallet-svr",
"cwd": "/data/apps/wallet-svr",
"max_memory_restart": "1024M",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"watch": true,
@ -15,7 +15,7 @@
"tasks"
],
"instances": 1,
"exec_mode": "fork",
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production"
},

View File

@ -1,27 +0,0 @@
{
"apps": [
{
"name": "wallet-svr-test",
"script": "npm",
"args": "run prod:api",
"cwd": "/home/kingsome/code/wallet-svr-test",
"max_memory_restart": "1024M",
"log_date_format" : "YYYY-MM-DD HH:mm Z",
"watch": true,
"ignore_watch": [
"node_modules",
"logs",
"fixtures",
"tasks"
],
"instances": 1,
"exec_mode": "fork",
"env": {
"NODE_ENV": "production"
},
"env_production": {
"NODE_ENV": "production"
}
}
]
}

2834
yarn.lock

File diff suppressed because it is too large Load Diff