Compare commits
No commits in common. "master" and "passhash" have entirely different histories.
@ -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'
|
@ -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
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "packages/zutils"]
|
||||
path = packages/zutils
|
||||
url = git@git.kingsome.cn:zhanghongliang/zutils.git
|
@ -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
|
@ -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
|
@ -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,
|
||||
|
@ -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
107
src/common/AsyncQueue.ts
Normal 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()
|
@ -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
13
src/common/ZError.ts
Normal 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
|
||||
}
|
||||
}
|
7
src/common/base.controller.ts
Normal file
7
src/common/base.controller.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import fastify = require('fastify')
|
||||
|
||||
export const ROLE_ANON = 'anon'
|
||||
class BaseController {
|
||||
aotoRoute(req: fastify.FastifyRequest, res) {}
|
||||
}
|
||||
export default BaseController
|
181
src/controllers/alchemy.controller.ts
Normal file
181
src/controllers/alchemy.controller.ts
Normal 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
|
||||
}
|
||||
}
|
158
src/controllers/alchemyout.controller.ts
Normal file
158
src/controllers/alchemyout.controller.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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>) {
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
/**
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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.'
|
||||
|
||||
|
@ -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.'
|
||||
|
||||
|
@ -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 {}
|
||||
}
|
||||
|
||||
|
@ -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
142
src/decorators/router.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
29
src/decorators/singleton.ts
Normal file
29
src/decorators/singleton.ts
Normal 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]
|
||||
},
|
||||
})
|
@ -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,
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export enum RelayTypeEnum {
|
||||
TO_WALLET = 0,
|
||||
FROM_WALLET = 1,
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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']
|
||||
|
@ -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 },
|
||||
|
@ -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'] })
|
@ -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'] })
|
@ -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'] })
|
@ -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 })
|
@ -73,9 +73,6 @@ class TranRecordClass extends BaseModule {
|
||||
@prop()
|
||||
public confirmTime: number
|
||||
|
||||
@prop()
|
||||
public okxOrderId?: string
|
||||
|
||||
public toJson() {
|
||||
return {
|
||||
transactionHash: this.transactionHash,
|
||||
|
@ -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 })
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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: {} }
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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 () => {
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
@ -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'
|
||||
|
@ -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
186
src/utils/net.util.ts
Normal 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;
|
||||
}
|
@ -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
47
src/utils/promise.util.ts
Normal 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)
|
||||
}
|
||||
}
|
74
src/utils/security.util.ts
Normal file
74
src/utils/security.util.ts
Normal 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
141
src/utils/string.util.ts
Normal 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('')
|
||||
}
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user