0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console): reuse existing oidc/saml form for OOTB SSO connector config (#4955)

This commit is contained in:
Darcy Ye 2023-11-23 16:05:55 +08:00 committed by GitHub
parent a36ae032e3
commit f4383a3e58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 70 additions and 29 deletions

View file

@ -29,7 +29,11 @@ function BasicInfo({ ssoConnectorId, providerName, providerConfig }: Props) {
const { tenantEndpoint } = useContext(AppDataContext);
const { applyDomain: applyCustomDomain } = useCustomDomain();
if (providerName === SsoProviderName.OIDC) {
if (
[SsoProviderName.OIDC, SsoProviderName.GOOGLE_WORKSPACE, SsoProviderName.OKTA].includes(
providerName
)
) {
return (
<FormField title="enterprise_sso.basic_info.oidc.redirect_uri_field_name">
{/* Generated and passed in by Admin console. */}

View file

@ -1,4 +1,5 @@
import { type SsoProviderName } from '@logto/schemas';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { type ParsedSsoIdentityProviderConfig } from '@/pages/EnterpriseSso/types.js';
@ -7,16 +8,17 @@ import * as styles from './index.module.scss';
type Props = {
providerConfig: ParsedSsoIdentityProviderConfig<SsoProviderName.OIDC>;
className?: string;
};
function ParsedConfigPreview({ providerConfig }: Props) {
function ParsedConfigPreview({ providerConfig, className }: Props) {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console.enterprise_sso_details.oidc_preview',
});
const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint, jwksUri, issuer } =
providerConfig;
return (
<div className={styles.container}>
<div className={classNames(styles.container, className)}>
<div>
<div className={styles.title}>{t('authorization_endpoint')}</div>
<div className={styles.content}>{authorizationEndpoint}</div>

View file

@ -0,0 +1,9 @@
@use '@/scss/underscore' as _;
.copyToClipboard {
display: block;
}
.oidcConfigPreview {
margin-top: _.unit(3);
}

View file

@ -1,7 +1,8 @@
import { type SsoProviderName } from '@logto/schemas';
import { SsoProviderName } from '@logto/schemas';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification';
import TextInput from '@/ds-components/TextInput';
@ -12,15 +13,17 @@ import {
} from '@/pages/EnterpriseSso/types.js';
import ParsedConfigPreview from './ParsedConfigPreview';
import * as styles from './index.module.scss';
type Props = {
isGuidePage?: boolean;
providerConfig?: ParsedSsoIdentityProviderConfig<SsoProviderName.OIDC>;
config?: SsoConnectorConfig<SsoProviderName.OIDC>;
providerName: SsoProviderName;
};
// Do not show inline notification and parsed config preview if it is on guide page.
function OidcMetadataForm({ isGuidePage, providerConfig, config }: Props) {
function OidcMetadataForm({ isGuidePage, providerConfig, config, providerName }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
register,
@ -56,22 +59,35 @@ function OidcMetadataForm({ isGuidePage, providerConfig, config }: Props) {
/>
</FormField>
<FormField
isRequired={isFieldCheckRequired}
isRequired={isFieldCheckRequired && providerName !== SsoProviderName.GOOGLE_WORKSPACE}
title="enterprise_sso.metadata.oidc.issuer_field_name"
>
<TextInput
{...register('issuer', { required: isFieldCheckRequired })}
error={Boolean(errors.issuer)}
/>
{isFieldCheckRequired && providerConfig && config?.issuer && (
<ParsedConfigPreview providerConfig={providerConfig} />
{providerName === SsoProviderName.GOOGLE_WORKSPACE ? (
<CopyToClipboard
className={styles.copyToClipboard}
variant="border"
// TODO: this hard-coded value should align with the `googleIssuer` value defined in `packages/core/src/sso/GoogleWorkspaceSsoConnector/index.ts`.
value={providerConfig?.issuer ?? 'https://accounts.google.com'}
/>
) : (
<TextInput
{...register('issuer', {
required: isFieldCheckRequired,
})}
error={Boolean(errors.issuer)}
/>
)}
{isFieldCheckRequired &&
providerConfig &&
(config?.issuer ?? providerName === SsoProviderName.GOOGLE_WORKSPACE) && (
<ParsedConfigPreview
className={styles.oidcConfigPreview}
providerConfig={providerConfig}
/>
)}
</FormField>
<FormField title="enterprise_sso.metadata.oidc.scope_field_name">
<TextInput
{...register('scope', { required: isFieldCheckRequired })}
error={Boolean(errors.scope)}
/>
<TextInput {...register('scope')} error={Boolean(errors.scope)} />
</FormField>
</>
);

View file

@ -30,7 +30,6 @@ type Props<T extends SsoProviderName> = {
isOpen: boolean;
connector: SsoConnectorWithProviderConfigWithGeneric<T>;
onClose: (ssoConnectorId?: string) => void;
isReadOnly?: boolean;
};
type GuideCardProps = {
@ -58,7 +57,7 @@ function GuideCard({ cardOrder, title, description, children, className }: Guide
);
}
function Guide<T extends SsoProviderName>({ isOpen, connector, onClose, isReadOnly }: Props<T>) {
function Guide<T extends SsoProviderName>({ isOpen, connector, onClose }: Props<T>) {
const {
id: ssoConnectorId,
connectorName: ssoConnectorName,
@ -143,13 +142,17 @@ function Guide<T extends SsoProviderName>({ isOpen, connector, onClose, isReadOn
providerConfig={providerConfig}
/>
</GuideCard>
{providerName === SsoProviderName.OIDC ? (
{[
SsoProviderName.OIDC,
SsoProviderName.GOOGLE_WORKSPACE,
SsoProviderName.OKTA,
].includes(providerName) ? (
<GuideCard
cardOrder={2}
title="enterprise_sso.metadata.title"
description="enterprise_sso.metadata.description"
>
<OidcMetadataForm isGuidePage />
<OidcMetadataForm isGuidePage providerName={providerName} />
</GuideCard>
) : (
<>
@ -158,7 +161,7 @@ function Guide<T extends SsoProviderName>({ isOpen, connector, onClose, isReadOn
title="enterprise_sso.attribute_mapping.title"
description="enterprise_sso.attribute_mapping.description"
>
<SamlAttributeMapping isReadOnly={isReadOnly} />
<SamlAttributeMapping />
</GuideCard>
<GuideCard
cardOrder={3}

View file

@ -139,10 +139,13 @@ function EnterpriseSsoConnectors() {
dataIndex: 'status',
colSpan: 186,
render: ({ providerConfig, providerName }) => {
const inUse =
providerName === SsoProviderName.OIDC
? Boolean(providerConfig)
: Boolean(providerConfig?.identityProvider);
const inUse = [
SsoProviderName.OIDC,
SsoProviderName.GOOGLE_WORKSPACE,
SsoProviderName.OKTA,
].includes(providerName)
? Boolean(providerConfig)
: Boolean(providerConfig?.identityProvider);
return (
<Tag type="state" status={inUse ? 'success' : 'error'} variant="plain">
{t(
@ -208,7 +211,6 @@ function EnterpriseSsoConnectors() {
// eslint-disable-next-line no-restricted-syntax
connectorForGuide as SsoConnectorWithProviderConfigWithGeneric<SsoProviderName>
}
isReadOnly={connectorForGuide.providerName !== 'SAML'}
onClose={async (connectorId) => {
if (connectorId) {
navigate(buildDetailsPathname(connectorId), { replace: true });

View file

@ -86,10 +86,13 @@ function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: P
title="enterprise_sso_details.upload_idp_metadata_title"
description="enterprise_sso_details.upload_idp_metadata_description"
>
{providerName === SsoProviderName.OIDC ? (
{[SsoProviderName.OIDC, SsoProviderName.GOOGLE_WORKSPACE, SsoProviderName.OKTA].includes(
providerName
) ? (
// Can not infer the type by narrowing down the value of `providerName`, so we need to cast it.
<OidcMetadataForm
isGuidePage={false}
providerName={providerName}
// eslint-disable-next-line no-restricted-syntax
config={config as SsoConnectorConfig<SsoProviderName.OIDC>}
providerConfig={
@ -127,7 +130,7 @@ function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: P
providerConfig={providerConfig}
/>
</FormCard>
{providerName === SsoProviderName.SAML && (
{[SsoProviderName.SAML, SsoProviderName.AZURE_AD].includes(providerName) && (
<FormCard
title="enterprise_sso_details.attribute_mapping_title"
description="enterprise_sso_details.attribute_mapping_description"

View file

@ -57,7 +57,9 @@ function EnterpriseSsoConnectorDetails<T extends SsoProviderName>() {
const inUse =
ssoConnector &&
(ssoConnector.providerName === SsoProviderName.OIDC
([SsoProviderName.OIDC, SsoProviderName.GOOGLE_WORKSPACE, SsoProviderName.OKTA].includes(
ssoConnector.providerName
)
? Boolean(ssoConnector.providerConfig)
: Boolean(
ssoConnector.providerConfig &&