mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(core,console): handle connector target confict (#3251)
This commit is contained in:
parent
f25a9d343c
commit
13fcf6f3c2
14 changed files with 225 additions and 76 deletions
|
@ -103,7 +103,6 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
learnMoreLink={getDocumentationUrl('/docs/references/connectors')}
|
||||
>
|
||||
<BasicForm
|
||||
connectorType={connectorData.type}
|
||||
isStandard={connectorData.isStandard}
|
||||
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,31 @@
|
|||
margin-top: _.unit(0.5);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-text);
|
||||
font: var(--font-body-2);
|
||||
margin-top: _.unit(0.5);
|
||||
background-color: var(--color-danger-toast-background);
|
||||
padding: _.unit(3) _.unit(4);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin-right: _.unit(3);
|
||||
}
|
||||
|
||||
.content {
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> ul {
|
||||
padding-left: _.unit(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldButton {
|
||||
margin-top: _.unit(2);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import CaretDown from '@/assets/images/caret-down.svg';
|
||||
import CaretUp from '@/assets/images/caret-up.svg';
|
||||
import Error from '@/assets/images/toast-error.svg';
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import Select from '@/components/Select';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import { uriValidator } from '@/utils/validator';
|
||||
|
||||
|
@ -21,14 +22,14 @@ type Props = {
|
|||
isAllowEditTarget?: boolean;
|
||||
isDarkDefaultVisible?: boolean;
|
||||
isStandard?: boolean;
|
||||
connectorType: ConnectorType;
|
||||
conflictConnectorName?: Record<string, string>;
|
||||
};
|
||||
|
||||
const BasicForm = ({
|
||||
isAllowEditTarget,
|
||||
isDarkDefaultVisible,
|
||||
connectorType,
|
||||
isStandard,
|
||||
conflictConnectorName,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
|
@ -104,48 +105,74 @@ const BasicForm = ({
|
|||
onClick={toggleDarkVisible}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
isRequired
|
||||
title="connectors.guide.target"
|
||||
tip={(closeTipHandler) => (
|
||||
<Trans
|
||||
components={{
|
||||
a: (
|
||||
<TextLink
|
||||
href={getDocumentationUrl('/docs/references/connectors/#target')}
|
||||
target="_blank"
|
||||
onClick={closeTipHandler}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{t('connectors.guide.target_tooltip')}
|
||||
</Trans>
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t('connectors.guide.target_placeholder')}
|
||||
hasError={Boolean(errors.target)}
|
||||
disabled={!isAllowEditTarget}
|
||||
{...register('target', { required: true })}
|
||||
/>
|
||||
<div className={styles.tip}>{t('connectors.guide.target_tip')}</div>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
{connectorType === ConnectorType.Social && (
|
||||
<FormField title="connectors.guide.sync_profile">
|
||||
<Controller
|
||||
name="syncProfile"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select options={syncProfileOptions} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<div className={styles.tip}>{t('connectors.guide.sync_profile_tip')}</div>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField
|
||||
isRequired
|
||||
title="connectors.guide.target"
|
||||
tip={(closeTipHandler) => (
|
||||
<Trans
|
||||
components={{
|
||||
a: (
|
||||
<TextLink
|
||||
href={getDocumentationUrl('/docs/references/connectors/#target')}
|
||||
target="_blank"
|
||||
onClick={closeTipHandler}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{t('connectors.guide.target_tooltip')}
|
||||
</Trans>
|
||||
)}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t('connectors.guide.target_placeholder')}
|
||||
hasError={Boolean(errors.target)}
|
||||
disabled={!isAllowEditTarget}
|
||||
{...register('target', { required: true })}
|
||||
/>
|
||||
<div className={styles.tip}>{t('connectors.guide.target_tip')}</div>
|
||||
{conflictConnectorName && (
|
||||
<div className={styles.error}>
|
||||
<div className={styles.icon}>
|
||||
<Error />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<Trans
|
||||
components={{
|
||||
span: <UnnamedTrans resource={conflictConnectorName} />,
|
||||
}}
|
||||
>
|
||||
{t('connectors.guide.target_conflict')}
|
||||
</Trans>
|
||||
<ul>
|
||||
<li>
|
||||
<Trans
|
||||
components={{
|
||||
span: <UnnamedTrans resource={conflictConnectorName} />,
|
||||
}}
|
||||
>
|
||||
{t('connectors.guide.target_conflict_line2')}
|
||||
</Trans>
|
||||
</li>
|
||||
<li>{t('connectors.guide.target_conflict_line3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField title="connectors.guide.sync_profile">
|
||||
<Controller
|
||||
name="syncProfile"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select options={syncProfileOptions} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<div className={styles.tip}>{t('connectors.guide.sync_profile_tip')}</div>
|
||||
</FormField>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { generateStandardId } from '@logto/core-kit';
|
||||
import { isLanguageTag } from '@logto/language-kit';
|
||||
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
|
||||
import type { ConnectorFactoryResponse, ConnectorResponse, RequestErrorBody } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import i18next from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -30,19 +31,30 @@ import { useConfigParser } from '../ConnectorForm/hooks';
|
|||
import { initFormData, parseFormConfig } from '../ConnectorForm/utils';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const targetErrorCode = 'connector.multiple_target_with_same_platform';
|
||||
|
||||
type Props = {
|
||||
connector: ConnectorFactoryResponse;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Guide = ({ connector, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const api = useApi({ hideErrorToast: true });
|
||||
const navigate = useNavigate();
|
||||
const [callbackConnectorId, setCallbackConnectorId] = useState<string>(generateStandardId());
|
||||
const { updateConfigs } = useConfigs();
|
||||
const parseJsonConfig = useConfigParser();
|
||||
const [conflictConnectorName, setConflictConnectorName] = useState<Record<string, string>>();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { id: connectorId, type: connectorType, name, readme, formItems } = connector;
|
||||
const {
|
||||
id: connectorId,
|
||||
type: connectorType,
|
||||
name,
|
||||
readme,
|
||||
formItems,
|
||||
target,
|
||||
isStandard,
|
||||
} = connector;
|
||||
const { title, content } = splitMarkdownByTitle(readme);
|
||||
const { language } = i18next;
|
||||
const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
|
||||
|
@ -59,13 +71,24 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
setError,
|
||||
} = methods;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSocialConnector && !isStandard) {
|
||||
setValue('target', target);
|
||||
}
|
||||
}, [isSocialConnector, target, isStandard, setValue]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recover error state
|
||||
setConflictConnectorName(undefined);
|
||||
|
||||
const { formItems, isStandard, id: connectorId, type } = connector;
|
||||
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
|
||||
const { syncProfile, name, logo, logoDark, target } = data;
|
||||
|
@ -88,23 +111,41 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn }
|
||||
: basePayload;
|
||||
|
||||
const createdConnector = await api
|
||||
.post('api/connectors', {
|
||||
json: payload,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
try {
|
||||
const createdConnector = await api
|
||||
.post('api/connectors', {
|
||||
json: payload,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
await updateConfigs({
|
||||
...conditional(!isSocialConnector && { passwordlessConfigured: true }),
|
||||
});
|
||||
await updateConfigs({
|
||||
...conditional(!isSocialConnector && { passwordlessConfigured: true }),
|
||||
});
|
||||
|
||||
onClose();
|
||||
toast.success(t('general.saved'));
|
||||
navigate(
|
||||
`/connectors/${isSocialConnector ? ConnectorsTabs.Social : ConnectorsTabs.Passwordless}/${
|
||||
createdConnector.id
|
||||
}`
|
||||
);
|
||||
onClose();
|
||||
toast.success(t('general.saved'));
|
||||
navigate(
|
||||
`/connectors/${isSocialConnector ? ConnectorsTabs.Social : ConnectorsTabs.Passwordless}/${
|
||||
createdConnector.id
|
||||
}`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const { response } = error;
|
||||
const metadata = await response.json<
|
||||
RequestErrorBody<{ connectorName: Record<string, string> }>
|
||||
>();
|
||||
|
||||
if (metadata.code === targetErrorCode) {
|
||||
setConflictConnectorName(metadata.data.connectorName);
|
||||
setError('target', {}, { shouldFocus: true });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -135,9 +176,9 @@ const Guide = ({ connector, onClose }: Props) => {
|
|||
<div>{t('connectors.guide.general_setting')}</div>
|
||||
</div>
|
||||
<BasicForm
|
||||
isAllowEditTarget
|
||||
connectorType={connector.type}
|
||||
isAllowEditTarget={connector.isStandard}
|
||||
isStandard={connector.isStandard}
|
||||
conflictConnectorName={conflictConnectorName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -158,16 +158,26 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
|
||||
if (connectorFactory.type === ConnectorType.Social) {
|
||||
const connectors = await getLogtoConnectors();
|
||||
const duplicateConnector = connectors
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.find(
|
||||
({ metadata: { target, platform } }) =>
|
||||
target ===
|
||||
(metadata ? cleanDeep(metadata).target : connectorFactory.metadata.target) &&
|
||||
platform === connectorFactory.metadata.platform
|
||||
);
|
||||
assertThat(
|
||||
!connectors
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.some(
|
||||
({ metadata: { target, platform } }) =>
|
||||
target ===
|
||||
(metadata ? cleanDeep(metadata).target : connectorFactory.metadata.target) &&
|
||||
platform === connectorFactory.metadata.platform
|
||||
),
|
||||
new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 })
|
||||
!duplicateConnector,
|
||||
new RequestError(
|
||||
{
|
||||
code: 'connector.multiple_target_with_same_platform',
|
||||
status: 422,
|
||||
},
|
||||
{
|
||||
connectorId: duplicateConnector?.metadata.id,
|
||||
connectorName: duplicateConnector?.metadata.name,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'"Target" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "target" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>', // UNTRANSLATED
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: 'Enter your config JSON', // UNTRANSLATED
|
||||
sync_profile: 'Sync profile information', // UNTRANSLATED
|
||||
sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED
|
||||
|
|
|
@ -55,6 +55,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.',
|
||||
target_tooltip:
|
||||
'"IdP name" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "IdP name" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>',
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span> connector. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.',
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".',
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.',
|
||||
config: 'Enter your config JSON',
|
||||
sync_profile: 'Sync profile information',
|
||||
sync_profile_only_at_sign_up: 'Only sync at sign-up',
|
||||
|
|
|
@ -56,6 +56,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'"Target" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "target" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>', // UNTRANSLATED
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: 'Enter your config JSON', // UNTRANSLATED
|
||||
sync_profile: 'Sync profile information', // UNTRANSLATED
|
||||
sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED
|
||||
|
|
|
@ -54,6 +54,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'Logto의 소셜 연동에서의 "목표"는 소셜 정보의 원천을 뜻해요. Logto의 디자인은 충돌을 피하기 위해서 같은 "목표"를 허용하지 않아요. 연동을 추가한 후에는 값을 변경할 수 없으므로 주의해주세요. <a>자세히 알아보기</a>',
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: '여기에 JSON을 입력',
|
||||
sync_profile: '프로필 정보 동기화',
|
||||
sync_profile_only_at_sign_up: '회원가입할 때 동기화',
|
||||
|
|
|
@ -54,6 +54,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'"Target" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "target" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>', // UNTRANSLATED
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: 'Digite seu JSON aqui',
|
||||
sync_profile: 'Sincronizar informações de perfil',
|
||||
sync_profile_only_at_sign_up: 'Sincronizar apenas no registro',
|
||||
|
|
|
@ -55,6 +55,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'"Target" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "target" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>', // UNTRANSLATED
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: 'Enter your config JSON', // UNTRANSLATED
|
||||
sync_profile: 'Sync profile information', // UNTRANSLATED
|
||||
sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED
|
||||
|
|
|
@ -56,6 +56,12 @@ const connectors = {
|
|||
'The value of “IdP name” can be a unique identifier string to distinguish your social identifies. This setting cannot be changed after the connector is built.', // UNTRANSLATED
|
||||
target_tooltip:
|
||||
'"Target" in Logto social connectors refers to the "source" of your social identities. In Logto design, we do not accept the same "target" of a specific platform to avoid conflicts. You should be very careful before you add a connector since you CAN NOT change its value once you create it. <a>Learn more.</a>', // UNTRANSLATED
|
||||
target_conflict:
|
||||
'The IdP name entered matches the existing <span>name</span>. Using the same idp name may cause unexpected sign-in behavior where users may access the same account through two different connectors.', // UNTRANSLATED
|
||||
target_conflict_line2:
|
||||
'If you\'d like to replace the current connector with the same identity provider and allow previous users to sign in without registering again, please delete the <span>name</span> connector and create a new one with the same "IdP name".', // UNTRANSLATED
|
||||
target_conflict_line3:
|
||||
'If you\'d like to connect to a different identity provider, please modify the "IdP name" and proceed.', // UNTRANSLATED
|
||||
config: 'Enter your config JSON', // UNTRANSLATED
|
||||
sync_profile: 'Sync profile information', // UNTRANSLATED
|
||||
sync_profile_only_at_sign_up: 'Only sync at sign-up', // UNTRANSLATED
|
||||
|
|
|
@ -51,6 +51,11 @@ const connectors = {
|
|||
'在“身份供应商名称”字段中输入唯一的标识符字符串,用于区分社交身份来源。注意,在连接器创建成功后,无法再次修改此设置。',
|
||||
target_tooltip:
|
||||
'Logto 社交连接器的「target」指的是社交身份的「来源」。在 Logto 的设计里,我们不允许某一平台的连接器中有相同的「target」以避免身份的冲突。在添加连接器时,你需要格外小心,我们「不允许」用户在创建之后更改「target」的值。 <a>了解更多</a>',
|
||||
target_conflict:
|
||||
'此「身份供应商名称」值与现有的 <span>name</span> 连接器相同。使用相同的身份供应商名称会导致不符合预期的登录行为,用户可能通过两个不同的连接器访问同一个帐户。',
|
||||
target_conflict_line2:
|
||||
'如果你想替换当前的连接器,并连接相同的身份提供商(IdP),以便先前的用户可以直接登录而无需重新注册,请先删除 <span>name</span> 连接器,再创建一个新的连接器并使用相同的「身份供应商名称」值。',
|
||||
target_conflict_line3: '如果您想连接一个新的身份验证提供程序,请修改「身份供应商名称」并继续。',
|
||||
config: '粘贴你的 JSON 代码',
|
||||
sync_profile: '开启用户资料同步',
|
||||
sync_profile_only_at_sign_up: '首次注册时同步',
|
||||
|
|
|
@ -6,9 +6,9 @@ export type RequestErrorMetadata = Record<string, unknown> & {
|
|||
expose?: boolean;
|
||||
};
|
||||
|
||||
export type RequestErrorBody = {
|
||||
export type RequestErrorBody<T = unknown> = {
|
||||
message: string;
|
||||
data: unknown;
|
||||
data: T;
|
||||
code: LogtoErrorCode;
|
||||
details?: string;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue