Serve API Portal under the /api-portal path prefix - #3126
Conversation
|
Warning Review limit reached
Next review available in: 9 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughThe API Portal now runs under the ChangesAPI Portal base-path support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant API Portal
participant Portal Router
participant Session and CSRF Middleware
participant API or Page Route
Browser->>API Portal: Request /api-portal/...
API Portal->>Session and CSRF Middleware: Apply mount-scoped middleware
Session and CSRF Middleware->>Portal Router: Pass prefixed request
Portal Router->>API or Page Route: Match mount-relative route
API or Page Route-->>Browser: Return prefixed page, API response, or redirect
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
portals/api-portal/src/utils/basePathAssetRefs.test.js (1)
65-109: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMake the asset scan enforce the allowed URL shapes.
The two tests only reject references that start with
/styles,/images, or another listed mount. A template reference such as/api-portal/styles/main.cssorstyles/main.csscan pass without using{{basePath}}. A stylesheet reference such as/api-portal/images/icon.svgcan also pass. These forms bypass the documented base-path or stylesheet-relative conventions. Match the allowed forms explicitly instead of checking only bare-root violations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/api-portal/src/utils/basePathAssetRefs.test.js` around lines 65 - 109, Update the asset-reference tests around the template and stylesheet regexes to validate allowed URL shapes explicitly: templates must use {{basePath}} before mounted assets, while stylesheets must use relative references and reject both server-root and mount-prefixed paths, including api-portal-prefixed and bare mount-relative forms. Ensure offenders are collected for any disallowed shape and retain the existing scans and assertions.portals/api-portal/src/config/configLoader.js (1)
479-483: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDerive reserved handles from the shared route constants.
RESERVED_ORG_HANDLESduplicates the mount and API segment as'api-portal'and'api'. If either shared route constant changes, startup validation can allow an organization handle that shadows the new route. Build these entries from the exported constants instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/api-portal/src/config/configLoader.js` around lines 479 - 483, Update RESERVED_ORG_HANDLES to derive the mount and API entries from the exported shared route constants instead of hard-coded 'api-portal' and 'api' values. Preserve all other reserved handles unchanged and use the existing route-constant symbols exposed by the configuration module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@portals/api-portal/docs/api-portal-openapi-spec-v0.9.yaml`:
- Around line 28-35: Update all inline route examples in the OpenAPI
descriptions, including the API, MCP-server, asset, and API-key references, to
include the `/api-portal` mount prefix or use paths relative to the declared
server URL. Ensure no examples still expose unmounted `/api/v0.9/...` routes.
In `@portals/api-portal/src/controllers/authController.js`:
- Line 86: Update the Passport failure redirects in authController.js at the
anchor lines 86-86 and sibling lines 242-243 to target the registered portal
login URL under /portal/login, or register the generic
${constants.ROUTE.BASE_PATH}/login route; ensure both authentication failure
paths resolve to the existing login handler instead of a 404.
In `@portals/api-portal/src/controllers/customContentController.js`:
- Line 101: Update the design-mode custom-content URL assignment near the
corresponding branch to prepend constants.ROUTE.BASE_PATH before
constants.ROUTE.VIEWS_PATH + viewName, matching the existing prefixed URL
construction in orgContentController. Preserve the non-design branch behavior.
In `@portals/api-portal/src/services/apiWorkflowService.js`:
- Around line 73-78: Update the llmsIndexUrl construction in the workflow URL
generation flow to include constants.ROUTE.BASE_PATH between baseUrl and
orgHandle, matching the prefixed workflowUrl route. Preserve the existing
orgHandle, viewName, and llms.txt segments.
In `@portals/api-portal/src/utils/util.js`:
- Line 247: Provide basePath in both rendering contexts: add basePath:
constants.ROUTE.BASE_PATH to the layout context and to every context passed to
template(enrichedContent), including the corresponding locations noted in the
diff. Preserve the existing nested apiPortalApiConfig while ensuring top-level
basePath is available when rendering both page bodies and layouts.
---
Nitpick comments:
In `@portals/api-portal/src/config/configLoader.js`:
- Around line 479-483: Update RESERVED_ORG_HANDLES to derive the mount and API
entries from the exported shared route constants instead of hard-coded
'api-portal' and 'api' values. Preserve all other reserved handles unchanged and
use the existing route-constant symbols exposed by the configuration module.
In `@portals/api-portal/src/utils/basePathAssetRefs.test.js`:
- Around line 65-109: Update the asset-reference tests around the template and
stylesheet regexes to validate allowed URL shapes explicitly: templates must use
{{basePath}} before mounted assets, while stylesheets must use relative
references and reject both server-root and mount-prefixed paths, including
api-portal-prefixed and bare mount-relative forms. Ensure offenders are
collected for any disallowed shape and retain the existing scans and assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 025df671-f4c4-4e57-90ca-0208140662bd
📒 Files selected for processing (89)
portals/api-portal/README.mdportals/api-portal/configs/config-template.tomlportals/api-portal/docker-compose.platform-api.yamlportals/api-portal/docker-compose.yamlportals/api-portal/docs/api-portal-openapi-spec-v0.9.yamlportals/api-portal/it/docker-compose.test.postgres.yamlportals/api-portal/it/docker-compose.test.yamlportals/api-portal/it/rest-api/ai-discovery/apis-md.spec.jsportals/api-portal/it/rest-api/ai-discovery/llms-txt.spec.jsportals/api-portal/it/rest-api/auth/authorization-mode.spec.jsportals/api-portal/it/rest-api/auth/file-based-login.spec.jsportals/api-portal/it/rest-api/auth/foreign-org-login.spec.jsportals/api-portal/it/rest-api/organizations/single-org-isolation.spec.jsportals/api-portal/it/rest-api/support/client.jsportals/api-portal/it/rest-api/views-and-labels/view-fallback-and-delete.spec.jsportals/api-portal/it/rest-api/views-and-labels/view-scoped-detail-pages.spec.jsportals/api-portal/it/ui/cypress.config.jsportals/api-portal/it/ui/cypress/e2e/000-smoke/001-smoke.cy.jsportals/api-portal/it/ui/cypress/e2e/applications/application-flows.cy.jsportals/api-portal/it/ui/cypress/e2e/applications/key-managers-multiple.cy.jsportals/api-portal/it/ui/cypress/e2e/settings/001-views-labels.cy.jsportals/api-portal/it/ui/cypress/e2e/settings/002-key-managers.cy.jsportals/api-portal/it/ui/cypress/support/commands/auth.jsportals/api-portal/it/ui/cypress/support/commands/portal.jsportals/api-portal/samples/layouts/green-theme/layout/main.hbsportals/api-portal/samples/layouts/green-theme/pages/apis/partials/api-listing.hbsportals/api-portal/samples/layouts/green-theme/pages/home/partials/home.hbsportals/api-portal/samples/layouts/green-theme/styles/main.cssportals/api-portal/src/app.jsportals/api-portal/src/config/configDefaults.jsportals/api-portal/src/config/configLoader.jsportals/api-portal/src/controllers/apiContentController.jsportals/api-portal/src/controllers/apiKeysOverviewController.jsportals/api-portal/src/controllers/apiKeysPageController.jsportals/api-portal/src/controllers/apiWorkflowsController.jsportals/api-portal/src/controllers/applicationsContentController.jsportals/api-portal/src/controllers/authController.jsportals/api-portal/src/controllers/customContentController.jsportals/api-portal/src/controllers/orgContentController.jsportals/api-portal/src/controllers/subscriptionsContentController.jsportals/api-portal/src/controllers/viewConfigureController.jsportals/api-portal/src/defaultContent/layout/main.hbsportals/api-portal/src/defaultContent/pages/api-landing/page.hbsportals/api-portal/src/defaultContent/pages/api-landing/partials/api-detail-banner.hbsportals/api-portal/src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbsportals/api-portal/src/defaultContent/pages/api-workflows/detail/page.hbsportals/api-portal/src/defaultContent/pages/apis/page.hbsportals/api-portal/src/defaultContent/pages/apis/partials/api-listing.hbsportals/api-portal/src/defaultContent/pages/home/page.hbsportals/api-portal/src/defaultContent/pages/home/partials/home.hbsportals/api-portal/src/defaultContent/pages/mcp-landing/page.hbsportals/api-portal/src/defaultContent/pages/mcp-landing/partials/mcp-subscription-plans.hbsportals/api-portal/src/defaultContent/pages/mcp/partials/mcp-listing.hbsportals/api-portal/src/defaultContent/partials/header.hbsportals/api-portal/src/defaultContent/partials/sidebar.hbsportals/api-portal/src/defaultContent/styles/components.cssportals/api-portal/src/defaultContent/styles/main.cssportals/api-portal/src/helpers/handlebarsHelpers.jsportals/api-portal/src/middlewares/ensureAuthenticated.jsportals/api-portal/src/middlewares/registerPartials.jsportals/api-portal/src/pages/api-keys-overview/page.hbsportals/api-portal/src/pages/api-keys/page.hbsportals/api-portal/src/pages/application/page.hbsportals/api-portal/src/pages/application/partials/manage-keys.hbsportals/api-portal/src/pages/application/partials/overview.hbsportals/api-portal/src/pages/applications/partials/applications-listing.hbsportals/api-portal/src/pages/error-layout/main.hbsportals/api-portal/src/pages/login-page/layout.hbsportals/api-portal/src/pages/partials/alert.hbsportals/api-portal/src/pages/partials/api-specification.hbsportals/api-portal/src/pages/partials/warning.hbsportals/api-portal/src/pages/settings/page.hbsportals/api-portal/src/pages/subscriptions/page.hbsportals/api-portal/src/routes/api/apiPortalRouter.jsportals/api-portal/src/routes/pages/customPageRoute.jsportals/api-portal/src/routes/pages/designModeRoute.jsportals/api-portal/src/routes/pages/orgContentRoute.jsportals/api-portal/src/routes/pages/tryoutProxyRoute.jsportals/api-portal/src/scripts/common.jsportals/api-portal/src/scripts/dev-reload.jsportals/api-portal/src/scripts/manage-api-workflows.jsportals/api-portal/src/server.jsportals/api-portal/src/services/apiWorkflowService.jsportals/api-portal/src/styles/subscriptions-page.cssportals/api-portal/src/utils/basePathAssetRefs.test.jsportals/api-portal/src/utils/constants.jsportals/api-portal/src/utils/sampleApiLoader.jsportals/api-portal/src/utils/util.jstests/integration-e2e/suite_test.go
Both follow the precedent set for AI Workspace in wso2#3119. Session and XSRF cookies moved from Path=/ to Path=/api-portal, but a browser keys a cookie by (name, domain, path), so an expiry written for one path creates a separate cookie instead of removing one at another. A pre-upgrade cookie at / would keep being sent with nothing able to remove it — and express-session emits no Set-Cookie at all once req.session is destroyed, so it never expires even its own. Expire both names at both paths wherever a session is torn down. Health is now served at /health and ${BASE_PATH}/health: probes dial the pod directly with no ingress to add the prefix, while an ingress-routed check only ever sees the prefixed path. Also corrects the rationale comment on the BASE_PATH-scoped session mount. It described a cookie-clobbering bug that express-session's own pathname-mismatch guard already prevents; the real defect was that req.session is absent for root-path requests, so passport.session() errored and every unmatched root path (including the /favicon.ico browsers fetch unprompted) answered 500 instead of 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Purpose
$subject
Mirrors what #3119 did for AI Workspace, and follows the same conventions.
Approach
The prefix is a fixed contract — every server-generated URL, cookie path, OpenAPI server base and asset reference has to agree with the mount point, so it is declared once and never configured:
src/utils/constants.js—ROUTE.BASE_PATH = '/api-portal', the single source of truthapp.jsmounts the entire route tree on one parent router atBASE_PATH, so sub-routers keep their bare paths (/:orgName/views/...) and need no knowledge of the prefix{{basePath}}/styles/...via a new Handlebars helper, so the references can't drift out of sync with the mount pointWhat stays at the true root
/health— also registered under the prefix. ContainerHEALTHCHECKand Kubernetes probes dial the pod directly with no ingress to add the prefix; an ingress-routed check only ever sees the prefixed path, so both exist.GET /— a convenience redirect into${BASE_PATH}/, for anyone hitting the container root directly. Only the exact root; nothing else unprefixed is served./robots.txt,/llms.txt— agent-discovery entry points, which belong at the origin.Session, passport and CSRF are mounted at the prefix, not app-wide
The session cookie is scoped to
path: BASE_PATH, andexpress-sessionskips itself entirely when the request path falls outside its cookie'spath— soreq.sessionis absent for true-root requests. Left app-wide,passport.session()then errors with "Login sessions require session support", and every unmatched root path (including the/favicon.icobrowsers fetch unprompted) answers 500 with a logged stack trace instead of a plain 404. Mounting all three at the prefix keeps the root a clean 404 and keeps healthcheck probes off the session store.Cookies are expired at both paths on logout
Session and XSRF cookies moved from
Path=/toPath=/api-portal. A browser keys a cookie by(name, domain, path), so an expiry written for one path creates a separate cookie rather than removing one at another — a pre-upgrade cookie at/would keep being sent with nothing able to remove it.express-sessionalso emits noSet-Cookieat all oncereq.sessionis destroyed, so it never expires even its own.clearPortalCookies()expires both names at both paths wherever a session is torn down.Reserved organization handles
/:orgNameis matched in the same namespace as the portal's own mounts, so a handle equal to one of them would make the organization silently unreachable —logout, for instance, would answer the org's front door by destroying the session.configLoadernow refuses to start on a reserved handle rather than letting it fail as a page that never loads.Theme pipeline
Uploaded themes are rewritten at upload/render time to point at the view's asset endpoint, and those rewrites key off the literal shapes templates use — so introducing
{{basePath}}had to be matched on the rewrite side:rewriteViewStylesnow consumes the{{basePath}}token instead of matching after it; the replacement already carries the prefix, so leaving it emitted/api-portaltwice and 404'd every stylesheet on a themed view@importrewriting covers every sibling stylesheet (relative, root-absolute or prefixed), not the three that were hardcoded — a relative import would otherwise resolve against the asset endpoint's directory and 404{{basePath}}/images/...and the stylesheet-relative../images/...validateScriptsallowlist derives its{{basePath}}variants mechanically rather than listing each tag twice, so the templates and the allowlist can't drift apart againBreaking changes / upgrade notes
https://<host>/api-portal/<org-handle>/callbackhttps://<host>/api-portal/<org-handle>path: /api-portal, pathType: Prefixwith no rewriting. Probes keep using/healthat the root./api-portal/api/v0.9— any external caller or script hitting/api/v0.9/...needs updating.organization.handle(api,logout,signin,styles, …) now fails startup instead of silently breaking that organization's routes./are cleaned up on the next logout — no manual cookie clearing needed.