1- import { type JSX } from 'react' ;
1+ import React , { type JSX } from 'react' ;
22
33import { Box , Col , descriptors , Heading , localizationKeys , Text } from '@/customizables' ;
44import { ClipboardInput } from '@/elements/ClipboardInput' ;
5+ import { useCardState } from '@/elements/contexts' ;
56import { Form } from '@/elements/Form' ;
67import { Checkmark , Clipboard } from '@/icons' ;
78import { useFormControl } from '@/ui/utils/useFormControl' ;
@@ -10,6 +11,16 @@ import { useConfigureSSO } from '../../../ConfigureSSOContext';
1011import { Step } from '../../../elements/Step' ;
1112import { useWizard , Wizard } from '../../../elements/Wizard' ;
1213import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter' ;
14+ import {
15+ applySamlSubmitError ,
16+ buildSamlConfigurationPayload ,
17+ IdentityProviderConfigurationForm ,
18+ type IdentityProviderConfigurationFormProps ,
19+ } from './shared/IdentityProviderConfigurationForm' ;
20+ import {
21+ IdentityProviderConfigurationModes ,
22+ type IdpConfigurationMode ,
23+ } from './shared/IdentityProviderConfigurationModes' ;
1324
1425export const SamlMicrosoftConfigureSteps = ( ) : JSX . Element => {
1526 return (
@@ -33,6 +44,18 @@ export const SamlMicrosoftConfigureSteps = (): JSX.Element => {
3344 </ Step . Header >
3445 < SamlMicrosoftServiceProviderStep />
3546 </ Wizard . Step >
47+
48+ < Wizard . Step id = 'identity-provider-metadata' >
49+ < Step . Header
50+ title = { localizationKeys ( 'configureSSO.configureStep.samlMicrosoft.mainHeaderTitle' ) }
51+ description = { localizationKeys (
52+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.headerSubtitle' ,
53+ ) }
54+ >
55+ < InnerStepCounter />
56+ </ Step . Header >
57+ < SamlMicrosoftIdentityProviderMetadataStep />
58+ </ Wizard . Step >
3659 </ >
3760 ) ;
3861} ;
@@ -370,3 +393,194 @@ const SamlMicrosoftServiceProviderStep = (): JSX.Element => {
370393 </ >
371394 ) ;
372395} ;
396+
397+ const MICROSOFT_SAML_IDP_MODES = [ 'metadataUrl' , 'manual' ] as const satisfies readonly IdpConfigurationMode [ ] ;
398+
399+ const SamlMicrosoftIdentityProviderMetadataStep = ( ) : JSX . Element => {
400+ const card = useCardState ( ) ;
401+ const { goNext, goPrev, isFirstStep } = useWizard ( ) ;
402+ const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO ( ) ;
403+
404+ const samlConnection = enterpriseConnection ?. samlConnection ;
405+ const hasExistingConfig = Boolean (
406+ samlConnection ?. idpSsoUrl ||
407+ samlConnection ?. idpEntityId ||
408+ samlConnection ?. idpCertificate ||
409+ samlConnection ?. idpMetadataUrl ,
410+ ) ;
411+ const existingCertPresent = Boolean ( samlConnection ?. idpCertificate ) ;
412+
413+ const [ mode , setMode ] = React . useState < IdpConfigurationMode > ( hasExistingConfig ? 'manual' : 'metadataUrl' ) ;
414+ const [ certFile , setCertFile ] = React . useState < File | null > ( null ) ;
415+
416+ const metadataUrlField = useFormControl ( 'idpMetadataUrl' , samlConnection ?. idpMetadataUrl ?? '' , {
417+ type : 'text' ,
418+ label : localizationKeys ( 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.label' ) ,
419+ placeholder : localizationKeys (
420+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.placeholder' ,
421+ ) ,
422+ isRequired : true ,
423+ } ) ;
424+
425+ const signOnUrlField = useFormControl ( 'idpSsoUrl' , samlConnection ?. idpSsoUrl ?? '' , {
426+ type : 'text' ,
427+ label : localizationKeys (
428+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signOnUrl.label' ,
429+ ) ,
430+ placeholder : localizationKeys (
431+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signOnUrl.placeholder' ,
432+ ) ,
433+ isRequired : true ,
434+ } ) ;
435+
436+ const issuerField = useFormControl ( 'idpEntityId' , samlConnection ?. idpEntityId ?? '' , {
437+ type : 'text' ,
438+ label : localizationKeys (
439+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.issuer.label' ,
440+ ) ,
441+ placeholder : localizationKeys (
442+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.issuer.placeholder' ,
443+ ) ,
444+ isRequired : true ,
445+ } ) ;
446+
447+ const certificateField = useFormControl ( 'idpCertificate' , '' , {
448+ type : 'text' ,
449+ label : localizationKeys (
450+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.label' ,
451+ ) ,
452+ isRequired : true ,
453+ } ) ;
454+
455+ const trimmedMetadataUrl = metadataUrlField . value . trim ( ) ;
456+ const trimmedSignOnUrl = signOnUrlField . value . trim ( ) ;
457+ const trimmedIssuer = issuerField . value . trim ( ) ;
458+ const hasCert = certFile !== null || existingCertPresent ;
459+
460+ const isValid =
461+ mode === 'metadataUrl'
462+ ? trimmedMetadataUrl . length > 0
463+ : trimmedSignOnUrl . length > 0 && trimmedIssuer . length > 0 && hasCert ;
464+
465+ const canSubmit = isValid && ! card . isLoading ;
466+
467+ const formProps : IdentityProviderConfigurationFormProps =
468+ mode === 'metadataUrl'
469+ ? {
470+ mode : 'metadataUrl' ,
471+ form : { field : metadataUrlField } ,
472+ labels : {
473+ description : localizationKeys (
474+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.description' ,
475+ ) ,
476+ } ,
477+ }
478+ : {
479+ mode : 'manual' ,
480+ form : {
481+ signOnUrlField,
482+ issuerField,
483+ certificateField,
484+ certFile,
485+ onCertFileChange : setCertFile ,
486+ existingCertPresent,
487+ } ,
488+ labels : {
489+ description : localizationKeys (
490+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.description' ,
491+ ) ,
492+ uploadFile : localizationKeys (
493+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.uploadFile' ,
494+ ) ,
495+ replaceFile : localizationKeys (
496+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.replaceFile' ,
497+ ) ,
498+ removeFile : localizationKeys (
499+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.removeFile' ,
500+ ) ,
501+ fileUploaded : localizationKeys (
502+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.fileUploaded' ,
503+ ) ,
504+ } ,
505+ } ;
506+
507+ const handleContinue = async ( ) : Promise < void > => {
508+ if ( ! enterpriseConnection || ! canSubmit ) {
509+ return ;
510+ }
511+
512+ card . setError ( undefined ) ;
513+ card . setLoading ( ) ;
514+
515+ try {
516+ const saml = await buildSamlConfigurationPayload ( {
517+ mode,
518+ metadataUrl : { value : metadataUrlField . value } ,
519+ manual : { signOnUrl : signOnUrlField . value , issuer : issuerField . value , certFile } ,
520+ } ) ;
521+
522+ await updateEnterpriseConnection ( enterpriseConnection . id , { saml } ) ;
523+ void goNext ( ) ;
524+ } catch ( err ) {
525+ if ( mode === 'metadataUrl' ) {
526+ applySamlSubmitError ( err , card , metadataUrlField ) ;
527+ } else {
528+ applySamlSubmitError ( err , card , signOnUrlField , [ issuerField , certificateField ] ) ;
529+ }
530+ } finally {
531+ card . setIdle ( ) ;
532+ }
533+ } ;
534+
535+ return (
536+ < >
537+ < Step . Body >
538+ < Step . Section
539+ fill
540+ gap = { 5 }
541+ >
542+ < Heading
543+ elementDescriptor = { descriptors . configureSSOInstructionsHeading }
544+ as = 'h3'
545+ textVariant = 'subtitle'
546+ localizationKey = { localizationKeys (
547+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.title' ,
548+ ) }
549+ />
550+ < IdentityProviderConfigurationModes
551+ modes = { MICROSOFT_SAML_IDP_MODES }
552+ value = { mode }
553+ onChange = { next => {
554+ card . setError ( undefined ) ;
555+ setMode ( next ) ;
556+ } }
557+ labels = { {
558+ ariaLabel : localizationKeys (
559+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.ariaLabel' ,
560+ ) ,
561+ metadataUrl : localizationKeys (
562+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.metadataUrl' ,
563+ ) ,
564+ manual : localizationKeys (
565+ 'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.manual' ,
566+ ) ,
567+ } }
568+ />
569+ < IdentityProviderConfigurationForm { ...formProps } />
570+ </ Step . Section >
571+ </ Step . Body >
572+
573+ < Step . Footer >
574+ < Step . Footer . Previous
575+ onClick = { ( ) => goPrev ( ) }
576+ isDisabled = { isFirstStep || card . isLoading }
577+ />
578+ < Step . Footer . Continue
579+ onClick = { handleContinue }
580+ isLoading = { card . isLoading }
581+ isDisabled = { ! canSubmit }
582+ />
583+ </ Step . Footer >
584+ </ >
585+ ) ;
586+ } ;
0 commit comments