Skip to content
Merged

Main #64

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions apis/enterprise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use strict'
const express = require('express')
const router = express.Router()
const crypto = require('crypto')
const config = require('config')
const db = require('../models/mongodb')
const axios = require('axios')
const FormData = require('form-data')

const IPFS_API_ADD_URL = 'https://ipfs.xinfin.network/api/v0/add'

function addFileToXinfinIpfs (buffer, filename, callback) {
const form = new FormData()
form.append('file', buffer, {
filename: filename || 'kyc.pdf',
contentType: 'application/pdf',
knownLength: buffer.length
})

axios.post(IPFS_API_ADD_URL, form, {
headers: form.getHeaders(),
maxBodyLength: Infinity,
maxContentLength: Infinity,
timeout: 10000
}).then((response) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const hash = response.data && response.data.Hash
if (!hash) {
return callback(new Error('IPFS API did not return a hash'))
}
callback(null, [{ hash: hash }])
}).catch(callback)
}

function unauthorized (res, reason) {
return res.status(401).json({
message: 'Unauthorized',
reason: reason
})
}

// Manually create API key and secret for an enterprise
router.post('/keys', async (req, res) => {
try {
const authHeader = req.headers['authorization']
const expectedToken = config.get('enterpriseMasterToken')

if (!authHeader || authHeader !== `Bearer ${expectedToken}`) {
return unauthorized(res, 'invalid_master_token')
}

const enterpriseName = req.body.enterpriseName
if (!enterpriseName) {
return res.status(400).json({ message: 'enterpriseName is required' })
}

const apiKey = crypto.randomBytes(16).toString('hex')
const apiSecret = crypto.randomBytes(32).toString('hex')

await db.EnterpriseKey.create({
apiKey,
apiSecret,
enterpriseName
})

res.status(201).json({
message: 'Enterprise API credentials generated successfully. Save the apiSecret as it will not be shown again.',
enterpriseName,
apiKey,
apiSecret
})
} catch (err) {
console.error('Error creating enterprise keys:', err)
res.status(500).send('Internal Server Error')
}
})

// Enterprise addKYC endpoint
router.post('/addKYC', async (req, res) => {
try {
const apiKey = req.headers['x-api-key']
const apiTimestamp = req.headers['x-api-timestamp']
const apiSignature = req.headers['x-api-signature']
const apiNonce = req.headers['x-api-nonce']

if (!apiKey || !apiTimestamp || !apiSignature || !apiNonce) {
return unauthorized(res, 'missing_auth_headers')
}

// Validate timestamp (5 minutes window)
const requestTime = parseInt(apiTimestamp, 10)
const currentTime = Date.now()
const fiveMinutes = 5 * 60 * 1000

if (isNaN(requestTime) || Math.abs(currentTime - requestTime) > fiveMinutes) {
return unauthorized(res, 'timestamp_expired')
}

// Check if nonce has already been used to prevent replay
const nonceExists = await db.EnterpriseNonce.findOne({ apiKey, nonce: apiNonce })
if (nonceExists) {
return unauthorized(res, 'nonce_reused')
}

// Validate file presence
if (!req.files || !req.files.filename) {
return res.status(400).json({ message: 'No file uploaded' })
}

const uploadedFile = req.files.filename

// Strict PDF checks: mimetype AND filename AND magic bytes
const isPdfMagic = uploadedFile.data && uploadedFile.data.length >= 5 &&
uploadedFile.data.toString('ascii', 0, 5) === '%PDF-'

if (uploadedFile.mimetype !== 'application/pdf' || !uploadedFile.name.toLowerCase().endsWith('.pdf') || !isPdfMagic) {
return res.status(400).json({ message: 'Only PDF files are allowed' })
}

// 10MB validation
const maxSize = 10 * 1024 * 1024
if (uploadedFile.size > maxSize) {
return res.status(400).json({ message: 'File size should not exceed 10MB' })
}

// Fetch enterprise key and select select: false fields (apiSecret)
const enterprise = await db.EnterpriseKey.findOne({ apiKey }).select('+apiSecret')
if (!enterprise) {
return unauthorized(res, 'invalid_api_key')
}

// Hash of file content
const fileHash = crypto.createHash('sha256').update(uploadedFile.data).digest('hex')

// Validate signature
// We expect signature to be HMAC SHA256 of "METHOD:PATH:TIMESTAMP:NONCE:FILENAME:FILESIZE:FILEHASH"
const method = req.method.toUpperCase()
const path = req.originalUrl.split('?')[0] // Strip query params just in case
const stringToSign = `${method}:${path}:${apiTimestamp}:${apiNonce}:${uploadedFile.name}:${uploadedFile.size}:${fileHash}`

const expectedSignature = crypto
.createHmac('sha256', enterprise.apiSecret)
.update(stringToSign)
.digest('hex')

if (typeof apiSignature !== 'string') {
return unauthorized(res, 'invalid_signature')
}

const sigBuf = Buffer.from(apiSignature, 'hex')
const expectedBuf = Buffer.from(expectedSignature, 'hex')

if (sigBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(sigBuf, expectedBuf)) {
return unauthorized(res, 'invalid_signature')
}

// Save nonce to prevent future replay
await db.EnterpriseNonce.create({ apiKey, nonce: apiNonce })

// Upload to IPFS
addFileToXinfinIpfs(uploadedFile.data, uploadedFile.name, async (err, ipfsHash) => {
if (err != null) {
console.error('Some error occured while adding KYC at /enterprise/addKYC: ', err)
return res.status(500).send('IPFS Upload Error')
}

let hash = ipfsHash[0].hash
res.status(200).json({ hash })
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
} catch (err) {
console.error('Error in /enterprise/addKYC:', err)
res.status(500).send('Internal Server Error')
}
})

module.exports = router
1 change: 1 addition & 0 deletions apis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ router.use('/api/transactions', require('./transactions'))
router.use('/api/search', require('./search'))
router.use('/api/auth', require('./auth'))
router.use('/api/ipfs', require('./ipfs'))
router.use('/api/enterprise', require('./enterprise'))

module.exports = router
8 changes: 8 additions & 0 deletions apis/ipfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ router.post('/addKYC', async function (req, res, next) {

let imageFile = req.files.filename

// Allow only PDF files
const isPdfMagic = imageFile.data && imageFile.data.length >= 5 &&
imageFile.data.toString('ascii', 0, 5) === '%PDF-'

if (imageFile.mimetype !== 'application/pdf' || !imageFile.name.toLowerCase().endsWith('.pdf') || !isPdfMagic) {
return res.status(400).json({ message: 'Only PDF files are allowed' })
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 10MB validation
const maxSize = 10 * 1024 * 1024
if (imageFile.size > maxSize) {
Expand Down
40 changes: 39 additions & 1 deletion apis/voters.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,44 @@ function normalizeSortField (sortBy) {
return sortBy
}

router.get('/getBalance/:address', async (req, res, next) => {
try {
const address = req.params.address
const rpcAddr = address.toLowerCase().startsWith('xdc') ? '0x' + address.substring(3) : address
const xdcAddr = 'xdc' + rpcAddr.substring(2)

let balance0x = '0'
let balanceXdc = '0'
let success = false

try {
balance0x = await web3.eth.getBalance(rpcAddr)
success = true
} catch (e) {
console.error(`[getBalance] Error for rpcAddr ${rpcAddr}:`, e)
}

const isZeroBalance0x = !balance0x || balance0x === '0' || balance0x === '0x0'
if (!success || isZeroBalance0x) {
try {
balanceXdc = await web3.eth.getBalance(xdcAddr)
success = true
} catch (e) {
console.error(`[getBalance] Error for xdcAddr ${xdcAddr}:`, e)
}
}

if (!success) {
return res.status(500).json({ error: 'Failed to fetch balance from RPC' })
}

const balance = (balance0x && balance0x !== '0' && balance0x !== '0x0') ? balance0x : balanceXdc
return res.json({ balance })
} catch (err) {
return next(err)
}
})

router.get('/:voter/candidates', [
query('limit')
.isInt({ min: 0, max: 200 }).optional().withMessage('limit should greater than 0 and less than 200'),
Expand Down Expand Up @@ -279,7 +317,7 @@ router.post('/verifyTx', [

// Fallback to XDC balance if 0x balance is empty or strictly zero
const balance = (balance0x && balance0x !== '0') ? balance0x : balanceXdc

if (balance) {
const convertedBalanc = new BigNumber(balance).div(10 ** 18)
const convertedAmount = new BigNumber(amount)
Expand Down
Loading