{"id":47716934,"url":"https://github.com/cloud-apim/otoroshi-api-portal","last_synced_at":"2026-04-02T19:02:07.192Z","repository":{"id":346375144,"uuid":"1078473288","full_name":"cloud-apim/otoroshi-api-portal","owner":"cloud-apim","description":"Dev portal for Otoroshi APIs","archived":false,"fork":false,"pushed_at":"2026-03-23T16:31:18.000Z","size":523,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T12:38:21.003Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cloud-apim.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-17T19:42:16.000Z","updated_at":"2026-03-23T16:31:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cloud-apim/otoroshi-api-portal","commit_stats":null,"previous_names":["cloud-apim/otoroshi-api-portal"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/cloud-apim/otoroshi-api-portal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloud-apim%2Fotoroshi-api-portal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloud-apim%2Fotoroshi-api-portal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloud-apim%2Fotoroshi-api-portal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloud-apim%2Fotoroshi-api-portal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cloud-apim","download_url":"https://codeload.github.com/cloud-apim/otoroshi-api-portal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloud-apim%2Fotoroshi-api-portal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31313856,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-02T19:01:43.719Z","updated_at":"2026-04-02T19:02:07.181Z","avatar_url":"https://github.com/cloud-apim.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Otoroshi API Portal\n\nA lightweight, self-contained developer portal plugin for [Otoroshi](https://www.otoroshi.io/). It turns any Otoroshi API into a full-featured portal with documentation rendering, OpenAPI exploration, self-service API key subscription, and a built-in API tester -- all without deploying a separate frontend application.\n\n## Why use it?\n\nIf you expose APIs through Otoroshi and need a developer-facing portal, you typically have two options: build a custom frontend or integrate a heavy third-party solution. This plugin offers a third path:\n\n- **Zero infrastructure** -- the portal is served directly by Otoroshi as a backend plugin. No extra server, no Node.js app, no static hosting to manage.\n- **Configuration-driven** -- everything (pages, navigation, OpenAPI specs, subscription plans) is declared in the API's `documentation` JSON section. Update the config, the portal updates instantly.\n- **Self-service subscriptions** -- authenticated users can subscribe to published plans, get API keys, and start calling your API immediately. Plans support auto-validation or manual approval workflows.\n- **Built-in API tester** -- users can test endpoints directly from the portal using their own API keys, without leaving the browser.\n- **Dark mode, responsive, modern UI** -- the portal ships with a polished Tailwind CSS interface that works on desktop and mobile out of the box.\n\n## Features\n\n- Home page with HTML or Markdown content\n- Documentation pages with sidebar navigation (categories and links)\n- Markdown rendering (via [zero-md](https://github.com/nickabal/zero-md) web component)\n- OpenAPI specification rendering (via [Scalar](https://github.com/scalar/scalar))\n- Multiple OpenAPI spec support with a selection page\n- Self-service API key subscription with plan selection\n- API key management (create, update, delete, copy bearer token)\n- Built-in API tester with request/response inspection\n- Remote content loading (documentation pages and resources fetched from URLs)\n- Remote portal configuration (load the entire `documentation` section from a URL)\n- Redirections\n- Light / Dark / System theme with persistence\n- Authentication integration via Otoroshi auth modules\n- Otoroshi cluster support (leader/worker mode)\n\n## Screenshots\n\n### Home\n\n![home](./screenshots/home.png)\n\n### Documentation\n\n![documentation](./screenshots/documentation.png)\n\n### OpenAPI reference\n\n![openapi](./screenshots/openapi.png)\n\n### Subscriptions\n\n![subscriptions](./screenshots/subscriptions.png)\n\n## Installation\n\n1. Download the latest JAR from the [releases page](https://github.com/cloud-apim/otoroshi-api-portal/releases)\n2. Add the JAR to your Otoroshi plugins directory (see [Otoroshi documentation](https://maif.github.io/otoroshi/manual/plugins/index.html) on loading external plugins)\n3. Restart Otoroshi -- the plugin `Otoroshi API Portal` will appear in the available plugins list\n\n## Setup\n\nUsing the portal requires two things: an **API** with a `documentation` section, and a **Route** that serves the portal.\n\n### 1. Configure your API documentation\n\nIn the Otoroshi admin, create or edit an API (`apis.otoroshi.io/Api`). Add a `documentation` section to it. Here is a minimal example:\n\n```json\n{\n  \"documentation\": {\n    \"enabled\": true,\n    \"home\": {\n      \"path\": \"/home\",\n      \"content_type\": \"text/html\",\n      \"site_page\": true,\n      \"transform\": \"markdown\",\n      \"text_content\": \"# Welcome\\n\\nThis is my API portal.\"\n    },\n    \"logo\": {\n      \"url\": \"https://example.com/logo.png\",\n      \"path\": \"/favicon.png\",\n      \"content_type\": \"image/png\"\n    },\n    \"references\": [\n      {\n        \"title\": \"My API\",\n        \"link\": \"/openapi.json\"\n      }\n    ],\n    \"resources\": [\n      {\n        \"title\": \"My API\",\n        \"path\": \"/openapi.json\",\n        \"content_type\": \"application/json\",\n        \"url\": \"https://example.com/openapi.json\"\n      }\n    ],\n    \"navigation\": [\n      {\n        \"label\": \"Documentation\",\n        \"icon\": { \"css_icon_class\": \"bi bi-journal-text me-2\" },\n        \"path\": \"/documentation\",\n        \"items\": [\n          {\n            \"label\": \"API\",\n            \"kind\": \"category\",\n            \"links\": [\n              {\n                \"label\": \"API Reference\",\n                \"link\": \"/api-references\",\n                \"icon\": { \"css_icon_class\": \"bi bi-braces me-2\" }\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"plans\": [\n      {\n        \"id\": \"free\",\n        \"name\": \"Free\",\n        \"description\": \"A free plan to try the API\",\n        \"access_mode_configuration_type\": \"apikey\",\n        \"access_mode_configuration\": {\n          \"throttling_quota\": 10,\n          \"daily_quota\": 100,\n          \"monthly_quota\": 1000\n        },\n        \"status\": \"published\",\n        \"tags\": [],\n        \"metadata\": {}\n      }\n    ],\n    \"redirections\": [],\n    \"search\": { \"enabled\": true },\n    \"metadata\": {},\n    \"tags\": []\n  }\n}\n```\n\n### 2. Create the portal route\n\nCreate an Otoroshi Route (`proxy.otoroshi.io/Route`) that will serve the portal. The route needs the following plugin chain:\n\n```json\n{\n  \"frontend\": {\n    \"domains\": [\"portal.example.com\"],\n    \"strip_path\": true,\n    \"exact\": false\n  },\n  \"backend\": {\n    \"targets\": [\n      {\n        \"hostname\": \"request.otoroshi.io\",\n        \"port\": 443,\n        \"tls\": true\n      }\n    ]\n  },\n  \"plugins\": [\n    {\n      \"plugin\": \"cp:otoroshi.next.plugins.OverrideHost\",\n      \"enabled\": true,\n      \"config\": {}\n    },\n    {\n      \"plugin\": \"cp:otoroshi.next.plugins.NgAuthModuleUserExtractor\",\n      \"enabled\": true,\n      \"config\": {\n        \"module\": \"\u003cyour-auth-module-id\u003e\"\n      }\n    },\n    {\n      \"plugin\": \"cp:otoroshi.next.plugins.AuthModule\",\n      \"enabled\": true,\n      \"include\": [\"/login\"],\n      \"config\": {\n        \"module\": \"\u003cyour-auth-module-id\u003e\"\n      }\n    },\n    {\n      \"plugin\": \"cp:otoroshi_plugins.com.cloud.apim.plugins.apiportal.OtoroshiApiPortal\",\n      \"enabled\": true,\n      \"config\": {\n        \"api_ref\": \"\u003cyour-api-id\u003e\"\n      }\n    }\n  ]\n}\n```\n\nThe plugins serve different roles:\n\n| Plugin | Role |\n|---|---|\n| `OverrideHost` | Rewrites the Host header for the backend target |\n| `NgAuthModuleUserExtractor` | Extracts the authenticated user on every request (without blocking). This allows the portal to show/hide subscription features based on login state |\n| `AuthModule` (include: `/login`) | Forces authentication **only** on the `/login` path. The portal remains publicly browsable; users log in explicitly to manage subscriptions |\n| `OtoroshiApiPortal` | The portal plugin itself |\n\n### Plugin configuration\n\n| Field | Type | Description |\n|---|---|---|\n| `api_ref` | `string` | **Required**. The ID of the Otoroshi API whose documentation will be rendered |\n| `prefix` | `string` | Optional path prefix if the portal is not served at the root of the domain (e.g. `/portal`) |\n\n## Documentation format reference\n\nThe `documentation` section of the API is a JSON object with the following fields:\n\n### Top-level fields\n\n| Field | Type | Description |\n|---|---|---|\n| `enabled` | `boolean` | Enable or disable the portal |\n| `source` | `object \\| null` | Load the entire documentation config from a remote URL. When set, the `url` field inside must point to a JSON file matching this schema. Useful for managing portal config in a git repository |\n| `home` | `resource` | The home page resource (displayed at `/`) |\n| `logo` | `resource` | The portal logo/favicon resource |\n| `references` | `reference[]` | List of OpenAPI specifications to expose |\n| `resources` | `resource[]` | All servable resources (pages, specs, images, etc.) |\n| `navigation` | `sidebar[]` | Top-level navigation tabs, each containing a sidebar with categories and links |\n| `plans` | `plan[]` | Subscription plans available to users |\n| `redirections` | `redirection[]` | URL redirections |\n| `search` | `object` | Search configuration (`{ \"enabled\": true }`) |\n| `footer` | `object \\| null` | Footer configuration (not yet implemented) |\n| `banner` | `object \\| null` | Banner configuration (not yet implemented) |\n| `metadata` | `object` | Arbitrary key-value metadata |\n| `tags` | `string[]` | Tags |\n\n### Remote source\n\nYou can host your entire documentation configuration as a JSON file and load it remotely:\n\n```json\n{\n  \"documentation\": {\n    \"enabled\": true,\n    \"source\": {\n      \"url\": \"https://raw.githubusercontent.com/my-org/my-api/main/portal.config.json\"\n    }\n  }\n}\n```\n\nThe remote JSON file must follow the same documentation schema. This is useful for managing your portal configuration in version control alongside your API code.\n\n### Resource\n\nA resource represents any servable content: a page, an image, a JSON file, etc.\n\n| Field | Type | Description |\n|---|---|---|\n| `path` | `string` | The URL path where this resource is served (e.g. `/documentation/getting-started`) |\n| `title` | `string` | Optional display title |\n| `content_type` | `string` | MIME type (`text/html`, `text/markdown`, `application/json`, `image/png`, etc.) |\n| `site_page` | `boolean` | If `true`, the resource is rendered inside the portal layout (header, sidebar, etc.). If `false` or absent, it is served raw |\n| `text_content` | `string` | Inline text content (HTML, Markdown, etc.) |\n| `url` | `string` | Fetch the content from this URL at render time. Takes precedence over `text_content` |\n| `base64_content` | `string` | Base64-encoded binary content (for images, etc.) |\n| `json_content` | `object` | Inline JSON content |\n| `transform` | `string` | Apply a transformation: `\"markdown\"` renders Markdown to HTML, `\"redoc\"` renders an OpenAPI spec with Redoc |\n| `transform_wrapper` | `string` | HTML wrapper around transformed content. Use `{content}` as a placeholder (e.g. `\u003cdiv class=\"container\"\u003e{content}\u003c/div\u003e`) |\n\nContent resolution priority: `url` \u003e `base64_content` \u003e `json_content` \u003e `text_content`.\n\n### Reference\n\nA reference points to an OpenAPI specification that will be rendered with Scalar in the API reference section.\n\n| Field | Type | Description |\n|---|---|---|\n| `title` | `string` | Display name of the specification |\n| `link` | `string` | Path to the corresponding resource (e.g. `/openapi.json`). Must match a resource's `path` |\n| `description` | `string` | Optional description shown on the reference selection page |\n| `icon` | `object` | Optional icon (`{ \"css_icon_class\": \"bi bi-rocket\" }`) |\n\nIf you have a single reference, the API reference page shows the spec directly. With multiple references, a selection page is displayed first.\n\n### Navigation (sidebar)\n\nNavigation entries appear as tabs in the top navigation bar. Each entry contains a sidebar with categories and links for its section.\n\n```json\n{\n  \"label\": \"Documentation\",\n  \"icon\": { \"css_icon_class\": \"bi bi-journal-text me-2\" },\n  \"path\": \"/documentation\",\n  \"items\": [\n    {\n      \"label\": \"Guides\",\n      \"kind\": \"category\",\n      \"links\": [\n        {\n          \"label\": \"Getting started\",\n          \"link\": \"/documentation/getting-started\",\n          \"icon\": { \"css_icon_class\": \"bi bi-book me-2\" }\n        }\n      ]\n    },\n    {\n      \"label\": \"FAQ\",\n      \"link\": \"/documentation/faq\",\n      \"icon\": { \"css_icon_class\": \"bi bi-question-circle me-2\" }\n    }\n  ]\n}\n```\n\nItems can be either **categories** (with `\"kind\": \"category\"` and a `links` array) or **direct links** (with a `link` field). Icons use [Bootstrap Icons](https://icons.getbootstrap.com/) CSS classes.\n\n### Plan\n\nPlans define the subscription options available to authenticated users.\n\n| Field | Type | Description |\n|---|---|---|\n| `id` | `string` | Unique identifier for the plan |\n| `name` | `string` | Display name |\n| `description` | `string` | Description shown to users when choosing a plan |\n| `access_mode_configuration_type` | `string` | Type of access: `\"apikey\"` for API key-based access |\n| `access_mode_configuration` | `object` | Quotas: `throttling_quota` (req/sec), `daily_quota`, `monthly_quota` |\n| `status` | `string` | `\"published\"` to make the plan available, any other value hides it |\n| `tags` | `string[]` | Tags applied to generated API keys |\n| `metadata` | `object` | Metadata applied to generated API keys |\n\nPlans with `access_mode_configuration_type: \"apikey\"` enable the **Subscriptions** tab in the portal navigation for logged-in users. When a user subscribes, an API key is generated with the configured quotas and authorized for the API.\n\n### Redirection\n\n```json\n{\n  \"from\": \"/old-path\",\n  \"to\": \"/new-path\"\n}\n```\n\nA `/logout` -\u003e `/.well-known/otoroshi/logout` redirection is always added automatically.\n\n## Full example\n\nHere is a complete documentation section for a Wine API portal with multiple documentation pages, a single OpenAPI spec, and a subscription plan:\n\n```json\n{\n  \"enabled\": true,\n  \"home\": {\n    \"path\": \"/home\",\n    \"content_type\": \"text/html\",\n    \"site_page\": true,\n    \"transform\": \"markdown\",\n    \"transform_wrapper\": \"\u003cdiv class=\\\"container-xxl\\\" style=\\\"margin-top: 30px;\\\"\u003e{content}\u003c/div\u003e\",\n    \"text_content\": \"\u003cdiv class=\\\"container-xxl\\\" style=\\\"margin-top: 30px;\\\"\u003e\\n\\n# Welcome to the Wine API\\n\\nThe **Wine API** gives you access to a rich catalog of wines, grape varieties, wineries, and wine regions.\\n\\n## Getting Started\\n\\n1. **Sign up** for an API key on this portal.\\n2. **Check out the documentation** to learn about available endpoints.\\n3. **Start querying**:\\n\\n   ```bash\\n   curl https://wines-api.example.com/api/wines?region=Bordeaux\\n   ```\\n\\nGo to the [documentation](/documentation) for more details.\\n\u003c/div\u003e\\n\"\n  },\n  \"logo\": {\n    \"url\": \"https://example.com/wine-logo.png\",\n    \"path\": \"/favicon.png\",\n    \"content_type\": \"image/png\"\n  },\n  \"references\": [\n    {\n      \"title\": \"Wines API\",\n      \"link\": \"/openapi.json\"\n    }\n  ],\n  \"resources\": [\n    {\n      \"title\": \"Wines API\",\n      \"path\": \"/openapi.json\",\n      \"content_type\": \"application/json\",\n      \"url\": \"https://wines-api.example.com/docs/openapi.json\"\n    },\n    {\n      \"path\": \"/documentation/getting-started\",\n      \"content_type\": \"text/html\",\n      \"site_page\": true,\n      \"transform\": \"markdown\",\n      \"url\": \"https://raw.githubusercontent.com/my-org/wines-api/main/docs/getting-started.md\"\n    },\n    {\n      \"path\": \"/documentation/latency\",\n      \"content_type\": \"text/markdown\",\n      \"site_page\": true,\n      \"transform\": \"markdown\",\n      \"url\": \"https://raw.githubusercontent.com/my-org/wines-api/main/docs/latency.md\"\n    },\n    {\n      \"path\": \"/docs/architecture.png\",\n      \"content_type\": \"image/png\",\n      \"url\": \"https://raw.githubusercontent.com/my-org/wines-api/main/docs/architecture.png\"\n    }\n  ],\n  \"navigation\": [\n    {\n      \"label\": \"Documentation\",\n      \"icon\": { \"css_icon_class\": \"bi bi-journal-text me-2\" },\n      \"path\": \"/documentation\",\n      \"items\": [\n        {\n          \"label\": \"Guides\",\n          \"kind\": \"category\",\n          \"links\": [\n            {\n              \"label\": \"Getting started\",\n              \"link\": \"/documentation/getting-started\",\n              \"icon\": { \"css_icon_class\": \"bi bi-book me-2\" }\n            },\n            {\n              \"label\": \"Latency\",\n              \"link\": \"/documentation/latency\",\n              \"icon\": { \"css_icon_class\": \"bi bi-speedometer2 me-2\" }\n            }\n          ]\n        },\n        {\n          \"label\": \"API\",\n          \"kind\": \"category\",\n          \"links\": [\n            {\n              \"label\": \"API Reference\",\n              \"link\": \"/api-references\",\n              \"icon\": { \"css_icon_class\": \"bi bi-braces me-2\" }\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"plans\": [\n    {\n      \"id\": \"dev\",\n      \"name\": \"Dev\",\n      \"description\": \"An API key to try the API on prototypes\",\n      \"access_mode_configuration_type\": \"apikey\",\n      \"access_mode_configuration\": {\n        \"throttling_quota\": 100,\n        \"daily_quota\": 1000,\n        \"monthly_quota\": 10000\n      },\n      \"status\": \"published\",\n      \"tags\": [],\n      \"metadata\": {\n        \"env\": \"dev\"\n      }\n    }\n  ],\n  \"redirections\": [\n    { \"from\": \"/docs\", \"to\": \"/documentation\" }\n  ],\n  \"search\": { \"enabled\": true },\n  \"footer\": null,\n  \"banner\": null,\n  \"metadata\": {},\n  \"tags\": []\n}\n```\n\n## Portal routes\n\nThe portal exposes the following routes internally:\n\n| Method | Path | Description |\n|---|---|---|\n| `GET` | `/` | Home page |\n| `GET` | `/login` | Triggers authentication (redirects to auth module) |\n| `GET` | `/logout` | Logs out (redirected to Otoroshi logout) |\n| `GET` | `/api-references` | OpenAPI reference page (or spec selection if multiple) |\n| `GET` | `/api-references/\u003cspec-path\u003e` | Specific OpenAPI spec rendering |\n| `GET` | `/subscriptions` | API key management page (requires authentication) |\n| `GET` | `/portal.js` | Portal JavaScript (theme, modals, API tester logic) |\n| `GET` | `/\u003cresource-path\u003e` | Any resource defined in the documentation |\n| `GET` | `/api/plans` | JSON list of available plans |\n| `GET` | `/api/documentation` | JSON documentation metadata |\n| `GET` | `/api/apikeys` | JSON list of current user's API keys |\n| `POST` | `/api/apikeys` | Create a new API key |\n| `PUT` | `/api/apikeys/\u003cclient_id\u003e` | Update an API key |\n| `DELETE` | `/api/apikeys/\u003cclient_id\u003e` | Delete an API key |\n| `POST` | `/api/_test` | Proxy a test request to any URL |\n\n## License\n\n[Apache 2.0](./LICENSE) -- Copyright 2023-2025 [Cloud APIM](https://www.cloud-apim.com/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloud-apim%2Fotoroshi-api-portal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcloud-apim%2Fotoroshi-api-portal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloud-apim%2Fotoroshi-api-portal/lists"}