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:
parent
5c6af3823c
commit
f1f6b1cd61
8 changed files with 85 additions and 25 deletions
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue