Skip to content

Commit

Permalink
Merge branch 'main' into rohin/fdr-plumb-oauth-header-key-and-token-p…
Browse files Browse the repository at this point in the history
…refix
  • Loading branch information
RohinBhargava authored Nov 18, 2024
2 parents 103381f + a31557f commit 8ccb234
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 96 deletions.
64 changes: 52 additions & 12 deletions packages/ui/app/src/atoms/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import {
type PlaygroundWebSocketRequestFormState,
} from "../playground/types";
import {
getInitialEndpointRequestFormState,
getInitialEndpointRequestFormStateWithExample,
getInitialWebSocketRequestFormState,
} from "../playground/utils";
import { pascalCaseHeaderKeys } from "../playground/utils/header-key-case";
import { FERN_USER_ATOM } from "./auth";
import { FEATURE_FLAGS_ATOM } from "./flags";
import { useAtomEffect } from "./hooks";
import { HEADER_HEIGHT_ATOM } from "./layout";
Expand Down Expand Up @@ -177,7 +178,12 @@ export const PLAYGROUND_AUTH_STATE_ATOM = atomWithStorageValidation<PlaygroundAu
);

export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth ?? PLAYGROUND_AUTH_STATE_BEARER_TOKEN_INITIAL,
(get) => ({
token:
get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth?.token ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.bearer_token ??
"",
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateBearerToken>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand All @@ -190,7 +196,12 @@ export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom(
);

export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).header ?? PLAYGROUND_AUTH_STATE_HEADER_INITIAL,
(get) => ({
headers: pascalCaseHeaderKeys({
...(get(PLAYGROUND_AUTH_STATE_ATOM).header?.headers ??
get(FERN_USER_ATOM)?.playground?.initial_state?.headers),
}),
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateHeader>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand All @@ -200,7 +211,16 @@ export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom(
);

export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM = atom(
(get) => get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth ?? PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL,
(get) => ({
username:
get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.username ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.username ??
"",
password:
get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.password ??
get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.password ??
"",
}),
(_get, set, update: SetStateAction<PlaygroundAuthStateBasicAuth>) => {
set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({
...prev,
Expand Down Expand Up @@ -265,6 +285,8 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA
return;
}

const playgroundInitialState = get(FERN_USER_ATOM)?.playground?.initial_state;

if (node.type === "endpoint") {
const context = createEndpointContext(node, definition);

Expand All @@ -277,7 +299,11 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA

set(
formStateAtom,
getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0]),
getInitialEndpointRequestFormStateWithExample(
context,
context.endpoint.examples?.[0],
playgroundInitialState,
),
);
} else if (node.type === "webSocket") {
const context = createWebSocketContext(node, definition);
Expand All @@ -290,7 +316,7 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA
return;
}

set(formStateAtom, getInitialWebSocketRequestFormState(context));
set(formStateAtom, getInitialWebSocketRequestFormState(context, playgroundInitialState));
}
playgroundFormStateFamily.remove(node.id);
},
Expand All @@ -304,9 +330,16 @@ export function usePlaygroundEndpointFormState(
): [PlaygroundEndpointRequestFormState, Dispatch<SetStateAction<PlaygroundEndpointRequestFormState>>] {
const formStateAtom = playgroundFormStateFamily(ctx.node.id);
const formState = useAtomValue(formStateAtom);
const user = useAtomValue(FERN_USER_ATOM);

return [
formState?.type === "endpoint" ? formState : getInitialEndpointRequestFormState(ctx),
formState?.type === "endpoint"
? formState
: getInitialEndpointRequestFormStateWithExample(
ctx,
ctx.endpoint.examples?.[0],
user?.playground?.initial_state,
),
useAtomCallback(
useCallbackOne(
(get, set, update: SetStateAction<PlaygroundEndpointRequestFormState>) => {
Expand All @@ -316,12 +349,16 @@ export function usePlaygroundEndpointFormState(
? update(
currentFormState?.type === "endpoint"
? currentFormState
: getInitialEndpointRequestFormState(ctx),
: getInitialEndpointRequestFormStateWithExample(
ctx,
ctx.endpoint.examples?.[0],
user?.playground?.initial_state,
),
)
: update;
set(formStateAtom, newFormState);
},
[formStateAtom, ctx],
[formStateAtom, ctx, user?.playground?.initial_state],
),
),
];
Expand All @@ -332,9 +369,12 @@ export function usePlaygroundWebsocketFormState(
): [PlaygroundWebSocketRequestFormState, Dispatch<SetStateAction<PlaygroundWebSocketRequestFormState>>] {
const formStateAtom = playgroundFormStateFamily(context.node.id);
const formState = useAtomValue(playgroundFormStateFamily(context.node.id));
const user = useAtomValue(FERN_USER_ATOM);

return [
formState?.type === "websocket" ? formState : getInitialWebSocketRequestFormState(context),
formState?.type === "websocket"
? formState
: getInitialWebSocketRequestFormState(context, user?.playground?.initial_state),
useAtomCallback(
useCallbackOne(
(get, set, update: SetStateAction<PlaygroundWebSocketRequestFormState>) => {
Expand All @@ -344,12 +384,12 @@ export function usePlaygroundWebsocketFormState(
? update(
currentFormState?.type === "websocket"
? currentFormState
: getInitialWebSocketRequestFormState(context),
: getInitialWebSocketRequestFormState(context, user?.playground?.initial_state),
)
: update;
set(formStateAtom, newFormState);
},
[formStateAtom, context],
[formStateAtom, context, user?.playground?.initial_state],
),
),
];
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/playground/WithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const WithLabel: FC<PropsWithChildren<WithLabelProps>> = ({
isRequired={!unwrapped.isOptional}
isList={unwrapped.shape.type === "list"}
isBoolean={unwrapped.shape.type === "primitive" && unwrapped.shape.value.type === "boolean"}
typeShorthand={renderTypeShorthandRoot(unwrapped.shape, types)}
typeShorthand={renderTypeShorthandRoot(property.valueShape, types, false, true)}
>
{children}
</WithLabelInternal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useAtomValue } from "jotai";
import { ReactElement } from "react";
import { PLAYGROUND_AUTH_STATE_ATOM } from "../../atoms";
import { PlaygroundAuthState } from "../types";
import { pascalCaseHeaderKey } from "../utils/header-key-case";

interface PlaygroundCardTriggerManualProps {
auth: APIV1Read.ApiAuth;
Expand Down Expand Up @@ -74,7 +75,7 @@ function isAuthed(auth: APIV1Read.ApiAuth, authState: PlaygroundAuthState): bool
bearerAuth: () => !isEmpty(authState.bearerAuth?.token.trim()),
basicAuth: () =>
!isEmpty(authState.basicAuth?.username.trim()) && !isEmpty(authState.basicAuth?.password.trim()),
header: (header) => !isEmpty(authState.header?.headers[header.headerWireValue]?.trim()),
header: (header) => !isEmpty(authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)]?.trim()),
oAuth: () => {
const authToken =
authState.oauth?.selectedInputMethod === "credentials"
Expand Down
15 changes: 10 additions & 5 deletions packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { APIV1Read } from "@fern-api/fdr-sdk/client/types";
import { unknownToString } from "@fern-api/ui-core-utils";
import { atom } from "jotai";
import { useAtom } from "jotai/react";
import type { ReactElement, SetStateAction } from "react";
import { useMemoOne } from "use-memo-one";
import { PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms";
import { PasswordInputGroup } from "../PasswordInputGroup";
import { pascalCaseHeaderKey } from "../utils/header-key-case";

export function PlaygroundHeaderAuthForm({
header,
Expand All @@ -17,14 +19,15 @@ export function PlaygroundHeaderAuthForm({
useMemoOne(
() =>
atom(
(get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[header.headerWireValue],
(get) =>
get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(header.headerWireValue)],
(_get, set, change: SetStateAction<string>) => {
set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({
headers: {
...headers,
[header.headerWireValue]:
[pascalCaseHeaderKey(header.headerWireValue)]:
typeof change === "function"
? change(headers[header.headerWireValue] ?? "")
? change(headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "")
: change,
},
}));
Expand All @@ -37,12 +40,14 @@ export function PlaygroundHeaderAuthForm({
return (
<li className="-mx-4 space-y-2 p-4">
<label className="inline-flex flex-wrap items-baseline">
<span className="font-mono text-sm">{header.nameOverride ?? header.headerWireValue}</span>
<span className="font-mono text-sm">
{header.nameOverride ?? pascalCaseHeaderKey(header.headerWireValue)}
</span>
</label>
<div>
<PasswordInputGroup
onValueChange={setValue}
value={value}
value={unknownToString(value ?? "")}
autoComplete="off"
data-1p-ignore="true"
disabled={disabled}
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/app/src/playground/code-snippets/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { toCurlyBraceEndpointPathLiteral } from "@fern-api/fdr-sdk/api-definition";
import { FdrAPI, type APIV1Read } from "@fern-api/fdr-sdk/client/types";
import { SnippetTemplateResolver } from "@fern-api/template-resolver";
import { unknownToString } from "@fern-api/ui-core-utils";
import { UnreachableCaseError } from "ts-essentials";
import { provideRegistryService } from "../../services/registry";
import { PlaygroundAuthState, PlaygroundEndpointRequestFormState } from "../types";
Expand Down Expand Up @@ -182,7 +183,7 @@ export class PlaygroundCodeSnippetResolver {
const headers = { ...this.headers };

// TODO: ensure case insensitivity
if (headers["Content-Type"] === "multipart/form-data") {
if (unknownToString(headers["Content-Type"]).includes("multipart/form-data")) {
delete headers["Content-Type"]; // fetch will set this automatically
}

Expand Down
23 changes: 14 additions & 9 deletions packages/ui/app/src/playground/endpoint/PlaygroundEndpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { Loadable, failed, loaded, loading, notStartedLoading } from "@fern-ui/l
import { useEventCallback } from "@fern-ui/react-commons";
import { mapValues } from "es-toolkit/object";
import { SendSolid } from "iconoir-react";
import { useSetAtom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { ReactElement, useCallback, useState } from "react";
import {
FERN_USER_ATOM,
PLAYGROUND_AUTH_STATE_ATOM,
PLAYGROUND_AUTH_STATE_OAUTH_ATOM,
store,
Expand All @@ -25,27 +26,31 @@ import { executeProxyRest } from "../fetch-utils/executeProxyRest";
import { executeProxyStream } from "../fetch-utils/executeProxyStream";
import type { GrpcProxyRequest, ProxyRequest } from "../types";
import { PlaygroundResponse } from "../types/playgroundResponse";
import {
buildAuthHeaders,
getInitialEndpointRequestFormState,
getInitialEndpointRequestFormStateWithExample,
serializeFormStateBody,
} from "../utils";
import { buildAuthHeaders, getInitialEndpointRequestFormStateWithExample, serializeFormStateBody } from "../utils";
import { usePlaygroundBaseUrl } from "../utils/select-environment";
import { PlaygroundEndpointContent } from "./PlaygroundEndpointContent";
import { PlaygroundEndpointPath } from "./PlaygroundEndpointPath";

export const PlaygroundEndpoint = ({ context }: { context: EndpointContext }): ReactElement => {
const user = useAtomValue(FERN_USER_ATOM);
const { node, endpoint, auth } = context;

const [formState, setFormState] = usePlaygroundEndpointFormState(context);

const resetWithExample = useEventCallback(() => {
setFormState(getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0]));
setFormState(
getInitialEndpointRequestFormStateWithExample(
context,
context.endpoint.examples?.[0],
user?.playground?.initial_state,
),
);
});

const resetWithoutExample = useEventCallback(() => {
setFormState(getInitialEndpointRequestFormState(context));
setFormState(
getInitialEndpointRequestFormStateWithExample(context, undefined, user?.playground?.initial_state),
);
});

const basePath = useBasePath();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { PropertyKey, type EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import { EMPTY_ARRAY, visitDiscriminatedUnion } from "@fern-api/ui-core-utils";
import { Dispatch, FC, SetStateAction, useCallback, useMemo } from "react";
import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm";
import { PlaygroundObjectForm } from "../form/PlaygroundObjectForm";
import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm";
import { PlaygroundEndpointRequestFormState, PlaygroundFormStateBody } from "../types";
import { pascalCaseHeaderKey } from "../utils/header-key-case";
import { PlaygroundEndpointAliasForm } from "./PlaygroundEndpointAliasForm";
import { PlaygroundEndpointFormSection } from "./PlaygroundEndpointFormSection";
import { PlaygroundEndpointMultipartForm } from "./PlaygroundEndpointMultipartForm";
Expand Down Expand Up @@ -104,7 +105,10 @@ export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Headers">
<PlaygroundObjectPropertiesForm
id="header"
properties={headers}
properties={headers.map((header) => ({
...header,
key: PropertyKey(pascalCaseHeaderKey(header.key)),
}))}
extraProperties={undefined}
onChange={setHeaders}
value={formState?.headers}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ export const PlaygroundObjectPropertiesForm = memo<PlaygroundObjectPropertiesFor
type: "value",
value: property.key,
label: property.key,
helperText: renderTypeShorthandRoot(property.valueShape, types),
helperText: renderTypeShorthandRoot(
{ type: "optional", shape: property.valueShape, default: undefined },
types,
false,
true,
),
labelClassName: "font-mono",
tooltip: property.description != null ? <Markdown size="xs" mdx={property.description} /> : undefined,
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/playground/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const PlaygroundAuthStateBasicAuthSchema = z.strictObject({
export type PlaygroundAuthStateBasicAuth = z.infer<typeof PlaygroundAuthStateBasicAuthSchema>;
export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL: PlaygroundAuthStateBasicAuth = { username: "", password: "" };

export const PlaygroundAuthStateOAuthSchema = z.strictObject({
export const PlaygroundAuthStateOAuthSchema = z.object({
clientId: z.string(),
clientSecret: z.string(),
accessToken: z.string(),
Expand Down
19 changes: 9 additions & 10 deletions packages/ui/app/src/playground/utils/auth-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedU
import { decodeJwt } from "jose";
import { noop } from "ts-essentials";
import { PlaygroundAuthState } from "../types";
import { pascalCaseHeaderKey } from "./header-key-case";
import {
OAuthClientCredentialReferencedEndpointLoginFlowProps,
oAuthClientCredentialReferencedEndpointLoginFlow,
Expand Down Expand Up @@ -30,11 +31,13 @@ export function buildAuthHeaders(
headers["Authorization"] = `Bearer ${token}`;
},
header: (header) => {
let value = authState.header?.headers[header.headerWireValue] ?? "";
// pluck the value from the headers object (avoid inheriting all the other headers)
let value = authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "";
if (redacted) {
value = obfuscateSecret(value);
}
headers[header.headerWireValue] = header.prefix != null ? `${header.prefix} ${value}` : value;
headers[pascalCaseHeaderKey(header.headerWireValue)] =
header.prefix != null ? `${header.prefix} ${value}` : value;
},
basicAuth: () => {
const username = authState.basicAuth?.username ?? "";
Expand Down Expand Up @@ -77,14 +80,10 @@ export function buildAuthHeaders(
} catch {}
}

const tokenPrefix = authState.oauth?.tokenPrefix?.length
? authState.oauth.tokenPrefix
: "Bearer";
if (redacted) {
headers["Authorization"] = `${tokenPrefix} ${obfuscateSecret(token)}`;
} else {
headers["Authorization"] = `${tokenPrefix} ${token}`;
}
const tokenPrefix = authState.oauth?.tokenPrefix || "Bearer";

headers["Authorization"] =
`${tokenPrefix.length ? `${tokenPrefix} ` : ""}${redacted ? obfuscateSecret(token) : token}`;
},
});
},
Expand Down
Loading

0 comments on commit 8ccb234

Please sign in to comment.