From 776ce7f57d5747d5d7f76b1189085c797fbd6f12 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 20 Mar 2024 10:15:09 +0800 Subject: [PATCH 1/3] feat(console): integrate jwt customizer test api integrate jwt customizer test api --- .../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 | 26 ++++++++++++- 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx b/packages/console/src/pages/JwtClaims/MonacoCodeEditor/index.tsx index 27335451489..485048dc3b1 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 9929483f88a..3f4299ae2ef 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/config/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,23 @@ export const formatFormDataToRequestData = (data: JwtClaimsFormType) => { }; }; +export const formatFormDataToTestRequestPayload = (data: JwtClaimsFormType) => { + const defaultTokenPayload = + data.tokenType === LogtoJwtTokenPath.AccessToken + ? defaultAccessTokenPayload + : defaultClientCredentialsPayload; + + const defaultContext = + data.tokenType === LogtoJwtTokenPath.AccessToken ? defaultUserTokenContextData : undefined; + + return { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- parse empty string as undefined + script: data.script || undefined, + envVars: formatEnvVariablesFormData(data.environmentVariables), + token: formatSampleCodeStringToJson(data.testSample?.tokenSample) ?? defaultTokenPayload, + context: formatSampleCodeStringToJson(data.testSample?.contextSample) ?? defaultContext, + }; +}; + export const getApiPath = (tokenType: LogtoJwtTokenPath) => `api/configs/jwt-customizer/${tokenType}`; From 0ea1dd678ac681756a887debd2ac0b86cbda10cb Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 21 Mar 2024 10:36:01 +0800 Subject: [PATCH 2/3] refactor(console,core): jwt test api integration jwt test api integration --- .../JwtClaims/SettingsSection/TestTab.tsx | 2 +- .../src/pages/JwtClaims/utils/format.ts | 28 ++++++++++++------- packages/core/src/routes/logto-config.ts | 4 +-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx b/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx index 3f4299ae2ef..06e102e50d0 100644 --- a/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx +++ b/packages/console/src/pages/JwtClaims/SettingsSection/TestTab.tsx @@ -26,7 +26,7 @@ type Props = { const userTokenModelSettings = [accessTokenPayloadTestModel, userContextTestModel]; const machineToMachineTokenModelSettings = [clientCredentialsPayloadTestModel]; -const testEndpointPath = 'api/config/jwt-customizer/test'; +const testEndpointPath = 'api/configs/jwt-customizer/test'; function TestTab({ isActive }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.jwt_claims' }); diff --git a/packages/console/src/pages/JwtClaims/utils/format.ts b/packages/console/src/pages/JwtClaims/utils/format.ts index fb91325449c..31c8308c5e1 100644 --- a/packages/console/src/pages/JwtClaims/utils/format.ts +++ b/packages/console/src/pages/JwtClaims/utils/format.ts @@ -86,21 +86,29 @@ export const formatFormDataToRequestData = (data: JwtClaimsFormType) => { }; }; -export const formatFormDataToTestRequestPayload = (data: JwtClaimsFormType) => { - const defaultTokenPayload = - data.tokenType === LogtoJwtTokenPath.AccessToken +export const formatFormDataToTestRequestPayload = ({ + tokenType, + script, + environmentVariables, + testSample, +}: JwtClaimsFormType) => { + const defaultTokenSample = + tokenType === LogtoJwtTokenPath.AccessToken ? defaultAccessTokenPayload : defaultClientCredentialsPayload; - const defaultContext = - data.tokenType === LogtoJwtTokenPath.AccessToken ? defaultUserTokenContextData : undefined; + const defaultContextSample = + tokenType === LogtoJwtTokenPath.AccessToken ? defaultUserTokenContextData : undefined; return { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- parse empty string as undefined - script: data.script || undefined, - envVars: formatEnvVariablesFormData(data.environmentVariables), - token: formatSampleCodeStringToJson(data.testSample?.tokenSample) ?? defaultTokenPayload, - context: formatSampleCodeStringToJson(data.testSample?.contextSample) ?? defaultContext, + tokenType, + payload: { + script, + envVars: formatEnvVariablesFormData(environmentVariables), + tokenSample: formatSampleCodeStringToJson(testSample?.tokenSample) ?? defaultTokenSample, + contextSample: + formatSampleCodeStringToJson(testSample?.contextSample) ?? defaultContextSample, + }, }; }; diff --git a/packages/core/src/routes/logto-config.ts b/packages/core/src/routes/logto-config.ts index 97e11799f0f..917a47c20b9 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, From 2b3421492e43cae224d34db61c8f7ee8eb74fbaa Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 21 Mar 2024 14:35:18 +0800 Subject: [PATCH 3/3] chore: add cloud connection scope config for fetching custom jwt --- packages/core/src/libraries/cloud-connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/libraries/cloud-connection.ts b/packages/core/src/libraries/cloud-connection.ts index d09343d06d4..1a3072a22a2 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. */