增加relay相关接口
This commit is contained in:
parent
b6d58c4f5b
commit
9835e55675
@ -18,10 +18,12 @@
|
||||
"@fastify/helmet": "^10.0.1",
|
||||
"@fastify/jwt": "^6.3.2",
|
||||
"@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",
|
||||
@ -31,6 +33,7 @@
|
||||
"nanoid": "^3.1.23",
|
||||
"node-schedule": "^2.1.1",
|
||||
"rustwallet": "file:./rustwallet",
|
||||
"siwe": "^2.1.4",
|
||||
"tracer": "^1.1.6",
|
||||
"verify-apple-id-token": "^3.0.0"
|
||||
},
|
||||
|
@ -8,6 +8,7 @@ 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'
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
@ -89,6 +90,7 @@ export class ApiServer {
|
||||
|
||||
initSchedules() {
|
||||
new CodeTaskSchedule().scheduleAll()
|
||||
new NonceRecordSchedule().scheduleAll()
|
||||
new PriceSvr().scheduleAll()
|
||||
}
|
||||
|
||||
@ -118,13 +120,7 @@ export class ApiServer {
|
||||
})
|
||||
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)
|
||||
}
|
||||
logger.info(error)
|
||||
reply.code(200).send({
|
||||
errcode: statusCode,
|
||||
errmsg: error ? error.message : 'unknown error',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import fastify = require('fastify')
|
||||
|
||||
export const ROLE_ANON = 'anon'
|
||||
export const ROLE_SESSION = 'session'
|
||||
class BaseController {
|
||||
aotoRoute(req: fastify.FastifyRequest, res) {}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { Wallet } from 'modules/Wallet'
|
||||
import { IPlat } from 'plats/IPlat'
|
||||
import { PlatApple } from 'plats/PlatApple'
|
||||
import { PlatClient } from 'plats/PlatClient'
|
||||
import { PlatExternalWallet } from 'plats/PlatExternalWallet'
|
||||
import { PlatFacebook } from 'plats/PlatFacebook'
|
||||
import { PlatGoogle } from 'plats/PlatGoogle'
|
||||
import { PlatTikTok } from 'plats/PlatTikTok'
|
||||
@ -20,6 +21,9 @@ const plats: Map<PlatEnum, IPlat> = new Map([
|
||||
[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()],
|
||||
])
|
||||
|
||||
// 如果客户端有传入account, 则说明该次登录是绑定账号
|
||||
@ -108,6 +112,9 @@ class LoginController extends BaseController {
|
||||
}
|
||||
const user = await Account.insertOrUpdate({ plat: channel, openId }, data)
|
||||
const { unionAccount, walletUser } = await parseBindAccount(account, channel, user)
|
||||
if (plat.afterLogin) {
|
||||
await plat.afterLogin(user);
|
||||
}
|
||||
const ztoken = await res.jwtSign({
|
||||
id: walletUser.id,
|
||||
uid: unionAccount?.id || '',
|
||||
|
86
src/controllers/relay.controller.ts
Normal file
86
src/controllers/relay.controller.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import BaseController, {ROLE_ANON, ROLE_SESSION} from 'common/base.controller'
|
||||
import {ZError} from 'common/ZError'
|
||||
import { role, router } from 'decorators/router'
|
||||
import logger from 'logger/logger'
|
||||
import {RelayRecord, RelayStatusEnum} from 'modules/RelayRecord'
|
||||
import {RelaySession} from 'modules/RelaySession'
|
||||
import {checkPersionalSign} from 'utils/ether.util'
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
45
src/controllers/sign.controller.ts
Normal file
45
src/controllers/sign.controller.ts
Normal file
@ -0,0 +1,45 @@
|
||||
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 {DEFAULT_EXPIRED, NonceRecord} from 'modules/NonceRecord'
|
||||
import {SiweMessage} from 'siwe'
|
||||
import { checkParamsNeeded } from 'utils/net.util'
|
||||
|
||||
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)
|
||||
if (!message.nonce) {
|
||||
throw new ZError(11, 'Invalid nonce');
|
||||
}
|
||||
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: record.id });
|
||||
} catch (e) {
|
||||
throw new ZError(14, 'signature invalid')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
@ -8,4 +8,7 @@ export enum PlatEnum {
|
||||
EMAIL = 6,
|
||||
DISCORD = 7,
|
||||
CLIENT = 10,
|
||||
RELAY_WALLET = 11,
|
||||
WC = 12,
|
||||
EXTERNAL_WALLET = 13,
|
||||
}
|
||||
|
4
src/enums/RelayTypeEnum.ts
Normal file
4
src/enums/RelayTypeEnum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum RelayTypeEnum {
|
||||
TO_WALLET = 0,
|
||||
FROM_WALLET = 1
|
||||
}
|
@ -27,11 +27,11 @@ export function verifyPass(userpassword: string, passwordDb: string, salt: strin
|
||||
return passwordData.passwordHash === passwordDb
|
||||
}
|
||||
|
||||
interface AccountClass extends Base, TimeStamps {}
|
||||
export interface AccountClass extends Base, TimeStamps {}
|
||||
@dbconn()
|
||||
@index({ plat: 1, openId: 1 }, { unique: true })
|
||||
@modelOptions({ schemaOptions: { collection: 'account', timestamps: true }, options: { allowMixed: Severity.ALLOW } })
|
||||
class AccountClass extends BaseModule {
|
||||
export class AccountClass extends BaseModule {
|
||||
@prop({ enum: PlatEnum, default: PlatEnum.GOOGLE })
|
||||
public plat!: PlatEnum
|
||||
@prop({ required: true })
|
||||
|
21
src/modules/NonceRecord.ts
Normal file
21
src/modules/NonceRecord.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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'] })
|
||||
|
46
src/modules/RelayRecord.ts
Normal file
46
src/modules/RelayRecord.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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'] })
|
||||
|
30
src/modules/RelaySession.ts
Normal file
30
src/modules/RelaySession.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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,3 +1,6 @@
|
||||
import { DocumentType } from '@typegoose/typegoose'
|
||||
import { AccountClass } from 'modules/Account'
|
||||
export interface IPlat {
|
||||
verifyToken(req: any): Promise<any>
|
||||
afterLogin?(user: DocumentType<AccountClass>): Promise<any>
|
||||
}
|
||||
|
48
src/plats/PlatExternalWallet.ts
Normal file
48
src/plats/PlatExternalWallet.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { checkParamsNeeded } from 'utils/net.util'
|
||||
import { IPlat } from './IPlat'
|
||||
import { ZError } from 'common/ZError';
|
||||
import { NonceRecord } from 'modules/NonceRecord';
|
||||
import { SiweMessage } from 'siwe';
|
||||
import { DocumentType } from '@typegoose/typegoose'
|
||||
import { AccountClass } from 'modules/Account'
|
||||
import { Wallet } from 'modules/Wallet';
|
||||
|
||||
export class PlatExternalWallet implements IPlat {
|
||||
async verifyToken(req: any): Promise<any> {
|
||||
// here code is signature
|
||||
let { code, message } = req.params
|
||||
checkParamsNeeded(code, message);
|
||||
if (!message.nonce) {
|
||||
throw new ZError(11, 'Invalid nonce');
|
||||
}
|
||||
|
||||
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: record.id });
|
||||
} 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,6 +1,8 @@
|
||||
import {ROLE_ANON, ROLE_SESSION} from 'common/base.controller'
|
||||
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
import { Account } from 'modules/Account'
|
||||
import {RelaySession} from 'modules/RelaySession'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
@ -28,7 +30,7 @@ const apiAuthPlugin: FastifyPluginAsync<ApiAuthOptions> = async function (fastif
|
||||
})
|
||||
// 只有路由配置的role为anon才不需要过滤
|
||||
fastify.decorate('apiAuth', async function (request: FastifyRequest, reply: FastifyReply) {
|
||||
if (!request.roles || request.roles.indexOf('anon') == -1) {
|
||||
if (!(!!request.roles && (request.roles.indexOf(ROLE_ANON) > -1 || request.roles.indexOf(ROLE_SESSION) > -1))) {
|
||||
try {
|
||||
if (!request.token) {
|
||||
return reply.send({ errcode: 11, errmsg: 'need login' })
|
||||
@ -46,6 +48,27 @@ 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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
25
src/schedule/noncerecord.schedule.ts
Normal file
25
src/schedule/noncerecord.schedule.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { singleton } from 'decorators/singleton'
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
27
src/utils/ether.util.ts
Normal file
27
src/utils/ether.util.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {bytesToHex} from '@noble/hashes/utils'
|
||||
import {keccak_256} from '@noble/hashes/sha3'
|
||||
import {recoverPersonalSignature} from '@metamask/eth-sig-util';
|
||||
|
||||
export function toEIP55(address: string) {
|
||||
const lowerAddress = `${address}`.toLowerCase().replace('0x', '');
|
||||
var hash = bytesToHex(keccak_256(lowerAddress))
|
||||
var ret = '0x';
|
||||
for (var i = 0; i < lowerAddress.length; i++) {
|
||||
if (parseInt(hash[i], 16) >= 8) {
|
||||
ret += lowerAddress[i].toUpperCase();
|
||||
}
|
||||
else {
|
||||
ret += lowerAddress[i];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
export function checkPersionalSign(message: string, address: string, signature: string ) {
|
||||
if (!signature.startsWith('0x')) {
|
||||
signature = '0x' + signature
|
||||
}
|
||||
const recovered = recoverPersonalSignature({data: message, signature})
|
||||
return recovered === address
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { ZError } from "common/ZError"
|
||||
|
||||
const TIMEOUT_ERROR = new Error('timeout')
|
||||
|
||||
const hexRe = /^[0-9A-Fa-f]+$/gu
|
||||
@ -184,3 +186,9 @@ export function keyValToObject(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const checkParamsNeeded = (...args) => {
|
||||
args.forEach((arg) => {if (!arg) {
|
||||
throw new ZError(10, 'params mismatch');
|
||||
}});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user