mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(console,phrases): integrate jwt customizer api (#5495)
* feat(console,phrases): integrate jwt customizer api integrate jwt customizer api * chore(console): update the comment update the comment * fix(console): clear the console logs clear the console logs
This commit is contained in:
parent
2c5a4491ae
commit
f11e95e1aa
25 changed files with 367 additions and 61 deletions
|
@ -206,7 +206,7 @@ function ConsoleContent() {
|
|||
</Route>
|
||||
)}
|
||||
{isDevFeaturesEnabled && (
|
||||
<Route path="jwt-claims">
|
||||
<Route path="jwt-customizer">
|
||||
<Route index element={<Navigate replace to={LogtoJwtTokenPath.AccessToken} />} />
|
||||
<Route
|
||||
path={LogtoJwtTokenPath.AccessToken}
|
||||
|
|
106
packages/console/src/pages/JwtClaims/Main.tsx
Normal file
106
packages/console/src/pages/JwtClaims/Main.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import {
|
||||
type JwtCustomizerAccessToken,
|
||||
LogtoJwtTokenPath,
|
||||
type JwtCustomizerClientCredentials,
|
||||
} from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { type KeyedMutator } from 'swr';
|
||||
|
||||
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import ScriptSection from './ScriptSection';
|
||||
import SettingsSection from './SettingsSection';
|
||||
import * as styles from './index.module.scss';
|
||||
import { type JwtClaimsFormType } from './type';
|
||||
import { formatResponseDataToFormData, formatFormDataToRequestData, getApiPath } from './utils';
|
||||
|
||||
type Props = {
|
||||
tab: LogtoJwtTokenPath;
|
||||
accessTokenJwtCustomizer: JwtCustomizerAccessToken | undefined;
|
||||
clientCredentialsJwtCustomizer: JwtCustomizerClientCredentials | undefined;
|
||||
mutateAccessTokenJwtCustomizer: KeyedMutator<JwtCustomizerAccessToken>;
|
||||
mutateClientCredentialsJwtCustomizer: KeyedMutator<JwtCustomizerClientCredentials>;
|
||||
};
|
||||
|
||||
function Main({
|
||||
tab,
|
||||
accessTokenJwtCustomizer,
|
||||
clientCredentialsJwtCustomizer,
|
||||
mutateAccessTokenJwtCustomizer,
|
||||
mutateClientCredentialsJwtCustomizer,
|
||||
}: Props) {
|
||||
const api = useApi();
|
||||
|
||||
const userJwtClaimsForm = useForm<JwtClaimsFormType>({
|
||||
defaultValues: formatResponseDataToFormData(
|
||||
LogtoJwtTokenPath.AccessToken,
|
||||
accessTokenJwtCustomizer
|
||||
),
|
||||
});
|
||||
|
||||
const machineToMachineJwtClaimsForm = useForm<JwtClaimsFormType>({
|
||||
defaultValues: formatResponseDataToFormData(
|
||||
LogtoJwtTokenPath.ClientCredentials,
|
||||
clientCredentialsJwtCustomizer
|
||||
),
|
||||
});
|
||||
|
||||
const activeForm = useMemo(
|
||||
() =>
|
||||
tab === LogtoJwtTokenPath.AccessToken ? userJwtClaimsForm : machineToMachineJwtClaimsForm,
|
||||
[machineToMachineJwtClaimsForm, tab, userJwtClaimsForm]
|
||||
);
|
||||
|
||||
const {
|
||||
formState: { isDirty, isSubmitting },
|
||||
reset,
|
||||
handleSubmit,
|
||||
} = activeForm;
|
||||
|
||||
const onSubmitHandler = handleSubmit(
|
||||
trySubmitSafe(async (data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tokenType } = data;
|
||||
const payload = formatFormDataToRequestData(data);
|
||||
|
||||
await api.put(getApiPath(tokenType), { json: payload });
|
||||
|
||||
const mutate =
|
||||
tokenType === LogtoJwtTokenPath.AccessToken
|
||||
? mutateAccessTokenJwtCustomizer
|
||||
: mutateClientCredentialsJwtCustomizer;
|
||||
|
||||
const result = await mutate();
|
||||
|
||||
reset(formatResponseDataToFormData(tokenType, result));
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...activeForm}>
|
||||
<form className={classNames(styles.tabContent)}>
|
||||
<ScriptSection />
|
||||
<SettingsSection />
|
||||
</form>
|
||||
</FormProvider>
|
||||
<SubmitFormChangesActionBar
|
||||
isOpen={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
onDiscard={reset}
|
||||
onSubmit={onSubmitHandler}
|
||||
/>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty && !isSubmitting} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Main;
|
|
@ -50,7 +50,6 @@ function MonacoCodeEditor({
|
|||
}: Props) {
|
||||
const monaco = useMonaco();
|
||||
const editorRef = useRef<Nullable<IStandaloneCodeEditor>>(null);
|
||||
console.log('code', value);
|
||||
|
||||
const activeModel = useMemo(
|
||||
() => activeModelName && models.find((model) => model.name === activeModelName),
|
||||
|
|
|
@ -26,6 +26,7 @@ function ScriptSection() {
|
|||
() => (tokenType === LogtoJwtTokenPath.AccessToken ? userJwtFile : machineToMachineJwtFile),
|
||||
[tokenType]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={styles.codePanel}>
|
||||
<div className={styles.cardTitle}>
|
||||
|
@ -37,10 +38,9 @@ function ScriptSection() {
|
|||
// Force rerender the controller when the token type changes
|
||||
// Otherwise the input field will not be updated
|
||||
key={tokenType}
|
||||
shouldUnregister
|
||||
control={control}
|
||||
name="script"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
render={({ field: { onChange, value }, formState: { defaultValues } }) => (
|
||||
<MonacoCodeEditor
|
||||
className={styles.flexGrow}
|
||||
enabledActions={['clear', 'copy']}
|
||||
|
@ -48,6 +48,12 @@ function ScriptSection() {
|
|||
activeModelName={activeModel.name}
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
// If the value is the same as the default code and the original form script value is undefined, reset the value to undefined as well
|
||||
if (newValue === activeModel.defaultValue && !defaultValues?.script) {
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -63,7 +63,7 @@ function EnvironmentVariablesField() {
|
|||
|
||||
const valueValidator = useCallback(
|
||||
(value: string, index: number) => {
|
||||
return getValues(`environmentVariables.${index}.value`)
|
||||
return getValues(`environmentVariables.${index}.key`)
|
||||
? Boolean(value) || t('webhook_details.settings.value_missing_error')
|
||||
: true;
|
||||
},
|
||||
|
|
|
@ -30,7 +30,7 @@ function TestTab({ isActive }: Props) {
|
|||
const [testResult, setTestResult] = useState<TestResultData>();
|
||||
const [activeModelName, setActiveModelName] = useState<string>();
|
||||
|
||||
const { watch, control } = useFormContext<JwtClaimsFormType>();
|
||||
const { watch, control, formState } = useFormContext<JwtClaimsFormType>();
|
||||
const tokenType = watch('tokenType');
|
||||
|
||||
const editorModels = useMemo(
|
||||
|
@ -78,6 +78,27 @@ function TestTab({ isActive }: Props) {
|
|||
[activeModelName]
|
||||
);
|
||||
|
||||
const validateSampleCode = useCallback(
|
||||
(value: JwtClaimsFormType['testSample']) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const [_, sampleCode] of Object.entries(value)) {
|
||||
if (sampleCode) {
|
||||
try {
|
||||
JSON.parse(sampleCode);
|
||||
} catch {
|
||||
return t('form_error.invalid_json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.tabContent, isActive && styles.active)}>
|
||||
<Card className={classNames(styles.card, styles.flexGrow, styles.flexColumn)}>
|
||||
|
@ -89,13 +110,18 @@ function TestTab({ isActive }: Props) {
|
|||
<Button title="jwt_claims.tester.run_button" type="primary" onClick={onTestHandler} />
|
||||
</div>
|
||||
<div className={classNames(styles.cardContent, styles.flexColumn, styles.flexGrow)}>
|
||||
{formState.errors.testSample && (
|
||||
<div className={styles.error}>{formState.errors.testSample.message}</div>
|
||||
)}
|
||||
<Controller
|
||||
// Force rerender the controller when the token type changes
|
||||
// Otherwise the input field will not be updated
|
||||
key={tokenType}
|
||||
shouldUnregister
|
||||
control={control}
|
||||
name="testSample"
|
||||
rules={{
|
||||
validate: validateSampleCode,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<MonacoCodeEditor
|
||||
className={styles.flexGrow}
|
||||
|
@ -108,7 +134,6 @@ function TestTab({ isActive }: Props) {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{testResult && (
|
||||
<TestResult
|
||||
testResult={testResult}
|
||||
|
|
|
@ -180,6 +180,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: _.unit(4) 0;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.flexColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,64 +1,29 @@
|
|||
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
|
||||
import { LogtoJwtTokenPath } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
||||
|
||||
import ScriptSection from './ScriptSection';
|
||||
import SettingsSection from './SettingsSection';
|
||||
import Main from './Main';
|
||||
import * as styles from './index.module.scss';
|
||||
import { type JwtClaimsFormType } from './type';
|
||||
import useJwtCustomizer from './use-jwt-customizer';
|
||||
|
||||
const tabPhrases = Object.freeze({
|
||||
[LogtoJwtTokenPath.AccessToken]: 'user_jwt_tab',
|
||||
[LogtoJwtTokenPath.ClientCredentials]: 'machine_to_machine_jwt_tab',
|
||||
});
|
||||
|
||||
const getPath = (tab: LogtoJwtTokenPath) => `/jwt-claims/${tab}`;
|
||||
const getPagePath = (tokenType: LogtoJwtTokenPath) => `/jwt-customizer/${tokenType}`;
|
||||
|
||||
type Props = {
|
||||
tab: LogtoJwtTokenPath;
|
||||
};
|
||||
|
||||
// TODO: API integration
|
||||
function JwtClaims({ tab }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const userJwtClaimsForm = useForm<JwtClaimsFormType>({
|
||||
defaultValues: {
|
||||
tokenType: LogtoJwtTokenPath.AccessToken,
|
||||
environmentVariables: [{ key: '', value: '' }],
|
||||
},
|
||||
});
|
||||
|
||||
const machineToMachineJwtClaimsForm = useForm<JwtClaimsFormType>({
|
||||
defaultValues: {
|
||||
tokenType: LogtoJwtTokenPath.ClientCredentials,
|
||||
environmentVariables: [{ key: '', value: '' }],
|
||||
},
|
||||
});
|
||||
|
||||
const activeForm = useMemo(
|
||||
() =>
|
||||
tab === LogtoJwtTokenPath.AccessToken ? userJwtClaimsForm : machineToMachineJwtClaimsForm,
|
||||
[machineToMachineJwtClaimsForm, tab, userJwtClaimsForm]
|
||||
);
|
||||
|
||||
const {
|
||||
formState: { isDirty, isSubmitting },
|
||||
reset,
|
||||
handleSubmit,
|
||||
} = activeForm;
|
||||
|
||||
const onSubmitHandler = handleSubmit(async (data) => {
|
||||
// TODO: API integration
|
||||
});
|
||||
const { isLoading, ...rest } = useJwtCustomizer();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
@ -69,24 +34,13 @@ function JwtClaims({ tab }: Props) {
|
|||
/>
|
||||
<TabNav className={styles.tabNav}>
|
||||
{Object.values(LogtoJwtTokenPath).map((tokenType) => (
|
||||
<TabNavItem key={tokenType} href={getPath(tokenType)} isActive={tokenType === tab}>
|
||||
<TabNavItem key={tokenType} href={getPagePath(tokenType)} isActive={tokenType === tab}>
|
||||
{t(`jwt_claims.${tabPhrases[tokenType]}`)}
|
||||
</TabNavItem>
|
||||
))}
|
||||
</TabNav>
|
||||
<FormProvider {...activeForm}>
|
||||
<form className={classNames(styles.tabContent)}>
|
||||
<ScriptSection />
|
||||
<SettingsSection />
|
||||
</form>
|
||||
</FormProvider>
|
||||
<SubmitFormChangesActionBar
|
||||
isOpen={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
onDiscard={reset}
|
||||
onSubmit={onSubmitHandler}
|
||||
/>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty && !isSubmitting} />
|
||||
{/* TODO: Loading skelton */}
|
||||
{!isLoading && <Main tab={tab} {...rest} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
67
packages/console/src/pages/JwtClaims/use-jwt-customizer.ts
Normal file
67
packages/console/src/pages/JwtClaims/use-jwt-customizer.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
LogtoJwtTokenPath,
|
||||
type JwtCustomizerAccessToken,
|
||||
type JwtCustomizerClientCredentials,
|
||||
} from '@logto/schemas';
|
||||
import { type ResponseError } from '@withtyped/client';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||
import { shouldRetryOnError } from '@/utils/request';
|
||||
|
||||
import { getApiPath } from './utils';
|
||||
|
||||
function useJwtCustomizer() {
|
||||
const fetchApi = useApi({ hideErrorToast: true });
|
||||
const accessTokenFetcher = useSwrFetcher<JwtCustomizerAccessToken>(fetchApi);
|
||||
const clientCredentialsFetcher = useSwrFetcher<JwtCustomizerClientCredentials>(fetchApi);
|
||||
|
||||
const {
|
||||
data: accessTokenJwtCustomizer,
|
||||
mutate: mutateAccessTokenJwtCustomizer,
|
||||
isLoading: isAccessTokenJwtDataLoading,
|
||||
error: accessTokenError,
|
||||
} = useSWR<JwtCustomizerAccessToken, ResponseError>(getApiPath(LogtoJwtTokenPath.AccessToken), {
|
||||
fetcher: accessTokenFetcher,
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
|
||||
});
|
||||
|
||||
const {
|
||||
data: clientCredentialsJwtCustomizer,
|
||||
mutate: mutateClientCredentialsJwtCustomizer,
|
||||
isLoading: isClientCredentialsJwtDataLoading,
|
||||
error: clientCredentialsError,
|
||||
} = useSWR<JwtCustomizerClientCredentials, ResponseError>(
|
||||
getApiPath(LogtoJwtTokenPath.ClientCredentials),
|
||||
{
|
||||
fetcher: clientCredentialsFetcher,
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
|
||||
}
|
||||
);
|
||||
|
||||
// Show global loading status only if any of the fetchers are loading and no errors are present
|
||||
const isLoading =
|
||||
(isAccessTokenJwtDataLoading && !accessTokenError) ||
|
||||
(isClientCredentialsJwtDataLoading && !clientCredentialsError);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
accessTokenJwtCustomizer,
|
||||
clientCredentialsJwtCustomizer,
|
||||
isLoading,
|
||||
mutateAccessTokenJwtCustomizer,
|
||||
mutateClientCredentialsJwtCustomizer,
|
||||
}),
|
||||
[
|
||||
accessTokenJwtCustomizer,
|
||||
clientCredentialsJwtCustomizer,
|
||||
isLoading,
|
||||
mutateAccessTokenJwtCustomizer,
|
||||
mutateClientCredentialsJwtCustomizer,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
export default useJwtCustomizer;
|
85
packages/console/src/pages/JwtClaims/utils.ts
Normal file
85
packages/console/src/pages/JwtClaims/utils.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import {
|
||||
type LogtoJwtTokenPath,
|
||||
type JwtCustomizerAccessToken,
|
||||
type JwtCustomizerClientCredentials,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import type { JwtClaimsFormType } from './type';
|
||||
|
||||
const formatEnvVariablesResponseToFormData = (
|
||||
enVariables?: JwtCustomizerAccessToken['envVars']
|
||||
) => {
|
||||
if (!enVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.entries(enVariables).map(([key, value]) => ({ key, value }));
|
||||
};
|
||||
|
||||
const formatSampleCodeJsonToString = (
|
||||
sampleJson?: JwtCustomizerAccessToken['contextSample'] | JwtCustomizerAccessToken['tokenSample']
|
||||
) => {
|
||||
if (!sampleJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
return JSON.stringify(sampleJson, null, 2);
|
||||
};
|
||||
|
||||
export const formatResponseDataToFormData = <T extends LogtoJwtTokenPath>(
|
||||
tokenType: T,
|
||||
data?: T extends LogtoJwtTokenPath.AccessToken
|
||||
? JwtCustomizerAccessToken
|
||||
: JwtCustomizerClientCredentials
|
||||
): JwtClaimsFormType => {
|
||||
return {
|
||||
script: data?.script,
|
||||
tokenType,
|
||||
environmentVariables: formatEnvVariablesResponseToFormData(data?.envVars) ?? [
|
||||
{ key: '', value: '' },
|
||||
],
|
||||
testSample: {
|
||||
tokenSample: formatSampleCodeJsonToString(data?.tokenSample),
|
||||
// Technically, contextSample is always undefined for client credentials token type
|
||||
contextSample: formatSampleCodeJsonToString(data?.contextSample),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const formatEnvVariablesFormData = (envVariables: JwtClaimsFormType['environmentVariables']) => {
|
||||
if (!envVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = envVariables.filter(({ key, value }) => key && value);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
|
||||
};
|
||||
|
||||
const formatSampleCodeStringToJson = (sampleCode?: string) => {
|
||||
if (!sampleCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-restricted-syntax -- guarded by back-end validation
|
||||
return JSON.parse(sampleCode) as Record<string, unknown>;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const formatFormDataToRequestData = (data: JwtClaimsFormType) => {
|
||||
return {
|
||||
script: data.script,
|
||||
envVars: formatEnvVariablesFormData(data.environmentVariables),
|
||||
tokenSample: formatSampleCodeStringToJson(data.testSample?.tokenSample),
|
||||
// Technically, contextSample is always undefined for client credentials token type
|
||||
contextSample: formatSampleCodeStringToJson(data.testSample?.contextSample),
|
||||
};
|
||||
};
|
||||
|
||||
export const getApiPath = (tokenType: LogtoJwtTokenPath) =>
|
||||
`api/configs/jwt-customizer/${tokenType}`;
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -45,6 +45,9 @@ const jwt_claims = {
|
|||
run_button: 'Run',
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
|
@ -74,6 +74,10 @@ const jwt_claims = {
|
|||
/** UNTRANSLATED */
|
||||
result_title: 'Test result',
|
||||
},
|
||||
form_error: {
|
||||
/** UNTRANSLATED */
|
||||
invalid_json: 'Invalid JSON format',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(jwt_claims);
|
||||
|
|
Loading…
Add table
Reference in a new issue