Files
vega-cloud/src/middlewares/apiKeyAuth.ts
T

69 lines
2.1 KiB
TypeScript

import Elysia, { t } from 'elysia'
import { db, table as $t } from '~/db'
import { and, eq } from 'drizzle-orm'
import { apiAuth } from '~/config'
import merchantNonces from '~/db/schema/merchant_nonces'
import { verify } from '~/helpers/signature'
import { UnauthenticatedError } from '~/helpers/errors'
export const apiKeyAuth = new Elysia().macro({
verifyKey: {
headers: t.Object({
'x-api-key': t.String(),
'x-nonce': t.String(),
'x-signature': t.String(),
'x-timestamp': t.Number(),
}),
body: t.Unknown(),
async beforeHandle({ headers, body }) {
const apiKey = headers['x-api-key']
const nonce = headers['x-nonce']
const signature = headers['x-signature']
const timestamp = headers['x-timestamp']
if (!apiKey || !nonce || !signature || !timestamp) {
throw new UnauthenticatedError('Missing auth headers')
}
const tsNum = Number(timestamp)
if (!Number.isFinite(tsNum)) {
throw new UnauthenticatedError('Invalid timestamp')
}
const nowSec = Math.floor(Date.now() / 1000)
if (Math.abs(nowSec - tsNum) > apiAuth.skewSeconds) {
throw new UnauthenticatedError('Timestamp out of range')
}
const merchant = await db.query.merchants.findFirst({
where: (t, { and, eq }) => and(
eq(t.apiKey, apiKey),
eq(t.isActive, true),
),
})
if (!merchant) {
throw new UnauthenticatedError('Invalid API key')
}
if (!nonce) {
throw new UnauthenticatedError('Nonce is required')
}
const existingNonce = await db.$count($t.merchantNonces, and(
eq($t.merchantNonces.merchantId, merchant.merchantId),
eq($t.merchantNonces.nonce, nonce),
))
if (existingNonce > 0) {
throw new UnauthenticatedError('API replay detected')
}
db.insert(merchantNonces).values({
merchantId: merchant.merchantId,
nonce,
})
if (!verify(body, signature, merchant.secretKey)) {
throw new UnauthenticatedError('Invalid signature')
}
},
},
})