diff --git a/README.md b/README.md index fdbd3d8..436e6ac 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/documentation/sap-business-one-service-layer.md b/documentation/sap-business-one-service-layer.md index bab826a..9d3a712 100644 --- a/documentation/sap-business-one-service-layer.md +++ b/documentation/sap-business-one-service-layer.md @@ -50,19 +50,21 @@ HTTP API hlavičky: Aplikace běží za AppFactory proxy na `/apps/` (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/`. -- `servers[0].url` se nastaví z `ROOT_PATH` (fallback hlavička `X-Forwarded-Prefix`, - nakonec `/`), takže Swagger `Try it out` volá `…/apps//api/`. +- `/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/` (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//openapi.json`. +- `servers[0].url` se nastaví z `ROOT_PATH` (jinak `/`), takže Swagger `Try it out` volá + `…/apps//api/`. | 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` | diff --git a/package-lock.json b/package-lock.json index d8576c8..f898471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1863a41..bb563da 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/index.ts b/src/index.ts index 5dcfd25..e09e694 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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/, 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/ 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//openapi.json behind the AppFactory reverse proxy. + const specUrl = withRootPath("/openapi.json"); + return ` + + + + + SAP Business One connector API + + + + +
+ + + +`; } 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/ 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) {