jackson/internal-ui/src/federated-saml/NewFederatedSAMLApp.tsx

266 lines
9.3 KiB
TypeScript

import { useFormik } from 'formik';
import TagsInput from 'react-tagsinput';
import { Card, Button } from 'react-daisyui';
import { useTranslation } from 'next-i18next';
import type { SAMLFederationApp } from '../types';
import QuestionMarkCircleIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon';
import { defaultHeaders } from '../utils';
import { AttributesMapping } from './AttributesMapping';
import { PageHeader } from '../shared';
import { ItemList } from '../shared/ItemList';
type NewSAMLFederationApp = Pick<
SAMLFederationApp,
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
>;
export const NewFederatedSAMLApp = ({
samlAudience = 'https://saml.boxyhq.com',
urls,
onSuccess,
onError,
onEntityIdGenerated,
excludeFields,
}: {
samlAudience?: string;
urls: { createApp: string };
onSuccess?: (data: SAMLFederationApp) => void;
onError?: (error: Error) => void;
onEntityIdGenerated?: (entityId: string) => void;
excludeFields?: 'product'[];
}) => {
const { t } = useTranslation('common');
const initialValues: NewSAMLFederationApp = {
type: 'saml',
name: '',
tenant: '',
product: '',
acsUrl: '',
entityId: '',
tenants: [],
mappings: [],
};
if (excludeFields) {
excludeFields.forEach((key) => {
delete initialValues[key as keyof NewSAMLFederationApp];
});
}
const formik = useFormik<NewSAMLFederationApp>({
initialValues: initialValues,
onSubmit: async (values) => {
const rawResponse = await fetch(urls.createApp, {
method: 'POST',
body: JSON.stringify(values),
headers: defaultHeaders,
});
const response = await rawResponse.json();
if (rawResponse.ok) {
onSuccess?.(response.data);
} else {
onError?.(response.error);
}
},
});
const connectionIsOIDC = formik.values.type === 'oidc';
const connectionIsSAML = !connectionIsOIDC;
const generateEntityId = () => {
const id = crypto.randomUUID().replace(/-/g, '');
const entityId = `${samlAudience}/${id}`;
formik.setFieldValue('entityId', entityId);
navigator.clipboard.writeText(entityId);
onEntityIdGenerated?.(entityId);
};
return (
<>
<PageHeader title={t('bui-fs-create-app')} />
<form onSubmit={formik.handleSubmit} method='POST'>
<Card className='p-6 rounded space-y-3'>
<div className='mb-4 flex items-center'>
<div className='mr-2 py-3'>{t('bui-fs-select-app-type')}:</div>
<div className='flex w-52'>
<div className='form-control'>
<label className='label mr-4 cursor-pointer'>
<input
type='radio'
name='type'
value='saml'
className='radio-primary radio'
checked={formik.values.type === 'saml'}
onChange={formik.handleChange}
/>
<span className='label-text ml-1'>{t('bui-fs-saml')}</span>
</label>
</div>
<div className='form-control'>
<label className='label mr-4 cursor-pointer' data-testid='sso-type-oidc'>
<input
type='radio'
name='type'
value='oidc'
className='radio-primary radio'
checked={formik.values.type === 'oidc'}
onChange={formik.handleChange}
/>
<span className='label-text ml-1'>{t('bui-fs-oidc')}</span>
</label>
</div>
</div>
</div>
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-shared-name')}</span>
</div>
<input
type='text'
placeholder='Your app'
className='input input-bordered w-full text-sm'
name='name'
value={formik.values.name}
onChange={formik.handleChange}
required
/>
</label>
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-shared-tenant')}</span>
</div>
<input
type='text'
placeholder='example.com'
className='input input-bordered w-full text-sm'
name='tenant'
value={formik.values.tenant}
onChange={formik.handleChange}
required
/>
</label>
{!excludeFields?.includes('product') && (
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-shared-product')}</span>
</div>
<input
type='text'
placeholder='MyApp'
className='input input-bordered w-full text-sm'
name='product'
value={formik.values.product}
onChange={formik.handleChange}
required
/>
</label>
)}
{connectionIsSAML && (
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-fs-acs-url')}</span>
</div>
<input
type='url'
placeholder='https://your-sp.com/saml/acs'
className='input input-bordered w-full text-sm'
name='acsUrl'
value={formik.values.acsUrl}
onChange={formik.handleChange}
required
/>
</label>
)}
{connectionIsOIDC && (
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-sl-allowed-redirect-urls-new')}</span>
</div>
<ItemList
currentlist={formik.values.redirectUrl || ['']}
onItemListChange={(newList) => formik.setFieldValue('redirectUrl', newList)}></ItemList>
</label>
)}
{connectionIsSAML && (
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-fs-entity-id')}</span>
<span className='label-text-alt'>
<div className='flex items-center gap-1'>
<span
className='cursor-pointer border-stone-600 border p-1 rounded'
onClick={generateEntityId}>
{t('bui-fs-generate-sp-entity-id')}
</span>
<div className='tooltip tooltip-left' data-tip={t('bui-fs-entity-id-instruction')}>
<QuestionMarkCircleIcon className='h-5 w-5' />
</div>
</div>
</span>
</div>
<input
type='text'
placeholder='https://your-sp.com/saml/entityId'
className='input input-bordered w-full text-sm'
name='entityId'
value={formik.values.entityId}
onChange={formik.handleChange}
required
/>
<label className='label'>
<span className='label-text-alt'>{t('bui-fs-entity-id-change-restriction')}</span>
</label>
</label>
)}
<label className='form-control w-full'>
<label className='label'>
<span className='label-text'>{t('bui-fs-tenants')}</span>
</label>
<TagsInput
value={formik.values.tenants}
onChange={(tags: string[]) => formik.setFieldValue('tenants', tags)}
onlyUnique={true}
inputProps={{
placeholder: t('bui-fs-enter-tenant'),
autocomplete: 'off',
}}
focusedClassName='input-focused'
addOnBlur={true}
/>
<label className='label'>
<span className='label-text-alt'>{t('bui-fs-tenants-mapping-desc')}</span>
</label>
</label>
{connectionIsSAML && (
<label className='form-control w-full'>
<div className='label'>
<span className='label-text'>{t('bui-fs-attribute-mappings')}</span>
</div>
<div className='label'>
<span className='label-text-alt'>{t('bui-fs-attribute-mappings-desc')}</span>
</div>
<AttributesMapping
mappings={formik.values.mappings || []}
onAttributeMappingsChange={(mappings) => formik.setFieldValue('mappings', mappings)}
/>
</label>
)}
<div className='flex gap-2 justify-end pt-6'>
<Button
type='submit'
className='btn btn-primary btn-md'
loading={formik.isSubmitting}
disabled={!formik.dirty || !formik.isValid}>
{t('bui-fs-create-app-btn')}
</Button>
</div>
</Card>
</form>
</>
);
};