0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(console,phrases): refactor the jwt customizer content (#5527)

* refactor(console,phrases): refactor the jwt customizer content

refactor the jwt customizer content

* fix(console): add isDev guard

add isDev guard
This commit is contained in:
simeng-li 2024-03-20 10:15:42 +08:00 committed by GitHub
parent d3d0f5133b
commit f638c8e6a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 140 additions and 28 deletions

View file

@ -0,0 +1,4 @@
<svg width="17" height="18" viewBox="0 0 17 18" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M10.1276 8.16645H9.2943V7.33312C9.2943 7.11211 9.2065 6.90014 9.05022 6.74386C8.89394 6.58758 8.68198 6.49979 8.46097 6.49979C8.23995 6.49979 8.02799 6.58758 7.87171 6.74386C7.71543 6.90014 7.62764 7.11211 7.62764 7.33312V8.16645H6.7943C6.57329 8.16645 6.36133 8.25425 6.20505 8.41053C6.04877 8.56681 5.96097 8.77877 5.96097 8.99979C5.96097 9.2208 6.04877 9.43276 6.20505 9.58904C6.36133 9.74532 6.57329 9.83312 6.7943 9.83312H7.62764V10.6665C7.62764 10.8875 7.71543 11.0994 7.87171 11.2557C8.02799 11.412 8.23995 11.4998 8.46097 11.4998C8.68198 11.4998 8.89394 11.412 9.05022 11.2557C9.2065 11.0994 9.2943 10.8875 9.2943 10.6665V9.83312H10.1276C10.3486 9.83312 10.5606 9.74532 10.7169 9.58904C10.8732 9.43276 10.961 9.2208 10.961 8.99979C10.961 8.77877 10.8732 8.56681 10.7169 8.41053C10.5606 8.25425 10.3486 8.16645 10.1276 8.16645ZM16.5526 8.40812L14.5943 6.49979V3.69979C14.5943 3.47877 14.5065 3.26681 14.3502 3.11053C14.1939 2.95425 13.982 2.86645 13.761 2.86645H11.0026L9.05264 0.90812C8.97517 0.830013 8.883 0.768017 8.78145 0.72571C8.6799 0.683403 8.57098 0.661621 8.46097 0.661621C8.35096 0.661621 8.24204 0.683403 8.14049 0.72571C8.03894 0.768017 7.94677 0.830013 7.8693 0.90812L5.96097 2.86645H3.16097C2.93995 2.86645 2.72799 2.95425 2.57171 3.11053C2.41543 3.26681 2.32763 3.47877 2.32763 3.69979V6.49979L0.369301 8.40812C0.291194 8.48559 0.229199 8.57776 0.186892 8.67931C0.144585 8.78086 0.122803 8.88978 0.122803 8.99979C0.122803 9.1098 0.144585 9.21872 0.186892 9.32027C0.229199 9.42182 0.291194 9.51398 0.369301 9.59145L2.32763 11.5415V14.2998C2.32763 14.5208 2.41543 14.7328 2.57171 14.889C2.72799 15.0453 2.93995 15.1331 3.16097 15.1331H5.96097L7.91097 17.0915C7.98844 17.1696 8.08061 17.2316 8.18215 17.2739C8.2837 17.3162 8.39263 17.338 8.50264 17.338C8.61265 17.338 8.72157 17.3162 8.82312 17.2739C8.92467 17.2316 9.01683 17.1696 9.0943 17.0915L11.0443 15.1331H13.8026C14.0236 15.1331 14.2356 15.0453 14.3919 14.889C14.5482 14.7328 14.636 14.5208 14.636 14.2998V11.5415L16.5943 9.59145C16.6697 9.51127 16.7285 9.41693 16.7673 9.31388C16.8061 9.21084 16.8241 9.10113 16.8202 8.9911C16.8163 8.88107 16.7907 8.7729 16.7447 8.67284C16.6988 8.57277 16.6335 8.4828 16.5526 8.40812ZM13.1776 10.6081C13.0989 10.6853 13.0363 10.7773 12.9933 10.8789C12.9504 10.9805 12.9281 11.0895 12.9276 11.1998V13.4665H10.661C10.5507 13.4669 10.4416 13.4892 10.3401 13.5322C10.2385 13.5751 10.1465 13.6377 10.0693 13.7165L8.46097 15.3248L6.85263 13.7165C6.77546 13.6377 6.68342 13.5751 6.58186 13.5322C6.48029 13.4892 6.37122 13.4669 6.26097 13.4665H3.9943V11.1998C3.99384 11.0895 3.97151 10.9805 3.9286 10.8789C3.88568 10.7773 3.82304 10.6853 3.7443 10.6081L2.13597 8.99979L3.7443 7.39145C3.82304 7.31427 3.88568 7.22224 3.9286 7.12067C3.97151 7.01911 3.99384 6.91004 3.9943 6.79979V4.53312H6.26097C6.37122 4.53266 6.48029 4.51033 6.58186 4.46742C6.68342 4.4245 6.77546 4.36186 6.85263 4.28312L8.46097 2.67479L10.0693 4.28312C10.1465 4.36186 10.2385 4.4245 10.3401 4.46742C10.4416 4.51033 10.5507 4.53266 10.661 4.53312H12.9276V6.79979C12.9281 6.91004 12.9504 7.01911 12.9933 7.12067C13.0363 7.22224 13.0989 7.31427 13.1776 7.39145L14.786 8.99979L13.1776 10.6081Z" fill="currentcolor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -8,6 +8,7 @@ import Box from '@/assets/icons/box.svg';
import Connection from '@/assets/icons/connection.svg';
import Gear from '@/assets/icons/gear.svg';
import Hook from '@/assets/icons/hook.svg';
import JwtClaims from '@/assets/icons/jwt-claims.svg';
import List from '@/assets/icons/list.svg';
import Organization from '@/assets/icons/organization.svg';
import UserProfile from '@/assets/icons/profile.svg';
@ -16,7 +17,7 @@ import Role from '@/assets/icons/role.svg';
import SecurityLock from '@/assets/icons/security-lock.svg';
import EnterpriseSso from '@/assets/icons/single-sign-on.svg';
import Web from '@/assets/icons/web.svg';
import { isCloud } from '@/consts/env';
import { isDevFeaturesEnabled, isCloud } from '@/consts/env';
type SidebarItem = {
Icon: FC;
@ -117,6 +118,11 @@ export const useSidebarMenuItems = (): {
Icon: Hook,
title: 'webhooks',
},
{
Icon: JwtClaims,
title: 'jwt_customizer',
isHidden: !isDevFeaturesEnabled,
},
],
},
{

View file

@ -23,4 +23,5 @@ export const defaultOptions: EditorProps['options'] = {
fontSize: 14,
automaticLayout: true,
tabSize: 2,
scrollBeyondLastLine: false,
};

View file

@ -166,7 +166,10 @@ function MonacoCodeEditor({
language={activeModel.language}
path={activeModel.name}
theme="logto-dark"
options={defaultOptions}
options={{
...defaultOptions,
...activeModel.options,
}}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string is falsy
value={value || activeModel.defaultValue}
beforeMount={handleEditorWillMount}

View file

@ -1,4 +1,4 @@
import { type Monaco, type OnMount } from '@monaco-editor/react';
import { type EditorProps, type Monaco, type OnMount } from '@monaco-editor/react';
export type IStandaloneThemeData = Parameters<Monaco['editor']['defineTheme']>[1];
@ -25,6 +25,7 @@ export type ModelSettings = {
* We use this to load the global type declarations for the active model
*/
extraLibs?: ExtraLibrary[];
options?: EditorProps['options'];
};
export type ModelControl = {

View file

@ -11,7 +11,11 @@ const isValidKey = (key: string) => {
return /^\w+$/.test(key);
};
function EnvironmentVariablesField() {
type Props = {
className?: string;
};
function EnvironmentVariablesField({ className }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
@ -98,7 +102,7 @@ function EnvironmentVariablesField() {
);
return (
<FormField title="jwt_claims.environment_variables.input_field_title">
<FormField title="jwt_claims.environment_variables.input_field_title" className={className}>
<KeyValueInputField
fields={fields}
// Force envVariableErrors to be an array, otherwise return undefined

View file

@ -9,6 +9,7 @@ import {
sampleCodeEditorOptions,
typeDefinitionCodeEditorOptions,
fetchExternalDataCodeExample,
environmentVariablesCodeExample,
} from '../utils/config';
import {
accessTokenPayloadTypeDefinition,
@ -82,7 +83,18 @@ function InstructionTab({ isActive }: Props) {
* In order to fix this, we need to re-render the EnvironmentVariablesField component when the tokenType changes.
* Achieve this by adding a key to the EnvironmentVariablesField component. Force a re-render when the tokenType changes.
*/}
<EnvironmentVariablesField key={tokenType} />
<EnvironmentVariablesField key={tokenType} className={styles.envVariablesField} />
<div className={styles.description}>
{t('jwt_claims.environment_variables.sample_code')}
</div>
<Editor
language="typescript"
className={styles.sampleCode}
value={environmentVariablesCodeExample}
height="400px"
theme="logto-dark"
options={sampleCodeEditorOptions}
/>
</GuideCard>
</div>
);

View file

@ -10,9 +10,9 @@ import Card from '@/ds-components/Card';
import MonacoCodeEditor, { type ModelControl } from '../MonacoCodeEditor/index.js';
import { type JwtClaimsFormType } from '../type.js';
import {
userTokenPayloadTestModel,
machineToMachineTokenPayloadTestModel,
userTokenContextTestModel,
accessTokenPayloadTestModel,
clientCredentialsPayloadTestModel,
userContextTestModel,
} from '../utils/config.js';
import TestResult, { type TestResultData } from './TestResult.js';
@ -22,8 +22,8 @@ type Props = {
isActive: boolean;
};
const userTokenModelSettings = [userTokenPayloadTestModel, userTokenContextTestModel];
const machineToMachineTokenModelSettings = [machineToMachineTokenPayloadTestModel];
const userTokenModelSettings = [accessTokenPayloadTestModel, userContextTestModel];
const machineToMachineTokenModelSettings = [clientCredentialsPayloadTestModel];
function TestTab({ isActive }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.jwt_claims' });
@ -52,7 +52,7 @@ function TestTab({ isActive }: Props) {
const getModelControllerProps = useCallback(
({ value, onChange }: ControllerRenderProps<JwtClaimsFormType, 'testSample'>): ModelControl => {
// User access token context test model (user data)
if (activeModelName === userTokenContextTestModel.name) {
if (activeModelName === userContextTestModel.name) {
return {
value: value?.contextSample,
onChange: (newValue: string | undefined) => {

View file

@ -79,6 +79,10 @@
> *:first-child {
margin-top: _.unit(6);
}
> *:not(:last-child) {
margin-bottom: _.unit(4);
}
}
.expandButton {
@ -124,8 +128,6 @@
}
.sampleCode {
margin-top: _.unit(4);
:global {
/* stylelint-disable-next-line selector-class-pattern */
.monaco-editor {
@ -144,6 +146,10 @@
}
}
.envVariablesField {
margin-bottom: _.unit(4);
}
.testResult {
margin-top: _.unit(3);
height: calc(50% - _.unit(3));

View file

@ -1,3 +1,4 @@
import { type AccessTokenPayload, type ClientCredentialsPayload } from '@logto/schemas';
import { type EditorProps } from '@monaco-editor/react';
import TokenFileIcon from '@/assets/icons/token-file-icon.svg';
@ -131,7 +132,6 @@ export const clientCredentialsModel: ModelSettings = {
/**
* JWT claims guide card configs
*/
export const sampleCodeEditorOptions: EditorProps['options'] = {
readOnly: true,
wordWrap: 'on',
@ -142,14 +142,13 @@ export const sampleCodeEditorOptions: EditorProps['options'] = {
overviewRulerBorder: false,
overviewRulerLanes: 0,
lineNumbers: 'off',
scrollbar: { vertical: 'hidden', horizontal: 'hidden', handleMouseWheel: false },
folding: false,
tabSize: 2,
scrollBeyondLastLine: false,
};
export const typeDefinitionCodeEditorOptions: EditorProps['options'] = {
...sampleCodeEditorOptions,
scrollbar: { vertical: 'auto', horizontal: 'auto' },
folding: true,
};
@ -165,24 +164,42 @@ return {
externalData: data,
};`;
export const environmentVariablesCodeExample = `exports.getCustomJwtClaims = async (token, data, envVariables) => {
const { apiKey } = envVariables;
const response = await fetch('https://api.example.com/data', {
headers: {
Authorization: apiKey,
}
});
const data = await response.json();
return {
externalData: data,
};
};`;
/**
* Tester Code Editor configs
*/
const standardTokenPayloadData = {
jti: '1234567890',
iat: 1_516_239_022,
exp: 1_516_239_022,
jti: 'f1d3d2d1-1f2d-3d4e-5d6f-7d8a9d0e1d2',
iat: 1_516_235_022,
exp: 1_516_235_022 + 3600,
client_id: 'my_app',
scope: 'read write',
aud: 'http://localhost:3000/api',
aud: 'http://localhost:3000/api/test',
};
const defaultUserTokenPayloadData = {
const defaultAccessTokenPayload: AccessTokenPayload = {
...standardTokenPayloadData,
grantId: 'grant_123',
accountId: 'uid_123',
kind: 'AccessToken',
};
const defaultMachineToMachineTokenPayloadData = {
const defaultClientCredentialsPayload: ClientCredentialsPayload = {
...standardTokenPayloadData,
kind: 'ClientCredentials',
};
@ -200,23 +217,23 @@ const defaultUserTokenContextData = {
},
};
export const userTokenPayloadTestModel: ModelSettings = {
export const accessTokenPayloadTestModel: ModelSettings = {
language: 'json',
icon: <TokenFileIcon />,
name: 'user-token-payload.json',
title: 'Token',
defaultValue: JSON.stringify(defaultUserTokenPayloadData, null, '\t'),
defaultValue: JSON.stringify(defaultAccessTokenPayload, null, '\t'),
};
export const machineToMachineTokenPayloadTestModel: ModelSettings = {
export const clientCredentialsPayloadTestModel: ModelSettings = {
language: 'json',
icon: <TokenFileIcon />,
name: 'machine-to-machine-token-payload.json',
title: 'Token',
defaultValue: JSON.stringify(defaultMachineToMachineTokenPayloadData, null, '\t'),
defaultValue: JSON.stringify(defaultClientCredentialsPayload, null, '\t'),
};
export const userTokenContextTestModel: ModelSettings = {
export const userContextTestModel: ModelSettings = {
language: 'json',
icon: <UserFileIcon />,
name: 'user-token-context.json',

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Dokumentation',
tenant_settings: 'Einstellungen',
mfa: 'Multi-Faktor-Authentifizierung',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -36,6 +36,7 @@ const jwt_claims = {
subtitle:
'Use environment variables to store sensitive information and access them in your custom claims handler.',
input_field_title: 'Add environment variables',
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
jwt_claims_hint:
'Limit custom claims to under 50KB. Default JWT claims are automatically included in the token and can not be overridden.',

View file

@ -14,6 +14,7 @@ const tabs = {
docs: 'Docs',
tenant_settings: 'Settings',
mfa: 'Multi-factor auth',
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Documentos',
tenant_settings: 'Configuraciones del inquilino',
mfa: 'Autenticación multifactor',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Documentation',
tenant_settings: 'Paramètres du locataire',
mfa: 'Authentification multi-facteur',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Documenti',
tenant_settings: 'Impostazioni',
mfa: 'Autenticazione multi-fattore',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'ドキュメント',
tenant_settings: '設定',
mfa: 'Multi-factor auth',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: '문서',
tenant_settings: '테넌트 설정',
mfa: '다중 요소 인증',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Dokumentacja',
tenant_settings: 'Ustawienia',
mfa: 'Multi-factor auth',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Documentação',
tenant_settings: 'Configurações',
mfa: 'Autenticação de multi-fator',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Documentação',
tenant_settings: 'Definições do inquilino',
mfa: 'Autenticação multi-fator',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Документация',
tenant_settings: 'Настройки',
mfa: 'Multi-factor auth',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: 'Dökümanlar',
tenant_settings: 'Ayarlar',
mfa: 'Çoklu faktörlü kimlik doğrulama',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: '文档',
tenant_settings: '租户设置',
mfa: '多因素认证',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: '文檔',
tenant_settings: '租戶設置',
mfa: '多重認證',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);

View file

@ -60,6 +60,8 @@ const jwt_claims = {
'Use environment variables to store sensitive information and access them in your custom claims handler.',
/** UNTRANSLATED */
input_field_title: 'Add environment variables',
/** UNTRANSLATED */
sample_code: 'Accessing environment variables in your custom JWT claims handler. Example: ',
},
/** UNTRANSLATED */
jwt_claims_hint:

View file

@ -14,6 +14,8 @@ const tabs = {
docs: '文件',
tenant_settings: '租戶設定',
mfa: '多重認證',
/** UNTRANSLATED */
jwt_customizer: 'JWT Claims',
};
export default Object.freeze(tabs);