diff --git a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx index 4557e84e5..66c8da42a 100644 --- a/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx +++ b/packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx @@ -1,7 +1,9 @@ import { type SsoConnectorFactoriesResponse, type SsoConnectorWithProviderConfig, + type RequestErrorBody, } from '@logto/schemas'; +import { HTTPError } from 'ky'; import { useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -32,6 +34,8 @@ type FormType = { connectorName: string; }; +const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name'; + function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [selectedProviderName, setSelectedProviderName] = useState(); @@ -43,8 +47,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { register, handleSubmit, formState: { isSubmitting, errors }, + setError, } = useForm(); - const api = useApi(); + const api = useApi({ hideErrorToast: true }); const isLoading = !data && !error; @@ -80,11 +85,22 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { return; } - const createdSsoConnector = await api - .post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } }) - .json(); + try { + const createdSsoConnector = await api + .post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } }) + .json(); - onClose(createdSsoConnector); + onClose(createdSsoConnector); + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { response } = error; + const metadata = await response.clone().json(); + + if (metadata.code === duplicateConnectorNameErrorCode) { + setError('connectorName', { type: 'custom', message: metadata.message }); + } + } + } }) ); @@ -142,7 +158,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/LogosUploader/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Experience/LogosUploader/index.module.scss similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/LogosUploader/index.module.scss rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/LogosUploader/index.module.scss diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/LogosUploader/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Experience/LogosUploader/index.tsx similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/LogosUploader/index.tsx rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/LogosUploader/index.tsx diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/MultiInput/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Experience/MultiInput/index.module.scss similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/MultiInput/index.module.scss rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/MultiInput/index.module.scss diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/MultiInput/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Experience/MultiInput/index.tsx similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/MultiInput/index.tsx rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/MultiInput/index.tsx diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.module.scss similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/index.module.scss rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/index.module.scss diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Settings/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx similarity index 87% rename from packages/console/src/pages/EnterpriseSsoDetails/Settings/index.tsx rename to packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx index f35d55aa5..ad3c8fff9 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Settings/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx @@ -49,6 +49,8 @@ const duplicatedDomainsErrorCode = 'single_sign_on.duplicated_domains'; const forbiddenDomainsErrorCode = 'single_sign_on.forbidden_domains'; const invalidDomainFormatErrorCode = 'single_sign_on.invalid_domain_format'; +const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name'; + const dataToFormParser = (data: DataType) => { const { branding, connectorName, domains, syncProfile } = data; return { @@ -71,7 +73,7 @@ const formDataToSsoConnectorParser = ( }; }; -function Settings({ data, isDeleted, onUpdated }: Props) { +function Experience({ data, isDeleted, onUpdated }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { isReady: isUserAssetsServiceReady } = useUserAssetsService(); const api = useApi({ hideErrorToast: true }); @@ -118,26 +120,38 @@ function Settings({ data, isDeleted, onUpdated }: Props) { toast.success(t('general.saved')); onUpdated(updatedSsoConnector); } catch (error: unknown) { - /** - * Should render invalid domains: - * - show `error` tag for forbidden domains - * - show `info` tag for duplicated domains - * - * Also manually passed the returned error message to `setError` to show the error message in-place. - */ if (error instanceof HTTPError) { const { response } = error; const metadata = await response.clone().json>(); - setValue( - 'domains', - watch('domains').map((domain) => ({ - ...domain, - ...conditional(metadata.data.data.includes(domain.value) && { status: 'info' }), - })), - { shouldDirty: true } - ); - setError('domains', { type: 'custom', message: metadata.message }); + /** + * Should render invalid domains: + * - show `error` tag for forbidden domains + * - show `info` tag for duplicated domains + * + * Also manually passed the returned error message to `setError` to show the error message in-place. + */ + if ( + [ + duplicatedDomainsErrorCode, + forbiddenDomainsErrorCode, + invalidDomainFormatErrorCode, + ].includes(metadata.code) + ) { + setValue( + 'domains', + watch('domains').map((domain) => ({ + ...domain, + ...conditional(metadata.data.data.includes(domain.value) && { status: 'info' }), + })), + { shouldDirty: true } + ); + setError('domains', { type: 'custom', message: metadata.message }); + } + + if (duplicateConnectorNameErrorCode === metadata.code) { + setError('connectorName', { type: 'custom', message: metadata.message }); + } } } }) @@ -152,6 +166,12 @@ function Settings({ data, isDeleted, onUpdated }: Props) { onSubmit={onSubmit} > + + +
{t('enterprise_sso_details.email_domain_field_description')} @@ -239,10 +259,10 @@ function Settings({ data, isDeleted, onUpdated }: Props) { title="enterprise_sso_details.custom_branding_title" description="enterprise_sso_details.custom_branding_description" > - + {isUserAssetsServiceReady ? ( @@ -283,4 +303,4 @@ function Settings({ data, isDeleted, onUpdated }: Props) { ); } -export default Settings; +export default Experience; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/index.tsx index da99a2e56..496af3c55 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/index.tsx @@ -28,7 +28,7 @@ import SsoConnectorLogo from '../EnterpriseSso/SsoConnectorLogo'; import { type SsoConnectorWithProviderConfigWithGeneric } from '../EnterpriseSso/types'; import Connection from './Connection'; -import Settings from './Settings'; +import Experience from './Experience'; import * as styles from './index.module.scss'; const enterpriseSsoPathname = '/enterprise-sso'; @@ -167,7 +167,7 @@ function EnterpriseSsoConnectorDetails() { {tab === EnterpriseSsoDetailsTabs.Experience && ( - { diff --git a/packages/core/src/routes/sso-connector/index.ts b/packages/core/src/routes/sso-connector/index.ts index b696213aa..9240bc476 100644 --- a/packages/core/src/routes/sso-connector/index.ts +++ b/packages/core/src/routes/sso-connector/index.ts @@ -250,7 +250,11 @@ export default function singleSignOnRoutes(...args: Rout // Validate the connector name is unique if (rest.connectorName) { const duplicateConnector = await ssoConnectors.findByConnectorName(rest.connectorName); - assertThat(!duplicateConnector, 'single_sign_on.duplicate_connector_name'); + // Should not block the update of the current connector. + assertThat( + !duplicateConnector || duplicateConnector.id === id, + 'single_sign_on.duplicate_connector_name' + ); } // Validate the connector config if it's provided diff --git a/packages/integration-tests/src/tests/api/sso-connectors.test.ts b/packages/integration-tests/src/tests/api/sso-connectors.test.ts index 59d698b36..51cca6c3e 100644 --- a/packages/integration-tests/src/tests/api/sso-connectors.test.ts +++ b/packages/integration-tests/src/tests/api/sso-connectors.test.ts @@ -214,6 +214,20 @@ describe('patch sso-connector by id', () => { await deleteSsoConnectorById(id2); }); + it('should not block the update of current connector', async () => { + const { id } = await createSsoConnector({ + providerName: 'OIDC', + connectorName: 'test connector name', + }); + + const updatedSsoConnector = await patchSsoConnectorById(id, { + connectorName: 'test connector name', + }); + expect(updatedSsoConnector).toHaveProperty('connectorName', 'test connector name'); + + await deleteSsoConnectorById(id); + }); + it.each(providerNames)('should patch sso connector without config', async (providerName) => { const { id } = await createSsoConnector({ providerName, diff --git a/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts index 9499c1b29..8281fa9a7 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts @@ -19,6 +19,7 @@ const enterprise_sso_details = { each_sign_in: 'Always sync at each sign-in', }, connector_name_field_name: 'Connector name', + display_name_field_name: 'Display name', connector_logo_field_name: 'Connector logo', branding_logo_context: 'Upload logo', branding_logo_error: 'Upload logo error: {{error}}', diff --git a/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo', diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts index e40b1a49b..6d06a9e2d 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts @@ -36,6 +36,8 @@ const enterprise_sso_details = { /** UNTRANSLATED */ connector_name_field_name: 'Connector name', /** UNTRANSLATED */ + display_name_field_name: 'Display name', + /** UNTRANSLATED */ connector_logo_field_name: 'Connector logo', /** UNTRANSLATED */ branding_logo_context: 'Upload logo',