From 59045824f61d6765ce0a364d5880e6df6d1ad4bf Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Thu, 30 Jul 2026 23:34:21 +0530 Subject: [PATCH 1/4] Fix design mode --- portals/api-portal/.dockerignore | 11 ++++++--- .../api-portal/configs/config-template.toml | 18 +++++++++++---- portals/api-portal/distribution/README.md | 11 +++++++++ portals/api-portal/src/config/configLoader.js | 20 ++++++++++++++-- portals/api-portal/src/db/driver.js | 23 +++++++++++++++++++ .../partials/api-subscription-plans.hbs | 4 +++- .../partials/mcp-subscription-plans.hbs | 4 +++- .../src/defaultContent/partials/header.hbs | 4 +++- .../src/defaultContent/partials/sidebar.hbs | 15 ++++++++---- .../src/defaultContent/styles/components.css | 11 ++++++++- .../src/helpers/handlebarsHelpers.js | 8 +++++++ .../partials/applications-listing.hbs | 3 ++- portals/api-portal/src/server.js | 18 +++++++++++---- 13 files changed, 127 insertions(+), 23 deletions(-) diff --git a/portals/api-portal/.dockerignore b/portals/api-portal/.dockerignore index 05ee1c0e0b..fd45cc9c38 100644 --- a/portals/api-portal/.dockerignore +++ b/portals/api-portal/.dockerignore @@ -29,9 +29,14 @@ docker-compose*.yaml # ── Database init / seed scripts (volume-mounted into postgres) ─── artifacts/ -# ── Sample APIs/MCPs (deployed via ./scripts/seed-samples.sh against the -# REST API, not read from disk by the running app) ───────────────── -samples/ +# ── Sample APIs/MCPs ────────────────────────────────────────────── +# Two consumers: ./scripts/seed-samples.sh deploys them via the REST API against +# a running container (host-side), AND design mode reads them straight from disk +# inside the container (the [api_portal.design_mode] api_samples_path/... default +# to ./samples/*). So they must be baked into the image. Sample dirs are source +# trees, never archive drops — exclude any stray .zip so a developer's local one +# can't get baked in (COPY honors this file, not .gitignore's repo-wide *.zip). +samples/**/*.zip # ── Host-side operational scripts (setup.sh, seed-samples.sh) — run against # a running container from the host, never executed inside it ────── diff --git a/portals/api-portal/configs/config-template.toml b/portals/api-portal/configs/config-template.toml index 93b29305b4..c74ed22287 100644 --- a/portals/api-portal/configs/config-template.toml +++ b/portals/api-portal/configs/config-template.toml @@ -259,12 +259,22 @@ enabled_types = ["apis", "mcp-servers", "api-workflows"] # ============================================================================= # DESIGN MODE CONFIGURATION # ============================================================================= -# Local, file-based preview mode — APIs/layouts load from disk instead of the -# database. For content authoring and theming workflows only; do not enable -# in a production environment. Disabled (commented) by default. +# Local, file-based preview mode — APIs, MCP servers, layouts, and applications +# render straight from the bundled sample files on disk (samples/, +# src/defaultContent/) instead of the database. The portal opens no database +# connection and never calls the Platform API in this mode, so it also waives the +# encryption_key/session_secret requirement (sessions are in-memory and an +# ephemeral secret is minted at startup). For content-authoring and theming +# previews only; never enable it in a production environment. OFF by default. +# +# To turn it on, copy the whole [api_portal.design_mode] block below into your +# active configs/config.toml (the same copy-it-from-the-template opt-in used for +# AI Workspace) and restart. The sample paths are relative to the app's working +# directory, where the bundled samples/ already live — leave them as-is unless +# you're pointing at your own sample set. # [api_portal.design_mode] -# enabled = false +# enabled = true # path_to_layout = "./src/defaultContent/" # api_samples_path = "./samples/apis/" # mcp_samples_path = "./samples/mcps/" diff --git a/portals/api-portal/distribution/README.md b/portals/api-portal/distribution/README.md index 418a6436ac..359eb3be96 100644 --- a/portals/api-portal/distribution/README.md +++ b/portals/api-portal/distribution/README.md @@ -108,6 +108,17 @@ docker compose --profile ai-workspace up -d AI Workspace comes up at `https://localhost:9643`, backed by the same Platform API. Omitting `--profile ai-workspace` on a later `docker compose` command neither starts nor stops it — an already-running instance keeps running. To stop it explicitly, run `docker compose stop ai-workspace`, or `docker compose --profile ai-workspace down` to remove it. +## Design Mode (optional) + +**Design mode** turns the API Portal into a file-based preview: it renders APIs, MCP servers, layouts, and applications straight from the bundled sample files instead of the database. With it on, the portal **opens no database connection and never calls the Platform API** — it's purely for previewing content and theming. Do **not** enable it in production. + +Like AI Workspace, it's an opt-in you turn on by editing `configs/config.toml` — there's no separate config file or Compose profile: + +1. **Copy the `[api_portal.design_mode]` block** from the "DESIGN MODE CONFIGURATION" section of the shipped `configs/config-template.toml` into `configs/config.toml` (keep `enabled = true`). The sample paths are already correct for the bundled samples — leave them as-is unless you're pointing at your own set. +2. **Restart the API Portal:** `docker compose up -d` (or `docker compose restart api-portal`). + +The portal then serves from disk at `/views/default` (e.g. `http://localhost:9543/views/default`). Because design mode never touches the database, the accompanying Platform API and its database go unused while it's on — set `enabled` back to `false` and restart to return to the normal, database-backed portal. + ## Configuration All settings live in the single `configs/config.toml`. It carries two sections — `[api_portal.*]` and `[platform_api.*]` — and the **same file is mounted into both containers**; each service reads only its own section and ignores the other's. Edit it in place — no rebuild required, just restart the affected service. diff --git a/portals/api-portal/src/config/configLoader.js b/portals/api-portal/src/config/configLoader.js index b30d1bee92..3cd599bc12 100644 --- a/portals/api-portal/src/config/configLoader.js +++ b/portals/api-portal/src/config/configLoader.js @@ -20,6 +20,7 @@ const path = require('path'); const fs = require('fs'); +const crypto = require('crypto'); const toml = require('smol-toml'); const Handlebars = require('handlebars'); const { DEFAULTS } = require('./configDefaults'); @@ -365,8 +366,23 @@ function requireHexSecret(value, fieldName) { } } -requireHexSecret(config.security.encryptionKey, 'encryptionKey'); -requireHexSecret(config.security.sessionSecret, 'sessionSecret'); +// Design mode renders entirely from disk: no database, so there are no stored +// secrets to encrypt (encryptionKey stays unused — createCryptoUtil is lazy and +// only throws if an encrypt/decrypt actually runs, which the disabled +// webhook/subscription paths never do here), and sessions use an in-memory store +// (see sessionStoreConfig.js). The session cookie is still signed, so mint an +// ephemeral secret when none was supplied rather than forcing an operator to +// configure secret files a local preview never persists anything with. This is +// the one place the fail-closed secret requirement is waived, gated on the +// explicit, off-by-default design_mode.enabled flag. +if (config.designMode?.enabled) { + if (!config.security.sessionSecret) { + config.security.sessionSecret = crypto.randomBytes(32).toString('hex'); + } +} else { + requireHexSecret(config.security.encryptionKey, 'encryptionKey'); + requireHexSecret(config.security.sessionSecret, 'sessionSecret'); +} /** * Fail-closed startup check: database connection-pool settings must resolve to diff --git a/portals/api-portal/src/db/driver.js b/portals/api-portal/src/db/driver.js index e078c4f48d..b89ad37e66 100644 --- a/portals/api-portal/src/db/driver.js +++ b/portals/api-portal/src/db/driver.js @@ -65,7 +65,30 @@ const dialect = config.database.driver; */ const txStorage = new AsyncLocalStorage(); +// Design mode renders every page from disk (see app.js's designMode branch) and +// never issues a query, so it needs no database connection at all — not even the +// empty sqlite file the sqlite adapter would create on open, and not the native +// better-sqlite3 binding it would load. Return a stub whose query methods fail +// loudly: reaching one means a code path that should have been gated on +// design_mode.enabled slipped through, which we want surfaced, not silently run +// against a phantom connection. +function createDesignModeStub() { + const refuse = () => { + throw new Error('Database access is not available in design mode (design_mode.enabled = true).'); + }; + return { + query: refuse, + queryOne: refuse, + execute: refuse, + withTransaction: refuse, + close: () => {}, + }; +} + function loadAdapter() { + if (config.designMode?.enabled) { + return createDesignModeStub(); + } switch (dialect) { case rebindHelpers.DIALECTS.SQLITE: return require('./adapters/sqliteAdapter').createSqliteAdapter(config); diff --git a/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs b/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs index 7624a0fc81..64ecc68fe7 100644 --- a/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs +++ b/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs @@ -55,7 +55,9 @@ >Subscribe {{/if}} {{else}} - Subscribe + {{!-- Unauthenticated Subscribe links to login, which 404s in design mode — disable it there. --}} + Subscribe {{/if}}
diff --git a/portals/api-portal/src/defaultContent/pages/mcp-landing/partials/mcp-subscription-plans.hbs b/portals/api-portal/src/defaultContent/pages/mcp-landing/partials/mcp-subscription-plans.hbs index 0e7e932062..ce7a16eecc 100644 --- a/portals/api-portal/src/defaultContent/pages/mcp-landing/partials/mcp-subscription-plans.hbs +++ b/portals/api-portal/src/defaultContent/pages/mcp-landing/partials/mcp-subscription-plans.hbs @@ -71,7 +71,9 @@ >Subscribe {{/if}} {{else}} - Subscribe + {{!-- Unauthenticated Subscribe links to login, which 404s in design mode — disable it there. --}} + Subscribe {{/if}}
diff --git a/portals/api-portal/src/defaultContent/partials/header.hbs b/portals/api-portal/src/defaultContent/partials/header.hbs index 45e06f9c20..1d202cb441 100644 --- a/portals/api-portal/src/defaultContent/partials/header.hbs +++ b/portals/api-portal/src/defaultContent/partials/header.hbs @@ -31,7 +31,9 @@ {{else}}
- + {{!-- Login isn't served in design mode, so render it disabled (no href) rather than linking to a 404 --}} +
{{/if}}
diff --git a/portals/api-portal/src/defaultContent/partials/sidebar.hbs b/portals/api-portal/src/defaultContent/partials/sidebar.hbs index e60f506d1b..17b011d859 100644 --- a/portals/api-portal/src/defaultContent/partials/sidebar.hbs +++ b/portals/api-portal/src/defaultContent/partials/sidebar.hbs @@ -20,7 +20,9 @@ Documentation {{#if showApiKeysNav}} - + {{!-- The API Keys view isn't served in design mode; disable the (JS-driven) link there. --}} + API Keys @@ -44,7 +46,10 @@ {{/if}} - + {{!-- API Workflows, Subscriptions and API Keys have no route in design mode; render + them disabled (no href) so they stay visible but don't lead to a 404. --}} + API Workflows @@ -52,11 +57,13 @@ Applications - + Subscriptions - + API Keys diff --git a/portals/api-portal/src/defaultContent/styles/components.css b/portals/api-portal/src/defaultContent/styles/components.css index 84e0342e23..ebd1aa51ee 100644 --- a/portals/api-portal/src/defaultContent/styles/components.css +++ b/portals/api-portal/src/defaultContent/styles/components.css @@ -747,4 +747,13 @@ @media (max-width: 1024px) { .aov-body { flex-direction: column; } .aov-body-sidebar { width: 100%; } -} \ No newline at end of file +} +/* Design mode: nav/action buttons whose routes aren't served in this mode are + rendered without an href and with this class so they stay visible but are + non-interactive (no click, no hover, no keyboard focus) instead of leading to + a 404. See partials/header.hbs and partials/sidebar.hbs. */ +.design-disabled { + pointer-events: none; + opacity: 0.45; + cursor: default; +} diff --git a/portals/api-portal/src/helpers/handlebarsHelpers.js b/portals/api-portal/src/helpers/handlebarsHelpers.js index 5dd9fb6d02..f7955b925c 100644 --- a/portals/api-portal/src/helpers/handlebarsHelpers.js +++ b/portals/api-portal/src/helpers/handlebarsHelpers.js @@ -17,8 +17,16 @@ */ const Handlebars = require('handlebars'); +const { config } = require('../config/configLoader'); const helpers = { + // True only when the portal is running in file-based design mode. Partials use + // it to disable nav/action buttons whose routes aren't served in design mode + // (login, subscriptions, api-keys, api-workflows) so they don't lead to a 404. + // Read from config at render time so it's correct regardless of which render + // path (design vs. normal) produced the context. + designMode: () => config.designMode?.enabled === true, + // Array helpers filterByStatus: (array, status) => { if (!Array.isArray(array)) return []; diff --git a/portals/api-portal/src/pages/applications/partials/applications-listing.hbs b/portals/api-portal/src/pages/applications/partials/applications-listing.hbs index 3932347ee0..120c53861a 100644 --- a/portals/api-portal/src/pages/applications/partials/applications-listing.hbs +++ b/portals/api-portal/src/pages/applications/partials/applications-listing.hbs @@ -35,8 +35,9 @@ {{#if applicationsMetadata.length}}
{{#applicationsMetadata}} + {{!-- The application detail page isn't served in design mode, so the card isn't a link there (no onclick / pointer cursor). --}}
+ {{#unless (designMode)}}style="cursor:pointer;" onclick="window.location.href='{{../baseUrl}}/applications/{{id}}'"{{/unless}}>
diff --git a/portals/api-portal/src/server.js b/portals/api-portal/src/server.js index 1bd7af3a41..fa4b82d0bd 100644 --- a/portals/api-portal/src/server.js +++ b/portals/api-portal/src/server.js @@ -72,11 +72,13 @@ function printBanner(visitUrl) { } function logStartupBanner() { - const orgSegment = config.designMode?.enabled ? '' : `/${orgContext.getHandle()}`; - // The bare org URL redirects server-side to /views/default (orgContentRoute.js) — - // shorter and avoids baking view-naming details into the banner. + // Non-design mode: the bare org URL (/{handle}) redirects server-side to + // /{handle}/views/default (orgContentRoute.js). Design mode has no organization + // segment and no such redirect, so point straight at the default view — the + // bare root 404s there (only /views/* is served). + const landingPath = config.designMode?.enabled ? '/views/default' : `/${orgContext.getHandle()}`; const scheme = config.server.https.enabled && !config.designMode?.enabled ? 'https' : 'http'; - const visitUrl = `${scheme}://localhost:${PORT}${orgSegment}`; + const visitUrl = `${scheme}://localhost:${PORT}${landingPath}`; printBanner(visitUrl); } @@ -127,7 +129,13 @@ async function startServer() { // queries to it. Seeding here means a failure aborts startup (see the catch on // the startServer() call below) rather than leaving a process that answers // /health 200 while being unable to serve a single page. - await seedDefaultOrg(); + // + // Design mode renders from disk and never touches the organization tables (nor + // any database), so there is nothing to seed — skip it, matching ensureSchema() + // above and the designMode branch in app.js. + if (!config.designMode?.enabled) { + await seedDefaultOrg(); + } if (!config.server.https.enabled || config.designMode?.enabled) { server = http.createServer(app).listen(PORT, '0.0.0.0', onListening); From dae0b722e87b7fe228e1569b1993cb970599bfdb Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Thu, 30 Jul 2026 23:38:34 +0530 Subject: [PATCH 2/4] Update docs --- portals/api-portal/README.md | 6 +++--- portals/api-portal/distribution/README.md | 6 +++--- portals/api-portal/docs/administer/asgardeo-setup.md | 2 +- portals/api-portal/docs/administer/authentication.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/portals/api-portal/README.md b/portals/api-portal/README.md index 3674be82d6..b68cd46b0a 100644 --- a/portals/api-portal/README.md +++ b/portals/api-portal/README.md @@ -243,7 +243,7 @@ Prompts for the admin username/password (or set `ADMIN_USERNAME`/`ADMIN_PASSWORD ## Configuration Reference -All settings live in `configs/config.toml`. Every setting can also be overridden with an `APIP_AP_*` environment variable. +All settings live in `configs/config.toml`. There is no automatic `APIP_AP_*` override layer: an environment variable takes effect only for a setting whose `config.toml` entry explicitly references it with a `{{ env "NAME" "fallback" }}` token (see [Environment variable overrides](#environment-variable-overrides)). The full annotated list of settings is in [`configs/config-template.toml`](configs/config-template.toml). @@ -265,9 +265,9 @@ Note there are two files with this name, read by different components in differe | File | Read by | When | |---|---|---| | [`platform-api/resources/role-to-scope-mapping.yaml`](../../platform-api/resources/role-to-scope-mapping.yaml) | Platform API | Local auth — expands a file user's `roles` into the `scope` claim of the token it issues (roles named `ap_*`) | -| [`resources/role-to-scope-mapping.yaml`](resources/role-to-scope-mapping.yaml) | This portal | `auth.authorization.mode = "role"` — expands an incoming token's roles claim on every request (roles named `dp_admin`, `dp_subscriber`) | +| [`resources/role-to-scope-mapping.yaml`](resources/role-to-scope-mapping.yaml) | This portal | `auth.authorization.mode = "role"` (the default) — expands an incoming token's roles claim on every request to authorize the portal's REST surface. Recognises `dp_admin`/`dp_subscriber`, and **aliases** `ap_admin`/`ap_subscriber` so local-auth tokens (which carry `ap_*` roles) are authorized unchanged | -Local auth uses the first; an external IDP in role mode uses the second. See [Authorization](docs/administer/authentication.md#authorization). +Because role mode is the default, the local-auth quickstart uses **both** files, each governing a different surface: the Platform API expands the file user's `roles` into the token it issues (and authorizes its own surface with that mapping), and the portal then expands that same token's roles claim on every request to authorize its REST surface — which is why the portal file aliases `ap_admin`/`ap_subscriber`. An external IDP in role mode drives the portal file directly with its own groups. So to change what a role may do, edit the file for the surface you mean — the portal file for the portal's REST permissions, the Platform API file for the Platform API's — and both if the change must hold across both components. See [Authorization](docs/administer/authentication.md#authorization). The portal config (or `APIP_AP_AUTH_LOCAL_*` env vars) must point to the Platform API. `config.toml`'s own defaults assume Docker Compose, where `platform-api` is a resolvable hostname on the compose network — `npm run start:local` already overrides `platform_api_url` to `https://localhost:9243` (the sidecar's port published to the host) and `tls_skip_verify = true` (self-signed cert), so no manual edit is needed for that flow: diff --git a/portals/api-portal/distribution/README.md b/portals/api-portal/distribution/README.md index 359eb3be96..065ac70158 100644 --- a/portals/api-portal/distribution/README.md +++ b/portals/api-portal/distribution/README.md @@ -137,10 +137,10 @@ Environment overrides go in `api-platform.env` (git-ignored; loaded into both co | `[api_portal.auth.local].platform_api_url` | Address of the Platform API local-auth sidecar | `https://platform-api:9243` | | `[api_portal.auth.local].public_key_path` | Path to the Platform API RS256 public key PEM used to verify login tokens | `/etc/api-portal/keys/jwt_public.pem` | | `[api_portal.auth.authorization].enabled` | Enforce each REST operation's declared `dp:*` scopes. `false` lets any authenticated caller through — development only | `true` | -| `[api_portal.auth.authorization].mode` | `scope` reads the token's own scope claim; `role` expands its roles claim through the grant table instead (for an IDP that emits roles, not `dp:*` scopes) | `scope` | -| `[api_portal.auth.authorization].role_to_scope_mapping` | Path to the mounted `resources/api-portal/role-to-scope-mapping.yaml` — required in `role` mode; edit that file to change what a role grants | _(empty)_ | +| `[api_portal.auth.authorization].mode` | `role` (the default) expands the token's roles claim through the grant table; `scope` reads the token's own scope claim instead | `role` | +| `[api_portal.auth.authorization].role_to_scope_mapping` | Path to the mounted `resources/api-portal/role-to-scope-mapping.yaml` — used in `role` mode; edit that file to change what a role grants | `./resources/role-to-scope-mapping.yaml` | | `[api_portal.auth.authorization].page_role_validation` | Gate portal pages on the caller's role tier (`portal_roles` below). Separate from `enabled`, which governs REST scopes | `false` | -| `[api_portal.auth.authorization.portal_roles]` | Which role name in the token's roles claim grants each page tier (`admin`, `super_admin`, `subscriber`) | `admin`, `superAdmin`, `Internal/subscriber` | +| `[api_portal.auth.authorization.portal_roles]` | Which role name in the token's roles claim grants each page tier (`admin`, `subscriber`) | `admin`, `Internal/subscriber` | | `[api_portal.organization].handle` | The single organization this instance serves, bootstrapped on first start. Required — the portal refuses to start without it | `default` | | `[api_portal.organization].display_name` | Display name applied when the organization is first seeded | `Default` | diff --git a/portals/api-portal/docs/administer/asgardeo-setup.md b/portals/api-portal/docs/administer/asgardeo-setup.md index 61a149c385..6ce82e72e3 100644 --- a/portals/api-portal/docs/administer/asgardeo-setup.md +++ b/portals/api-portal/docs/administer/asgardeo-setup.md @@ -97,7 +97,7 @@ For local development, the default `ASGARDEO_RESOURCE_IDENTIFIER=https://localho Update `configs/config.toml`: ```toml -[idp] +[api_portal.auth.idp] name = "Asgardeo" issuer = "https://api.asgardeo.io/t//oauth2/token" authorization_url = "https://api.asgardeo.io/t//oauth2/authorize" diff --git a/portals/api-portal/docs/administer/authentication.md b/portals/api-portal/docs/administer/authentication.md index 563acb422e..2abe74f57e 100644 --- a/portals/api-portal/docs/administer/authentication.md +++ b/portals/api-portal/docs/administer/authentication.md @@ -257,7 +257,7 @@ When multiple devportal organizations share one IDP, the portal enforces per-org ## Keycloak Example ```toml -[idp] +[api_portal.auth.idp] name = "Keycloak" issuer = "https://keycloak.example.com/realms/myrealm" authorization_url = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth" From 37e8f84d44d5d9412270747e9232c913e070f7f2 Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Thu, 30 Jul 2026 23:40:09 +0530 Subject: [PATCH 3/4] Fix design mode sample mount issue --- portals/api-portal/.dockerignore | 14 +++++++------- portals/api-portal/Makefile | 5 +++++ portals/api-portal/distribution/README.md | 4 +++- portals/api-portal/docker-compose.yaml | 6 ++++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/portals/api-portal/.dockerignore b/portals/api-portal/.dockerignore index fd45cc9c38..dfdac31a07 100644 --- a/portals/api-portal/.dockerignore +++ b/portals/api-portal/.dockerignore @@ -30,13 +30,13 @@ docker-compose*.yaml artifacts/ # ── Sample APIs/MCPs ────────────────────────────────────────────── -# Two consumers: ./scripts/seed-samples.sh deploys them via the REST API against -# a running container (host-side), AND design mode reads them straight from disk -# inside the container (the [api_portal.design_mode] api_samples_path/... default -# to ./samples/*). So they must be baked into the image. Sample dirs are source -# trees, never archive drops — exclude any stray .zip so a developer's local one -# can't get baked in (COPY honors this file, not .gitignore's repo-wide *.zip). -samples/**/*.zip +# Not baked into the image. Both consumers reach them from OUTSIDE the image: +# - ./scripts/seed-samples.sh deploys them via the REST API (host-side), and +# - design mode reads them from disk via a bind-mount — docker-compose.yaml +# mounts ./samples at /app/samples (the standalone dist rewrites this to +# ./resources/samples; see the Makefile), so operators can edit the sample +# set and restart to preview it without rebuilding the image. +samples/ # ── Host-side operational scripts (setup.sh, seed-samples.sh) — run against # a running container from the host, never executed inside it ────── diff --git a/portals/api-portal/Makefile b/portals/api-portal/Makefile index 627507c464..35f98fbe56 100644 --- a/portals/api-portal/Makefile +++ b/portals/api-portal/Makefile @@ -270,6 +270,10 @@ dist: clean-dist ## Build standalone API Portal distribution zip @mkdir -p $(DIST_DIR)/resources/samples @cp -R samples/apis $(DIST_DIR)/resources/samples/ @cp -R samples/mcps $(DIST_DIR)/resources/samples/ +# subscription-plans.yaml and applications.yaml complete the design-mode sample +# set (seed-samples.sh only needs apis/ + mcps/, but design mode reads these too). + @cp samples/subscription-plans.yaml $(DIST_DIR)/resources/samples/ + @cp samples/applications.yaml $(DIST_DIR)/resources/samples/ # Stage platform-api configs + db-scripts (from tag or working tree) for the merge below. ifeq ($(PLATFORM_API_FROM_TAG),true) @echo "Fetching platform-api configs and db-scripts from tag $(PLATFORM_API_TAG)..." @@ -317,6 +321,7 @@ endif @sed -e 's#\.\./\.\./platform-api/config/config\.toml:#./configs/config.toml:#' \ -e 's#\.\./\.\./platform-api/resources/role-to-scope-mapping\.yaml:#./resources/platform-api/role-to-scope-mapping.yaml:#' \ -e 's#\./resources/role-to-scope-mapping\.yaml:#./resources/api-portal/role-to-scope-mapping.yaml:#' \ + -e 's#\./samples:/app/samples:#./resources/samples:/app/samples:#' \ docker-compose.yaml > $(DIST_DIR)/docker-compose.yaml @cp distribution/README.md $(DIST_DIR)/README.md @mkdir -p $(DIST_DIR)/scripts diff --git a/portals/api-portal/distribution/README.md b/portals/api-portal/distribution/README.md index 065ac70158..fe9e534195 100644 --- a/portals/api-portal/distribution/README.md +++ b/portals/api-portal/distribution/README.md @@ -114,11 +114,13 @@ AI Workspace comes up at `https://localhost:9643`, backed by the same Platform A Like AI Workspace, it's an opt-in you turn on by editing `configs/config.toml` — there's no separate config file or Compose profile: -1. **Copy the `[api_portal.design_mode]` block** from the "DESIGN MODE CONFIGURATION" section of the shipped `configs/config-template.toml` into `configs/config.toml` (keep `enabled = true`). The sample paths are already correct for the bundled samples — leave them as-is unless you're pointing at your own set. +1. **Copy the `[api_portal.design_mode]` block** from the "DESIGN MODE CONFIGURATION" section of the shipped `configs/config-template.toml` into `configs/config.toml` (keep `enabled = true`). The sample paths are already correct for the bundled samples — leave them as-is. 2. **Restart the API Portal:** `docker compose up -d` (or `docker compose restart api-portal`). The portal then serves from disk at `/views/default` (e.g. `http://localhost:9543/views/default`). Because design mode never touches the database, the accompanying Platform API and its database go unused while it's on — set `enabled` back to `false` and restart to return to the normal, database-backed portal. +The sample content lives in **`resources/samples/`** (`apis/`, `mcps/`, `applications.yaml`, `subscription-plans.yaml`), which the API Portal container mounts at `/app/samples`. To preview **your own** APIs and MCP servers, add or edit files there and restart — no image rebuild needed. + ## Configuration All settings live in the single `configs/config.toml`. It carries two sections — `[api_portal.*]` and `[platform_api.*]` — and the **same file is mounted into both containers**; each service reads only its own section and ignores the other's. Edit it in place — no rebuild required, just restart the affected service. diff --git a/portals/api-portal/docker-compose.yaml b/portals/api-portal/docker-compose.yaml index 28d9cd1f11..f26aab1159 100644 --- a/portals/api-portal/docker-compose.yaml +++ b/portals/api-portal/docker-compose.yaml @@ -62,6 +62,12 @@ services: # editing this file on the host changes what a role grants after a restart, with # no rebuild and no env-var indirection. - ./resources/role-to-scope-mapping.yaml:/app/resources/role-to-scope-mapping.yaml:ro + # Design-mode sample content (APIs, MCP servers, applications, subscription + # plans), read from disk when [api_portal.design_mode] is enabled. Edit these + # files on the host and restart to preview your own content — no image rebuild + # (design mode isn't baked into the image). Unused when design mode is off. The + # standalone dist rewrites the host path to ./resources/samples (see Makefile). + - ./samples:/app/samples:ro - ./resources/certificates:/etc/api-portal/tls:ro - ./resources/keys/jwt_public.pem:/etc/api-portal/keys/jwt_public.pem:ro - ./resources/keys/api-portal-encryption.key:/etc/api-portal/keys/encryption.key:ro From e8e9069721ff57d95e69b997cdfbc5649bd5d527 Mon Sep 17 00:00:00 2001 From: Piumal Rathnayake Date: Thu, 30 Jul 2026 23:55:19 +0530 Subject: [PATCH 4/4] Address comments --- portals/api-portal/docker-compose.yaml | 2 +- portals/api-portal/src/config/configLoader.js | 5 +++++ .../applications/partials/applications-listing.hbs | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/portals/api-portal/docker-compose.yaml b/portals/api-portal/docker-compose.yaml index f26aab1159..4a44e9dd88 100644 --- a/portals/api-portal/docker-compose.yaml +++ b/portals/api-portal/docker-compose.yaml @@ -39,7 +39,7 @@ services: networks: - api-portal-network - devportal: + api-portal: image: ghcr.io/wso2/api-platform/api-portal:1.0.0-SNAPSHOT container_name: api-portal restart: unless-stopped diff --git a/portals/api-portal/src/config/configLoader.js b/portals/api-portal/src/config/configLoader.js index 3cd599bc12..1bd3a14491 100644 --- a/portals/api-portal/src/config/configLoader.js +++ b/portals/api-portal/src/config/configLoader.js @@ -379,6 +379,11 @@ if (config.designMode?.enabled) { if (!config.security.sessionSecret) { config.security.sessionSecret = crypto.randomBytes(32).toString('hex'); } + // Validate whatever we ended up with — the freshly minted value passes, and an + // operator-supplied one is held to the same format check as every other mode + // rather than silently bypassing it. encryptionKey stays unchecked: it is + // genuinely unused in design mode (no database, createCryptoUtil is lazy). + requireHexSecret(config.security.sessionSecret, 'sessionSecret'); } else { requireHexSecret(config.security.encryptionKey, 'encryptionKey'); requireHexSecret(config.security.sessionSecret, 'sessionSecret'); diff --git a/portals/api-portal/src/pages/applications/partials/applications-listing.hbs b/portals/api-portal/src/pages/applications/partials/applications-listing.hbs index 120c53861a..38f12f4fe9 100644 --- a/portals/api-portal/src/pages/applications/partials/applications-listing.hbs +++ b/portals/api-portal/src/pages/applications/partials/applications-listing.hbs @@ -11,10 +11,13 @@

Applications hold the API keys and subscriptions you use to call APIs.

{{#if applicationsMetadata.length}} + {{!-- Create/Delete mutate through the REST API, which design mode can't serve (no database) — omit them there. --}} + {{#unless (designMode)}} + {{/unless}} {{/if}}
@@ -24,10 +27,12 @@

No applications yet

Create an application to generate API keys and subscribe to APIs. You can manage all your credentials in one place.

+ {{#unless (designMode)}} + {{/unless}}
{{/unless}} @@ -49,10 +54,12 @@

{{description}}

@@ -62,6 +69,8 @@ +{{!-- Create/Delete modals and their handlers are omitted in design mode (no REST API to call). --}} +{{#unless (designMode)}} +{{/unless}}