@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
159 lines (137 loc) • 6.18 kB
JavaScript
const cds = require('../../cds.js')
const express = require('express')
const getTenantInfo = require('./getTenantInfo.js')
const _isAll = a => a && a.includes('all')
const _xsuaa_fallback = () => {
let xsuaa = cds.env.requires.messaging.xsuaa
xsuaa = typeof xsuaa === 'string' ? xsuaa : 'xsuaa'
const xsuaa_config = cds.env.requires[xsuaa]
if (!xsuaa_config) throw new Error(`Fallback XSUAA instance '${xsuaa}' not found!`)
if (!xsuaa_config.credentials) throw new Error(`Fallback XSUAA instance '${xsuaa}' has no credentials!`)
const jwt_auth = require('../../../../lib/srv/middlewares/auth/jwt-auth.js')
return jwt_auth(xsuaa_config)
}
class EndpointRegistry {
constructor(basePath, LOG) {
const deployPath = basePath + '/deploy'
this.webhookCallbacks = new Map()
this.deployCallbacks = new Map()
const _IS_SECURED = !!(
cds.env.requires.auth &&
(cds.env.requires.auth.impl || !(cds.env.requires.auth.kind in { mocked: 1, 'mocked-auth': 1 }))
)
const _IS_PRODUCTION = process.env.NODE_ENV === 'production'
cds.app.use(basePath, cds.middlewares.context())
if (_IS_SECURED) {
// unofficial XSUAA fallback
if (cds.env.requires.messaging?.xsuaa) {
cds.app.use(basePath, _xsuaa_fallback())
} else {
cds.app.use(basePath, cds.middlewares.auth())
}
// ensure that anonymous users get 401 on messaging endpoints
cds.app.use(basePath, (_req, _res, next) => {
next(cds.context.user._is_anonymous ? 401 : undefined)
})
} else if (_IS_PRODUCTION) LOG.warn('Messaging endpoints not secured')
cds.app.use(basePath, express.json({ type: 'application/*+json' }))
cds.app.use(basePath, express.json())
cds.app.use(basePath, express.urlencoded({ extended: true }))
LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
cds.app.options(basePath, (req, res) => {
try {
if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403)
res.set('webhook-allowed-origin', req.headers['webhook-request-origin'])
res.sendStatus(200)
} catch {
res.sendStatus(500)
}
})
LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'POST' })
cds.app.post(basePath, (req, res) => {
try {
if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403)
const queueName = req.query.q
if (!queueName) {
LOG.error('Query parameter `q` not found.')
return res.sendStatus(400)
}
const xAddress = req.headers['x-address']
const topic = xAddress && xAddress.match(/^topic:(.*)/)?.[1]
if (!topic) {
LOG.error('Incoming message does not contain a topic in header `x-address`: ' + xAddress)
return res.sendStatus(400)
}
const payload = req.body
const cb = this.webhookCallbacks.get(queueName)
if (!cb) return res.sendStatus(200)
const { tenant } = cds.context
const other = tenant
? {
_: { req, res }, // For `cds.context.http`
tenant
}
: {}
if (!cb) return res.sendStatus(200)
cb(topic, payload, other, {
done: () => {
res.sendStatus(200)
},
failed: () => {
res.sendStatus(500)
}
})
} catch (error) {
LOG.error(error)
return res.sendStatus(500)
}
})
cds.app.post(deployPath, async (req, res) => {
LOG.debug('Handling deploy request')
try {
if (_IS_SECURED && !cds.context.user.is('emmanagement')) return res.sendStatus(403)
const tenants = req.body && !_isAll(req.body.tenants) && req.body.tenants
const queues = req.body && !_isAll(req.body.queues) && req.body.queues
const options = { wipeData: req.body && req.body.wipeData }
if (tenants && !Array.isArray(tenants)) res.status(400).send('Request parameter `tenants` must be an array.')
if (queues && !Array.isArray(queues)) res.status(400).send('Request parameter `queues` must be an array.')
const tenantInfo = tenants ? await Promise.all(tenants.map(t => getTenantInfo(t))) : await getTenantInfo()
const callbacks = queues ? queues.map(q => this.deployCallbacks.get(q)) : [...this.deployCallbacks.values()]
const results = await Promise.all(callbacks.map(c => c(tenantInfo, options)))
// [{ queue: '...', failed: [...], succeeded: [...] }, ...]
const hasError = results.some(r => r.failed.length)
if (hasError) return res.status(500).send(results)
return res.status(201).send(results)
} catch (e) {
LOG.error('Error while handling deploy request: ', e)
// REVISIT: Still needed with cds-mtxs?
// If an unknown tenant id is provided, cds-mtx will crash ("Cannot read property 'hanaClient' of undefined")
return res.sendStatus(500)
}
})
cds.app.use(basePath, cds.middlewares.errors())
}
registerWebhookCallback(queueName, cb) {
this.webhookCallbacks.set(queueName, cb)
}
registerDeployCallback(queueName, cb) {
this.deployCallbacks.set(queueName, cb)
}
}
// Singleton registries per basePath
const registries = new Map()
// REVISIT: Use cds mechanism instead of express? -> Need option method and handler for specifica
const registerWebhookEndpoints = (basePath, queueName, LOG, cb) => {
const registry =
registries.get(basePath) ||
(registries.set(basePath, new EndpointRegistry(basePath, LOG)) && registries.get(basePath))
registry.registerWebhookCallback(queueName, cb)
}
const registerDeployEndpoints = (basePath, queueName, cb) => {
const registry =
registries.get(basePath) || (registries.set(basePath, new EndpointRegistry(basePath)) && registries.get(basePath))
registry.registerDeployCallback(queueName, cb)
}
// Only needed for testing, not used in productive code
const __clearRegistries = () => registries.clear()
module.exports = { registerWebhookEndpoints, registerDeployEndpoints, __clearRegistries }