Skip to content

Commit

Permalink
feat(console): integrate jwt customizer test api (#5532)
Browse files Browse the repository at this point in the history
* feat(console): integrate jwt customizer test api

integrate jwt customizer test api

* refactor(console,core): jwt test api integration

jwt test api integration

* chore: add cloud connection scope config for fetching custom jwt

---------

Co-authored-by: Darcy Ye <[email protected]>
  • Loading branch information
simeng-li and darcyYe authored Mar 21, 2024
1 parent 5c6af38 commit f1f6b1c
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function MonacoCodeEditor({
const isMultiModals = useMemo(() => models.length > 1, [models]);

// Get the container ref and the editor height
const { containerRef, editorHeight } = useEditorHeight();
const { containerRef, headerRef, editorHeight } = useEditorHeight();

useEffect(() => {
// Monaco will be ready after the editor is mounted, useEffect will be called after the monaco is ready
Expand Down Expand Up @@ -108,8 +108,8 @@ function MonacoCodeEditor({
);

return (
<div className={classNames(className, styles.codeEditor)}>
<header>
<div ref={containerRef} className={classNames(className, styles.codeEditor)}>
<header ref={headerRef}>
<div className={styles.tabList}>
{models.map(({ name, title, icon }) => (
<div
Expand Down Expand Up @@ -159,7 +159,7 @@ function MonacoCodeEditor({
)}
</div>
</header>
<div ref={containerRef} className={styles.editorContainer}>
<div className={styles.editorContainer}>
{activeModel && (
<Editor
height={editorHeight}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import { useRef, useState, useLayoutEffect } from 'react';
// @see {@link https://github.com/react-monaco-editor/react-monaco-editor/issues/391}
const useEditorHeight = () => {
const containerRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);

const [editorHeight, setEditorHeight] = useState<number | string>('100%');
const safeArea = 16;

useLayoutEffect(() => {
const handleResize = () => {
const safeAreaHeight = headerRef.current?.clientHeight
? headerRef.current.clientHeight + safeArea
: safeArea;

if (containerRef.current) {
setEditorHeight(containerRef.current.clientHeight - safeArea);
setEditorHeight(containerRef.current.clientHeight - safeAreaHeight);
}
};

Expand All @@ -29,7 +34,7 @@ const useEditorHeight = () => {
};
}, []);

return { containerRef, editorHeight };
return { containerRef, headerRef, editorHeight };
};

export default useEditorHeight;
39 changes: 29 additions & 10 deletions packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { LogtoJwtTokenPath } from '@logto/schemas';
import { type JsonObject, LogtoJwtTokenPath } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFormContext, Controller, type ControllerRenderProps } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import useApi from '@/hooks/use-api';

import MonacoCodeEditor, { type ModelControl } from '../MonacoCodeEditor/index.js';
import { type JwtClaimsFormType } from '../type.js';
import MonacoCodeEditor, { type ModelControl } from '../MonacoCodeEditor';
import { type JwtClaimsFormType } from '../type';
import {
accessTokenPayloadTestModel,
clientCredentialsPayloadTestModel,
userContextTestModel,
} from '../utils/config.js';
} from '../utils/config';
import { formatFormDataToTestRequestPayload } from '../utils/format';

import TestResult, { type TestResultData } from './TestResult.js';
import TestResult, { type TestResultData } from './TestResult';
import * as styles from './index.module.scss';

type Props = {
Expand All @@ -24,13 +26,15 @@ type Props = {

const userTokenModelSettings = [accessTokenPayloadTestModel, userContextTestModel];
const machineToMachineTokenModelSettings = [clientCredentialsPayloadTestModel];
const testEndpointPath = 'api/configs/jwt-customizer/test';

function TestTab({ isActive }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.jwt_claims' });
const [testResult, setTestResult] = useState<TestResultData>();
const [activeModelName, setActiveModelName] = useState<string>();
const api = useApi({ hideErrorToast: true });

const { watch, control, formState } = useFormContext<JwtClaimsFormType>();
const { watch, control, formState, getValues } = useFormContext<JwtClaimsFormType>();
const tokenType = watch('tokenType');

const editorModels = useMemo(
Expand All @@ -45,9 +49,24 @@ function TestTab({ isActive }: Props) {
setActiveModelName(editorModels[0]?.name);
}, [editorModels, tokenType]);

const onTestHandler = useCallback(() => {
// TODO: API integration, read form data and send the request to the server
}, []);
const onTestHandler = useCallback(async () => {
const payload = getValues();

const result = await api
.post(testEndpointPath, {
json: formatFormDataToTestRequestPayload(payload),
})
.json<JsonObject>()
.catch((error: unknown) => {
setTestResult({
error: error instanceof Error ? error.message : String(error),
});
});

if (result) {
setTestResult({ payload: JSON.stringify(result, null, 2) });
}
}, [api, getValues]);

const getModelControllerProps = useCallback(
({ value, onChange }: ControllerRenderProps<JwtClaimsFormType, 'testSample'>): ModelControl => {
Expand Down Expand Up @@ -124,7 +143,7 @@ function TestTab({ isActive }: Props) {
}}
render={({ field }) => (
<MonacoCodeEditor
className={styles.flexGrow}
className={testResult ? styles.shrinkCodeEditor : styles.flexGrow}
enabledActions={['restore', 'copy']}
models={editorModels}
activeModelName={activeModelName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,13 @@
margin-bottom: _.unit(4);
}

.shrinkCodeEditor {
height: 50%;
}

.testResult {
margin-top: _.unit(3);
height: calc(50% - _.unit(3));
flex: 1;
background-color: var(--color-bg-layer-2);
border-radius: 8px;
border: 1px solid var(--color-divider);
Expand Down Expand Up @@ -197,5 +201,5 @@
}

.flexGrow {
flex-grow: 1;
flex: 1;
}
6 changes: 3 additions & 3 deletions packages/console/src/pages/JwtClaims/utils/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,19 @@ const standardTokenPayloadData = {
aud: 'http://localhost:3000/api/test',
};

const defaultAccessTokenPayload: AccessTokenPayload = {
export const defaultAccessTokenPayload: AccessTokenPayload = {
...standardTokenPayloadData,
grantId: 'grant_123',
accountId: 'uid_123',
kind: 'AccessToken',
};

const defaultClientCredentialsPayload: ClientCredentialsPayload = {
export const defaultClientCredentialsPayload: ClientCredentialsPayload = {
...standardTokenPayloadData,
kind: 'ClientCredentials',
};

const defaultUserTokenContextData = {
export const defaultUserTokenContextData = {
user: {
id: '123',
primaryEmail: '[email protected]',
Expand Down
34 changes: 33 additions & 1 deletion packages/console/src/pages/JwtClaims/utils/format.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {
type LogtoJwtTokenPath,
LogtoJwtTokenPath,
type AccessTokenJwtCustomizer,
type ClientCredentialsJwtCustomizer,
} from '@logto/schemas';

import type { JwtClaimsFormType } from '../type';

import {
defaultAccessTokenPayload,
defaultClientCredentialsPayload,
defaultUserTokenContextData,
} from './config';

const formatEnvVariablesResponseToFormData = (
enVariables?: AccessTokenJwtCustomizer['envVars']
) => {
Expand Down Expand Up @@ -80,5 +86,31 @@ export const formatFormDataToRequestData = (data: JwtClaimsFormType) => {
};
};

export const formatFormDataToTestRequestPayload = ({
tokenType,
script,
environmentVariables,
testSample,
}: JwtClaimsFormType) => {
const defaultTokenSample =
tokenType === LogtoJwtTokenPath.AccessToken
? defaultAccessTokenPayload
: defaultClientCredentialsPayload;

const defaultContextSample =
tokenType === LogtoJwtTokenPath.AccessToken ? defaultUserTokenContextData : undefined;

return {
tokenType,
payload: {
script,
envVars: formatEnvVariablesFormData(environmentVariables),
tokenSample: formatSampleCodeStringToJson(testSample?.tokenSample) ?? defaultTokenSample,
contextSample:
formatSampleCodeStringToJson(testSample?.contextSample) ?? defaultContextSample,
},
};
};

export const getApiPath = (tokenType: LogtoJwtTokenPath) =>
`api/configs/jwt-customizer/${tokenType}`;
2 changes: 1 addition & 1 deletion packages/core/src/libraries/cloud-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const accessTokenResponseGuard = z.object({
* The scope here can be empty and still work, because the cloud API requests made using this client do not rely on scope verification.
* The `CloudScope.SendEmail` is added for now because it needs to call the cloud email service API.
*/
const scopes: string[] = [CloudScope.SendEmail];
const scopes: string[] = [CloudScope.SendEmail, CloudScope.FetchCustomJwt];
const accessTokenExpirationMargin = 60;

/** The library for connecting to Logto Cloud service. */
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/routes/logto-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,14 @@ export default function logtoConfigRoutes<T extends AuthedRouter>(
*/
body: z.discriminatedUnion('tokenType', [
z.object({
tokenType: z.literal(LogtoJwtTokenKey.AccessToken),
tokenType: z.literal(LogtoJwtTokenPath.AccessToken),
payload: accessTokenJwtCustomizerGuard.required({
script: true,
tokenSample: true,
}),
}),
z.object({
tokenType: z.literal(LogtoJwtTokenKey.ClientCredentials),
tokenType: z.literal(LogtoJwtTokenPath.ClientCredentials),
payload: clientCredentialsJwtCustomizerGuard.required({
script: true,
tokenSample: true,
Expand Down

0 comments on commit f1f6b1c

Please sign in to comment.