0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

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 <darcyye@silverhand.io>
This commit is contained in:
simeng-li 2024-03-21 15:26:30 +08:00 committed by GitHub
parent 5c6af3823c
commit f1f6b1cd61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 85 additions and 25 deletions

View file

@ -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 (
<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
@ -159,7 +159,7 @@ function MonacoCodeEditor({
)}
</div>
</header>
<div ref={containerRef} className={styles.editorContainer}>
<div className={styles.editorContainer}>
{activeModel && (
<Editor
height={editorHeight}

View file

@ -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);
}
};
@ -29,7 +34,7 @@ const useEditorHeight = () => {
};
}, []);
return { containerRef, editorHeight };
return { containerRef, headerRef, editorHeight };
};
export default useEditorHeight;

View file

@ -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<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(
@ -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 => {
@ -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}

View file

@ -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);
@ -197,5 +201,5 @@
}
.flexGrow {
flex-grow: 1;
flex: 1;
}

View file

@ -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: 'foo@logto.io',

View file

@ -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']
) => {
@ -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}`;

View file

@ -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. */

View file

@ -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,