fix
This commit is contained in:
@@ -186,12 +186,12 @@ Pokryté oblasti:
|
||||
|
||||
Aplikace poslouchá na `0.0.0.0` a portu `PORT` s výchozí hodnotou `3000`. `ROOT_PATH` se používá pro dokumentaci a testovací requesty za reverse proxy, například `/apps/sap-bo`.
|
||||
|
||||
`/docs` je Swagger UI, které načítá OpenAPI dokument přes relativní endpoint
|
||||
`openapi.json` (servírovaný jako `/docs/openapi.json`, alias `/openapi.json`). Díky tomu
|
||||
se dokument načte lokálně i za AppFactory proxy bez hardcodování prefixu. V Swaggeru použij
|
||||
`Authorize` pro vyplnění povinných `X-SAP-B1-*` hlaviček a potom `Try it out` u konkrétní
|
||||
operace. OpenAPI `servers` se nastaví z `ROOT_PATH` (fallback `X-Forwarded-Prefix`, jinak
|
||||
`/`), takže za proxy volá například `/apps/sap-bo/api/business-partners`, ne root doménu.
|
||||
`/docs` je Swagger UI servírované přímo jako HTML (stejně jako sousední `google-service`),
|
||||
bez statického middleware – proto nevzniká redirect `/docs` → `/docs/`, který by za proxy
|
||||
zahodil prefix. Assety se načítají z CDN, spec URL je `ROOT_PATH + /openapi.json`. V Swaggeru
|
||||
použij `Authorize` pro vyplnění povinných `X-SAP-B1-*` hlaviček a potom `Try it out` u
|
||||
konkrétní operace. OpenAPI `servers` se nastaví z `ROOT_PATH` (jinak `/`), takže za proxy
|
||||
volá například `/apps/sap-bo/api/business-partners`, ne root doménu.
|
||||
|
||||
## TODO ověřit v konkrétní instalaci SAP Business One
|
||||
|
||||
|
||||
@@ -50,19 +50,21 @@ HTTP API hlavičky:
|
||||
|
||||
Aplikace běží za AppFactory proxy na `/apps/<app-id>` (Caddy `handle_path` prefix před
|
||||
předáním do containeru odstraní, takže container vidí routy bez prefixu a z requestu
|
||||
veřejnou cestu nelze odvodit). Stejný přístup jako sousední služba `idoklad`:
|
||||
veřejnou cestu nelze odvodit). Stejný přístup jako sousední služba `google-service`:
|
||||
|
||||
- OpenAPI dokument se servíruje **pod stejným `/docs` prefixem** jako UI na
|
||||
`GET /docs/openapi.json` (plus alias `GET /openapi.json` pro přímý přístup).
|
||||
- Swagger UI načítá dokument přes **relativní** endpoint `openapi.json`, takže se
|
||||
v prohlížeči vyhodnotí jako `{prefix}/docs/openapi.json` lokálně i za proxy, bez
|
||||
hardcodování `/apps/<app-id>`.
|
||||
- `servers[0].url` se nastaví z `ROOT_PATH` (fallback hlavička `X-Forwarded-Prefix`,
|
||||
nakonec `/`), takže Swagger `Try it out` volá `…/apps/<app-id>/api/<resource>`.
|
||||
- `/docs` se servíruje **přímo jako HTML** (`GET /docs`), bez `swagger-ui-express` a bez
|
||||
statického middleware – proto **nevzniká žádný redirect `/docs` → `/docs/`**, který by za
|
||||
proxy zahodil prefix `/apps/<app-id>` (to byla příčina bílé stránky).
|
||||
- Swagger UI assety se načítají z CDN (`unpkg.com/swagger-ui-dist@5`).
|
||||
- Spec URL je `ROOT_PATH + /openapi.json`, takže odkazuje na veřejné
|
||||
`/apps/<app-id>/openapi.json`.
|
||||
- `servers[0].url` se nastaví z `ROOT_PATH` (jinak `/`), takže Swagger `Try it out` volá
|
||||
`…/apps/<app-id>/api/<resource>`.
|
||||
|
||||
| Kontrola | Lokálně (bez ROOT_PATH) | S `ROOT_PATH=/apps/sap-bo` |
|
||||
|---|---|---|
|
||||
| `GET /health` | 200 | 200 |
|
||||
| `GET /docs` (Swagger UI) | 200 | 200 |
|
||||
| `GET /docs/openapi.json` | 200 | 200 |
|
||||
| `GET /docs` (Swagger UI) | 200, žádný redirect | 200, žádný redirect |
|
||||
| `GET /openapi.json` | 200 | 200 |
|
||||
| spec URL v `/docs` | `/openapi.json` | `/apps/sap-bo/openapi.json` |
|
||||
| `servers[0].url` v OpenAPI | `/` | `/apps/sap-bo` |
|
||||
|
||||
Generated
-44
@@ -11,13 +11,11 @@
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
@@ -356,13 +354,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -524,17 +515,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/swagger-ui-express": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
|
||||
@@ -2131,30 +2111,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.32.8",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz",
|
||||
"integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">=5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
|
||||
+52
-45
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user