UNPKG

@cap-js-community/sap-afc-sdk

Version:

SAP Advanced Financial Closing SDK for CDS

446 lines (411 loc) 13.8 kB
"use strict"; const fs = require("fs"); const path = require("path"); const cds = require("@sap/cds"); const express = require("express"); const helmet = require("helmet"); const cors = require("cors"); const swaggerUi = require("swagger-ui-express"); const { StatusCodes, getReasonPhrase } = require("http-status-codes"); const toggles = require("@cap-js-community/feature-toggle-library"); const { config: eventQueueConfig } = require("@cap-js-community/event-queue"); const { merge, toObject } = require("./src/util/helper"); const config = merge([require("./config.json"), cds.env.requires?.["sap-afc-sdk"]?.config ?? {}]); const SERVER_SUFFIX = "srv"; const APPROUTER_SUFFIX = "approuter"; process.env.SAP_AFC_SDK_PLUGIN_PACKAGE ??= "@cap-js-community/sap-afc-sdk"; cds.on("bootstrap", () => { secureRoutes(); addMiddlewares(); serveBroker(); serveUIs(); serveSwaggerUI(); }); cds.on("connect", (srv) => { if (srv.name === "db") { registerAfterReadJobFillLink(srv); } }); cds.on("listening", () => { rerouteWebsocket(); serveApiRoot(); outboxServices(); handleFeatureToggles(); }); function secureRoutes() { if (cds.env.requires?.["sap-afc-sdk"]?.api?.cors) { const corsOptions = toObject(cds.env.requires?.["sap-afc-sdk"]?.api?.cors); let origin = cds.env.requires.multitenancy ? approuterUrlRegExp() : approuterUrl(); if (corsOptions.origin === false || typeof corsOptions.origin !== "boolean") { origin = corsOptions.origin; } cds.app.use( "/api", cors({ ...corsOptions, origin, }), ); } if (serverUrl().startsWith("http://localhost")) { return; } if (cds.env.requires?.["sap-afc-sdk"]?.api?.csp) { const csp = toObject(cds.env.requires?.["sap-afc-sdk"]?.api?.csp); const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); cds.app.use( "/api", helmet({ contentSecurityPolicy: { directives: { ...defaultDirectives, "default-src": [ ...(defaultDirectives["default-src"] || []), cds.env.requires.multitenancy ? approuterWildcardUrl() : approuterUrl(), serverUrl(), authorizationUrl(), ], ...csp, }, }, }), ); } } function addMiddlewares() { cds.middlewares.after.unshift(handleError); } function handleError(err, req, res, next) { if (!err) { return next(err); } if (req.baseUrl?.startsWith("/api/")) { return handleAPIError(err, req, res); } next(err); } function handleAPIError(err, req, res) { let statusCode = StatusCodes.INTERNAL_SERVER_ERROR; const codes = [err.statusCode, err.status, err.code, err]; for (const code of codes) { if (!isNaN(code) && StatusCodes[String(code)]) { statusCode = parseInt(code); break; } } res.status(statusCode).send({ code: String(err.code || statusCode), message: err.message || getReasonPhrase(statusCode), }); } function serveApiRoot() { const result = Object.keys(cds.services).reduce((result, name) => { const service = cds.services[name]; if (service?.definition?.["@openapi"] && service?.path) { result[service?.path] = `${serverUrl()}${service.path}`; } return result; }, {}); cds.app.get( "/api", ...cds.middlewares.before, (req, res) => { res.status(200).json(result); }, ...cds.middlewares.after, ); } function serveBroker() { if (!cds.env.requires?.["sap-afc-sdk"]?.broker) { return; } let brokerConfig = toObject(cds.env.requires?.["sap-afc-sdk"]?.broker); const brokerPath = path.join(cds.root, config.paths.broker); try { brokerConfig = merge([require(brokerPath), brokerConfig]); } catch (err) { if (Object.keys(brokerConfig).length === 0) { cds.log("/broker").info(`broker.json not found at '${brokerPath}'. Call 'afc add broker'`); } } for (const service in brokerConfig?.SBF_SERVICE_CONFIG ?? {}) { const endpoints = brokerConfig?.SBF_SERVICE_CONFIG[service]?.extend_credentials?.shared?.endpoints ?? {}; for (const endpoint in endpoints) { if (!/https?:\/\//.test(endpoints[endpoint])) { endpoints[endpoint] = `${serverUrl()}${endpoints[endpoint]}`; } } } for (const key in brokerConfig) { if (key.startsWith("SBF_")) { process.env[key] ??= JSON.stringify(brokerConfig[key]); } } try { const catalogPath = brokerConfig.catalog ?? config.paths.catalog; const router = express.Router(); const Broker = require("@sap/sbf"); const broker = new Broker({ enableAuditLog: false, catalog: catalogPath, }); router.use("/broker", broker.app); cds.app.use(router); } catch (err) { cds.log("/broker").error("Failed to start broker", err); } } function serveUIs() { if (process.env.SAP_AFC_SDK_PLUGIN_PACKAGE === "." && process.env.NODE_ENV !== "test") { return; } if (!cds.env.requires?.["sap-afc-sdk"]?.ui) { return; } const uiPath = cds.env.requires?.["sap-afc-sdk"]?.ui?.path; let uiShowLaunchpad = cds.env.requires?.["sap-afc-sdk"]?.ui?.launchpad; if (uiShowLaunchpad) { const packageRoot = cds.utils.path.resolve( require.resolve(path.join(process.env.SAP_AFC_SDK_PLUGIN_PACKAGE, "package.json"), { paths: [cds.root] }), "..", ); if (!fs.existsSync(`${cds.root}/${cds.env.folders.app}appconfig/fioriSandboxConfig.json`)) { cds.app .serve(`${uiPath}/${config.paths.launchpad}`) .from(process.env.SAP_AFC_SDK_PLUGIN_PACKAGE, "/app/launchpad.html"); cds.app.use(`/appconfig`, express.static(`${packageRoot}/app/appconfig`)); } else { serveMergedAppConfig(packageRoot, uiPath); } } for (const app in config.apps) { const uiShowApp = cds.env.requires?.["sap-afc-sdk"]?.ui?.[app]; if ( (uiShowLaunchpad || uiShowApp) && (!fs.existsSync(`${cds.root}/${cds.env.folders.app}${app}`) || process.env.NODE_ENV === "test") ) { if (uiShowLaunchpad) { cds.app .serve(`${uiPath}/${app}/webapp`) .from(process.env.SAP_AFC_SDK_PLUGIN_PACKAGE, config.paths[app] ?? `${cds.env.folders.app}${app}/webapp`); } cds.app .serve(`${uiPath}/${app}`) .from(process.env.SAP_AFC_SDK_PLUGIN_PACKAGE, config.paths[app] ?? `${cds.env.folders.app}${app}/webapp`); } } } function rerouteWebsocket() { cds.app.server.on("upgrade", function (req) { for (const app in config.apps) { if (req.url.startsWith(`/${app}/webapp/ws`)) { req.url = req.url.replace(`/${app}/webapp/ws`, "/ws"); return; } else if (req.url.startsWith(`/${app}/ws`)) { req.url = req.url.replace(`/${app}/ws`, "/ws"); return; } } }); } function serveMergedAppConfig(packageRoot, uiPath) { cds.app.use(`/appconfig/fioriSandboxConfig.json`, (req, res) => { const projectFioriSandboxConfig = require(`${cds.root}/${cds.env.folders.app}appconfig/fioriSandboxConfig.json`); const packageFioriSandboxConfig = require(`${packageRoot}/app/appconfig/fioriSandboxConfig.json`); if (uiPath) { for (const name in packageFioriSandboxConfig.applications ?? {}) { const application = packageFioriSandboxConfig.applications[name]; application.url = `${uiPath}/${application.url}`; } for (const name in packageFioriSandboxConfig.services?.ClientSideTargetResolution?.adapter?.config?.inbounds ?? {}) { const inbound = packageFioriSandboxConfig.services?.ClientSideTargetResolution?.adapter?.config?.inbounds[name]; inbound.resolutionResult.url = `${uiPath}/${inbound.resolutionResult.url}`; } } const mergedFioriSandboxConfig = merge([packageFioriSandboxConfig, projectFioriSandboxConfig], { array: "merge", mergeKey: "id", }); res.send(mergedFioriSandboxConfig); }); } function serveSwaggerUI() { const swaggerConfig = cds.env.requires?.["sap-afc-sdk"]?.ui?.swagger; if (!swaggerConfig) { return; } const router = express.Router(); cds.on("serving", (service) => { const openAPI = service.definition?.["@openapi"]; if (openAPI) { if (!(swaggerConfig === true || swaggerConfig[service.name])) { return; } const apiPath = config.paths.swaggerUi + service.path; cds.log("/swagger").info("Serving Swagger UI for ", { service: service.name, at: apiPath }); router.use( apiPath, ...cds.middlewares.before, (req, res, next) => { const restrict_all = cds.env.requires?.auth?.restrict_all_services !== false; if (!restrict_all || cds.context?.user?._is_privileged || !cds.context?.user?._is_anonymous) { return next(); } res.status(401).send("Unauthorized"); }, (req, res, next) => { req.swaggerDoc = toOpenApiDoc(req, service, openAPI); if (req.swaggerDoc) { return next(); } res.status(404).send("Not found"); }, swaggerUi.serveFiles(), swaggerUi.setup(null, {}), ...cds.middlewares.after, ); addLinkToIndexHtml(service, apiPath); } }); cds.app.use(router); } const openAPICache = new Map(); function toOpenApiDoc(req, service, name) { const filePath = `${name === true ? service.name : name}.${config.extensions.openapi}`; if (openAPICache.has(filePath)) { return openAPICache.get(filePath); } let openAPI; const paths = [ path.join(cds.root, filePath), path.join(cds.root, "openapi", filePath), path.join(__dirname, filePath), path.join(__dirname, "openapi", filePath), ]; for (const path of paths) { if (fs.existsSync(path)) { openAPI = JSON.parse(fs.readFileSync(path)); if (openAPI) { break; } } } if (openAPI) { const clientCredentials = openAPI?.components?.securitySchemes?.oauth2?.flows?.clientCredentials; if (clientCredentials) { clientCredentials.tokenUrl = authorizationUrl() + "/oauth/token"; } openAPI.servers.forEach((server) => { if (!server.url.startsWith("https://") && !server.url.startsWith("http://")) { server.url = `${serverUrl()}${server.url}`; } }); openAPICache.set(filePath, openAPI); } return openAPI; } let _approuterUrl; function approuterUrl() { if (_approuterUrl) { return _approuterUrl; } if (cds.env.requires?.["sap-afc-sdk"]?.endpoints?.approuter) { return (_approuterUrl = cds.env.requires["sap-afc-sdk"].endpoints.approuter); } if (process.env.VCAP_APPLICATION) { return (_approuterUrl = serverUrl().replace(new RegExp(`(https:\\/\\/.*?)-${SERVER_SUFFIX}(.*)`), `$1$2`)); } else { return (_approuterUrl = serverUrl().replace( new RegExp(`(https:\\/\\/.*?)-${SERVER_SUFFIX}(.*)`), `$1-${APPROUTER_SUFFIX}$2`, )); } } function approuterDomain() { let url = approuterUrl(); if (url?.startsWith("https://")) { url = url.substring(8); } return url; } function approuterWildcardUrl() { return `*.${approuterDomain()}`; } function approuterTenantUrl(req) { if (cds.env.requires.multitenancy) { const subdomain = req.user?.tokenInfo?.extAttributes?.zdn; if (subdomain) { return `https://${subdomain}${cds.env.tenant_separator ?? "."}${approuterDomain()}`; } } return approuterUrl(); } function approuterUrlRegExp() { let url = approuterDomain(); url = url.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"); return RegExp(url + "$"); } let _serverUrl; function serverUrl() { if (_serverUrl) { return _serverUrl; } if (cds.env.requires?.["sap-afc-sdk"]?.endpoints?.server) { return (_serverUrl = cds.env.requires["sap-afc-sdk"].endpoints.server); } if (process.env.VCAP_APPLICATION) { const url = JSON.parse(process.env.VCAP_APPLICATION).uris?.[0]; if (url) { return (_serverUrl = `https://${url}`); } } return (_serverUrl = cds.server.url ?? `http://localhost:${process.env.PORT || cds.env.server?.port || 4004}`); } function authorizationUrl() { return cds.env.requires?.auth?.credentials?.url ?? config.endpoints.authentication; } function addLinkToIndexHtml(service, apiPath) { const provider = () => { return { href: apiPath, name: "Open API", title: "Show in Swagger UI" }; }; service.$linkProviders ? service.$linkProviders.push(provider) : (service.$linkProviders = [provider]); } function outboxServices() { for (const service in config.services) { if (cds.services[service] && cds.requires[service]?.outbox && config.services[service].outbox) { cds.services[service].options.outbox = cds.requires[service].outbox; cds.services[service] = cds.outboxed(cds.services[service]); } } } function handleFeatureToggles() { // Event-Queue for (const name in config.toggles.eventQueue) { const toggle = config.toggles.eventQueue[name]; eventQueueConfig[name] = toggles.getFeatureValue(toggle); toggles.registerFeatureValueChangeHandler(toggle, (value) => { eventQueueConfig[name] = value; }); } } function registerAfterReadJobFillLink(db) { if (!cds.env.requires?.["sap-afc-sdk"]?.ui?.link) { return; } db.after("READ", async (result, req) => { if (req.target.name.split(".").pop() !== "Job") { return; } result = Array.isArray(result) ? result : [result]; for (const row of result) { if (row.ID && row.link === null) { row.link = `${approuterTenantUrl(req)}/${config.paths.launchpad}#Job-monitor&/Job(${row.ID})`; } } }); } // Plugins module.exports = (async () => { return await Promise.all(config.plugins.map((plugin) => require(`${plugin}/cds-plugin`))); })();