diff --git a/.changeset/clever-ducks-double.md b/.changeset/clever-ducks-double.md new file mode 100644 index 0000000000..cad73cffbf --- /dev/null +++ b/.changeset/clever-ducks-double.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': patch +'gitbook': patch +--- + +Enhance OpenAPI security scopes handling diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 1d26b03867..5a22166d36 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -317,10 +317,11 @@ } .openapi-securities-oauth-flows { - @apply flex flex-col gap-2 divide-y divide-tint-subtle; + @apply flex flex-col gap-3; } -.openapi-securities-oauth-content { +.openapi-securities-oauth-content, +.openapi-securities-scopes { @apply prose *:!prose-sm *:text-tint; } @@ -328,7 +329,7 @@ @apply text-xs; } -.openapi-securities-oauth-content ul { +.openapi-securities-scopes ul { @apply !my-0; } diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx index 8223b15888..72d27bd381 100644 --- a/packages/react-openapi/src/OpenAPISecurities.tsx +++ b/packages/react-openapi/src/OpenAPISecurities.tsx @@ -1,11 +1,12 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { Fragment } from 'react'; import { InteractiveSection } from './InteractiveSection'; import { Markdown } from './Markdown'; import { OpenAPICopyButton } from './OpenAPICopyButton'; import { OpenAPISchemaName } from './OpenAPISchemaName'; import type { OpenAPIClientContext } from './context'; import { t } from './translate'; -import type { OpenAPISecuritySchemeWithRequired } from './types'; +import type { OpenAPICustomSecurityScheme, OpenAPISecurityScope } from './types'; import type { OpenAPIOperationData } from './types'; import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils'; @@ -53,6 +54,12 @@ export function OpenAPISecurities(props: { className="openapi-securities-description" /> ) : null} + {security.scopes?.length ? ( + + ) : null} ); })} @@ -63,10 +70,7 @@ export function OpenAPISecurities(props: { ); } -function getLabelForType( - security: OpenAPISecuritySchemeWithRequired, - context: OpenAPIClientContext -) { +function getLabelForType(security: OpenAPICustomSecurityScheme, context: OpenAPIClientContext) { switch (security.type) { case 'apiKey': return ( @@ -90,7 +94,6 @@ function getLabelForType( } if (security.scheme === 'bearer') { - const description = resolveDescription(security); return ( <> {/** Show a default description if none is provided */} - {!description ? ( + {!security.description ? ( {flows.map(([name, flow], index) => ( - + + + {index < flows.length - 1 ?
: null} +
))} ); @@ -170,7 +175,7 @@ function OpenAPISchemaOAuth2Item(props: { return null; } - const scopes = Object.entries(flow?.scopes ?? {}); + const scopes = flow.scopes ? Object.entries(flow.scopes) : []; return (
@@ -221,22 +226,62 @@ function OpenAPISchemaOAuth2Item(props: { ) : null} - {scopes.length ? ( -
- {t(context.translation, 'available_scopes')}:{' '} -
    - {scopes.map(([key, value]) => ( -
  • - - {key} - - : {value} -
  • - ))} -
-
- ) : null} + {scopes.length ? : null}
); } + +/** + * Render a list of available scopes. + */ +function OpenAPISchemaScopes(props: { + scopes: OpenAPISecurityScope[]; + context: OpenAPIClientContext; +}) { + const { scopes, context } = props; + + return ( +
+ {t(context.translation, 'required_scopes')}: +
    + {scopes.map((scope) => ( + + ))} +
+
+ ); +} + +/** + * Display a scope item. Either a key-value pair or a single string. + */ +function OpenAPIScopeItem(props: { + scope: OpenAPISecurityScope; + context: OpenAPIClientContext; +}) { + const { scope, context } = props; + + return ( +
  • + + {scope[1] ? `: ${scope[1]}` : null} +
  • + ); +} + +/** + * Displays the scope name within a copyable button. + */ +function OpenAPIScopeItemKey(props: { + name: string; + context: OpenAPIClientContext; +}) { + const { name, context } = props; + + return ( + + {name} + + ); +} diff --git a/packages/react-openapi/src/resolveOpenAPIOperation.ts b/packages/react-openapi/src/resolveOpenAPIOperation.ts index db99c7cb11..8e7e2c0d62 100644 --- a/packages/react-openapi/src/resolveOpenAPIOperation.ts +++ b/packages/react-openapi/src/resolveOpenAPIOperation.ts @@ -7,7 +7,7 @@ import type { OpenAPIV3xDocument, } from '@gitbook/openapi-parser'; import { dereferenceFilesystem } from './dereference'; -import type { OpenAPIOperationData } from './types'; +import type { OpenAPIOperationData, OpenAPISecurityScope } from './types'; import { checkIsReference } from './utils'; export { fromJSON, toJSON }; @@ -54,18 +54,21 @@ export async function resolveOpenAPIOperation( // Resolve securities const securities: OpenAPIOperationData['securities'] = []; for (const entry of flatSecurities) { - const securityKey = Object.keys(entry)[0]; + const [securityKey, operationScopes] = Object.entries(entry)[0] ?? []; if (securityKey) { const securityScheme = schema.components?.securitySchemes?.[securityKey]; - if (securityScheme && !checkIsReference(securityScheme)) { - securities.push([ - securityKey, - { - ...securityScheme, - required: !isOptionalSecurity, - }, - ]); - } + const scopes = resolveSecurityScopes({ + securityScheme, + operationScopes, + }); + securities.push([ + securityKey, + { + ...securityScheme, + required: !isOptionalSecurity, + scopes, + }, + ]); } } @@ -91,10 +94,7 @@ function getPathObject( schema: OpenAPIV3.Document | OpenAPIV3_1.Document, path: string ): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null { - if (schema.paths?.[path]) { - return schema.paths[path]; - } - return null; + return schema.paths?.[path] || null; } /** @@ -149,3 +149,33 @@ function flattenSecurities(security: OpenAPIV3.SecurityRequirementObject[]) { })); }); } + +/** + * Resolve the scopes for a security scheme. + */ +function resolveSecurityScopes({ + securityScheme, + operationScopes, +}: { + securityScheme?: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject; + operationScopes?: string[]; +}): OpenAPISecurityScope[] | null { + if ( + !securityScheme || + checkIsReference(securityScheme) || + isOAuthSecurityScheme(securityScheme) + ) { + return null; + } + + return operationScopes?.map((scope) => [scope, undefined]) || []; +} + +/** + * Check if a security scheme is an OAuth or OpenID Connect security scheme. + */ +function isOAuthSecurityScheme( + securityScheme: OpenAPIV3.SecuritySchemeObject +): securityScheme is OpenAPIV3.OAuth2SecurityScheme { + return securityScheme.type === 'oauth2'; +} diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts index 5babf5557e..3ebbf72e13 100644 --- a/packages/react-openapi/src/translations/de.ts +++ b/packages/react-openapi/src/translations/de.ts @@ -36,7 +36,7 @@ export const de = { show: 'Zeige ${1}', hide: 'Verstecke ${1}', available_items: 'Verfügbare Elemente', - available_scopes: 'Verfügbare scopes', + required_scopes: 'Erforderliche Scopes', properties: 'Eigenschaften', or: 'oder', and: 'und', diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts index 9c1607c385..648ea6591a 100644 --- a/packages/react-openapi/src/translations/en.ts +++ b/packages/react-openapi/src/translations/en.ts @@ -36,7 +36,7 @@ export const en = { show: 'Show ${1}', hide: 'Hide ${1}', available_items: 'Available items', - available_scopes: 'Available scopes', + required_scopes: 'Required scopes', possible_values: 'Possible values', properties: 'Properties', or: 'or', diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts index 93b8bf2be3..b9a1c653ad 100644 --- a/packages/react-openapi/src/translations/es.ts +++ b/packages/react-openapi/src/translations/es.ts @@ -36,7 +36,7 @@ export const es = { show: 'Mostrar ${1}', hide: 'Ocultar ${1}', available_items: 'Elementos disponibles', - available_scopes: 'Scopes disponibles', + required_scopes: 'Scopes requeridos', properties: 'Propiedades', or: 'o', and: 'y', diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts index 5f83e3cd12..194726eb87 100644 --- a/packages/react-openapi/src/translations/fr.ts +++ b/packages/react-openapi/src/translations/fr.ts @@ -36,7 +36,7 @@ export const fr = { show: 'Afficher ${1}', hide: 'Masquer ${1}', available_items: 'Éléments disponibles', - available_scopes: 'Scopes disponibles', + required_scopes: 'Scopes requis', properties: 'Propriétés', or: 'ou', and: 'et', diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts index 73ae37f1f2..7519517835 100644 --- a/packages/react-openapi/src/translations/ja.ts +++ b/packages/react-openapi/src/translations/ja.ts @@ -36,7 +36,7 @@ export const ja = { show: '${1}を表示', hide: '${1}を非表示', available_items: '利用可能なアイテム', - available_scopes: '利用可能なスコープ', + required_scopes: '必須スコープ', properties: 'プロパティ', or: 'または', and: 'かつ', diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts index f4e62c474a..4b66ea147e 100644 --- a/packages/react-openapi/src/translations/nl.ts +++ b/packages/react-openapi/src/translations/nl.ts @@ -36,7 +36,7 @@ export const nl = { show: 'Toon ${1}', hide: 'Verberg ${1}', available_items: 'Beschikbare items', - available_scopes: 'Beschikbare scopes', + required_scopes: 'Vereiste scopes', properties: 'Eigenschappen', or: 'of', and: 'en', diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts index abf45c490a..3804034211 100644 --- a/packages/react-openapi/src/translations/no.ts +++ b/packages/react-openapi/src/translations/no.ts @@ -36,7 +36,7 @@ export const no = { show: 'Vis ${1}', hide: 'Skjul ${1}', available_items: 'Tilgjengelige elementer', - available_scopes: 'Tilgjengelige scopes', + required_scopes: 'Påkrevde scopes', properties: 'Egenskaper', or: 'eller', and: 'og', diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts index cf27289547..37ac92cd86 100644 --- a/packages/react-openapi/src/translations/pt-br.ts +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -36,7 +36,7 @@ export const pt_br = { show: 'Mostrar ${1}', hide: 'Ocultar ${1}', available_items: 'Itens disponíveis', - available_scopes: 'Scopes disponíveis', + required_scopes: 'Scopes obrigatórios', properties: 'Propriedades', or: 'ou', and: 'e', diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts index de8faa204b..3d2e20d2d5 100644 --- a/packages/react-openapi/src/translations/zh.ts +++ b/packages/react-openapi/src/translations/zh.ts @@ -36,7 +36,7 @@ export const zh = { show: '显示${1}', hide: '隐藏${1}', available_items: '可用项', - available_scopes: '可用范围', + required_scopes: '必需范围', properties: '属性', or: '或', and: '和', diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 75446c6cff..a6183fb883 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -17,8 +17,13 @@ export type OpenAPIServerWithCustomProperties = Omit; /** Securities that should be used for this operation */ - securities: [string, OpenAPISecuritySchemeWithRequired][]; + securities: [string, OpenAPICustomSecurityScheme][]; } export interface OpenAPIWebhookData extends OpenAPICustomSpecProperties { diff --git a/packages/react-openapi/src/util/tryit-prefill.ts b/packages/react-openapi/src/util/tryit-prefill.ts index ca97e560b9..b583b64ba4 100644 --- a/packages/react-openapi/src/util/tryit-prefill.ts +++ b/packages/react-openapi/src/util/tryit-prefill.ts @@ -3,8 +3,8 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import type { ApiClientConfiguration } from '@scalar/types'; import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider'; import type { + OpenAPICustomSecurityScheme, OpenAPIOperationData, - OpenAPISecuritySchemeWithRequired, OpenAPIServerWithCustomProperties, } from '../types'; @@ -170,7 +170,7 @@ function resolveTryItPrefillServersForOperationServers(args: { * Return a X-GITBOOK-PREFILL placeholder based on the prefill custom property in the provided security scheme. */ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: { - security: OpenAPISecuritySchemeWithRequired; + security: OpenAPICustomSecurityScheme; defaultPlaceholderValue?: string; }) { const { security, defaultPlaceholderValue } = args; @@ -185,7 +185,7 @@ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: { } function extractPrefillExpressionPartsFromSecurityScheme( - security: OpenAPISecuritySchemeWithRequired + security: OpenAPICustomSecurityScheme ): TemplatePart[] { const expression = security[PREFILL_CUSTOM_PROPERTY]; diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 304e1442af..f6d0afad58 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -2,7 +2,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser' import type { OpenAPIUniversalContext } from './context'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { tString } from './translate'; -import type { OpenAPIOperationData, OpenAPISecuritySchemeWithRequired } from './types'; +import type { OpenAPICustomSecurityScheme, OpenAPIOperationData } from './types'; export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject { return typeof input === 'object' && !!input && '$ref' in input; @@ -258,7 +258,7 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { export type OperationSecurityInfo = { key: string; label: string; - schemes: OpenAPISecuritySchemeWithRequired[]; + schemes: OpenAPICustomSecurityScheme[]; }; /** @@ -288,7 +288,7 @@ export function extractOperationSecurityInfo(args: { label: schemeKeys.join(' & '), schemes: schemeKeys .map((schemeKey) => securitiesMap.get(schemeKey)) - .filter((s) => s !== undefined), + .filter((s): s is OpenAPICustomSecurityScheme => s !== undefined), }; }); }