project init
This commit is contained in:
commit
3621d8cc44
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.env.development
|
||||
.env.production
|
||||
.idea
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
.DS_Store
|
||||
tmp
|
||||
target
|
||||
boundle.log
|
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Api",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"dev:api"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
]
|
||||
}
|
54
package.json
Normal file
54
package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "oauth-svr",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"update": "yarn upgrade-interactive --latest",
|
||||
"dev:api2": "ts-node -r tsconfig-paths/register src/api.ts",
|
||||
"debug:api": "node --require ts-node/register --inspect src/api.ts",
|
||||
"dev:api": "npx ts-node src/api.ts",
|
||||
"build": "tsc",
|
||||
"prod:api": "NODE_ENV=production NODE_PATH=./dist node dist/api.js",
|
||||
"lint": "eslint --ext .ts src/**",
|
||||
"format": "eslint --ext .ts src/** --fix"
|
||||
},
|
||||
"author": "zhl",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.1.0",
|
||||
"@fastify/formbody": "^7.3.0",
|
||||
"@fastify/helmet": "^10.0.1",
|
||||
"@fastify/jwt": "^6.3.2",
|
||||
"@fastify/view": "^7.4.1",
|
||||
"axios": "^1.1.3",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.9",
|
||||
"fast-rbac": "^2.0.1",
|
||||
"fastify": "^4.8.1",
|
||||
"fastify-plugin": "^4.2.1",
|
||||
"google-auth-library": "^8.5.2",
|
||||
"mongoose": "^6.6.5",
|
||||
"mongoose-findorcreate": "^3.0.0",
|
||||
"nanoid": "^3.1.23",
|
||||
"node-schedule": "^2.1.1",
|
||||
"tracer": "^1.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typegoose/typegoose": "^9.12.1",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/node-schedule": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"node": "^18.14.0",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
176
src/api.server.ts
Normal file
176
src/api.server.ts
Normal file
@ -0,0 +1,176 @@
|
||||
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'
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
const zTokenParserPlugin = require('plugins/zTokenParser')
|
||||
|
||||
const apiAuthPlugin = require('plugins/apiauth')
|
||||
|
||||
const fs = require('fs')
|
||||
const join = require('path').join
|
||||
|
||||
require('./common/Extend')
|
||||
|
||||
export class ApiServer {
|
||||
server: FastifyInstance<Server, IncomingMessage, ServerResponse>
|
||||
|
||||
public constructor() {
|
||||
this.server = fastify({ logger: true, trustProxy: true })
|
||||
this.registerPlugins()
|
||||
console.log('version::' + process.version)
|
||||
}
|
||||
private registerPlugins() {
|
||||
this.server.register(require('@fastify/formbody'))
|
||||
this.server.register(zReqParserPlugin)
|
||||
this.server.register(helmet, { hidePoweredBy: false, contentSecurityPolicy: false })
|
||||
this.server.register(zTokenParserPlugin)
|
||||
|
||||
this.server.register(apiAuthPlugin, {
|
||||
secret: { private: config.api.token_secret_private, public: config.api.token_secret_public },
|
||||
expiresIn: config.api.token_expiresIn,
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.server.register(require('@fastify/cors'), {})
|
||||
}
|
||||
|
||||
this.server.register(require('@fastify/view'), {
|
||||
engine: {
|
||||
ejs: require('ejs'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private registerRouter() {
|
||||
logger.log('register api routers')
|
||||
let self = this
|
||||
for (let [controller, config] of RouterMap.decoratedRouters) {
|
||||
for (let data of config.data) {
|
||||
logger.info(
|
||||
'add router',
|
||||
data.method || 'all',
|
||||
data.path,
|
||||
`${data.target.constructor.name}.${controller.name}()`,
|
||||
)
|
||||
// @ts-ignore
|
||||
self.server[data.method || 'all'](
|
||||
data.path,
|
||||
{
|
||||
preValidation: async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
request.roles = config.roles
|
||||
await this.apiAuth(request, reply)
|
||||
},
|
||||
},
|
||||
controller,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 加载所有的controller
|
||||
*/
|
||||
initControllers() {
|
||||
logger.info('Bootstrap controllers...')
|
||||
const controllers = join(__dirname, './controllers')
|
||||
fs.readdirSync(controllers)
|
||||
.filter((file: string) => ~file.search(/^[^.].*\.(ts|js)$/))
|
||||
.forEach((file: any) => {
|
||||
// logger.log(file);
|
||||
return require(join(controllers, file))
|
||||
})
|
||||
}
|
||||
|
||||
initSchedules() {}
|
||||
|
||||
async connectDB() {
|
||||
const options: ConnectOptions = {
|
||||
minPoolSize: 5,
|
||||
maxPoolSize: 10,
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelay: 300000,
|
||||
}
|
||||
const uri = config.db_main
|
||||
logger.info(`connect to ${uri} ...`)
|
||||
try {
|
||||
// await mongoose.createConnection(uri, options)
|
||||
await mongoose.connect(uri, options)
|
||||
logger.log('DB Connected')
|
||||
} catch (err) {
|
||||
logger.log(`DB Connection Error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
private setErrHandler() {
|
||||
this.server.setNotFoundHandler(function (
|
||||
request: any,
|
||||
reply: { send: (arg0: { errcode: number; errmsg: string }) => void },
|
||||
) {
|
||||
reply.send({ errcode: 404, errmsg: 'page not found' })
|
||||
})
|
||||
this.server.setErrorHandler(function (error: FastifyError, request: FastifyRequest, reply: FastifyReply) {
|
||||
let statusCode = (error && error.statusCode) || 100
|
||||
if (statusCode >= 500) {
|
||||
logger.error(error)
|
||||
} else if (statusCode >= 400) {
|
||||
logger.info(error)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
reply.code(200).send({
|
||||
errcode: statusCode,
|
||||
errmsg: error ? error.message : 'unknown error',
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 格式化接口返回数据, 统一封装成如下格式
|
||||
* {
|
||||
* errcode: 0,
|
||||
* errmsg?: '',
|
||||
* data: any
|
||||
* }
|
||||
* @private
|
||||
*/
|
||||
private setFormatSend() {
|
||||
this.server.addHook('preSerialization', async (request: FastifyRequest, reply: FastifyReply, payload) => {
|
||||
reply.header('X-Powered-By', 'PHP/5.4.16')
|
||||
// @ts-ignore
|
||||
if (!payload.errcode) {
|
||||
// @ts-ignore
|
||||
if (payload.direct) {
|
||||
// @ts-ignore
|
||||
delete payload.direct
|
||||
return payload
|
||||
}
|
||||
payload = {
|
||||
errcode: 0,
|
||||
data: payload,
|
||||
}
|
||||
}
|
||||
return payload
|
||||
})
|
||||
}
|
||||
public async start() {
|
||||
let self = this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await self.connectDB()
|
||||
self.initControllers()
|
||||
self.registerRouter()
|
||||
self.setErrHandler()
|
||||
self.setFormatSend()
|
||||
self.initSchedules()
|
||||
this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => {
|
||||
if (err) {
|
||||
logger.log(err)
|
||||
process.exit(0)
|
||||
}
|
||||
resolve && resolve(address)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
12
src/api.ts
Normal file
12
src/api.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiServer } from './api.server'
|
||||
import logger from './logger/logger'
|
||||
|
||||
class Server extends ApiServer {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
||||
let server = new Server()
|
||||
server.start().then(address => {
|
||||
logger.log(`Api Server listening at ${address}`)
|
||||
})
|
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()
|
6
src/common/Debug.ts
Normal file
6
src/common/Debug.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import debug from 'debug'
|
||||
|
||||
debug.log = console.info.bind(console)
|
||||
|
||||
export const error = debug('chain:error')
|
||||
error.log = console.error.bind(console)
|
966
src/common/Extend.ts
Normal file
966
src/common/Extend.ts
Normal file
@ -0,0 +1,966 @@
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param value 要补0的数值
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
function zeroize(value: number | string, length: number = 2): string {
|
||||
let str = '' + value
|
||||
let zeros = ''
|
||||
for (let i = 0, len = length - str.length; i < len; i++) {
|
||||
zeros += '0'
|
||||
}
|
||||
return zeros + str
|
||||
}
|
||||
|
||||
/****************************************扩展Object****************************************/
|
||||
interface Object {
|
||||
/**
|
||||
* 返回一个浅副本的对象
|
||||
* 此对象会拷贝key value
|
||||
*
|
||||
* @memberOf Object
|
||||
*/
|
||||
clone?(): Object
|
||||
|
||||
/**
|
||||
* 将数据拷贝到 to
|
||||
* @param to 目标
|
||||
*/
|
||||
copyto?(to: Object): void
|
||||
|
||||
/**
|
||||
* 获取指定属性的描述,会查找当前数据和原型数据
|
||||
* @param property 指定的属性名字
|
||||
*/
|
||||
getPropertyDescriptor?(property: string): PropertyDescriptor
|
||||
|
||||
zssign?(target: any): any
|
||||
}
|
||||
|
||||
Object.defineProperties(Object.prototype, {
|
||||
clone: {
|
||||
value: function () {
|
||||
let o = {}
|
||||
for (let n in this) {
|
||||
// @ts-ignore
|
||||
o[n] = this[n]
|
||||
}
|
||||
return o
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
getPropertyDescriptor: {
|
||||
value: function (property: string): any {
|
||||
let data = Object.getOwnPropertyDescriptor(this, property)
|
||||
if (data) {
|
||||
return data
|
||||
}
|
||||
let prototype = Object.getPrototypeOf(this)
|
||||
if (prototype) {
|
||||
return prototype.getPropertyDescriptor(property)
|
||||
}
|
||||
return
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
copyto: {
|
||||
value: function (to: Object) {
|
||||
for (let p in this) {
|
||||
if (!(p in to)) {
|
||||
// 本身没有这个属性
|
||||
// @ts-ignore
|
||||
to[p] = this[p]
|
||||
} else {
|
||||
let data: PropertyDescriptor = to.getPropertyDescriptor(p)
|
||||
if (data) {
|
||||
if (data.set || data.writable) {
|
||||
// 可进行赋值
|
||||
// @ts-ignore
|
||||
to[p] = this[p]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
zssign: {
|
||||
value: function (target: Object) {
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert undefined or null to object')
|
||||
}
|
||||
|
||||
let output = Object(target)
|
||||
for (let nextKey in this) {
|
||||
if (!(nextKey in output)) {
|
||||
// 本身没有这个属性
|
||||
output[nextKey] = this[nextKey]
|
||||
}
|
||||
// else {
|
||||
// let data: PropertyDescriptor = output.getPropertyDescriptor(nextKey);
|
||||
// if (data) {
|
||||
// if (data.set || data.writable) {// 可进行赋值
|
||||
// output[nextKey] = this[nextKey];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
return output
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展Math****************************************/
|
||||
interface Math {
|
||||
/**
|
||||
* 角度转弧度的乘数
|
||||
* Math.PI / 180
|
||||
* @type {number}
|
||||
* @memberOf Math
|
||||
*/
|
||||
DEG_TO_RAD: number
|
||||
/**
|
||||
* 弧度转角度的乘数
|
||||
* 180 / Math.PI
|
||||
*/
|
||||
RAD_TO_DEG: number
|
||||
/**
|
||||
* 整圆的弧度
|
||||
*/
|
||||
PI2: number
|
||||
/**
|
||||
* 90°的弧度
|
||||
*
|
||||
* @type {number}
|
||||
* @memberOf Math
|
||||
*/
|
||||
PI_1_2: number
|
||||
|
||||
/**
|
||||
* 让数值处于指定的最大值和最小值之间,低于最小值取最小值,高于最大值取最大值
|
||||
* @param value 要处理的数值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
clamp?(value: number, min: number, max: number): number
|
||||
|
||||
/**
|
||||
* 从最小值到最大值之间随机[min,max)
|
||||
*/
|
||||
random2?(min: number, max: number): number
|
||||
}
|
||||
|
||||
Math.DEG_TO_RAD = Math.PI / 180
|
||||
|
||||
Math.RAD_TO_DEG = 180 / Math.PI
|
||||
|
||||
Math.PI2 = 2 * Math.PI
|
||||
|
||||
Math.PI_1_2 = Math.PI * 0.5
|
||||
|
||||
Math.clamp = (value, min, max) => {
|
||||
if (value < min) {
|
||||
value = min
|
||||
}
|
||||
if (value > max) {
|
||||
value = max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
Math.random2 = (min, max) => {
|
||||
return min + Math.random() * (max - min)
|
||||
}
|
||||
|
||||
/****************************************扩展Number********************************************/
|
||||
interface Number {
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?(length: number): string
|
||||
|
||||
/**
|
||||
* 数值介于,`min` `max`直接,包含min,max
|
||||
* 即:[min,max]
|
||||
*
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {boolean}
|
||||
*/
|
||||
between?(min: number, max: number): boolean
|
||||
}
|
||||
|
||||
Object.defineProperties(Number.prototype, {
|
||||
zeroize: {
|
||||
value: function (this: number, length: number) {
|
||||
return zeroize(this, length)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
between: {
|
||||
value: function (this: number, min: number, max: number) {
|
||||
return min <= this && max >= this
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展String****************************************/
|
||||
interface String {
|
||||
/**
|
||||
* 替换字符串中{0}{1}{2}{a} {b}这样的数据,用obj对应key替换,或者是数组中对应key的数据替换
|
||||
*/
|
||||
substitute?(args: any[]): string
|
||||
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?(length: number): string
|
||||
|
||||
/**
|
||||
* 将一个字符串转换成一个很小几率重复的数值
|
||||
* <font color="#ff0000">此方法hash的字符串并不一定唯一,慎用</font>
|
||||
*/
|
||||
hash?(): number
|
||||
|
||||
/**
|
||||
* 获取字符串长度,中文方块字符算两个长度
|
||||
*/
|
||||
trueLength?(): number
|
||||
|
||||
/**
|
||||
* 中文字符个数
|
||||
* */
|
||||
cnLength?(): number
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
* */
|
||||
versionCompare?(target: string): number
|
||||
}
|
||||
|
||||
Object.defineProperties(String.prototype, {
|
||||
zeroize: {
|
||||
value: function (length: number) {
|
||||
return zeroize(this, length)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
substitute: {
|
||||
value: function (this: string) {
|
||||
let len = arguments.length
|
||||
if (len > 0) {
|
||||
let obj: IArguments
|
||||
if (len == 1) {
|
||||
obj = arguments[0]
|
||||
if (typeof obj !== 'object') {
|
||||
obj = arguments
|
||||
}
|
||||
} else {
|
||||
obj = arguments
|
||||
}
|
||||
if (obj instanceof Object && !(obj instanceof RegExp)) {
|
||||
return this.replace(/\{(?:%([^{}]+)%)?([^{}]+)\}/g, function (match: string, handler: string, key: string) {
|
||||
//检查key中,是否为%开头,如果是,则尝试按方法处理
|
||||
// @ts-ignore
|
||||
let value = obj[key]
|
||||
if (handler) {
|
||||
//如果有处理器,拆分处理器
|
||||
let func = String.subHandler[handler]
|
||||
if (func) {
|
||||
value = func(value)
|
||||
}
|
||||
}
|
||||
return value !== undefined ? '' + value : match
|
||||
})
|
||||
}
|
||||
}
|
||||
return this.toString() //防止生成String对象,ios反射String对象会当成一个NSDictionary处理
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
hash: {
|
||||
value: function () {
|
||||
let len = this.length
|
||||
let hash = 5381
|
||||
for (let i = 0; i < len; i++) {
|
||||
hash += (hash << 5) + this.charCodeAt(i)
|
||||
}
|
||||
return hash & 0x7fffffff
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
trueLength: {
|
||||
value: function () {
|
||||
let arr: string[] = this.match(/[^x00-xff]/gi)
|
||||
return this.length + (arr ? arr.length : 0)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
cnLength: {
|
||||
value: function () {
|
||||
// /[\u2E80-\u9FBF]
|
||||
let arr: string[] = this.match(/[^x00-xff]/gi)
|
||||
return arr ? arr.length : 0
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
versionCompare: {
|
||||
value: function (target: string): number {
|
||||
const GTR = 1 //大于
|
||||
const LSS = -1 //小于
|
||||
const EQU = 0 //等于
|
||||
if (!target) {
|
||||
return GTR
|
||||
}
|
||||
let v1arr = String(this)
|
||||
.split('.')
|
||||
.map(function (a) {
|
||||
return parseInt(a)
|
||||
})
|
||||
let v2arr = String(target)
|
||||
.split('.')
|
||||
.map(function (a) {
|
||||
return parseInt(a)
|
||||
})
|
||||
let arrLen = Math.max(v1arr.length, v2arr.length)
|
||||
let result
|
||||
|
||||
//排除错误调用
|
||||
if (this == undefined || target == undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
//检查空字符串,任何非空字符串都大于空字符串
|
||||
if (this.length == 0 && target.length == 0) {
|
||||
return EQU
|
||||
} else if (this.length == 0) {
|
||||
return LSS
|
||||
} else if (target.length == 0) {
|
||||
return GTR
|
||||
}
|
||||
//循环比较版本号
|
||||
for (let i = 0; i < arrLen; i++) {
|
||||
result = versionComp(v1arr[i], v2arr[i])
|
||||
if (result == EQU) {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
function versionComp(n1: number, n2: number) {
|
||||
if (typeof n1 != 'number') {
|
||||
n1 = 0
|
||||
}
|
||||
if (typeof n2 != 'number') {
|
||||
n2 = 0
|
||||
}
|
||||
if (n1 > n2) {
|
||||
return GTR
|
||||
} else if (n1 < n2) {
|
||||
return LSS
|
||||
} else {
|
||||
return EQU
|
||||
}
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface StringConstructor {
|
||||
/**
|
||||
* 对数字进行补0操作
|
||||
* @param value 要补0的数值
|
||||
* @param length 要补的总长度
|
||||
* @return 补0之后的字符串
|
||||
*/
|
||||
zeroize?: (value: number, length: number) => string
|
||||
|
||||
/**
|
||||
* substitute的回调函数
|
||||
*
|
||||
* @type {Readonly<{ [index: string]: { (input: any): string } }>}
|
||||
* @memberOf StringConstructor
|
||||
*/
|
||||
subHandler?: Readonly<{ [index: string]: { (input: any): string } }>
|
||||
}
|
||||
|
||||
String.zeroize = zeroize
|
||||
|
||||
/****************************************扩展Date****************************************/
|
||||
|
||||
interface Date {
|
||||
/**
|
||||
* 格式化日期
|
||||
*
|
||||
* @param {string} mask 时间字符串
|
||||
* @param {boolean} [local] 是否基于本地时间显示,目前项目,除了报错信息,其他时间都用UTC时间显示
|
||||
* @returns {string} 格式化后的时间
|
||||
*/
|
||||
format(mask: string, local?: boolean): string
|
||||
|
||||
/**
|
||||
* 增加n天
|
||||
* @param {number} days
|
||||
* @return {Date}
|
||||
*/
|
||||
addDays(days: number): Date
|
||||
}
|
||||
|
||||
Object.defineProperties(Date.prototype, {
|
||||
format: {
|
||||
value: function (mask: string, local?: boolean) {
|
||||
let d: Date = this
|
||||
// @ts-ignore
|
||||
return mask.replace(/"[^"]*"|'[^']*'|(?:d{1,2}|m{1,2}|yy(?:yy)?|([hHMs])\1?)/g, function ($0: string) {
|
||||
switch ($0) {
|
||||
case 'd':
|
||||
return gd()
|
||||
case 'dd':
|
||||
return zeroize(gd())
|
||||
case 'M':
|
||||
return gM() + 1
|
||||
case 'MM':
|
||||
return zeroize(gM() + 1)
|
||||
case 'yy':
|
||||
return (gy() + '').substr(2)
|
||||
case 'yyyy':
|
||||
return gy()
|
||||
case 'h':
|
||||
return gH() % 12 || 12
|
||||
case 'hh':
|
||||
return zeroize(gH() % 12 || 12)
|
||||
case 'H':
|
||||
return gH()
|
||||
case 'HH':
|
||||
return zeroize(gH())
|
||||
case 'm':
|
||||
return gm()
|
||||
case 'mm':
|
||||
return zeroize(gm())
|
||||
case 's':
|
||||
return gs()
|
||||
case 'ss':
|
||||
return zeroize(gs())
|
||||
default:
|
||||
return $0.substr(1, $0.length - 2)
|
||||
}
|
||||
})
|
||||
|
||||
function gd() {
|
||||
return local ? d.getDate() : d.getUTCDate()
|
||||
}
|
||||
|
||||
function gM() {
|
||||
return local ? d.getMonth() : d.getUTCMonth()
|
||||
}
|
||||
|
||||
function gy() {
|
||||
return local ? d.getFullYear() : d.getUTCFullYear()
|
||||
}
|
||||
|
||||
function gH() {
|
||||
return local ? d.getHours() : d.getUTCHours()
|
||||
}
|
||||
|
||||
function gm() {
|
||||
return local ? d.getMinutes() : d.getUTCMinutes()
|
||||
}
|
||||
|
||||
function gs() {
|
||||
return local ? d.getSeconds() : d.getUTCSeconds()
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
addDays: {
|
||||
value: function (days: number) {
|
||||
this.setDate(this.getDate() + days)
|
||||
return this
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
/****************************************扩展Array****************************************/
|
||||
const enum ArraySort {
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
ASC = 0,
|
||||
/**
|
||||
* 降序
|
||||
*/
|
||||
DESC = 1,
|
||||
}
|
||||
|
||||
interface ArrayConstructor {
|
||||
// binaryInsert<T>(partArr: T[], item: T, filter: { (tester: T, ...args): boolean }, ...args);
|
||||
SORT_DEFAULT: { number: 0; string: ''; boolean: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于对Array排序时,处理undefined
|
||||
*/
|
||||
Array.SORT_DEFAULT = {
|
||||
number: 0,
|
||||
string: '',
|
||||
boolean: false,
|
||||
}
|
||||
Object.freeze(Array.SORT_DEFAULT)
|
||||
|
||||
interface Array<T> {
|
||||
/**
|
||||
* 如果数组中没有要放入的对象,则将对象放入数组
|
||||
*
|
||||
* @param {T} t 要放入的对象
|
||||
* @returns {number} 放入的对象,在数组中的索引
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
pushOnce?(t: T): number
|
||||
|
||||
/**
|
||||
*
|
||||
* 删除某个数据
|
||||
* @param {T} t
|
||||
* @returns {boolean} true 有这个数据并且删除成功
|
||||
* false 没有这个数据
|
||||
*/
|
||||
zremove?(t: T): boolean
|
||||
|
||||
/**
|
||||
* 排序 支持多重排序
|
||||
* 降序, 升序
|
||||
* @param {(keyof T)[]} kArr 参数属性列表
|
||||
* @param {(boolean[] | ArraySort[])} [dArr] 是否降序,默认升序
|
||||
* @returns {this}
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
multiSort?(kArr: (keyof T)[], dArr?: boolean[] | ArraySort[]): this
|
||||
|
||||
/**
|
||||
* 默认排序
|
||||
*
|
||||
* @param {string} [key]
|
||||
* @param {boolean} [descend]
|
||||
*
|
||||
* @member Array
|
||||
*/
|
||||
doSort?(key?: keyof T, descend?: boolean | ArraySort): this
|
||||
|
||||
doSort?(descend?: boolean | ArraySort, key?: keyof T): this
|
||||
|
||||
/**
|
||||
* 将数组克隆到to
|
||||
* to的数组长度会和当前数组一致
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<T>} to
|
||||
*/
|
||||
cloneTo?<T>(to: Array<T>): void
|
||||
|
||||
/**
|
||||
* 将数组附加到to中
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<T>} to
|
||||
*
|
||||
* @member ArrayConstructor
|
||||
*/
|
||||
appendTo?<T>(to: Array<T>): void
|
||||
|
||||
/**
|
||||
* 移除数组index位置的元素, 比slice效率高
|
||||
* @param index
|
||||
*/
|
||||
spliceOne?(index: number): boolean
|
||||
|
||||
/**
|
||||
* 随机排序
|
||||
*/
|
||||
randomSort?(): void
|
||||
|
||||
/**
|
||||
* 检查数组中是否含有另外一个object
|
||||
* @param obj 与数组同类型的obj | 同类型的数组 | 指定child字段的值 | 指定child字段的数组
|
||||
* @param child 比较字段
|
||||
*/
|
||||
contains?<T>(obj: T | T[] | {} | {}[], child?: string): boolean
|
||||
|
||||
/**
|
||||
* 将数组随机插入当前数组中
|
||||
* @param arr
|
||||
*/
|
||||
randomInsert?<T>(arr: Array<T>): void
|
||||
|
||||
/**
|
||||
* 随机获取n个元素
|
||||
* @param count
|
||||
*/
|
||||
randomGet?<T>(count?: number): T[]
|
||||
|
||||
/**
|
||||
* 随机获取1个元素
|
||||
*/
|
||||
randomOne?<T>(): T
|
||||
|
||||
/**
|
||||
* 随机移除n个元素
|
||||
* @param count
|
||||
*/
|
||||
randomRemove?<T>(count?: number): T[]
|
||||
|
||||
/**
|
||||
* 数组移动n位
|
||||
* @param n n > 0 右移, n<0 左移
|
||||
*/
|
||||
moveElement?<T>(n: number): T[]
|
||||
|
||||
/**
|
||||
* 两个数组并集
|
||||
* @param arr
|
||||
*/
|
||||
union?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 两个数组交集
|
||||
* @param arr
|
||||
*/
|
||||
intersect?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 相对于arr的差集
|
||||
* @param arr
|
||||
*/
|
||||
difference?<T>(arr: T[]): T[]
|
||||
|
||||
/**
|
||||
* 转换成Map
|
||||
* @param {string} key 用于生成map的key字段名
|
||||
* @return {Map<any, T>}
|
||||
*/
|
||||
toMap?<T>(key: string): Map<any, T>
|
||||
}
|
||||
|
||||
Object.defineProperties(Array.prototype, {
|
||||
cloneTo: {
|
||||
value: function <T>(this: T[], b: any[]) {
|
||||
b.length = this.length
|
||||
let len = this.length
|
||||
b.length = len
|
||||
for (let i = 0; i < len; i++) {
|
||||
b[i] = this[i]
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
appendTo: {
|
||||
value: function <T>(this: T[], b: any[]) {
|
||||
let len = this.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
b.push(this[i])
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
pushOnce: {
|
||||
value: function <T>(this: T[], t: T) {
|
||||
let idx = this.indexOf(t)
|
||||
if (!~idx) {
|
||||
idx = this.length
|
||||
this.push(t)
|
||||
}
|
||||
return idx
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
zremove: {
|
||||
value: function <T>(this: T[], t: T) {
|
||||
let idx = this.indexOf(t)
|
||||
if (~idx) {
|
||||
this.splice(idx, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
doSort: {
|
||||
value: function () {
|
||||
let key: string, descend: boolean
|
||||
let len = arguments.length
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
let arg = arguments[i]
|
||||
let t = typeof arg
|
||||
if (t === 'string') {
|
||||
key = arg
|
||||
} else {
|
||||
descend = !!arg
|
||||
}
|
||||
}
|
||||
if (key) {
|
||||
return this.sort((a: any, b: any) => (descend ? b[key] - a[key] : a[key] - b[key]))
|
||||
} else {
|
||||
return this.sort((a: any, b: any) => (descend ? b - a : a - b))
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
multiSort: {
|
||||
value: function (kArr: string[], dArr?: boolean[] | boolean) {
|
||||
let isArr = Array.isArray(dArr)
|
||||
return this.sort((a: any, b: any): number => {
|
||||
const def = Array.SORT_DEFAULT
|
||||
for (let idx = 0, len = kArr.length; idx < len; idx++) {
|
||||
let key = kArr[idx]
|
||||
// @ts-ignore
|
||||
let mode = isArr ? !!dArr[idx] : !!dArr
|
||||
let av = a[key]
|
||||
let bv = b[key]
|
||||
let typea = typeof av
|
||||
let typeb = typeof bv
|
||||
if (typea == 'object' || typeb == 'object') {
|
||||
return 0
|
||||
} else if (typea != typeb) {
|
||||
if (typea == 'undefined') {
|
||||
// @ts-ignore
|
||||
bv = def[typeb]
|
||||
} else if (typeb == 'undefined') {
|
||||
// @ts-ignore
|
||||
av = def[typea]
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if (av < bv) {
|
||||
return mode ? 1 : -1
|
||||
} else if (av > bv) {
|
||||
return mode ? -1 : 1
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
spliceOne: {
|
||||
value: function (index: number): boolean {
|
||||
if (index === -1 || index >= this.length) {
|
||||
return false
|
||||
}
|
||||
const len = this.length - 1
|
||||
for (let i = index; i < len; i++) {
|
||||
this[i] = this[i + 1]
|
||||
}
|
||||
this.length = len
|
||||
return true
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
randomSort: {
|
||||
value: function <T>() {
|
||||
for (let j, x, i = this.length; i; j = (Math.random() * i) | 0, x = this[--i], this[i] = this[j], this[j] = x) {}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
contains: {
|
||||
value: function <T>(obj: T | T[] | {} | {}[], child?: string): boolean {
|
||||
let result = false
|
||||
if (child) {
|
||||
const isArr = Array.isArray(obj)
|
||||
if (isArr) {
|
||||
// @ts-ignore
|
||||
if (obj[0].hasOwnProperty(child)) {
|
||||
let set0 = new Set()
|
||||
// @ts-ignore
|
||||
for (let s of obj) {
|
||||
// @ts-ignore
|
||||
set0.add(s[child])
|
||||
}
|
||||
// @ts-ignore
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return set0.size === set1.size
|
||||
} else {
|
||||
// @ts-ignore
|
||||
let set0 = new Set(obj)
|
||||
let set1 = new Set(this.filter((x: {}) => set0.has(x)))
|
||||
return set1.size === set0.size
|
||||
}
|
||||
} else {
|
||||
if (obj.hasOwnProperty(child)) {
|
||||
for (let sub of this) {
|
||||
if (sub.hasOwnProperty(child)) {
|
||||
// @ts-ignore
|
||||
if (sub[child] === obj[child]) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let sub of this) {
|
||||
if (sub.hasOwnProperty(child)) {
|
||||
// @ts-ignore
|
||||
if (sub[child] === obj) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不指定 比较字段 的话, 只处理2种情况
|
||||
// 1: obj 为数组
|
||||
// 2: obj 不是数组
|
||||
if (Array.isArray(obj)) {
|
||||
let set0 = new Set(obj)
|
||||
// @ts-ignore
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return set1.size === set0.size
|
||||
} else {
|
||||
let idx = this.indexOf(obj)
|
||||
return !!~idx
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomInsert: {
|
||||
value: function <T>(arr: Array<T>) {
|
||||
const length = this.length
|
||||
arr.forEach(value => {
|
||||
this.splice(Math.random() * length, 0, value)
|
||||
})
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomGet: {
|
||||
value: function <T>(count: number = 1): T[] {
|
||||
let shuffled: T[] = this.slice(0),
|
||||
i = this.length,
|
||||
min = i - count,
|
||||
temp,
|
||||
index
|
||||
if (min < 0) {
|
||||
return shuffled
|
||||
}
|
||||
while (i-- > min) {
|
||||
index = Math.floor((i + 1) * Math.random())
|
||||
temp = shuffled[index]
|
||||
shuffled[index] = shuffled[i]
|
||||
shuffled[i] = temp
|
||||
}
|
||||
return shuffled.slice(min)
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomOne: {
|
||||
value: function <T>(): T {
|
||||
let results = this.randomGet(1)
|
||||
if (results.length > 0) {
|
||||
return results[0]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
randomRemove: {
|
||||
value: function <T>(count: number = 1): T[] {
|
||||
let result = []
|
||||
while (count-- > 0 && this.length > 0) {
|
||||
let index = (Math.random() * this.length) | 0
|
||||
result.push(...this.splice(index, 1))
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
moveElement: {
|
||||
value: function <T>(n: number): T[] {
|
||||
if (Math.abs(n) > this.length) n = n % this.length
|
||||
return this.slice(-n).concat(this.slice(0, -n))
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
union: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let a = this.concat(b)
|
||||
return [...new Set(a)]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
intersect: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let set0 = new Set(b)
|
||||
let set1 = new Set(this.filter(x => set0.has(x)))
|
||||
return [...set1]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
difference: {
|
||||
value: function <T>(this: T[], b: any[]): T[] {
|
||||
let set0 = new Set(b)
|
||||
let set1 = new Set(this.filter(x => !set0.has(x)))
|
||||
return [...set1]
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
|
||||
toMap: {
|
||||
value: function <T>(this: T[], key: string) {
|
||||
let result: Map<any, T> = new Map()
|
||||
for (const o of this) {
|
||||
// @ts-ignore
|
||||
result.set(o[key], o)
|
||||
}
|
||||
return result
|
||||
},
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface Map<K, V> {
|
||||
/**
|
||||
* 只针对V为number的Map, 有值的话, 加上V, 没值则直接set
|
||||
* V为其他类型时, 直接set
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
inc?(key: K, value: V): number
|
||||
}
|
||||
|
||||
Object.defineProperties(Map.prototype, {
|
||||
inc: {
|
||||
value: function <K, V>(key: K, value: V) {
|
||||
if (typeof value == 'number') {
|
||||
this.set(key, (this.get(key) || 0) + value)
|
||||
} else {
|
||||
this.set(key, value)
|
||||
}
|
||||
return this.get(key)
|
||||
},
|
||||
},
|
||||
})
|
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
|
50
src/config/config.ts
Normal file
50
src/config/config.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import assert from "assert";
|
||||
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
dotenv.config();
|
||||
let path;
|
||||
switch (process.env.NODE_ENV) {
|
||||
case "test":
|
||||
path = `${__dirname}/../../.env.development`;
|
||||
break;
|
||||
case "production":
|
||||
path = `${__dirname}/../../.env.production`;
|
||||
break;
|
||||
default:
|
||||
path = `${__dirname}/../../.env.development`;
|
||||
}
|
||||
dotenv.config({ path: path, debug: NODE_ENV === "development" });
|
||||
|
||||
assert(
|
||||
process.env.API_TOKEN_SECRET_PRIVATE,
|
||||
"API_TOKEN_SECRET_PRIVATE not set"
|
||||
);
|
||||
assert(process.env.API_TOKEN_SECRET_PUBLIC, "API_TOKEN_SECRET_PUBLIC not set");
|
||||
assert(process.env.API_TOKEN_EXPIRESIN, "API_TOKEN_EXPIRESIN not set");
|
||||
|
||||
const privateKey = `
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
${process.env.API_TOKEN_SECRET_PRIVATE}
|
||||
-----END PRIVATE KEY-----
|
||||
`;
|
||||
const publicKey = `
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
${process.env.API_TOKEN_SECRET_PUBLIC}
|
||||
-----END PUBLIC KEY-----
|
||||
`;
|
||||
|
||||
let baseConfig = {
|
||||
api: {
|
||||
port: parseInt(process.env.API_PORT),
|
||||
host: process.env.API_HOST,
|
||||
token_secret_private: privateKey,
|
||||
token_secret_public: publicKey,
|
||||
token_expiresIn: process.env.API_TOKEN_EXPIRESIN,
|
||||
},
|
||||
|
||||
db_main: process.env.DB_MAIN,
|
||||
db_second: process.env.DB_SECOND,
|
||||
};
|
||||
|
||||
export default baseConfig;
|
2158
src/config/datas.ts
Normal file
2158
src/config/datas.ts
Normal file
File diff suppressed because it is too large
Load Diff
22
src/controllers/discord.controller.ts
Normal file
22
src/controllers/discord.controller.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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 { exchangeDiscrodCodeForToken, userInfo } from "services/discord.svr";
|
||||
|
||||
class DiscordController extends BaseController {
|
||||
@role(ROLE_ANON)
|
||||
@router("get /discord/redirect_uri")
|
||||
async discordCallback(req, res) {
|
||||
let { code } = req.params;
|
||||
logger.info("discord redirect: ", req.params);
|
||||
let access_token = "";
|
||||
if (code) {
|
||||
access_token = await exchangeDiscrodCodeForToken(code);
|
||||
let uinfo = await userInfo(access_token);
|
||||
return res.view("/templates/discord_redirect.ejs");
|
||||
} else {
|
||||
return res.view("/templates/discord_redirect.ejs");
|
||||
}
|
||||
}
|
||||
}
|
15
src/controllers/main.controller.ts
Normal file
15
src/controllers/main.controller.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import BaseController, { ROLE_ANON } from "common/base.controller";
|
||||
import { ZError } from "common/ZError";
|
||||
import { role, router } from "decorators/router";
|
||||
import logger from "logger/logger";
|
||||
|
||||
class MainController extends BaseController {
|
||||
/**
|
||||
* Refresh token
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router("post /open/api/v3/merchant/getToken")
|
||||
async getToken(req, res) {
|
||||
return {};
|
||||
}
|
||||
}
|
11
src/controllers/page.controller.ts
Normal file
11
src/controllers/page.controller.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import BaseController, { ROLE_ANON } from "common/base.controller";
|
||||
import { role, router } from "decorators/router";
|
||||
import { checkPageSign } from "services/alchemy.svr";
|
||||
|
||||
class PageController extends BaseController {
|
||||
@role(ROLE_ANON)
|
||||
@router("get /discord/redirect_page")
|
||||
async discordResirect(req, res) {
|
||||
return res.view("/templates/discord_redirect.ejs");
|
||||
}
|
||||
}
|
13
src/decorators/dbconn.ts
Normal file
13
src/decorators/dbconn.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { mongoose } from '@typegoose/typegoose'
|
||||
import config from 'config/config'
|
||||
/**
|
||||
* 为model指定数据库连接
|
||||
* @param {string} name 数据库连接名字, 在config中必须要有对应的配置, 比如main, 则必须要有 db_main
|
||||
* */
|
||||
export function dbconn(name?: string) {
|
||||
return target => {
|
||||
name = name || 'main'
|
||||
const url = config['db_' + name]
|
||||
target['db'] = mongoose.createConnection(url, {})
|
||||
}
|
||||
}
|
29
src/decorators/nojson.ts
Normal file
29
src/decorators/nojson.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
import { singleton } from './singleton'
|
||||
|
||||
@singleton
|
||||
export class NoJsonClass {
|
||||
private noJsonPropSet: Set<string> = new Set()
|
||||
|
||||
public addKey(className: string, propertyKey: string) {
|
||||
this.noJsonPropSet.add(className + '_' + propertyKey)
|
||||
}
|
||||
|
||||
public checkExist(className: string, propertyKey: string) {
|
||||
return this.noJsonPropSet.has(className + '_' + propertyKey)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 在不需要toJson方法输出的字段上加上 @noJson
|
||||
* @return {{(target: Function): void, (target: Object, propertyKey: (string | symbol)): void}}
|
||||
*/
|
||||
export function noJson() {
|
||||
// return Reflect.metadata(noJsonMetadataKey, !0)
|
||||
return function (target: Object, propertyKey: string) {
|
||||
new NoJsonClass().addKey(target.constructor.name, propertyKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function checkJson(target: any, propertyKey: string) {
|
||||
return !new NoJsonClass().checkExist(target.constructor.modelName, propertyKey)
|
||||
}
|
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]
|
||||
},
|
||||
})
|
3
src/logger/logger.ts
Normal file
3
src/logger/logger.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const level = process.env.NODE_ENV === 'production' ? 'info' : 'log'
|
||||
const logger = require('tracer').colorConsole({ dateformat: 'yyyy-mm-dd HH:MM:ss.L', level })
|
||||
export default logger
|
219
src/modules/Base.ts
Normal file
219
src/modules/Base.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import { FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses'
|
||||
import { checkJson, noJson } from '../decorators/nojson'
|
||||
import { plugin, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||
|
||||
// @ts-ignore
|
||||
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'
|
||||
|
||||
const jsonExcludeKeys = ['updatedAt', '__v']
|
||||
const saveExcludeKeys = ['createdAt', 'updatedAt', '__v', '_id']
|
||||
|
||||
@plugin(findOrCreate)
|
||||
export abstract class BaseModule extends FindOrCreate {
|
||||
static db: Connection
|
||||
|
||||
public updateFromReq(data: any) {
|
||||
for (let key in data) {
|
||||
if (saveExcludeKeys.indexOf(key) == -1) {
|
||||
this[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新
|
||||
* @param condition
|
||||
* @param data
|
||||
*/
|
||||
public static insertOrUpdate<T extends BaseModule>(
|
||||
this: ReturnModelType<AnyParamConstructor<T>>,
|
||||
condition: any,
|
||||
data: any,
|
||||
) {
|
||||
return this.findOneAndUpdate(condition, data, { upsert: true, new: true, setDefaultsOnInsert: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟删除
|
||||
* @param {string[]} ids
|
||||
*/
|
||||
public static deleteVirtual<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, ids: string[]) {
|
||||
return this.updateMany(
|
||||
// @ts-ignore
|
||||
{
|
||||
_id: { $in: ids },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
deleted: true,
|
||||
deleteTime: new Date(),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public static deleteRecords<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, params) {
|
||||
return this.updateMany(params, {
|
||||
$set: {
|
||||
deleted: true,
|
||||
deleteTime: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义分页查询
|
||||
* @param data
|
||||
* @param {boolean} json
|
||||
*/
|
||||
public static async pageQuery<T extends BaseModule>(
|
||||
this: ReturnModelType<AnyParamConstructor<T>>,
|
||||
data: any,
|
||||
options?: any,
|
||||
) {
|
||||
let { start, limit, page } = data
|
||||
limit = +limit || 20
|
||||
start = +start || (+page - 1) * limit || 0
|
||||
// @ts-ignore
|
||||
let { opt, sort } = this.parseQueryParam(data, options)
|
||||
let records = await this.find(opt).sort(sort).skip(start).limit(limit)
|
||||
let total = await this.countDocuments(opt)
|
||||
if (options?.json) {
|
||||
records = records.map((o: T) => o.toJson())
|
||||
}
|
||||
return { records, total, start, limit }
|
||||
}
|
||||
|
||||
public toJson() {
|
||||
let result: any = {}
|
||||
// @ts-ignore
|
||||
for (let key in this._doc) {
|
||||
if (checkJson(this, key + '') && jsonExcludeKeys.indexOf(key) == -1) {
|
||||
result[key] = this[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的查询条件拼接方法
|
||||
* @param {{}} params req.params
|
||||
* @param options
|
||||
* sort: 排序 比如: {createdAt: 1} 默认是 {_id: 1}
|
||||
* opt: 设置一些特殊的过滤条件, 比如{deleted: 0}
|
||||
* timeKey: 如果需要查询创建时间, 而且创建时间不为 createdAt, 可以用此字段设置
|
||||
* matchKey: 指定关键字查询的匹配字段, 可为string或[string]
|
||||
*
|
||||
* @return {{opt: any, sort: {_id: number}}}
|
||||
*/
|
||||
public static parseQueryParam(params: {}, options?: any) {
|
||||
const opt: any = { deleted: { $ne: true } }
|
||||
// @ts-ignore
|
||||
let obj = this.schema.paths
|
||||
for (let key in params) {
|
||||
if (key !== 'sort' && obj.hasOwnProperty(key)) {
|
||||
switch (obj[key].instance) {
|
||||
case 'String':
|
||||
if (typeof params[key] === 'object') {
|
||||
opt[key] = params[key]
|
||||
} else {
|
||||
opt[key] = { $regex: params[key], $options: 'i' }
|
||||
}
|
||||
break
|
||||
case 'Number':
|
||||
opt[key] = params[key]
|
||||
break
|
||||
case 'Array':
|
||||
if (Array.isArray(params[key])) {
|
||||
opt[key] = { $in: params[key] }
|
||||
} else {
|
||||
opt[key] = params[key]
|
||||
}
|
||||
break
|
||||
case 'Date':
|
||||
// TODO:
|
||||
break
|
||||
case 'Boolean':
|
||||
opt[key] = isTrue(params[key])
|
||||
break
|
||||
case 'ObjectID':
|
||||
if (/^[0-9a-fA-F]{24}$/.test(params[key])) {
|
||||
opt[key] = new ObjectId(params[key])
|
||||
}
|
||||
break
|
||||
}
|
||||
} else if (key === '$or' || key == '$and') {
|
||||
opt[key] = params[key]
|
||||
} else if (key.indexOf('.') > 0) {
|
||||
// 如果.在第一个字符, 明显是有问题的, 不需要处理这种
|
||||
opt[key] = params[key]
|
||||
}
|
||||
}
|
||||
if (params.hasOwnProperty('key') && params['key']) {
|
||||
let orArr = []
|
||||
if (options?.matchKey) {
|
||||
if (Array.isArray(options?.matchKey)) {
|
||||
for (let key in options?.matchKey) {
|
||||
let _tmp = {}
|
||||
_tmp[key] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
} else {
|
||||
let _tmp = {}
|
||||
_tmp[options.matchKey] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
} else {
|
||||
for (let key in obj) {
|
||||
if (obj[key].instance === 'String') {
|
||||
let _tmp = {}
|
||||
_tmp[key] = { $regex: params['key'], $options: 'i' }
|
||||
orArr.push(_tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(opt, { $or: orArr })
|
||||
}
|
||||
let timeKey = options?.timeKey ? options.timeKey : 'createdAt'
|
||||
if (params.hasOwnProperty('timeBegin') && !params.hasOwnProperty('timeEnd')) {
|
||||
let timeBegin = params['timeBegin']
|
||||
if (!(timeBegin instanceof Date)) {
|
||||
timeBegin = new Date(parseInt(timeBegin))
|
||||
}
|
||||
opt[timeKey] = { $gte: timeBegin }
|
||||
} else if (params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) {
|
||||
let timeBegin = params['timeBegin']
|
||||
if (!(timeBegin instanceof Date)) {
|
||||
timeBegin = new Date(parseInt(timeBegin))
|
||||
}
|
||||
let timeEnd = params['timeEnd']
|
||||
if (!(timeEnd instanceof Date)) {
|
||||
timeEnd = new Date(parseInt(timeEnd))
|
||||
}
|
||||
let tmpB = {}
|
||||
tmpB[timeKey] = { $gte: timeBegin }
|
||||
let tmpE = {}
|
||||
tmpE[timeKey] = { $lte: timeEnd }
|
||||
opt['$and'] = [tmpB, tmpE]
|
||||
} else if (!params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) {
|
||||
let timeEnd = params['timeEnd']
|
||||
if (!(timeEnd instanceof Date)) {
|
||||
timeEnd = new Date(parseInt(timeEnd))
|
||||
}
|
||||
opt[timeKey] = { $lte: timeEnd }
|
||||
}
|
||||
if (options?.opt) {
|
||||
Object.assign(opt, options.opt)
|
||||
}
|
||||
let sort = { _id: -1 }
|
||||
if (params.hasOwnProperty('sort')) {
|
||||
sort = params['sort']
|
||||
}
|
||||
return { opt, sort }
|
||||
}
|
||||
}
|
96
src/modules/PayRecord.ts
Normal file
96
src/modules/PayRecord.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
export enum PayType {
|
||||
BUY = 1,
|
||||
SELL = 2,
|
||||
}
|
||||
|
||||
export enum PayStatus {
|
||||
PENDING = 0,
|
||||
TRANSFERING = 1,
|
||||
TRANSFERED = 2, //只有国库模式才会有该状态
|
||||
SUCCESS = 9,
|
||||
TRANSFER_FAIL = 98, // 转账错误
|
||||
FAIL = 99,
|
||||
}
|
||||
|
||||
@dbconn()
|
||||
@index({ merchantOrderNo: 1 }, { unique: true, partialFilterExpression: { outOrderId: { $exists: true } } })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'pay_record', timestamps: true },
|
||||
options: { allowMixed: Severity.ALLOW },
|
||||
})
|
||||
export class PayRecordClass extends BaseModule {
|
||||
@prop({ required: true, default: PayType.BUY })
|
||||
public type: PayType
|
||||
|
||||
@prop()
|
||||
public address: string
|
||||
|
||||
@prop()
|
||||
public network?: string
|
||||
|
||||
@prop()
|
||||
public crypto?: string
|
||||
|
||||
// 法币
|
||||
@prop()
|
||||
public fiat?: string
|
||||
// 法币数量
|
||||
@prop()
|
||||
public fiatAmount?: string
|
||||
|
||||
@prop()
|
||||
public processFee?: string
|
||||
|
||||
@prop()
|
||||
public networkFee?: string
|
||||
|
||||
// 加密货币数量
|
||||
@prop()
|
||||
public cryptoAmount?: string
|
||||
|
||||
// 加密货币价格
|
||||
@prop()
|
||||
public cryptoPrice?: string
|
||||
// 该笔交易渠道会给我们多少usdt
|
||||
@prop()
|
||||
public usdtAmount?: string
|
||||
// 国家
|
||||
@prop()
|
||||
public country?: string
|
||||
|
||||
@prop({ required: true, default: PayStatus.PENDING })
|
||||
public status: PayStatus
|
||||
// 渠道返回的原始资料
|
||||
@prop({ type: mongoose.Schema.Types.Mixed })
|
||||
public outData: any
|
||||
|
||||
// 商户订单id
|
||||
@prop()
|
||||
public merchantOrderNo: string
|
||||
|
||||
@prop()
|
||||
public email: string
|
||||
|
||||
@prop()
|
||||
public callbackUrl: string
|
||||
|
||||
@prop()
|
||||
public merchantName: string
|
||||
|
||||
// 交易的txHash
|
||||
@prop()
|
||||
public txHash?: string
|
||||
|
||||
@prop({ default: 0 })
|
||||
public version: number
|
||||
|
||||
public static async findByRecordId(this: ReturnModelType<typeof PayRecordClass>, merchantOrderNo: string) {
|
||||
return this.findOne({ merchantOrderNo }).exec()
|
||||
}
|
||||
}
|
||||
|
||||
export const PayRecord = getModelForClass(PayRecordClass, { existingConnection: PayRecordClass.db })
|
47
src/plugins/apiauth.ts
Normal file
47
src/plugins/apiauth.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
roles?: string[]
|
||||
user?: any
|
||||
token?: string
|
||||
}
|
||||
interface FastifyInstance {
|
||||
apiAuth: (request: FastifyRequest, reply: FastifyReply) => {}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiAuthOptions {
|
||||
secret: {
|
||||
private: string
|
||||
public: string
|
||||
}
|
||||
expiresIn: string
|
||||
}
|
||||
|
||||
const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastify, opts) {
|
||||
fastify.register(require('@fastify/jwt'), {
|
||||
secret: opts.secret,
|
||||
sign: { expiresIn: opts.expiresIn, algorithm: 'EdDSA' },
|
||||
})
|
||||
// 只有路由配置的role为anon才不需要过滤
|
||||
fastify.decorate('apiAuth', async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
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)
|
||||
// if (!data || !data.id) {
|
||||
// return reply.send({ errcode: 10, errmsg: 'need login' })
|
||||
// }
|
||||
// } catch (err) {
|
||||
// return reply.send({ errcode: 401, errmsg: 'need auth' })
|
||||
// }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default fastifyPlugin(apiAuthPlugin, '4.x')
|
37
src/plugins/zReqParser.ts
Normal file
37
src/plugins/zReqParser.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
FastifyInstance,
|
||||
FastifyPluginAsync,
|
||||
FastifyReply,
|
||||
FastifyRequest,
|
||||
} from "fastify";
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
|
||||
/**
|
||||
* 将post 和 get 的参数统一到 req.params
|
||||
*/
|
||||
declare module "fastify" {
|
||||
interface FastifyInstance {
|
||||
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);
|
||||
}
|
||||
if (request.body) {
|
||||
Object.assign(params, request.body);
|
||||
}
|
||||
request.params = params;
|
||||
}
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
export default fastifyPlugin(zReqParserPlugin, "4.x");
|
73
src/plugins/zTokenParser.ts
Normal file
73
src/plugins/zTokenParser.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
FastifyInstance,
|
||||
FastifyPluginAsync,
|
||||
FastifyReply,
|
||||
FastifyRequest,
|
||||
} from "fastify";
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
|
||||
const getTokenFromHeader = function (request) {
|
||||
let token: string | undefined;
|
||||
if (request.headers && request.headers.authorization) {
|
||||
const parts = request.headers.authorization.split(" ");
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
token = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return token;
|
||||
};
|
||||
const getTokenFromCookie = function (request) {
|
||||
let token: string | undefined;
|
||||
if (request.cookies) {
|
||||
if (request.cookies["token"]) {
|
||||
token = request.cookies["token"];
|
||||
}
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const getTokenFromParams = function (request) {
|
||||
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;
|
||||
};
|
||||
|
||||
const getTokenFromBody = function (request) {
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* 依次从request的header, cookie, params, query和body中获取token, 加入到request.token中
|
||||
* header中的字段key为authorization, 格式为 Bearer xxxx
|
||||
* 其他位置的key都为 token
|
||||
*/
|
||||
|
||||
export default fastifyPlugin(zTokenParserPlugin, "4.x");
|
26
src/plugins/zrbac.ts
Normal file
26
src/plugins/zrbac.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
import RBAC from 'fast-rbac'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
/**
|
||||
* RBAC interface
|
||||
*/
|
||||
rbac: RBAC
|
||||
}
|
||||
}
|
||||
|
||||
const zRBACPlugin: FastifyPluginAsync = async function fastifyMetrics(
|
||||
fastify: FastifyInstance,
|
||||
options?: RBAC.Options,
|
||||
) {
|
||||
const rbac = new RBAC(options)
|
||||
fastify.decorate('rbac', rbac)
|
||||
return
|
||||
}
|
||||
|
||||
export = fastifyPlugin(zRBACPlugin, {
|
||||
fastify: '>=3.0.0',
|
||||
name: 'zrbac',
|
||||
})
|
114
src/services/alchemy.svr.ts
Normal file
114
src/services/alchemy.svr.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import axios from 'axios'
|
||||
import { hmacsha256, sha1 } from 'utils/security.util'
|
||||
import crypto from 'crypto'
|
||||
import { generateKVStr } from 'utils/net.util'
|
||||
import logger from 'logger/logger'
|
||||
|
||||
export function createSimpleSign(data: any) {
|
||||
let timestamp = Date.now()
|
||||
let appid = process.env.ALCHEMY_APPID
|
||||
let secret = process.env.ALCHEMY_APP_SECRET
|
||||
let signData = { appid, timestamp }
|
||||
signData = Object.assign(signData, data)
|
||||
let signStr = Object.keys(signData)
|
||||
.sort()
|
||||
.map(key => `${key}=${signData[key]}`)
|
||||
.join('&')
|
||||
let sign = hmacsha256(signStr, secret)
|
||||
return {
|
||||
appid,
|
||||
timestamp,
|
||||
sign,
|
||||
}
|
||||
}
|
||||
|
||||
export function createSha1Sign() {
|
||||
let timestamp = Date.now()
|
||||
let appid = process.env.ALCHEMY_APPID
|
||||
let secret = process.env.ALCHEMY_APP_SECRET
|
||||
let sign = sha1(appid + secret + timestamp)
|
||||
return {
|
||||
appid,
|
||||
timestamp,
|
||||
sign,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the pay result sign is valid
|
||||
* @param data - the data to be checked
|
||||
* @returns true if the sign is valid, false otherwise
|
||||
*/
|
||||
export function checkPayResultSign(data: any) {
|
||||
const { appId, orderNo, crypto, network, address, signature } = data
|
||||
const sign = hmacsha256(appId + orderNo + crypto + network + address, process.env.ALCHEMY_APP_SECRET)
|
||||
return sign === signature
|
||||
}
|
||||
|
||||
export function createPayResultSign(data: any) {
|
||||
const { appId, orderNo, crypto, network, address } = data
|
||||
const sign = hmacsha256(appId + orderNo + crypto + network + address, process.env.ALCHEMY_APP_SECRET)
|
||||
return sign
|
||||
}
|
||||
|
||||
export function checkSimpleSign(headers: any, data: any) {
|
||||
// alchemy 很不严谨, 有时候是 appid, 有时候是 appId
|
||||
const { appid, appId, timestamp, sign } = headers
|
||||
let appIdToCheck = appId || appid
|
||||
let signData = { appid: appIdToCheck, timestamp }
|
||||
signData = Object.assign(signData, data)
|
||||
let signStr = Object.keys(signData)
|
||||
.sort()
|
||||
.map(key => `${key}=${signData[key]}`)
|
||||
.join('&')
|
||||
const expectedSign = hmacsha256(signStr, process.env.ALCHEMY_APP_SECRET)
|
||||
// const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp)
|
||||
return sign === expectedSign
|
||||
}
|
||||
|
||||
export function checkSha1Sign(headers: any) {
|
||||
const { appid, appId, timestamp, sign } = headers
|
||||
let appIdToCheck = appId || appid
|
||||
const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp)
|
||||
return sign === expectedSign
|
||||
}
|
||||
|
||||
/**
|
||||
* Create page sign
|
||||
* @param plainText - plain text to be encrypted
|
||||
* @returns encrypted text
|
||||
*/
|
||||
export function createPageSign(plainText: string) {
|
||||
let secret = process.env.ALCHEMY_APP_SECRET
|
||||
try {
|
||||
const plainTextData = Buffer.from(plainText, 'utf8')
|
||||
const secretKey = Buffer.from(secret, 'utf8')
|
||||
const iv = secret.substring(0, 16)
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv)
|
||||
|
||||
let encrypted = cipher.update(plainTextData)
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()])
|
||||
|
||||
return encrypted.toString('base64')
|
||||
} catch (e) {
|
||||
console.log(`AES encrypting exception, msg is ${e.toString()}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function checkPageSign(params: any) {
|
||||
const keySet = new Set(['appId', 'address', 'callbackUrl'])
|
||||
let signData = {}
|
||||
for (let k in params) {
|
||||
if (keySet.has(k)) {
|
||||
signData[k] = params[k]
|
||||
}
|
||||
}
|
||||
|
||||
let signStr = generateKVStr({ data: signData, sort: true })
|
||||
let expectedSign = createPageSign(signStr)
|
||||
logger.info(`signStr is ${signStr}, expectedSign is ${expectedSign}, origin sign is ${params.sign}`)
|
||||
let { sign } = params
|
||||
return sign === expectedSign
|
||||
}
|
36
src/services/discord.svr.ts
Normal file
36
src/services/discord.svr.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export async function exchangeDiscrodCodeForToken(code: string) {
|
||||
const clientId = process.env.DISCORD_CLIENT_ID;
|
||||
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
|
||||
const redirectUri = "http://localhost:3010/discord/redirect_uri";
|
||||
|
||||
const response = await fetch("https://discord.com/api/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",
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
export async function userInfo(token: string) {
|
||||
const response = await fetch("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
return data;
|
||||
}
|
45
src/services/merchant.svr.ts
Normal file
45
src/services/merchant.svr.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { createSimpleSign } from './alchemy.svr'
|
||||
|
||||
const MECHANT_API_BASE = process.env.MERCHANT_API_BASE_URL
|
||||
/**
|
||||
* 购买回调
|
||||
*/
|
||||
export async function byCb(data: any) {
|
||||
let url = MECHANT_API_BASE + '/pay/out/alchemy/buycb'
|
||||
const { appid, timestamp, sign } = createSimpleSign(data)
|
||||
let res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', appId: appid, timestamp: timestamp + '', sign: sign },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
let result = await res.json()
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 向商户查询币价
|
||||
*/
|
||||
export async function queryCryptoPrice(crypto: string) {
|
||||
let url = `${MECHANT_API_BASE}/pay/out/alchemy/queryprice?crypto=${crypto}`
|
||||
const { appid, timestamp, sign } = createSimpleSign({ crypto })
|
||||
let res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json', appId: appid, timestamp: timestamp + '', sign: sign },
|
||||
})
|
||||
let result = await res.json()
|
||||
let resdata = Object.assign(result.data, { process: 5 })
|
||||
return resdata
|
||||
}
|
||||
/**
|
||||
* 通知商户打币
|
||||
*/
|
||||
export async function notifyMechant(data: any) {
|
||||
let url = MECHANT_API_BASE + '/pay/out/alchemy/distribute'
|
||||
const { appid, timestamp, sign } = createSimpleSign(data)
|
||||
let res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', appId: appid, timestamp: timestamp + '', sign: sign },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
// let result = await res.json()
|
||||
return res.json()
|
||||
}
|
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;
|
||||
}
|
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')
|
||||
}
|
136
src/utils/string.util.ts
Normal file
136
src/utils/string.util.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 根据key升序生成 key1=val1&key2=val2的字符串
|
||||
* @param {object} data 需要处理的对象
|
||||
* @param {boolean} ignoreNull 是否过滤空值(空格或者null值不参与拼接)
|
||||
* @param splitChar 连接的字符, 默认是&
|
||||
* @param equalChar =
|
||||
*/
|
||||
export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string = '&', 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)
|
||||
}
|
||||
|
||||
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('')
|
||||
}
|
22
templates/discord_redirect.ejs
Normal file
22
templates/discord_redirect.ejs
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Discord Redirect</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
.form-label i {
|
||||
color: green;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function() {
|
||||
window.close();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
2
templates/error.ejs
Normal file
2
templates/error.ejs
Normal file
@ -0,0 +1,2 @@
|
||||
<h1>Pay error</h1>
|
||||
<p><%= msg %></p>
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"ts-node": {
|
||||
"files": true,
|
||||
"require": ["tsconfig-paths/register"]
|
||||
},
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es2018",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./src",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"lib": ["es2018"],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"typings/extend.d.ts"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user