From f1f6b1cd610f5addb8b3aec90f3546c39897ca5d Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 21 Mar 2024 15:26:30 +0800 Subject: [PATCH] feat(console): integrate jwt customizer test api (#5532) * 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 --- .../JwtClaims/MonacoCodeEditor/index.tsx | 8 ++-- .../MonacoCodeEditor/use-editor-height.ts | 9 ++++- .../JwtClaims/SettingsSection/TestTab.tsx | 39 ++++++++++++++----- .../SettingsSection/index.module.scss | 8 +++- .../src/pages/JwtClaims/utils/config.tsx | 6 +-- .../src/pages/JwtClaims/utils/format.ts | 34 +++++++++++++++- .../core/src/libraries/cloud-connection.ts | 2 +- packages/core/src/routes/logto-config.ts | 4 +- 8 files changed, 85 insertions(+), 25 deletions(-) diff --git a/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx b/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx index 273354514..485048dc3 100644 --- a/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx +++ b/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx @@ -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 @@ -108,8 +108,8 @@ function MonacoCodeEditor({ ); return ( -
-
+
+
{models.map(({ name, title, icon }) => (
-
+
{activeModel && ( { const containerRef = useRef(null); + const headerRef = useRef(null); const [editorHeight, setEditorHeight] = useState('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); } }; @@ -29,7 +34,7 @@ const useEditorHeight = () => { }; }, []); - return { containerRef, editorHeight }; + return { containerRef, headerRef, editorHeight }; }; export default useEditorHeight; diff --git a/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx b/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx index 9929483f8..06e102e50 100644 --- a/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx +++ b/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx @@ -1,4 +1,4 @@ -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'; @@ -6,16 +6,18 @@ 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 = { @@ -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(); const [activeModelName, setActiveModelName] = useState(); + const api = useApi({ hideErrorToast: true }); - const { watch, control, formState } = useFormContext(); + const { watch, control, formState, getValues } = useFormContext(); const tokenType = watch('tokenType'); const editorModels = useMemo( @@ -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() + .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): ModelControl => { @@ -124,7 +143,7 @@ function TestTab({ isActive }: Props) { }} render={({ field }) => ( { @@ -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}`; diff --git a/packages/core/src/libraries/cloud-connection.ts b/packages/core/src/libraries/cloud-connection.ts index d09343d06..1a3072a22 100644 --- a/packages/core/src/libraries/cloud-connection.ts +++ b/packages/core/src/libraries/cloud-connection.ts @@ -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. */ diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 97e11799f..917a47c20 100644 --- a/packages/core/src/routes/logto-config.ts +++ b/packages/core/src/routes/logto-config.ts @@ -304,14 +304,14 @@ export default function logtoConfigRoutes( */ 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,