mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #5046 from logto-io/gao-add-api-tag-descriptions
chore(core): add api tags descriptions
This commit is contained in:
commit
0bec4cd1d3
22 changed files with 215 additions and 35 deletions
5
.changeset/calm-ladybugs-doubt.md
Normal file
5
.changeset/calm-ladybugs-doubt.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/core": patch
|
||||
---
|
||||
|
||||
add summary and description to APIs
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Users",
|
||||
"description": "Endpoints for user management. Including creating, updating, deleting, and querying users with flexible filters. In addition to the endpoints, see [🧑🚀 Manage users](https://docs.logto.io/docs/recipes/manage-users/) for more insights."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/users/{userId}": {
|
||||
"get": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Applications",
|
||||
"description": "Application represents a registered software program or service that has been authorized to access user information and perform actions on behalf of users within the system."
|
||||
"description": "Application represents your registered software program or service that has been authorized to access user information and perform actions on behalf of users within the system. Currently, Logto supports four types of applications:\n\n- Traditional web\n\n- Single-page app\n- Native app\n- Machine-to-machine app.\n\nDepending on the application type, it may have different authentication flows and access to the system. See [🔗 Integrate Logto in your application](https://docs.logto.io/docs/recipes/integrate-logto/) to learn more about how to integrate Logto into your application.\n\nRole-based access control (RBAC) is supported for machine-to-machine applications. See [🔐 Role-based access control (RBAC)](https://docs.logto.io/docs/recipes/rbac/) to get started with role-based access control."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Authn",
|
||||
"description": "Authentication endpoints for third-party integrations and identity providers."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/authn/hasura": {
|
||||
"get": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Custom phrases",
|
||||
"description": "APIs for managing custom phrases that allow you to customize the phrases displayed when experiencing the sign-in flow."
|
||||
"description": "Endpoints for managing custom phrases that allow you to customize the phrases displayed in the sign-in experience.\n\nSee [Localized language](https://docs.logto.io/docs/recipes/customize-sie/localized-language/) to learn more about custom phrases for localization."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
@ -12,26 +12,27 @@
|
|||
"description": "Get all custom phrases for all languages.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of custom phrases."
|
||||
"description": "An array of custom phrases."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/custom-phrases/{languageTag}": {
|
||||
"get": {
|
||||
"summary": "Get custom phrases by language tag",
|
||||
"summary": "Get custom phrases",
|
||||
"description": "Get custom phrases for the specified language tag.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Custom phrases for the specified language tag."
|
||||
},
|
||||
"404": {
|
||||
"description": "Custom phrases not found for the specified language tag."
|
||||
"description": "Custom phrases not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Upsert custom phrases by language tag",
|
||||
"summary": "Upsert custom phrases",
|
||||
"description": "Upsert custom phrases for the specified language tag. Upsert means that if the custom phrases already exist, they will be updated. Otherwise, they will be created.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
|
@ -45,7 +46,7 @@
|
|||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Custom phrases created/updated successfully."
|
||||
"description": "Custom phrases created or updated successfully."
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid translation structure."
|
||||
|
@ -53,16 +54,17 @@
|
|||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete custom phrases by language tag",
|
||||
"summary": "Delete custom phrase",
|
||||
"description": "Delete custom phrases for the specified language tag.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Custom phrases deleted successfully."
|
||||
},
|
||||
"404": {
|
||||
"description": "Custom phrases not found for the specified language tag."
|
||||
"description": "Custom phrases not found."
|
||||
},
|
||||
"409": {
|
||||
"description": "Cannot delete default language."
|
||||
"description": "Cannot delete the default language."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"description": "The APIs that power the dashboard page of Console to show the statistics of the current tenant."
|
||||
"description": "Endpoints that power the dashboard page of Console to show the statistics of the current tenant."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Hooks",
|
||||
"description": "Hook enables you to effortlessly receive real-time updates regarding specific events, such as user registration, sign-in, or password reset."
|
||||
"description": "Hook enables you to effortlessly receive real-time updates regarding specific events, such as user registration, sign-in, or password reset. See [🪝 Webhooks] to get started and learn more."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
8
packages/core/src/routes/interaction/index.openapi.json
Normal file
8
packages/core/src/routes/interaction/index.openapi.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Interaction",
|
||||
"description": "Interaction endpoints are used to manage and process interactions for end-users, such as sign-in experience. Currently, all interaction endpoints are used internally as part of the authentication flow, and they are not useful to developers directly."
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Configs",
|
||||
"description": "Endpoints for managing Logto global configurations for the tenant."
|
||||
"description": "Endpoints for managing Logto global configurations for the tenant, such as admin console config and OIDC signing keys.\n\nSee [🔑 Signing keys](https://docs.logto.io/docs/recipes/signing-keys-rotation/) to learn more about signing keys and key rotation."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
@ -35,7 +35,7 @@
|
|||
"/api/configs/oidc/{keyType}": {
|
||||
"get": {
|
||||
"summary": "Get OIDC keys",
|
||||
"description": "Get OIDC keys by key type. The actual key will be redacted from the result.",
|
||||
"description": "Get OIDC signing keys by key type. The actual key will be redacted from the result.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
@ -45,7 +45,7 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of OIDC keys for the given key type."
|
||||
"description": "An array of OIDC signing keys for the given key type."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
|||
"/api/configs/oidc/{keyType}/{keyId}": {
|
||||
"delete": {
|
||||
"summary": "Delete OIDC key",
|
||||
"description": "Delete an OIDC key by key type and key ID.",
|
||||
"description": "Delete an OIDC signing key by key type and key ID.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
|
@ -100,7 +100,7 @@
|
|||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of OIDC keys after rotation."
|
||||
"description": "An array of OIDC signing keys after rotation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Organizations",
|
||||
"description": "Organization is a concept that brings together multiple identities (mostly users). Logto supports multiple organizations, and each organization can have multiple users.\n\nEvery organization shares the same set (organization template) of roles and permissions. Each user can have different roles in different organizations."
|
||||
"description": "Organization is a concept that brings together multiple identities (mostly users). Logto supports multiple organizations, and each organization can have multiple users.\n\nEvery organization shares the same set (organization template) of roles and permissions. Each user can have different roles in different organizations. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Organization roles",
|
||||
"description": "Organization roles are used to define a set of organization scopes that can be assigned to users. Every organization role is a part of the organization template.\n\nOrganization roles will only be meaningful within an organization context. For example, a user may have an `admin` role for organization A, but not for organization B."
|
||||
"description": "Organization roles are used to define a set of organization scopes that can be assigned to users. Every organization role is a part of the organization template.\n\nOrganization roles will only be meaningful within an organization context. For example, a user may have an `admin` role for organization A, but not for organization B. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Organization scopes",
|
||||
"description": "Organization scopes (permissions) are used to define actions that can be performed on a organization. Every organization scope is a part of the organization template.\n\nOrganization scopes will only be meaningful within an organization context. For example, a user may have a `read` scope for organization A, but not for organization B."
|
||||
"description": "Organization scopes (permissions) are used to define actions that can be performed on a organization. Every organization scope is a part of the organization template.\n\nOrganization scopes will only be meaningful within an organization context. For example, a user may have a `read` scope for organization A, but not for organization B. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Resources",
|
||||
"description": "Resources (API resources) represent the APIs that you want to protect with Logto. Each resource has a unique indicator (URI) and a set of scopes (permissions). The resources will be used in the authorization process which conforms to [RFC 8707: Resource Indicators for OAuth 2.0](https://www.rfc-editor.org/rfc/rfc8707.html).\n\nSee [⚔️ Protect your API](https://docs.logto.io/docs/recipes/protect-your-api/) to learn more about how to define API resources and protect your APIs with Logto."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/resources": {
|
||||
"get": {
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Roles",
|
||||
"description": "Role management for API resource RBAC (role-based access control). See [🔐 Role-based access control (RBAC)](https://docs.logto.io/docs/recipes/rbac/) to get started with role-based access control."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/roles": {
|
||||
"get": {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Sign-in experience",
|
||||
"description": "Set the Sign-in experience configuration to customize your sign-in experience."
|
||||
"description": "Endpoints for customizing Logto sign-in experience. See [🎨 Customize sign-in experience](https://docs.logto.io/docs/recipes/customize-sie/) to learn more about how the configuration works and reflects on the user interface."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Status",
|
||||
"description": "Endpoints for health check."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/status": {
|
||||
"get": {
|
||||
|
|
21
packages/core/src/routes/swagger/index.openapi.json
Normal file
21
packages/core/src/routes/swagger/index.openapi.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "Swagger.json",
|
||||
"description": "Endpoints for the Swagger JSON document."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/swagger.json": {
|
||||
"get": {
|
||||
"summary": "Get Swagger JSON",
|
||||
"description": "The endpoint for the current JSON document. The JSON conforms to the [OpenAPI v3.0.1](https://spec.openapis.org/oas/v3.0.1) (a.k.a. Swagger) specification.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The JSON document."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import {
|
|||
findSupplementFiles,
|
||||
normalizePath,
|
||||
validateSupplement,
|
||||
validateSwaggerDocument,
|
||||
} from './utils/general.js';
|
||||
import {
|
||||
buildParameters,
|
||||
|
@ -220,19 +221,22 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
|
|||
tags: [...tags].map((tag) => ({ name: tag })),
|
||||
};
|
||||
|
||||
if (EnvSet.values.isUnitTest) {
|
||||
consoleLog.warn('Skip validating supplement documents in unit test.');
|
||||
} else {
|
||||
for (const document of supplementDocuments) {
|
||||
validateSupplement(baseDocument, document);
|
||||
}
|
||||
}
|
||||
|
||||
const data = supplementDocuments.reduce(
|
||||
(document, supplement) => deepmerge(document, supplement, { arrayMerge: mergeParameters }),
|
||||
baseDocument
|
||||
);
|
||||
|
||||
if (EnvSet.values.isUnitTest) {
|
||||
consoleLog.warn('Skip validating swagger document in unit test.');
|
||||
}
|
||||
// Don't throw for integrity check in production as it has no benefit.
|
||||
else if (!EnvSet.values.isProduction || EnvSet.values.isIntegrationTest) {
|
||||
for (const document of supplementDocuments) {
|
||||
validateSupplement(baseDocument, document);
|
||||
}
|
||||
validateSwaggerDocument(data);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
...data,
|
||||
tags: data.tags?.slice().sort((tagA, tagB) => tagA.name.localeCompare(tagB.name)),
|
||||
|
|
|
@ -6,6 +6,8 @@ import { isKeyInObject, type Optional } from '@silverhand/essentials';
|
|||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { consoleLog } from '#src/utils/console.js';
|
||||
|
||||
const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
|
||||
|
||||
/**
|
||||
|
@ -142,3 +144,60 @@ export const validateSupplement = (
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given OpenAPI document is valid for being served as the swagger document:
|
||||
*
|
||||
* - Every path + method combination must have a tag, summary, and description.
|
||||
* - Every tag must have a description.
|
||||
*
|
||||
* @throws {TypeError} if the document is invalid.
|
||||
*/
|
||||
export const validateSwaggerDocument = (document: OpenAPIV3.Document) => {
|
||||
for (const [path, operations] of Object.entries(document.paths)) {
|
||||
if (path.startsWith('/api/interaction')) {
|
||||
consoleLog.warn(`Path \`${path}\` is not documented. Do something!`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// This path is for admin tenant only, skip it.
|
||||
if (path === '/api/.well-known/endpoints/{tenantId}') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!operations) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const method of Object.values(OpenAPIV3.HttpMethods)) {
|
||||
const operation = operations[method];
|
||||
|
||||
if (!operation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(operation)) {
|
||||
throw new TypeError(
|
||||
`Path \`${path}\` and operation \`${method}\` must be an object, not an array.`
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
operation.tags?.length,
|
||||
`Path \`${path}\` and operation \`${method}\` must have at least one tag.`
|
||||
);
|
||||
assert(
|
||||
operation.summary,
|
||||
`Path \`${path}\` and operation \`${method}\` must have a summary.`
|
||||
);
|
||||
assert(
|
||||
operation.description,
|
||||
`Path \`${path}\` and operation \`${method}\` must have a description.`
|
||||
);
|
||||
}
|
||||
|
||||
for (const tag of document.tags ?? []) {
|
||||
assert(tag.description, `Tag \`${tag.name}\` must have a description.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -120,14 +120,33 @@ const isObjectArray = (value: unknown): value is Array<Record<string, unknown>>
|
|||
* Merge two arrays. If the two arrays are both object arrays, merge them with the following
|
||||
* rules:
|
||||
*
|
||||
* - If the source array has an item with `name` and `in` properties, and the destination array
|
||||
* also has an item with the same `name` and `in` properties, merge the two items with
|
||||
* `deepmerge`.
|
||||
* - If the source array has an item with `name` properties, and the destination array
|
||||
* also has an item with the same `name` and `in` properties, merge the two items with
|
||||
* `deepmerge`. It is assumed that the two items are using `name` for identifying the same
|
||||
* parameter, and may use `in` to distinguish the same parameter in different places.
|
||||
* - Otherwise, append the item to the destination array (the default behavior of
|
||||
* `deepmerge`).
|
||||
* `deepmerge`).
|
||||
*
|
||||
* Otherwise, use `deepmerge` to merge the two arrays.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* mergeParameters(
|
||||
* [{ name: 'foo', in: 'query', required: true }, { name: 'bar', in: 'query', required: true }],
|
||||
* [{ name: 'foo', in: 'query', required: false }]
|
||||
* ); // [{ name: 'foo', in: 'query', required: false }, { name: 'bar', in: 'query', required: true }]
|
||||
*
|
||||
* mergeParameters(
|
||||
* [{ name: 'foo', required: true }, { name: 'bar', required: true }],
|
||||
* [{ name: 'foo', in: 'query', required: false }]
|
||||
* );
|
||||
* // [
|
||||
* // { name: 'foo', required: true },
|
||||
* // { name: 'bar', required: true },
|
||||
* // { name: 'foo', in: 'query', required: false }
|
||||
* // ]
|
||||
* ```
|
||||
*
|
||||
* @param destination The destination array.
|
||||
* @param source The source array.
|
||||
* @returns The merged array.
|
||||
|
@ -140,7 +159,7 @@ export const mergeParameters = (destination: unknown[], source: unknown[]) => {
|
|||
const result = destination.slice();
|
||||
|
||||
for (const item of source) {
|
||||
if (!('name' in item) || !('in' in item)) {
|
||||
if (!('name' in item)) {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
result.push(item);
|
||||
continue;
|
||||
|
|
32
packages/core/src/routes/user-assets.openapi.json
Normal file
32
packages/core/src/routes/user-assets.openapi.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "User assets",
|
||||
"description": "Endpoints for managing user uploaded assets."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/user-assets/service-status": {
|
||||
"get": {
|
||||
"summary": "Get service status",
|
||||
"description": "Get user assets service status.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An object containing the service status and metadata."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/user-assets": {
|
||||
"post": {
|
||||
"summary": "Upload asset",
|
||||
"description": "Upload a user asset.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An object containing the uploaded asset metadata."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"tags": [
|
||||
{
|
||||
"name": "Verification codes",
|
||||
"description": "Endpoints for handling verification codes.\n\nNote: before you call the endpoints, you need to setup your email/SMS connector first."
|
||||
"description": "Endpoints for handling verification codes. It is helpful when building a custom profile page in your app. See [👤 User profile](https://docs.logto.io/docs/recipes/user-profile/#optional-validate-verification-code) for more details.\n\nNote: Before you call the endpoints, you need to setup your email/SMS connector first."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
|
|
Loading…
Reference in a new issue