mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Implement multiple security schemes support
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Implement multiple security schemes support
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type {
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
import { ChevronDown, LoaderCircle } from 'lucide-react';
|
||||
import type { Security } from '@/utils/get-security';
|
||||
import { useRequestData } from '@/ui/contexts/code-example';
|
||||
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
|
||||
import type { RequestData } from '@/requests/_shared';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
@@ -50,12 +50,6 @@ import {
|
||||
} from '@/playground/schema';
|
||||
|
||||
interface FormValues {
|
||||
authorization:
|
||||
| string
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
path: Record<string, string>;
|
||||
query: Record<string, string>;
|
||||
header: Record<string, string>;
|
||||
@@ -75,13 +69,15 @@ export interface CustomField<TName extends FieldPath<FormValues>, Info> {
|
||||
}) => ReactElement;
|
||||
}
|
||||
|
||||
type SecurityEntry = Security & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ClientProps = HTMLAttributes<HTMLFormElement> & {
|
||||
route: string;
|
||||
method: string;
|
||||
authorization?: Security & {
|
||||
persistentId: string;
|
||||
};
|
||||
parameters?: ParameterField[];
|
||||
securities: SecurityEntry[][];
|
||||
body?: {
|
||||
schema: RequestSchema;
|
||||
mediaType: string;
|
||||
@@ -97,7 +93,7 @@ export type ClientProps = HTMLAttributes<HTMLFormElement> & {
|
||||
`${ParameterField['in']}.${string}`,
|
||||
ParameterField
|
||||
>;
|
||||
auth?: CustomField<'authorization', RequestSchema>;
|
||||
auth?: CustomField<FieldPath<FormValues>, RequestSchema>;
|
||||
body?: CustomField<'body', RequestSchema>;
|
||||
};
|
||||
|
||||
@@ -127,7 +123,7 @@ const ServerSelect = lazy(() => import('@/ui/server-select'));
|
||||
export default function Client({
|
||||
route,
|
||||
method = 'GET',
|
||||
authorization,
|
||||
securities,
|
||||
parameters,
|
||||
body,
|
||||
fields,
|
||||
@@ -139,18 +135,17 @@ export default function Client({
|
||||
const { server } = useServerSelectContext();
|
||||
const requestData = useRequestData();
|
||||
const fieldInfoMap = useMemo(() => new Map<string, FieldInfo>(), []);
|
||||
const authInfo = usePersistentAuthInfo(authorization);
|
||||
const { mediaAdapters } = useApiContext();
|
||||
const submitHandler = useRef<SubmitHandler | null>(null);
|
||||
const defaultValues: FormValues = useMemo(
|
||||
() => ({
|
||||
authorization: authInfo.info,
|
||||
path: requestData.data.path,
|
||||
query: requestData.data.query,
|
||||
header: requestData.data.header,
|
||||
body: requestData.data.body,
|
||||
cookie: requestData.data.cookie,
|
||||
}),
|
||||
[authInfo.info, requestData.data],
|
||||
[requestData.data],
|
||||
);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@@ -180,19 +175,8 @@ export default function Client({
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((_value) => {
|
||||
const value = _value as FormValues;
|
||||
|
||||
if (authorization && value.authorization) {
|
||||
authInfo.saveInfo(value.authorization);
|
||||
|
||||
writeAuthInfo(
|
||||
authorization,
|
||||
value.authorization,
|
||||
value.header,
|
||||
value.query,
|
||||
value.cookie,
|
||||
);
|
||||
}
|
||||
let value = _value as FormValues;
|
||||
if (submitHandler.current) value = submitHandler.current(value);
|
||||
|
||||
requestData.saveData(toRequestData(method, body?.mediaType, value));
|
||||
});
|
||||
@@ -204,6 +188,8 @@ export default function Client({
|
||||
}, []);
|
||||
|
||||
const onSubmit = form.handleSubmit((value) => {
|
||||
if (submitHandler.current) value = submitHandler.current(value);
|
||||
|
||||
testQuery.start(value);
|
||||
});
|
||||
|
||||
@@ -237,109 +223,78 @@ export default function Client({
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<FormBody
|
||||
body={body}
|
||||
fields={fields}
|
||||
parameters={parameters}
|
||||
authorization={authorization}
|
||||
/>
|
||||
|
||||
{securities.length > 0 && (
|
||||
<SecuritiesTabs
|
||||
securities={securities}
|
||||
onSetSubmitHandler={(v) => (submitHandler.current = v)}
|
||||
/>
|
||||
)}
|
||||
<FormBody body={body} fields={fields} parameters={parameters} />
|
||||
{testQuery.data ? <ResultDisplay data={testQuery.data} /> : null}
|
||||
{authorization && <AuthFooter authorization={authorization} />}
|
||||
</form>
|
||||
</SchemaProvider>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SecuritiesTabs({
|
||||
securities,
|
||||
onSetSubmitHandler,
|
||||
}: {
|
||||
securities: SecurityEntry[][];
|
||||
onSetSubmitHandler: (handler: SubmitHandler) => void;
|
||||
}) {
|
||||
const [idx, setIdx] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<div className="flex items-center mb-4 overflow-auto border bg-fd-muted rounded-full">
|
||||
{securities.map((security, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
onClick={() => setIdx(i)}
|
||||
className={cn(
|
||||
'flex-1 text-xs font-medium font-mono px-2 py-1.5 text-nowrap transition-colors',
|
||||
i === idx
|
||||
? 'bg-fd-primary/10 text-fd-primary'
|
||||
: 'text-fd-muted-foreground hover:text-fd-accent-foreground',
|
||||
)}
|
||||
>
|
||||
{security.map((item) => item.id).join(' & ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<AuthInput
|
||||
securities={securities[idx]}
|
||||
onSetSubmitHandler={onSetSubmitHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const paramNames = ['Headers', 'Cookies', 'Query', 'Path'] as const;
|
||||
const paramTypes = ['header', 'cookie', 'query', 'path'] as const;
|
||||
|
||||
function FormBody({
|
||||
authorization,
|
||||
parameters = [],
|
||||
fields = {},
|
||||
body,
|
||||
}: Pick<ClientProps, 'parameters' | 'authorization' | 'body' | 'fields'>) {
|
||||
}: Pick<ClientProps, 'parameters' | 'body' | 'fields'>) {
|
||||
const params = useMemo(() => {
|
||||
return paramTypes.map((param) => parameters.filter((v) => v.in === param));
|
||||
}, [parameters]);
|
||||
|
||||
const auth = useMemo(():
|
||||
| {
|
||||
in: string;
|
||||
field: RequestSchema;
|
||||
}
|
||||
| undefined => {
|
||||
if (!authorization) return;
|
||||
|
||||
if (authorization.type === 'http' && authorization.scheme === 'basic') {
|
||||
return {
|
||||
in: 'header',
|
||||
field: {
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
description: authorization.description,
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (
|
||||
authorization.type === 'http' ||
|
||||
authorization.type === 'oauth2'
|
||||
) {
|
||||
return {
|
||||
in: 'header',
|
||||
field: {
|
||||
type: 'string',
|
||||
description:
|
||||
authorization.description ?? `The Authorization access token.`,
|
||||
},
|
||||
};
|
||||
} else if (authorization.type === 'apiKey') {
|
||||
return {
|
||||
in: authorization.in,
|
||||
field: {
|
||||
type: 'string',
|
||||
description: authorization.description ?? 'The API key.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: handle OpenID connect
|
||||
}, [authorization]);
|
||||
|
||||
function renderAuth() {
|
||||
if (!auth) return null;
|
||||
|
||||
if (fields?.auth)
|
||||
return renderCustomField('authorization', auth.field, fields.auth);
|
||||
|
||||
return (
|
||||
<FieldSet
|
||||
fieldName="authorization"
|
||||
name="Authorization"
|
||||
field={auth.field}
|
||||
isRequired
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{params.map((param, i) => {
|
||||
if (param.length === 0) return;
|
||||
const name = paramNames[i];
|
||||
const type = paramTypes[i];
|
||||
if ((!auth || type !== auth.in) && param.length === 0) return;
|
||||
|
||||
return (
|
||||
<CollapsiblePanel key={name} title={name}>
|
||||
{auth && type === auth.in ? renderAuth() : null}
|
||||
{param.map((field) => {
|
||||
const fieldName = `${type}.${field.name}` as const;
|
||||
|
||||
@@ -428,6 +383,204 @@ function BodyInput({ field: _field }: { field: RequestSchema }) {
|
||||
);
|
||||
}
|
||||
|
||||
type SubmitHandler = (values: FormValues) => FormValues;
|
||||
type AuthField = {
|
||||
fieldName: string;
|
||||
defaultValue: unknown;
|
||||
|
||||
original: SecurityEntry;
|
||||
children: ReactNode;
|
||||
|
||||
mapOutput?: (values: unknown) => unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* manipulate values without mutating the original object
|
||||
*
|
||||
* @returns a new manipulated object
|
||||
*/
|
||||
function manipulateValues(
|
||||
values: Record<string, unknown>,
|
||||
fieldName: string,
|
||||
update: (v: unknown) => unknown,
|
||||
): Record<string, unknown> {
|
||||
const root = { ...values };
|
||||
let current = root;
|
||||
const segments = fieldName.split('.');
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
|
||||
if (i !== segments.length - 1) {
|
||||
let v = current[segment] as Record<string, unknown>;
|
||||
v = { ...v };
|
||||
|
||||
current[segment] = v;
|
||||
current = v;
|
||||
continue;
|
||||
}
|
||||
|
||||
const updated = update(current[segment]);
|
||||
if (updated === undefined) {
|
||||
delete current[segment];
|
||||
} else {
|
||||
current[segment] = updated;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function AuthInput({
|
||||
securities,
|
||||
onSetSubmitHandler,
|
||||
}: {
|
||||
securities: SecurityEntry[];
|
||||
onSetSubmitHandler: (handler: SubmitHandler) => void;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
const auth = useMemo(() => {
|
||||
const result: AuthField[] = [];
|
||||
|
||||
for (const security of securities) {
|
||||
if (security.type === 'http' && security.scheme === 'basic') {
|
||||
const fieldName = `header.Authorization`;
|
||||
|
||||
result.push({
|
||||
fieldName,
|
||||
original: security,
|
||||
defaultValue: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
mapOutput(out) {
|
||||
if (
|
||||
out &&
|
||||
typeof out === 'object' &&
|
||||
'username' in out &&
|
||||
'password' in out
|
||||
) {
|
||||
return `Basic ${btoa(`${out.username}:${out.password}`)}`;
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
children: (
|
||||
<FieldSet
|
||||
name="Authorization (header)"
|
||||
field={{
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
description: security.description,
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else if (security.type === 'http' || security.type === 'oauth2') {
|
||||
const fieldName = 'header.Authorization';
|
||||
result.push({
|
||||
fieldName: fieldName,
|
||||
original: security,
|
||||
defaultValue: 'Bearer ',
|
||||
children: (
|
||||
<>
|
||||
<FieldSet
|
||||
name="Authorization (header)"
|
||||
fieldName={fieldName}
|
||||
field={{
|
||||
type: 'string',
|
||||
description:
|
||||
security.description ?? `The Authorization access token.`,
|
||||
}}
|
||||
/>
|
||||
<AuthFooter authorization={security} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
} else if (security.type === 'apiKey') {
|
||||
const fieldName = `${security.in}.${security.name}`;
|
||||
|
||||
result.push({
|
||||
fieldName,
|
||||
defaultValue: '',
|
||||
original: security,
|
||||
children: (
|
||||
<FieldSet
|
||||
fieldName={fieldName}
|
||||
name={security.name}
|
||||
field={{
|
||||
type: 'string',
|
||||
description:
|
||||
security.description ?? `The API key in ${security.in}.`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: handle OpenID connect
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [securities]);
|
||||
|
||||
useEffect(() => {
|
||||
const prefix = '__fumadocs_auth';
|
||||
|
||||
for (const item of auth) {
|
||||
const value = localStorage.getItem(prefix + item.original.id);
|
||||
|
||||
form.setValue(item.fieldName, value ? JSON.parse(value) : '');
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const item of auth) {
|
||||
const v = form.getValues(item.fieldName);
|
||||
if (v) {
|
||||
localStorage.setItem(prefix + item.original.id, JSON.stringify(v));
|
||||
}
|
||||
|
||||
form.reset((values) =>
|
||||
manipulateValues(values, item.fieldName, () => undefined),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- ignore `form`
|
||||
}, [auth, securities]);
|
||||
|
||||
onSetSubmitHandler((values) => {
|
||||
for (const item of auth) {
|
||||
if (!item.mapOutput) continue;
|
||||
|
||||
values = manipulateValues(
|
||||
values as unknown as Record<string, unknown>,
|
||||
item.fieldName,
|
||||
item.mapOutput,
|
||||
) as unknown as FormValues;
|
||||
}
|
||||
|
||||
return values;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{auth.map((item) => (
|
||||
<Fragment key={item.fieldName}>{item.children}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCustomField(
|
||||
fieldName: string,
|
||||
info: RequestSchema & { name?: string },
|
||||
@@ -540,40 +693,6 @@ function DefaultResultDisplay({ data }: { data: FetchResult }) {
|
||||
);
|
||||
}
|
||||
|
||||
function usePersistentAuthInfo(authorization?: ClientProps['authorization']) {
|
||||
const key = authorization
|
||||
? `__fumadocs_auth_${authorization.persistentId}`
|
||||
: null;
|
||||
const [info, setInfo] = useState<FormValues['authorization']>(() => {
|
||||
if (!authorization || authorization.type === 'apiKey') return '';
|
||||
if (authorization.type === 'http' && authorization.scheme === 'basic') {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
return 'Bearer';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!key) return;
|
||||
const item = localStorage.getItem(key);
|
||||
|
||||
if (item) {
|
||||
setInfo(JSON.parse(item));
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
return {
|
||||
info,
|
||||
saveInfo: useEffectEvent((value: FormValues['authorization']) => {
|
||||
if (!key) return;
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function CollapsiblePanel({
|
||||
title,
|
||||
children,
|
||||
@@ -593,39 +712,3 @@ function CollapsiblePanel({
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function writeAuthInfo(
|
||||
authorization: Security,
|
||||
input: FormValues['authorization'],
|
||||
header: Record<string, unknown>,
|
||||
query: Record<string, unknown>,
|
||||
cookie: Record<string, string>,
|
||||
) {
|
||||
if (authorization.type === 'apiKey') {
|
||||
if (authorization.in === 'header') {
|
||||
header[authorization.name] = input as string;
|
||||
}
|
||||
|
||||
if (authorization.in === 'query') {
|
||||
query[authorization.name] = input as string;
|
||||
}
|
||||
|
||||
if (authorization.in === 'cookie') {
|
||||
cookie[authorization.name] = input as string;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
authorization.type === 'http' &&
|
||||
authorization.scheme === 'basic' &&
|
||||
typeof input === 'object'
|
||||
) {
|
||||
header.Authorization = `Basic ${btoa(`${input.username}:${input.password}`)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof input === 'string') {
|
||||
header.Authorization = input;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { getPreferredType, type ParsedSchema } from '@/utils/schema';
|
||||
import { getSecurities } from '@/utils/get-security';
|
||||
import { type ClientProps } from './client';
|
||||
import { ClientLazy } from '@/ui/lazy';
|
||||
|
||||
@@ -48,7 +47,7 @@ export async function APIPlayground({
|
||||
};
|
||||
|
||||
const props: ClientProps = {
|
||||
authorization: getAuthorizationField(method, ctx),
|
||||
securities: parseSecurities(method, ctx),
|
||||
method: method.method,
|
||||
route: path,
|
||||
parameters: method.parameters?.map((v) => ({
|
||||
@@ -118,42 +117,30 @@ function writeReferences(
|
||||
return output;
|
||||
}
|
||||
|
||||
function getAuthorizationField(
|
||||
function parseSecurities(
|
||||
method: MethodInformation,
|
||||
{ schema: { document } }: RenderContext,
|
||||
): ClientProps['authorization'] {
|
||||
): ClientProps['securities'] {
|
||||
const result: ClientProps['securities'] = [];
|
||||
const security = method.security ?? document.security ?? [];
|
||||
if (security.length === 0) return;
|
||||
if (security.length === 0) return result;
|
||||
|
||||
let item;
|
||||
for (const requirements of security) {
|
||||
const keys = Object.keys(requirements).length;
|
||||
for (const map of security) {
|
||||
const list: ClientProps['securities'][number] = [];
|
||||
|
||||
if (keys === 0) return;
|
||||
else if (keys === 1) item = requirements;
|
||||
for (const [key, scopes] of Object.entries(map)) {
|
||||
const scheme = document.components?.securitySchemes?.[key];
|
||||
if (!scheme) continue;
|
||||
|
||||
list.push({
|
||||
...scheme,
|
||||
scopes,
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
result.push(list);
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
console.warn(
|
||||
`Cannot find suitable security scheme for API Playground from ${JSON.stringify(security, null, 2)}. Only schemes with one requirement are allowed at the moment.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scheme = getSecurities(item, document)[0];
|
||||
|
||||
if (scheme.type === 'oauth2') {
|
||||
const flow = Object.keys(scheme.flows).at(0);
|
||||
if (!flow) throw new Error("security scheme's `flows` must not be empty");
|
||||
|
||||
if (flow === 'implicit' || flow === 'password')
|
||||
throw new Error(
|
||||
`OAuth 2.0 flow type: ${flow} is not supported, consider other types like \`authorizationCode\` instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
persistentId: Object.keys(item)[0],
|
||||
...scheme,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import type { MethodInformation, RenderContext } from '@/types';
|
||||
import type { ResolvedSchema } from '@/utils/schema';
|
||||
import { getPreferredType } from '@/utils/schema';
|
||||
import { sample } from 'openapi-sampler';
|
||||
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
|
||||
import type { RequestData } from '@/requests/_shared';
|
||||
|
||||
export function getRequestData(
|
||||
path: string,
|
||||
method: MethodInformation,
|
||||
sampleKey: string | null,
|
||||
{ schema: { document } }: RenderContext,
|
||||
_ctx: RenderContext,
|
||||
): RequestData {
|
||||
const result: RequestData = {
|
||||
path: {},
|
||||
@@ -49,16 +48,6 @@ export function getRequestData(
|
||||
}
|
||||
}
|
||||
|
||||
const requirements = method.security ?? document.security;
|
||||
if (requirements && requirements.length > 0) {
|
||||
for (const security of getSecurities(requirements[0], document)) {
|
||||
const prefix = getSecurityPrefix(security);
|
||||
const name = security.type === 'apiKey' ? security.name : 'Authorization';
|
||||
|
||||
result.header[name] = prefix ? `${prefix} <token>` : '<token>';
|
||||
}
|
||||
}
|
||||
|
||||
if (method.requestBody) {
|
||||
const body = method.requestBody.content;
|
||||
const type = getPreferredType(body);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type NoReference,
|
||||
type ResolvedSchema,
|
||||
} from '@/utils/schema';
|
||||
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
|
||||
import { getSecurityPrefix } from '@/utils/get-security';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { Markdown } from '../markdown';
|
||||
import { heading } from '../heading';
|
||||
@@ -25,7 +25,13 @@ import {
|
||||
} from '@/render/operation/api-example';
|
||||
import { Badge, MethodLabel } from '@/ui/components/method-label';
|
||||
import { type SampleGenerator } from '@/requests/_shared';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import {
|
||||
Tab,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from 'fumadocs-ui/components/tabs';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { CopyResponseTypeScript } from '@/ui/client';
|
||||
|
||||
@@ -323,63 +329,90 @@ function AuthSection({
|
||||
requirements: SecurityRequirementObject[];
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
let id = 0;
|
||||
const info: ReactNode[] = [];
|
||||
|
||||
for (const requirement of requirements) {
|
||||
for (const schema of getSecurities(requirement, document)) {
|
||||
const prefix = getSecurityPrefix(schema);
|
||||
const scopeElement =
|
||||
schema.scopes.length > 0 ? (
|
||||
<p>
|
||||
Scope: <code>{schema.scopes.join(', ')}</code>
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
if (schema.type === 'http' || schema.type === 'oauth2') {
|
||||
info.push(
|
||||
<renderer.Property
|
||||
key={id++}
|
||||
name="Authorization"
|
||||
type={prefix ? `${prefix} <token>` : '<token>'}
|
||||
required
|
||||
return (
|
||||
<Tabs defaultValue="0">
|
||||
<TabsList>
|
||||
{requirements.map((securities, i) => (
|
||||
<TabsTrigger key={i} value={i.toString()}>
|
||||
{Object.keys(securities).join(' & ')}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{requirements.map((securities, i) => {
|
||||
return (
|
||||
<TabsContent
|
||||
key={i}
|
||||
value={i.toString()}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
{schema.description ? <Markdown text={schema.description} /> : null}
|
||||
<p>
|
||||
In: <code>header</code>
|
||||
</p>
|
||||
{scopeElement}
|
||||
</renderer.Property>,
|
||||
);
|
||||
}
|
||||
{Object.entries(securities).map(([key, scopes]) => {
|
||||
const schema = document.components?.securitySchemes?.[key];
|
||||
if (!schema) return;
|
||||
|
||||
if (schema.type === 'apiKey') {
|
||||
info.push(
|
||||
<renderer.Property key={id++} name={schema.name} type="<token>">
|
||||
{schema.description ? <Markdown text={schema.description} /> : null}
|
||||
<p>
|
||||
In: <code>{schema.in}</code>
|
||||
{scopeElement}
|
||||
</p>
|
||||
</renderer.Property>,
|
||||
);
|
||||
}
|
||||
const prefix = getSecurityPrefix(schema);
|
||||
const scopeElement =
|
||||
scopes.length > 0 ? (
|
||||
<p>
|
||||
Scope: <code>{scopes.join(', ')}</code>
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
if (schema.type === 'openIdConnect') {
|
||||
info.push(
|
||||
<renderer.Property
|
||||
key={id++}
|
||||
name="OpenID Connect"
|
||||
type="<token>"
|
||||
required
|
||||
>
|
||||
{schema.description ? <Markdown text={schema.description} /> : null}
|
||||
{scopeElement}
|
||||
</renderer.Property>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (schema.type === 'http' || schema.type === 'oauth2') {
|
||||
return (
|
||||
<renderer.Property
|
||||
key={key}
|
||||
name="Authorization"
|
||||
type={prefix ? `${prefix} <token>` : '<token>'}
|
||||
required
|
||||
>
|
||||
{schema.description && (
|
||||
<Markdown text={schema.description} />
|
||||
)}
|
||||
<p>
|
||||
In: <code>header</code>
|
||||
</p>
|
||||
{scopeElement}
|
||||
</renderer.Property>
|
||||
);
|
||||
}
|
||||
|
||||
return info;
|
||||
if (schema.type === 'apiKey') {
|
||||
return (
|
||||
<renderer.Property
|
||||
key={key}
|
||||
name={schema.name}
|
||||
type="<token>"
|
||||
>
|
||||
{schema.description && (
|
||||
<Markdown text={schema.description} />
|
||||
)}
|
||||
<p>
|
||||
In: <code>{schema.in}</code>
|
||||
{scopeElement}
|
||||
</p>
|
||||
</renderer.Property>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'openIdConnect') {
|
||||
return (
|
||||
<renderer.Property
|
||||
key={key}
|
||||
name="OpenID Connect"
|
||||
type="<token>"
|
||||
required
|
||||
>
|
||||
{schema.description && (
|
||||
<Markdown text={schema.description} />
|
||||
)}
|
||||
{scopeElement}
|
||||
</renderer.Property>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export function getSecurities(
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getSecurityPrefix(security: Security): string | undefined {
|
||||
export function getSecurityPrefix(
|
||||
security: SecuritySchemeObject,
|
||||
): string | undefined {
|
||||
if (security.type === 'http')
|
||||
return {
|
||||
basic: 'Basic',
|
||||
|
||||
Reference in New Issue
Block a user