diff --git a/README.md b/README.md index 5c1564b..fdbd3d8 100644 --- a/README.md +++ b/README.md @@ -186,7 +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 napojené na `/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` respektuje `ROOT_PATH`, takže za AppFactory proxy volá například `/apps/sap-bo/api/business-partners`, ne root doménu. +`/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. ## 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 5fccb92..bab826a 100644 --- a/documentation/sap-business-one-service-layer.md +++ b/documentation/sap-business-one-service-layer.md @@ -49,9 +49,20 @@ HTTP API hlavičky: ## Reverse proxy a Swagger Aplikace běží za AppFactory proxy na `/apps/` (Caddy `handle_path` prefix před -předáním do containeru odstraní). Proto: +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`: -- OpenAPI `servers` obsahuje `ROOT_PATH` (`/apps/`), takže Swagger `Try it out` - volá `…/apps//api/`. -- Swagger UI načítá OpenAPI dokument z `ROOT_PATH + /openapi.json`, ne z kořene domény. -- Lokálně bez `ROOT_PATH` se vše chová relativně ke kořeni (`/openapi.json`, server `/`). +- 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/`. + +| 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 | +| `servers[0].url` v OpenAPI | `/` | `/apps/sap-bo` | diff --git a/src/index.ts b/src/index.ts index 7b377cd..dbe4190 100644 --- a/src/index.ts +++ b/src/index.ts @@ -581,11 +581,23 @@ function openApiDocument(basePath = "") { }; } -function swaggerOptions(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: { - url: `${basePath}/openapi.json`.replace("//", "/"), + // 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, persistAuthorization: true, tryItOutEnabled: true, @@ -604,36 +616,22 @@ app.get("/health", (_req, res) => { res.json({ status: "ok" }); }); -app.get("/openapi.json", (_req, res) => { - res.json(openApiDocument(rootPath)); +// 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))); }); +// Documented root alias for direct access to the OpenAPI document. +app.get("/openapi.json", (req, res) => { + res.json(openApiDocument(resolveBasePath(req))); +}); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(undefined, swaggerUiOptions())); + addResourceEndpoints(); -// Behind the AppFactory reverse proxy the container is reached via /apps/ and -// the prefix is stripped (handle_path) before requests arrive here. The Swagger UI page is -// therefore served at the public /apps//docs, so the OpenAPI document URL and the -// "Try it out" base path must carry ROOT_PATH; otherwise the browser would resolve them -// against the bare origin and bypass the app. -app.use("/docs", swaggerUi.serveFiles(undefined, swaggerOptions(rootPath)), swaggerUi.setup(undefined, swaggerOptions(rootPath))); - -if (rootPath) { - app.get(rootPath, (_req, res) => { - res.json(serviceMetadata()); - }); - - app.get(rootPath + "/health", (_req, res) => { - res.json({ status: "ok" }); - }); - - app.get(rootPath + "/openapi.json", (_req, res) => { - res.json(openApiDocument(rootPath)); - }); - - addResourceEndpoints(rootPath); - app.use(rootPath + "/docs", swaggerUi.serveFiles(undefined, swaggerOptions(rootPath)), swaggerUi.setup(undefined, swaggerOptions(rootPath))); -} - if (require.main === module) { app.listen(port, "0.0.0.0", () => { console.log(serviceId + " listening on port " + port);