This commit is contained in:
JiriUhlir
2026-06-29 11:46:55 +02:00
parent f308a1bc29
commit e9d5f6bd36
5 changed files with 70 additions and 107 deletions
+52 -45
View File
@@ -1,5 +1,4 @@
import express, { Request, Response } from "express";
import swaggerUi from "swagger-ui-express";
import { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer";
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
import { buildODataParams, extractNextLink } from "./client/odata";
@@ -38,11 +37,25 @@ interface ResourceRoute {
}
const app = express();
app.set("trust proxy", true);
const port = Number(process.env.PORT || 3000);
const rootPath = process.env.ROOT_PATH || "";
const rootPath = normalizeRootPath(process.env.ROOT_PATH || "");
const serviceName = "SAP Business One";
const serviceId = "sap-bo";
function normalizeRootPath(value: string): string {
if (!value) {
return "";
}
const withLeadingSlash = value.startsWith("/") ? value : "/" + value;
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
}
function withRootPath(path: string): string {
return rootPath + path;
}
const resourceRoutes: ResourceRoute[] = [
{
slug: "business-partners",
@@ -581,31 +594,42 @@ function openApiDocument(basePath = "") {
};
}
function resolveBasePath(req: Request): string {
// Behind the AppFactory reverse proxy the public prefix is /apps/<app-id>, but the
// handle_path route strips it before the request reaches this container, so it cannot be
// read from the request path. ROOT_PATH is the sanctioned source; X-Forwarded-Prefix is a
// fallback for proxies that forward it. Empty when running without a proxy.
const forwardedPrefix = getHeader(req, "x-forwarded-prefix");
return (rootPath || forwardedPrefix || "").replace(/\/+$/, "");
}
function swaggerUiOptions() {
return {
customSiteTitle: "SAP Business One connector API",
swaggerOptions: {
// Relative endpoint (same approach as the sibling idoklad service): the browser resolves
// it against the docs page, i.e. {prefix}/docs/openapi.json, so the spec loads locally and
// behind the AppFactory proxy without hardcoding the /apps/<app-id> prefix.
url: "openapi.json",
displayRequestDuration: true,
function renderDocsHtml(): string {
// Same approach as the sibling google-service: serve the Swagger UI page directly at /docs
// (no static-file middleware, so no /docs -> /docs/ redirect that would drop the proxy prefix),
// load the UI assets from CDN, and point the spec URL at ROOT_PATH + /openapi.json so it resolves
// to the public /apps/<app-id>/openapi.json behind the AppFactory reverse proxy.
const specUrl = withRootPath("/openapi.json");
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SAP Business One connector API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
<style>
body { margin: 0; background: #f7f7f7; }
.topbar { display: none; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.ui = SwaggerUIBundle({
url: ${JSON.stringify(specUrl)},
dom_id: "#swagger-ui",
deepLinking: true,
persistAuthorization: true,
tryItOutEnabled: true,
filter: true,
displayRequestDuration: true,
tagsSorter: "alpha",
operationsSorter: "method"
}
};
});
</script>
</body>
</html>`;
}
app.get("/", (_req, res) => {
@@ -616,33 +640,16 @@ app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
// OpenAPI document served under the same /docs prefix as the UI so the relative UI endpoint
// resolves correctly locally and behind the reverse-proxy prefix. servers[0].url advertises the
// public prefix (ROOT_PATH) so Swagger UI "Try it out" targets {prefix}/api/..., not the host root.
app.get("/docs/openapi.json", (req, res) => {
res.json(openApiDocument(resolveBasePath(req)));
app.get("/docs", (_req, res) => {
res.type("html").send(renderDocsHtml());
});
// Documented root alias for direct access to the OpenAPI document.
app.get("/openapi.json", (req, res) => {
res.json(openApiDocument(resolveBasePath(req)));
// servers[0].url advertises the public prefix (ROOT_PATH) so Swagger UI "Try it out" targets
// {prefix}/api/..., not the host root.
app.get("/openapi.json", (_req, res) => {
res.json(openApiDocument(rootPath));
});
// swagger-ui-express's static handler redirects /docs -> absolute "/docs/", which behind the
// AppFactory handle_path proxy drops the /apps/<app-id> prefix and breaks the page. Intercept
// the no-slash form first and redirect to the ROOT_PATH-prefixed URL (mirrors idoklad's
// UsePathBase), so the trailing-slash form keeps the public prefix.
app.get("/docs", (req, res, next) => {
// Express non-strict routing matches both /docs and /docs/ here; only the no-slash form
// needs the redirect (otherwise /docs/ would redirect to itself in a loop).
if (req.path.endsWith("/")) {
return next();
}
res.redirect(301, `${resolveBasePath(req)}/docs/`);
});
app.use("/docs", swaggerUi.serve, swaggerUi.setup(undefined, swaggerUiOptions()));
addResourceEndpoints();
if (require.main === module) {