mirror of https://github.com/boxyhq/jackson.git
Standardize the setup link feature (#784)
* Tweaks to NewSetupLink * Standardize the setup link feature * Fix the connection APIs * Standardize the Setup Link + Directory sync * Tweaks to components * Move the directory listing to a components * Tweaks to connectons * Updates connections page * Make variable naming consistent * Standardize the page export * Remove unnecessary named export from API handler
This commit is contained in:
parent
c0bf71acff
commit
4e10d501ea
|
@ -1,177 +0,0 @@
|
|||
import { FormEvent, useState } from 'react';
|
||||
import ConfirmationModal from '@components/ConfirmationModal';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { ButtonPrimary } from './ButtonPrimary';
|
||||
import { LinkBack } from './LinkBack';
|
||||
import { InputWithCopyButton } from './ClipboardButton';
|
||||
|
||||
const CreateSetupLink = (props: { service: 'sso' | 'dsync' }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
const createLink = async (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
const { tenant, product, type } = formObj;
|
||||
const res = await fetch('/api/admin/setup-links', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenant,
|
||||
product,
|
||||
type,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setUrl(json.data.url);
|
||||
successToast(t('link_generated'));
|
||||
} else {
|
||||
const rsp = await res.json();
|
||||
errorToast(rsp?.error?.message || t('server_error'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const regenerateLink = async () => {
|
||||
setLoading1(true);
|
||||
setDelModalVisible(!delModalVisible);
|
||||
const { tenant, product, type } = formObj;
|
||||
|
||||
const res = await fetch('/api/admin/setup-links', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenant,
|
||||
product,
|
||||
type,
|
||||
regenerate: true,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setLoading1(false);
|
||||
const json = await res.json();
|
||||
setUrl(json.data.url);
|
||||
successToast(t('link_regenerated'));
|
||||
} else {
|
||||
// save failed
|
||||
setLoading1(false);
|
||||
errorToast(t('server_error'));
|
||||
}
|
||||
};
|
||||
const getHandleChange = (event: FormEvent) => {
|
||||
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
||||
setFormObj((cur) => ({ ...cur, [target.name]: target.value }));
|
||||
};
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [loading1, setLoading1] = useState<boolean>(false);
|
||||
const [formObj, setFormObj] = useState<Record<string, string>>({
|
||||
tenant: '',
|
||||
product: '',
|
||||
type: props.service || 'sso',
|
||||
});
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
const toggleDelConfirm = () => setDelModalVisible(!delModalVisible);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack onClick={() => router.back()} />
|
||||
<div className='mt-5 min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<h2 className='mb-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||
{t('create_setup_link', {
|
||||
service: props.service === 'sso' ? t('enterprise_sso') : t('directory_sync'),
|
||||
})}
|
||||
</h2>
|
||||
<div>
|
||||
<div className='mb-6'>
|
||||
<label
|
||||
htmlFor='tenant'
|
||||
className={`mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300`}>
|
||||
{'Tenant'}
|
||||
</label>
|
||||
<input
|
||||
id='tenant'
|
||||
name='tenant'
|
||||
type='text'
|
||||
placeholder='acme.com'
|
||||
value={formObj['tenant']}
|
||||
required={true}
|
||||
readOnly={false}
|
||||
onChange={getHandleChange}
|
||||
className='input-bordered input w-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-6'>
|
||||
<label
|
||||
htmlFor='tenant'
|
||||
className={`mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300`}>
|
||||
{'Product'}
|
||||
</label>
|
||||
<input
|
||||
id='product'
|
||||
name='product'
|
||||
type='text'
|
||||
placeholder='demo'
|
||||
value={formObj['product']}
|
||||
required={true}
|
||||
readOnly={false}
|
||||
onChange={getHandleChange}
|
||||
className='input-bordered input w-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<ButtonPrimary
|
||||
loading={loading}
|
||||
disabled={!formObj.tenant || !formObj.product || !formObj.type}
|
||||
onClick={createLink}>
|
||||
{t('generate')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
title='Delete the setup link'
|
||||
description='This action cannot be undone. This will permanently delete the link.'
|
||||
visible={delModalVisible}
|
||||
onConfirm={regenerateLink}
|
||||
onCancel={toggleDelConfirm}
|
||||
actionButtonText={t('regenerate')}
|
||||
/>
|
||||
</div>
|
||||
{url && (
|
||||
<div className='mt-5 min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<h2 className='mb-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||
{url
|
||||
? t('setup_link_info')
|
||||
: t('create_setup_link', {
|
||||
service: props.service === 'sso' ? t('enterprise_sso') : t('directory_sync'),
|
||||
})}
|
||||
</h2>
|
||||
<div className='form-control'>
|
||||
<InputWithCopyButton text={url} label={t('setup_link_url')} />
|
||||
</div>
|
||||
<div className='mt-5 flex'>
|
||||
<ButtonPrimary
|
||||
loading={loading1}
|
||||
disabled={!formObj.tenant || !formObj.product || !formObj.type}
|
||||
onClick={
|
||||
url
|
||||
? () => {
|
||||
setDelModalVisible(true);
|
||||
}
|
||||
: createLink
|
||||
}>
|
||||
{url ? t('regenerate') : t('generate')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSetupLink;
|
|
@ -8,51 +8,64 @@ import { LinkPrimary } from '@components/LinkPrimary';
|
|||
import { IconButton } from '@components/IconButton';
|
||||
import { InputWithCopyButton } from '@components/ClipboardButton';
|
||||
import { Pagination, pageLimit } from '@components/Pagination';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
import type { OIDCSSORecord, SAMLSSORecord } from '@boxyhq/saml-jackson';
|
||||
|
||||
type ConnectionListProps = {
|
||||
connections: (SAMLSSORecord | OIDCSSORecord)[];
|
||||
setupToken?: string;
|
||||
idpEntityID?: string;
|
||||
paginate: {
|
||||
offset: number;
|
||||
};
|
||||
setPaginate: (paginate: { offset: number }) => void;
|
||||
};
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import Loading from '@components/Loading';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
|
||||
const ConnectionList = ({
|
||||
paginate,
|
||||
setPaginate,
|
||||
connections,
|
||||
setupToken,
|
||||
setupLinkToken,
|
||||
idpEntityID,
|
||||
}: ConnectionListProps) => {
|
||||
}: {
|
||||
setupLinkToken?: string;
|
||||
idpEntityID?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
const router = useRouter();
|
||||
|
||||
if (!connections) {
|
||||
const displayTenantProduct = setupLinkToken ? false : true;
|
||||
const getConnectionsUrl = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/sso-connection`
|
||||
: `/api/admin/connections?pageOffset=${paginate.offset}&pageLimit=${pageLimit}`;
|
||||
const createConnectionUrl = setupLinkToken
|
||||
? `/setup/${setupLinkToken}/sso-connection/new`
|
||||
: '/admin/sso-connection/new';
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<(SAMLSSORecord | OIDCSSORecord)[]>, ApiError>(
|
||||
getConnectionsUrl,
|
||||
fetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connections.length === 0 && setupToken) {
|
||||
router.replace(`/setup/${setupToken}/sso-connection/new`);
|
||||
const connections = data.data || [];
|
||||
|
||||
if (connections && setupLinkToken && connections.length === 0) {
|
||||
router.replace(`/setup/${setupLinkToken}/sso-connection/new`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayTenantProduct = setupToken ? false : true;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('enterprise_sso')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<LinkPrimary
|
||||
Icon={PlusIcon}
|
||||
href={setupToken ? `/setup/${setupToken}/sso-connection/new` : `/admin/sso-connection/new`}
|
||||
data-test-id='create-connection'>
|
||||
<LinkPrimary Icon={PlusIcon} href={createConnectionUrl} data-test-id='create-connection'>
|
||||
{t('new_connection')}
|
||||
</LinkPrimary>
|
||||
{!setupToken && (
|
||||
{!setupLinkToken && (
|
||||
<LinkPrimary
|
||||
Icon={LinkIcon}
|
||||
href='/admin/sso-connection/setup-link/new'
|
||||
|
@ -62,7 +75,7 @@ const ConnectionList = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{idpEntityID && setupToken && (
|
||||
{idpEntityID && setupLinkToken && (
|
||||
<div className='mb-5 mt-5 items-center justify-between'>
|
||||
<div className='form-control'>
|
||||
<InputWithCopyButton text={idpEntityID} label={t('idp_entity_id')} />
|
||||
|
@ -70,10 +83,7 @@ const ConnectionList = ({
|
|||
</div>
|
||||
)}
|
||||
{connections.length === 0 && paginate.offset === 0 ? (
|
||||
<EmptyState
|
||||
title={t('no_connections_found')}
|
||||
href={setupToken ? `/setup/${setupToken}/sso-connection/new` : `/admin/sso-connection/new`}
|
||||
/>
|
||||
<EmptyState title={t('no_connections_found')} href={createConnectionUrl} />
|
||||
) : (
|
||||
<>
|
||||
<div className='rounder border'>
|
||||
|
@ -137,8 +147,8 @@ const ConnectionList = ({
|
|||
className='hover:text-green-200'
|
||||
onClick={() => {
|
||||
router.push(
|
||||
setupToken
|
||||
? `/setup/${setupToken}/sso-connection/edit/${connection.clientID}`
|
||||
setupLinkToken
|
||||
? `/setup/${setupLinkToken}/sso-connection/edit/${connection.clientID}`
|
||||
: `/admin/sso-connection/edit/${connection.clientID}`
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -12,41 +12,57 @@ import { InputWithCopyButton } from '@components/ClipboardButton';
|
|||
|
||||
const fieldCatalog = [...getCommonFields()];
|
||||
|
||||
type AddProps = {
|
||||
setupToken?: string;
|
||||
const CreateConnection = ({
|
||||
setupLinkToken,
|
||||
idpEntityID,
|
||||
}: {
|
||||
setupLinkToken?: string;
|
||||
idpEntityID?: string;
|
||||
};
|
||||
|
||||
const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// STATE: New connection type
|
||||
const [newConnectionType, setNewConnectionType] = useState<'saml' | 'oidc'>('saml');
|
||||
|
||||
const handleNewConnectionTypeChange = (event) => {
|
||||
setNewConnectionType(event.target.value);
|
||||
};
|
||||
|
||||
const connectionIsSAML = newConnectionType === 'saml';
|
||||
const connectionIsOIDC = newConnectionType === 'oidc';
|
||||
|
||||
const backUrl = setupLinkToken ? `/setup/${setupLinkToken}` : '/admin/sso-connection';
|
||||
const redirectUrl = setupLinkToken ? `/setup/${setupLinkToken}/sso-connection` : '/admin/sso-connection';
|
||||
const mutationUrl = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/sso-connection`
|
||||
: '/api/admin/connections';
|
||||
|
||||
// FORM LOGIC: SUBMIT
|
||||
const save = async (event) => {
|
||||
const save = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
saveConnection({
|
||||
|
||||
setLoading(true);
|
||||
|
||||
await saveConnection({
|
||||
formObj: formObj,
|
||||
connectionIsSAML: connectionIsSAML,
|
||||
connectionIsOIDC: connectionIsOIDC,
|
||||
setupToken,
|
||||
callback: async (res) => {
|
||||
const response: ApiResponse = await res.json();
|
||||
setupLinkToken,
|
||||
callback: async (rawResponse) => {
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
await mutate(setupToken ? `/api/setup/${setupToken}/connections` : '/api/admin/connections');
|
||||
router.replace(setupToken ? `/setup/${setupToken}/sso-connection` : '/admin/sso-connection');
|
||||
if (rawResponse.ok) {
|
||||
await mutate(mutationUrl);
|
||||
router.replace(redirectUrl);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -57,8 +73,8 @@ const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={setupToken ? `/setup/${setupToken}` : '/admin/sso-connection'} />
|
||||
{idpEntityID && setupToken && (
|
||||
<LinkBack href={backUrl} />
|
||||
{idpEntityID && setupLinkToken && (
|
||||
<div className='mb-5 mt-5 items-center justify-between'>
|
||||
<div className='form-control'>
|
||||
<InputWithCopyButton text={idpEntityID} label={t('idp_entity_id')} />
|
||||
|
@ -80,7 +96,8 @@ const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
|||
className='peer sr-only'
|
||||
checked={newConnectionType === 'saml'}
|
||||
onChange={handleNewConnectionTypeChange}
|
||||
id='saml-conn'></input>
|
||||
id='saml-conn'
|
||||
/>
|
||||
<label
|
||||
htmlFor='saml-conn'
|
||||
className='cursor-pointer rounded-md border-2 border-solid py-3 px-8 font-semibold hover:shadow-md peer-checked:border-secondary-focus peer-checked:bg-secondary peer-checked:text-white'>
|
||||
|
@ -95,7 +112,8 @@ const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
|||
className='peer sr-only'
|
||||
checked={newConnectionType === 'oidc'}
|
||||
onChange={handleNewConnectionTypeChange}
|
||||
id='oidc-conn'></input>
|
||||
id='oidc-conn'
|
||||
/>
|
||||
<label
|
||||
htmlFor='oidc-conn'
|
||||
className='cursor-pointer rounded-md border-2 border-solid px-8 py-3 font-semibold hover:shadow-md peer-checked:bg-secondary peer-checked:text-white'>
|
||||
|
@ -108,10 +126,10 @@ const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
|||
<div className='min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
{fieldCatalog
|
||||
.filter(fieldCatalogFilterByConnection(newConnectionType))
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupToken ? !hideInSetupView : true))
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true))
|
||||
.map(renderFieldList({ formObj, setFormObj }))}
|
||||
<div className='flex'>
|
||||
<ButtonPrimary type='submit'>{t('save_changes')}</ButtonPrimary>
|
||||
<ButtonPrimary loading={loading}>{t('save_changes')}</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -120,4 +138,4 @@ const Add = ({ setupToken, idpEntityID }: AddProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Add;
|
||||
export default CreateConnection;
|
|
@ -36,10 +36,10 @@ function getInitialState(connection) {
|
|||
|
||||
type EditProps = {
|
||||
connection?: Record<string, any>;
|
||||
setupToken?: string;
|
||||
setupLinkToken?: string;
|
||||
};
|
||||
|
||||
const Edit = ({ connection, setupToken }: EditProps) => {
|
||||
const EditConnection = ({ connection, setupLinkToken }: EditProps) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
|
@ -54,7 +54,7 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
connectionIsSAML: connectionIsSAML,
|
||||
connectionIsOIDC: connectionIsOIDC,
|
||||
isEditView: true,
|
||||
setupToken: setupToken,
|
||||
setupLinkToken,
|
||||
callback: async (res) => {
|
||||
const response: ApiResponse = await res.json();
|
||||
|
||||
|
@ -67,8 +67,8 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
successToast(t('saved'));
|
||||
// revalidate on save
|
||||
mutate(
|
||||
setupToken
|
||||
? `/api/setup/${setupToken}/connections`
|
||||
setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/connections`
|
||||
: `/api/admin/connections/${connectionClientId}`
|
||||
);
|
||||
}
|
||||
|
@ -80,13 +80,16 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
const toggleDelConfirm = () => setDelModalVisible(!delModalVisible);
|
||||
const deleteConnection = async () => {
|
||||
const res = await fetch(setupToken ? `/api/setup/${setupToken}/connections` : '/api/admin/connections', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ clientID: connection?.clientID, clientSecret: connection?.clientSecret }),
|
||||
});
|
||||
const res = await fetch(
|
||||
setupLinkToken ? `/api/setup/${setupLinkToken}/connections` : '/api/admin/connections',
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ clientID: connection?.clientID, clientSecret: connection?.clientSecret }),
|
||||
}
|
||||
);
|
||||
|
||||
const response: ApiResponse = await res.json();
|
||||
|
||||
|
@ -98,8 +101,8 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
}
|
||||
|
||||
if (res.ok) {
|
||||
await mutate(setupToken ? `/api/setup/${setupToken}/connections` : '/api/admin/connections');
|
||||
router.replace(setupToken ? `/setup/${setupToken}/sso-connection` : '/admin/sso-connection');
|
||||
await mutate(setupLinkToken ? `/api/setup/${setupLinkToken}/connections` : '/api/admin/connections');
|
||||
router.replace(setupLinkToken ? `/setup/${setupLinkToken}/sso-connection` : '/admin/sso-connection');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -117,7 +120,7 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={setupToken ? `/setup/${setupToken}` : '/admin/sso-connection'} />
|
||||
<LinkBack href={setupLinkToken ? `/setup/${setupLinkToken}` : '/admin/sso-connection'} />
|
||||
<div>
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||
{t('edit_sso_connection')}
|
||||
|
@ -128,13 +131,13 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
<div className='w-full rounded border-gray-200 dark:border-gray-700 lg:w-3/5 lg:border lg:p-3'>
|
||||
{filteredFieldsByConnection
|
||||
.filter((field) => field.attributes.editable !== false)
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupToken ? !hideInSetupView : true))
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true))
|
||||
.map(renderFieldList({ isEditView: true, formObj, setFormObj }))}
|
||||
</div>
|
||||
<div className='w-full rounded border-gray-200 dark:border-gray-700 lg:w-2/5 lg:border lg:p-3'>
|
||||
{filteredFieldsByConnection
|
||||
.filter((field) => field.attributes.editable === false)
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupToken ? !hideInSetupView : true))
|
||||
.filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true))
|
||||
.map(renderFieldList({ isEditView: true, formObj, setFormObj }))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -165,4 +168,4 @@ const Edit = ({ connection, setupToken }: EditProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
export default EditConnection;
|
|
@ -6,14 +6,14 @@ export const saveConnection = async ({
|
|||
isEditView,
|
||||
connectionIsSAML,
|
||||
connectionIsOIDC,
|
||||
setupToken,
|
||||
setupLinkToken,
|
||||
callback,
|
||||
}: {
|
||||
formObj: Record<string, string>;
|
||||
isEditView?: boolean;
|
||||
connectionIsSAML: boolean;
|
||||
connectionIsOIDC: boolean;
|
||||
setupToken?: string;
|
||||
setupLinkToken?: string;
|
||||
callback: (res: Response) => void;
|
||||
}) => {
|
||||
const { rawMetadata, redirectUrl, oidcDiscoveryUrl, oidcClientId, oidcClientSecret, metadataUrl, ...rest } =
|
||||
|
@ -26,21 +26,24 @@ export const saveConnection = async ({
|
|||
const encodedRawMetadata = btoa(rawMetadata || '');
|
||||
const redirectUrlList = redirectUrl.split(/\r\n|\r|\n/);
|
||||
|
||||
const res = await fetch(setupToken ? `/api/setup/${setupToken}/connections` : '/api/admin/connections', {
|
||||
method: isEditView ? 'PATCH' : 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...rest,
|
||||
encodedRawMetadata: connectionIsSAML ? encodedRawMetadata : undefined,
|
||||
oidcDiscoveryUrl: connectionIsOIDC ? oidcDiscoveryUrl : undefined,
|
||||
oidcClientId: connectionIsOIDC ? oidcClientId : undefined,
|
||||
oidcClientSecret: connectionIsOIDC ? oidcClientSecret : undefined,
|
||||
redirectUrl: JSON.stringify(redirectUrlList),
|
||||
metadataUrl: connectionIsSAML ? metadataUrl : undefined,
|
||||
}),
|
||||
});
|
||||
const res = await fetch(
|
||||
setupLinkToken ? `/api/setup/${setupLinkToken}/sso-connection` : '/api/admin/connections',
|
||||
{
|
||||
method: isEditView ? 'PATCH' : 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...rest,
|
||||
encodedRawMetadata: connectionIsSAML ? encodedRawMetadata : undefined,
|
||||
oidcDiscoveryUrl: connectionIsOIDC ? oidcDiscoveryUrl : undefined,
|
||||
oidcClientId: connectionIsOIDC ? oidcClientId : undefined,
|
||||
oidcClientSecret: connectionIsOIDC ? oidcClientSecret : undefined,
|
||||
redirectUrl: JSON.stringify(redirectUrlList),
|
||||
metadataUrl: connectionIsSAML ? metadataUrl : undefined,
|
||||
}),
|
||||
}
|
||||
);
|
||||
callback(res);
|
||||
};
|
||||
export function fieldCatalogFilterByConnection(connection) {
|
||||
|
|
|
@ -8,14 +8,10 @@ import { LinkBack } from '@components/LinkBack';
|
|||
import { ButtonPrimary } from '@components/ButtonPrimary';
|
||||
import useDirectoryProviders from '@lib/ui/hooks/useDirectoryProviders';
|
||||
|
||||
type CreateDirectoryProps = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
const CreateDirectory = ({ token }: CreateDirectoryProps) => {
|
||||
const CreateDirectory = ({ setupLinkToken }: { setupLinkToken?: string }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
const { providers } = useDirectoryProviders();
|
||||
const { providers } = useDirectoryProviders(setupLinkToken);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [directory, setDirectory] = useState({
|
||||
name: '',
|
||||
|
@ -32,7 +28,7 @@ const CreateDirectory = ({ token }: CreateDirectoryProps) => {
|
|||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(
|
||||
token ? `/api/setup/${token}/directory-sync` : '/api/admin/directory-sync',
|
||||
setupLinkToken ? `/api/setup/${setupLinkToken}/directory-sync` : '/api/admin/directory-sync',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -53,8 +49,8 @@ const CreateDirectory = ({ token }: CreateDirectoryProps) => {
|
|||
|
||||
if (rawResponse.ok) {
|
||||
router.replace(
|
||||
token
|
||||
? `/setup/${token}/directory-sync/${response.data.id}`
|
||||
setupLinkToken
|
||||
? `/setup/${setupLinkToken}/directory-sync/${response.data.id}`
|
||||
: `/admin/directory-sync/${response.data.id}`
|
||||
);
|
||||
successToast(t('directory_created_successfully'));
|
||||
|
@ -71,7 +67,7 @@ const CreateDirectory = ({ token }: CreateDirectoryProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const backUrl = token ? `/setup/${token}/directory-sync` : '/admin/directory-sync';
|
||||
const backUrl = setupLinkToken ? `/setup/${setupLinkToken}/directory-sync` : '/admin/directory-sync';
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -107,7 +103,7 @@ const CreateDirectory = ({ token }: CreateDirectoryProps) => {
|
|||
})}
|
||||
</select>
|
||||
</div>
|
||||
{!token && (
|
||||
{!setupLinkToken && (
|
||||
<>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
|
|
|
@ -1,27 +1,40 @@
|
|||
import { Directory } from '@lib/jackson';
|
||||
import DirectoryTab from './DirectoryTab';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { InputWithCopyButton } from '@components/ClipboardButton';
|
||||
import { LinkBack } from '@components/LinkBack';
|
||||
import React from 'react';
|
||||
import useDirectory from '@lib/ui/hooks/useDirectory';
|
||||
import Loading from '@components/Loading';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
|
||||
type DirectoryInfoProps = {
|
||||
directory: Directory;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
const DirectoryInfo = ({ directory, token }: DirectoryInfoProps) => {
|
||||
const DirectoryInfo = ({ directoryId, setupLinkToken }: { directoryId: string; setupLinkToken?: string }) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const { directory, isLoading, error } = useDirectory(directoryId, setupLinkToken);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!directory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayWebhook = directory.webhook.endpoint && directory.webhook.secret;
|
||||
const displayTenantProduct = token ? false : true;
|
||||
const backUrl = token ? `/setup/${token}/directory-sync` : '/admin/directory-sync';
|
||||
const displayTenantProduct = setupLinkToken ? false : true;
|
||||
const backUrl = setupLinkToken ? `/setup/${setupLinkToken}/directory-sync` : '/admin/directory-sync';
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={backUrl} />
|
||||
<h2 className='mt-5 font-bold text-gray-700 md:text-xl'>{directory.name}</h2>
|
||||
<div className='w-full md:w-3/4'>
|
||||
<DirectoryTab directory={directory} activeTab='directory' token={token} />
|
||||
<DirectoryTab directory={directory} activeTab='directory' token={setupLinkToken} />
|
||||
<div className='my-3 rounded border'>
|
||||
<dl className='divide-y'>
|
||||
<div className='px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6'>
|
||||
|
|
|
@ -10,44 +10,61 @@ import { IconButton } from '@components/IconButton';
|
|||
import { useRouter } from 'next/router';
|
||||
import { pageLimit, Pagination } from '@components/Pagination';
|
||||
import useDirectoryProviders from '@lib/ui/hooks/useDirectoryProviders';
|
||||
import useSWR from 'swr';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import Loading from '@components/Loading';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
|
||||
type DirectoryListProps = {
|
||||
directories: Directory[];
|
||||
token?: string;
|
||||
paginate: {
|
||||
offset: number;
|
||||
};
|
||||
setPaginate: (paginate: { offset: number }) => void;
|
||||
};
|
||||
|
||||
const DirectoryList = ({ directories, token, paginate, setPaginate }: DirectoryListProps) => {
|
||||
const DirectoryList = ({ setupLinkToken }: { setupLinkToken?: string }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
const router = useRouter();
|
||||
|
||||
const { providers } = useDirectoryProviders();
|
||||
const displayTenantProduct = setupLinkToken ? false : true;
|
||||
const getDirectoriesUrl = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/directory-sync`
|
||||
: '/api/admin/directory-sync';
|
||||
const createDirectoryUrl = setupLinkToken
|
||||
? `/setup/${setupLinkToken}/directory-sync/new`
|
||||
: '/admin/directory-sync/new';
|
||||
|
||||
const { providers, isLoading: isLoadingProviders } = useDirectoryProviders(setupLinkToken);
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<Directory[]>, ApiError>(
|
||||
`${getDirectoriesUrl}?offset=${paginate.offset}&limit=${pageLimit}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
if (!data || isLoadingProviders) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const directories = data.data || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('directory_sync')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<LinkPrimary
|
||||
Icon={PlusIcon}
|
||||
href={token ? `/setup/${token}/directory-sync/new` : '/admin/directory-sync/new'}>
|
||||
<LinkPrimary Icon={PlusIcon} href={createDirectoryUrl}>
|
||||
{t('new_directory')}
|
||||
</LinkPrimary>
|
||||
{!token && (
|
||||
<LinkPrimary Icon={LinkIcon} href={`/admin/directory-sync/setup-link/new`}>
|
||||
{!setupLinkToken && (
|
||||
<LinkPrimary Icon={LinkIcon} href='/admin/directory-sync/setup-link/new'>
|
||||
{t('new_setup_link')}
|
||||
</LinkPrimary>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{directories?.length === 0 && paginate.offset === 0 ? (
|
||||
<EmptyState
|
||||
title={t('no_directories_found')}
|
||||
href={token ? `/setup/${token}/directory-sync/new` : '/admin/directory-sync/new'}
|
||||
/>
|
||||
{directories.length === 0 && paginate.offset === 0 ? (
|
||||
<EmptyState title={t('no_directories_found')} href={createDirectoryUrl} />
|
||||
) : (
|
||||
<>
|
||||
<div className='rounder border'>
|
||||
|
@ -57,7 +74,7 @@ const DirectoryList = ({ directories, token, paginate, setPaginate }: DirectoryL
|
|||
<th scope='col' className='px-6 py-3'>
|
||||
{t('name')}
|
||||
</th>
|
||||
{!token && (
|
||||
{displayTenantProduct && (
|
||||
<>
|
||||
<th scope='col' className='px-6 py-3'>
|
||||
{t('tenant')}
|
||||
|
@ -85,7 +102,7 @@ const DirectoryList = ({ directories, token, paginate, setPaginate }: DirectoryL
|
|||
<td className='whitespace-nowrap px-6 py-3 text-sm text-gray-500 dark:text-gray-400'>
|
||||
{directory.name}
|
||||
</td>
|
||||
{!token && (
|
||||
{displayTenantProduct && (
|
||||
<>
|
||||
<td className='px-6'>{directory.tenant}</td>
|
||||
<td className='px-6'>{directory.product}</td>
|
||||
|
@ -100,8 +117,8 @@ const DirectoryList = ({ directories, token, paginate, setPaginate }: DirectoryL
|
|||
className='mr-3 hover:text-green-200'
|
||||
onClick={() => {
|
||||
router.push(
|
||||
token
|
||||
? `/setup/${token}/directory-sync/${directory.id}`
|
||||
setupLinkToken
|
||||
? `/setup/${setupLinkToken}/directory-sync/${directory.id}`
|
||||
: `/admin/directory-sync/${directory.id}`
|
||||
);
|
||||
}}
|
||||
|
@ -112,8 +129,8 @@ const DirectoryList = ({ directories, token, paginate, setPaginate }: DirectoryL
|
|||
className='hover:text-green-200'
|
||||
onClick={() => {
|
||||
router.push(
|
||||
token
|
||||
? `/setup/${token}/directory-sync/${directory.id}/edit`
|
||||
setupLinkToken
|
||||
? `/setup/${setupLinkToken}/directory-sync/${directory.id}/edit`
|
||||
: `/admin/directory-sync/${directory.id}/edit`
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
import type { ApiResponse } from 'types';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { LinkBack } from '@components/LinkBack';
|
||||
import { ButtonPrimary } from '@components/ButtonPrimary';
|
||||
import Loading from '@components/Loading';
|
||||
import useDirectory from '@lib/ui/hooks/useDirectory';
|
||||
|
||||
type FormState = Pick<Directory, 'name' | 'log_webhook_events'> & {
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
};
|
||||
|
||||
const defaultFormState: FormState = {
|
||||
name: '',
|
||||
webhook_url: '',
|
||||
webhook_secret: '',
|
||||
log_webhook_events: false,
|
||||
};
|
||||
|
||||
const EditDirectory = ({ directoryId, setupLinkToken }: { directoryId: string; setupLinkToken?: string }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const [directory, setDirectory] = React.useState<FormState>(defaultFormState);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const { directory: data, isLoading, error } = useDirectory(directoryId, setupLinkToken);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
const directory = data;
|
||||
|
||||
setDirectory({
|
||||
name: directory.name,
|
||||
webhook_url: directory.webhook.endpoint,
|
||||
webhook_secret: directory.webhook.secret,
|
||||
log_webhook_events: directory.log_webhook_events,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const backUrl = setupLinkToken ? `/setup/${setupLinkToken}/directory-sync` : '/admin/directory-sync';
|
||||
const putUrl = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/directory-sync/${directoryId}`
|
||||
: `/api/admin/directory-sync/${directoryId}`;
|
||||
const redirectUrl = setupLinkToken ? `/setup/${setupLinkToken}/directory-sync` : '/admin/directory-sync';
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(directory),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<Directory> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawResponse.ok) {
|
||||
successToast(t('directory_updated_successfully'));
|
||||
router.replace(redirectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
|
||||
setDirectory({
|
||||
...directory,
|
||||
[target.id]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LinkBack href={backUrl} />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>{t('update_directory')}</h2>
|
||||
<div className='min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800 md:w-3/4 md:max-w-lg'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('directory_name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input w-full'
|
||||
required
|
||||
onChange={onChange}
|
||||
value={directory.name}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('webhook_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_url'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_url}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('webhook_secret')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_secret'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_secret}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full py-2'>
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
id='log_webhook_events'
|
||||
type='checkbox'
|
||||
checked={directory.log_webhook_events}
|
||||
onChange={onChange}
|
||||
className='h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600'
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('enable_webhook_events_logging')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('save_changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditDirectory;
|
|
@ -0,0 +1,184 @@
|
|||
import { FormEvent, useState } from 'react';
|
||||
import ConfirmationModal from '@components/ConfirmationModal';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { ButtonPrimary } from '../ButtonPrimary';
|
||||
import { LinkBack } from '../LinkBack';
|
||||
import { InputWithCopyButton } from '../ClipboardButton';
|
||||
import type { SetupLinkService, SetupLink } from '@boxyhq/saml-jackson';
|
||||
import type { ApiResponse } from 'types';
|
||||
|
||||
const CreateSetupLink = ({ service }: { service: SetupLinkService }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading1, setLoading1] = useState(false);
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const [setupLink, setSetupLink] = useState<SetupLink | null>(null);
|
||||
const [formObj, setFormObj] = useState({
|
||||
tenant: '',
|
||||
product: '',
|
||||
service,
|
||||
});
|
||||
|
||||
// Create a new setup link
|
||||
const createSetupLink = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch('/api/admin/setup-links', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formObj),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<SetupLink> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawResponse.ok) {
|
||||
setSetupLink(response.data);
|
||||
successToast(t('link_generated'));
|
||||
}
|
||||
};
|
||||
|
||||
// Regenerate setup link
|
||||
const regenerateSetupLink = async () => {
|
||||
setLoading1(true);
|
||||
setDelModalVisible(!delModalVisible);
|
||||
|
||||
const res = await fetch('/api/admin/setup-links', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...formObj,
|
||||
regenerate: true,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setLoading1(false);
|
||||
const json = await res.json();
|
||||
setSetupLink(json.data);
|
||||
successToast(t('link_regenerated'));
|
||||
} else {
|
||||
setLoading1(false);
|
||||
errorToast(t('server_error'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: FormEvent) => {
|
||||
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
||||
setFormObj((cur) => ({ ...cur, [target.name]: target.value }));
|
||||
};
|
||||
|
||||
const toggleDelConfirm = () => setDelModalVisible(!delModalVisible);
|
||||
|
||||
const buttonDisabled = !formObj.tenant || !formObj.product || !formObj.service;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack onClick={() => router.back()} />
|
||||
<div className='mt-5 min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<h2 className='mb-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||
{t('create_setup_link', {
|
||||
service: service === 'sso' ? t('enterprise_sso') : t('directory_sync'),
|
||||
})}
|
||||
</h2>
|
||||
<form onSubmit={createSetupLink} method='POST'>
|
||||
<div>
|
||||
<div className='mb-6'>
|
||||
<label
|
||||
htmlFor='tenant'
|
||||
className={`mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300`}>
|
||||
{t('tenant')}
|
||||
</label>
|
||||
<input
|
||||
id='tenant'
|
||||
name='tenant'
|
||||
type='text'
|
||||
placeholder='acme.com'
|
||||
value={formObj['tenant']}
|
||||
onChange={handleChange}
|
||||
className='input-bordered input w-full'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-6'>
|
||||
<label
|
||||
htmlFor='tenant'
|
||||
className={`mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300`}>
|
||||
{t('product')}
|
||||
</label>
|
||||
<input
|
||||
id='product'
|
||||
name='product'
|
||||
type='text'
|
||||
placeholder='demo'
|
||||
value={formObj['product']}
|
||||
onChange={handleChange}
|
||||
className='input-bordered input w-full'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<ButtonPrimary loading={loading} disabled={buttonDisabled}>
|
||||
{t('generate')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ConfirmationModal
|
||||
title='Delete the setup link'
|
||||
description='This action cannot be undone. This will permanently delete the link.'
|
||||
visible={delModalVisible}
|
||||
onConfirm={regenerateSetupLink}
|
||||
onCancel={toggleDelConfirm}
|
||||
actionButtonText={t('regenerate')}
|
||||
/>
|
||||
</div>
|
||||
{setupLink && (
|
||||
<div className='mt-5 min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<h2 className='mb-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||
{setupLink
|
||||
? t('setup_link_info')
|
||||
: t('create_setup_link', {
|
||||
service: service === 'sso' ? t('enterprise_sso') : t('directory_sync'),
|
||||
})}
|
||||
</h2>
|
||||
<div className='form-control'>
|
||||
<InputWithCopyButton text={setupLink.url} label={t('setup_link_url')} />
|
||||
</div>
|
||||
<div className='mt-5 flex'>
|
||||
<ButtonPrimary
|
||||
loading={loading1}
|
||||
disabled={buttonDisabled}
|
||||
onClick={
|
||||
setupLink
|
||||
? () => {
|
||||
setDelModalVisible(true);
|
||||
}
|
||||
: createSetupLink
|
||||
}>
|
||||
{setupLink ? t('regenerate') : t('generate')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSetupLink;
|
|
@ -5,7 +5,7 @@ import { checkSession } from '@lib/middleware';
|
|||
import type { SAMLFederationApp } from '@boxyhq/saml-jackson';
|
||||
import { strings } from '@lib/strings';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
if (!(await checkLicense())) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { checkSession } from '@lib/middleware';
|
|||
import jackson from '@lib/jackson';
|
||||
import { strings } from '@lib/strings';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
if (!(await checkLicense())) {
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import type {
|
||||
IAdminController,
|
||||
IConnectionAPIController,
|
||||
SAMLSSOConnectionWithEncodedMetadata,
|
||||
SAMLSSOConnectionWithRawMetadata,
|
||||
OIDCSSOConnection,
|
||||
ILogoutController,
|
||||
IOAuthController,
|
||||
IHealthCheckController,
|
||||
ISetupLinkController,
|
||||
IDirectorySyncController,
|
||||
DirectoryType,
|
||||
Directory,
|
||||
User,
|
||||
Group,
|
||||
DirectorySyncEvent,
|
||||
HTTPMethod,
|
||||
DirectorySyncRequest,
|
||||
IOidcDiscoveryController,
|
||||
ISPSAMLConfig,
|
||||
ISAMLFederationController,
|
||||
GetConnectionsQuery,
|
||||
GetIDPEntityIDBody,
|
||||
GetConfigQuery,
|
||||
} from '@boxyhq/saml-jackson';
|
||||
|
||||
import jackson from '@boxyhq/saml-jackson';
|
||||
|
@ -108,19 +95,3 @@ export default async function init() {
|
|||
checkLicense,
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
SAMLSSOConnectionWithEncodedMetadata,
|
||||
SAMLSSOConnectionWithRawMetadata,
|
||||
OIDCSSOConnection,
|
||||
DirectoryType,
|
||||
Directory,
|
||||
User,
|
||||
Group,
|
||||
DirectorySyncEvent,
|
||||
HTTPMethod,
|
||||
DirectorySyncRequest,
|
||||
GetConnectionsQuery,
|
||||
GetIDPEntityIDBody,
|
||||
GetConfigQuery,
|
||||
};
|
||||
|
|
|
@ -3,8 +3,10 @@ import type { Directory } from '@boxyhq/saml-jackson';
|
|||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
|
||||
const useDirectory = (directoryId: string) => {
|
||||
const url = `/api/admin/directory-sync/${directoryId}`;
|
||||
const useDirectory = (directoryId: string, setupLinkToken?: string) => {
|
||||
const url = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/directory-sync/${directoryId}`
|
||||
: `/api/admin/directory-sync/${directoryId}`;
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<Directory>, ApiError>(url, fetcher);
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@ import type { DirectorySyncProviders } from '@boxyhq/saml-jackson';
|
|||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
|
||||
const useDirectoryProviders = () => {
|
||||
const url = '/api/admin/directory-sync/providers';
|
||||
const useDirectoryProviders = (setupLinkToken?: string) => {
|
||||
const url = setupLinkToken
|
||||
? `/api/setup/${setupLinkToken}/directory-sync/providers`
|
||||
: '/api/admin/directory-sync/providers';
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<DirectorySyncProviders>, ApiError>(url, fetcher);
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import useSWR from 'swr';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
|
||||
const useIdpEntityID = (setupLinkToken: string) => {
|
||||
const url = setupLinkToken ? `/api/setup/${setupLinkToken}/sso-connection/idp-entityid` : null;
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<{ idpEntityID: string }>, ApiError>(url, fetcher);
|
||||
|
||||
return {
|
||||
idpEntityID: data?.data.idpEntityID,
|
||||
isLoading: !data && !error,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIdpEntityID;
|
|
@ -1,22 +1,35 @@
|
|||
import { ApiResponse, ISetupLinkController, SetupLink, SetupLinkCreatePayload, Storable } from '../typings';
|
||||
import { SetupLink, SetupLinkCreatePayload, Storable } from '../typings';
|
||||
import * as dbutils from '../db/utils';
|
||||
import { IndexNames, validateTenantAndProduct } from './utils';
|
||||
import crypto from 'crypto';
|
||||
import { JacksonError } from './error';
|
||||
|
||||
export class SetupLinkController implements ISetupLinkController {
|
||||
export class SetupLinkController {
|
||||
setupLinkStore: Storable;
|
||||
|
||||
constructor({ setupLinkStore }) {
|
||||
this.setupLinkStore = setupLinkStore;
|
||||
}
|
||||
async create(body: SetupLinkCreatePayload): Promise<ApiResponse<SetupLink>> {
|
||||
|
||||
// Create a new setup link
|
||||
async create(body: SetupLinkCreatePayload): Promise<SetupLink> {
|
||||
const { tenant, product, service, regenerate } = body;
|
||||
|
||||
validateTenantAndProduct(tenant, product);
|
||||
|
||||
const setupID = dbutils.keyDigest(dbutils.keyFromParts(tenant, product, service));
|
||||
const token = crypto.randomBytes(24).toString('hex');
|
||||
const val = {
|
||||
|
||||
const existing: SetupLink[] = await this.setupLinkStore.getByIndex({
|
||||
name: IndexNames.TenantProductService,
|
||||
value: dbutils.keyFromParts(tenant, product, service),
|
||||
});
|
||||
|
||||
if (existing.length > 0 && !regenerate && !this.isExpired(existing[0])) {
|
||||
return existing[0];
|
||||
}
|
||||
|
||||
const setupLink = {
|
||||
setupID,
|
||||
tenant,
|
||||
product,
|
||||
|
@ -24,91 +37,76 @@ export class SetupLinkController implements ISetupLinkController {
|
|||
validTill: +new Date(new Date().setDate(new Date().getDate() + 3)),
|
||||
url: `${process.env.NEXTAUTH_URL}/setup/${token}`,
|
||||
};
|
||||
const existing = await this.setupLinkStore.getByIndex({
|
||||
name: IndexNames.TenantProductService,
|
||||
value: dbutils.keyFromParts(tenant, product, service),
|
||||
});
|
||||
if (existing.length > 0 && !regenerate && existing[0].validTill > +new Date()) {
|
||||
return { data: existing[0], error: null };
|
||||
} else {
|
||||
await this.setupLinkStore.put(
|
||||
setupID,
|
||||
val,
|
||||
{
|
||||
name: IndexNames.SetupToken,
|
||||
value: token,
|
||||
},
|
||||
{
|
||||
name: IndexNames.TenantProductService,
|
||||
value: dbutils.keyFromParts(tenant, product, service),
|
||||
},
|
||||
{
|
||||
name: IndexNames.Service,
|
||||
value: service,
|
||||
}
|
||||
);
|
||||
return { data: val, error: null };
|
||||
}
|
||||
}
|
||||
async getByToken(token: string): Promise<ApiResponse<SetupLink>> {
|
||||
if (!token) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
message: 'Invalid setup token',
|
||||
code: 404,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const val = await this.setupLinkStore.getByIndex({
|
||||
|
||||
await this.setupLinkStore.put(
|
||||
setupID,
|
||||
setupLink,
|
||||
{
|
||||
name: IndexNames.SetupToken,
|
||||
value: token,
|
||||
});
|
||||
if (val.length === 0) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
message: 'Link not found!',
|
||||
code: 404,
|
||||
},
|
||||
};
|
||||
} else if (val.validTill < new Date()) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
message: 'Link is expired!',
|
||||
code: 401,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { data: val[0], error: null };
|
||||
},
|
||||
{
|
||||
name: IndexNames.TenantProductService,
|
||||
value: dbutils.keyFromParts(tenant, product, service),
|
||||
},
|
||||
{
|
||||
name: IndexNames.Service,
|
||||
value: service,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return setupLink;
|
||||
}
|
||||
async getByService(service: string): Promise<ApiResponse<SetupLink[]>> {
|
||||
if (!service) {
|
||||
return { data: [], error: null };
|
||||
|
||||
// Get a setup link by token
|
||||
async getByToken(token: string): Promise<SetupLink> {
|
||||
if (!token) {
|
||||
throw new JacksonError('Missing setup link token', 400);
|
||||
}
|
||||
const val = await this.setupLinkStore.getByIndex({
|
||||
|
||||
const setupLink: SetupLink[] = await this.setupLinkStore.getByIndex({
|
||||
name: IndexNames.SetupToken,
|
||||
value: token,
|
||||
});
|
||||
|
||||
if (!setupLink) {
|
||||
throw new JacksonError('Setup link not found', 404);
|
||||
}
|
||||
|
||||
if (this.isExpired(setupLink[0])) {
|
||||
throw new JacksonError('Setup link is expired', 401);
|
||||
}
|
||||
|
||||
return setupLink[0];
|
||||
}
|
||||
|
||||
// Get setup links by service
|
||||
async getByService(service: string): Promise<SetupLink[]> {
|
||||
if (!service) {
|
||||
throw new JacksonError('Missing service name', 400);
|
||||
}
|
||||
|
||||
const setupLink = await this.setupLinkStore.getByIndex({
|
||||
name: IndexNames.Service,
|
||||
value: service,
|
||||
});
|
||||
return { data: val, error: null };
|
||||
|
||||
return setupLink;
|
||||
}
|
||||
async remove(key: string): Promise<ApiResponse<boolean>> {
|
||||
|
||||
// Remove a setup link
|
||||
async remove(key: string): Promise<boolean> {
|
||||
if (!key) {
|
||||
return {
|
||||
data: false,
|
||||
error: {
|
||||
message: 'Invalid setup key sent!',
|
||||
code: 400,
|
||||
},
|
||||
};
|
||||
throw new JacksonError('Missing setup link key', 400);
|
||||
}
|
||||
|
||||
await this.setupLinkStore.delete(key);
|
||||
return { data: true, error: null };
|
||||
|
||||
return true;
|
||||
}
|
||||
getAll(): Promise<ApiResponse<SetupLink[]>> {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
// Check if a setup link is expired or not
|
||||
isExpired(setupLink: SetupLink): boolean {
|
||||
return setupLink.validTill < +new Date();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,3 +151,4 @@ export default controllers;
|
|||
export * from './typings';
|
||||
export * from './ee/federated-saml/types';
|
||||
export type SAMLJackson = Awaited<ReturnType<typeof controllers>>;
|
||||
export type ISetupLinkController = InstanceType<typeof SetupLinkController>;
|
||||
|
|
|
@ -681,7 +681,7 @@ export interface WebhookEventLog extends DirectorySyncEvent {
|
|||
export type SetupLinkCreatePayload = {
|
||||
tenant: string;
|
||||
product: string;
|
||||
service: 'sso' | 'dsync';
|
||||
service: SetupLinkService;
|
||||
regenerate?: boolean;
|
||||
};
|
||||
|
||||
|
@ -694,7 +694,7 @@ export type SetupLink = {
|
|||
tenant: string;
|
||||
product: string;
|
||||
url: string;
|
||||
service: string;
|
||||
service: SetupLinkService;
|
||||
validTill: number;
|
||||
};
|
||||
|
||||
|
@ -703,10 +703,4 @@ export type ApiResponse<T> = {
|
|||
error: ApiError | null;
|
||||
};
|
||||
|
||||
export interface ISetupLinkController {
|
||||
create(body: SetupLinkCreatePayload): Promise<ApiResponse<SetupLink>>;
|
||||
getAll(): Promise<ApiResponse<SetupLink[]>>;
|
||||
getByService(service): Promise<ApiResponse<SetupLink[]>>;
|
||||
getByToken(token): Promise<ApiResponse<SetupLink>>;
|
||||
remove(key: string): Promise<ApiResponse<boolean>>;
|
||||
}
|
||||
export type SetupLinkService = 'sso' | 'dsync';
|
||||
|
|
|
@ -1,168 +1,15 @@
|
|||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
import EditDirectory from '@components/dsync/EditDirectory';
|
||||
|
||||
import type { ApiResponse } from 'types';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { LinkBack } from '@components/LinkBack';
|
||||
import { ButtonPrimary } from '@components/ButtonPrimary';
|
||||
import Loading from '@components/Loading';
|
||||
import useDirectory from '@lib/ui/hooks/useDirectory';
|
||||
|
||||
type FormState = Pick<Directory, 'name' | 'log_webhook_events'> & {
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
};
|
||||
|
||||
const defaultFormState: FormState = {
|
||||
name: '',
|
||||
webhook_url: '',
|
||||
webhook_secret: '',
|
||||
log_webhook_events: false,
|
||||
};
|
||||
|
||||
const Edit: NextPage = () => {
|
||||
const { t } = useTranslation('common');
|
||||
const DirectoryEditPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const [directory, setDirectory] = React.useState<FormState>(defaultFormState);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const { directoryId } = router.query as { directoryId: string };
|
||||
|
||||
const { directory: data, isLoading, error } = useDirectory(directoryId);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
const directory = data;
|
||||
|
||||
setDirectory({
|
||||
name: directory.name,
|
||||
webhook_url: directory.webhook.endpoint,
|
||||
webhook_secret: directory.webhook.secret,
|
||||
log_webhook_events: directory.log_webhook_events,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(`/api/admin/directory-sync/${directoryId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(directory),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<Directory> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
errorToast(response.error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawResponse.ok) {
|
||||
successToast(t('directory_updated_successfully'));
|
||||
router.replace('/admin/directory-sync');
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
|
||||
setDirectory({
|
||||
...directory,
|
||||
[target.id]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LinkBack href='/admin/directory-sync' />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>{t('update_directory')}</h2>
|
||||
<div className='min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800 md:w-3/4 md:max-w-lg'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('directory_name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input w-full'
|
||||
required
|
||||
onChange={onChange}
|
||||
value={directory.name}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('webhook_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_url'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_url}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('webhook_secret')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_secret'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_secret}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full py-2'>
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
id='log_webhook_events'
|
||||
type='checkbox'
|
||||
checked={directory.log_webhook_events}
|
||||
onChange={onChange}
|
||||
className='h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600'
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('enable_webhook_events_logging')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('save_changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <EditDirectory directoryId={directoryId} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
|
@ -175,4 +22,4 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
};
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
export default DirectoryEditPage;
|
||||
|
|
|
@ -71,7 +71,7 @@ const Events: NextPage = () => {
|
|||
<h2 className='mt-5 font-bold text-gray-700 md:text-xl'>{directory.name}</h2>
|
||||
<div className='w-full md:w-3/4'>
|
||||
<DirectoryTab directory={directory} activeTab='events' />
|
||||
{events.length === 0 && paginate.offset ? (
|
||||
{events.length === 0 && paginate.offset === 0 ? (
|
||||
<EmptyState title={t('no_webhook_events_found')} />
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -1,33 +1,14 @@
|
|||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import DirectoryInfo from '@components/dsync/DirectoryInfo';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
import Loading from '@components/Loading';
|
||||
import useDirectory from '@lib/ui/hooks/useDirectory';
|
||||
|
||||
const Info: NextPage = () => {
|
||||
const DirectoryInfoPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { directoryId } = router.query as { directoryId: string };
|
||||
|
||||
const { directory, isLoading, error } = useDirectory(directoryId);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!directory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DirectoryInfo directory={directory} />;
|
||||
return <DirectoryInfo directoryId={directoryId} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
|
@ -40,4 +21,4 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
};
|
||||
};
|
||||
|
||||
export default Info;
|
||||
export default DirectoryInfoPage;
|
||||
|
|
|
@ -1,35 +1,9 @@
|
|||
import type { GetStaticPropsContext, NextPage } from 'next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import useSWR from 'swr';
|
||||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
import Loading from '@components/Loading';
|
||||
import DirectoryList from '@components/dsync/DirectoryList';
|
||||
import { pageLimit } from '@components/Pagination';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<Directory[]>, ApiError>(
|
||||
`/api/admin/directory-sync?offset=${paginate.offset}&limit=${pageLimit}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const directories = data.data || [];
|
||||
|
||||
return <DirectoryList directories={directories} paginate={paginate} setPaginate={setPaginate} />;
|
||||
const DirectoryIndexPage: NextPage = () => {
|
||||
return <DirectoryList />;
|
||||
};
|
||||
|
||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
|
@ -42,4 +16,4 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default DirectoryIndexPage;
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import CreateDirectory from '@components/dsync/CreateDirectory';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
const New: NextPage = () => {
|
||||
const DirectoryCreatePage: NextPage = () => {
|
||||
return <CreateDirectory />;
|
||||
};
|
||||
|
||||
|
@ -17,4 +17,4 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default New;
|
||||
export default DirectoryCreatePage;
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import type { GetServerSidePropsContext, NextPage } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import CreateSetupLink from '@components/CreateSetupLink';
|
||||
import CreateSetupLink from '@components/setup-link/CreateSetupLink';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import type { SetupLinkService } from '@boxyhq/saml-jackson';
|
||||
|
||||
type Service = 'sso' | 'dsync';
|
||||
const serviceMaps = {
|
||||
'sso-connection': 'sso',
|
||||
'directory-sync': 'dsync',
|
||||
};
|
||||
|
||||
const SetupLink: NextPage = () => {
|
||||
const SetupLinkCreatePage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const service = router.asPath.includes('sso-connection')
|
||||
? 'sso'
|
||||
: router.asPath.includes('directory-sync')
|
||||
? 'dsync'
|
||||
: '';
|
||||
|
||||
// Extract the service name from the path
|
||||
const serviceName = router.asPath.split('/')[2];
|
||||
|
||||
const service = serviceMaps[serviceName] as SetupLinkService;
|
||||
|
||||
if (!service) {
|
||||
return null;
|
||||
}
|
||||
return <CreateSetupLink service={service as Service} />;
|
||||
|
||||
return <CreateSetupLink service={service} />;
|
||||
};
|
||||
|
||||
export async function getServerSideProps({ locale }: GetServerSidePropsContext) {
|
||||
|
@ -26,4 +32,4 @@ export async function getServerSideProps({ locale }: GetServerSidePropsContext)
|
|||
};
|
||||
}
|
||||
|
||||
export default SetupLink;
|
||||
export default SetupLinkCreatePage;
|
||||
|
|
|
@ -3,14 +3,14 @@ import useSWR from 'swr';
|
|||
import { useRouter } from 'next/router';
|
||||
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import Edit from '@components/connection/Edit';
|
||||
import EditConnection from '@components/connection/EditConnection';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import Loading from '@components/Loading';
|
||||
import { OIDCSSORecord, SAMLSSORecord } from '@boxyhq/saml-jackson';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
|
||||
const EditConnection: NextPage = () => {
|
||||
const ConnectionEditPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { id } = router.query as { id: string };
|
||||
|
@ -32,7 +32,7 @@ const EditConnection: NextPage = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
return <Edit connection={data?.data} />;
|
||||
return <EditConnection connection={data?.data} />;
|
||||
};
|
||||
|
||||
export async function getServerSideProps({ locale }: GetServerSidePropsContext) {
|
||||
|
@ -43,4 +43,4 @@ export async function getServerSideProps({ locale }: GetServerSidePropsContext)
|
|||
};
|
||||
}
|
||||
|
||||
export default EditConnection;
|
||||
export default ConnectionEditPage;
|
||||
|
|
|
@ -1,36 +1,9 @@
|
|||
import type { GetServerSidePropsContext, NextPage } from 'next';
|
||||
import ConnectionList from '@components/connection/ConnectionList';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import { pageLimit } from '@components/Pagination';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
import Loading from '@components/Loading';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
import type { SAMLSSORecord, OIDCSSORecord } from '@boxyhq/saml-jackson';
|
||||
|
||||
const Connections: NextPage = () => {
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<(SAMLSSORecord | OIDCSSORecord)[]>, ApiError>(
|
||||
[`/api/admin/connections`, `?pageOffset=${paginate.offset}&pageLimit=${pageLimit}`],
|
||||
fetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const connections = data.data || [];
|
||||
|
||||
return <ConnectionList connections={connections} paginate={paginate} setPaginate={setPaginate} />;
|
||||
const ConnectionsIndexPage: NextPage = () => {
|
||||
return <ConnectionList />;
|
||||
};
|
||||
|
||||
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
||||
|
@ -41,4 +14,4 @@ export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
|||
};
|
||||
}
|
||||
|
||||
export default Connections;
|
||||
export default ConnectionsIndexPage;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { GetServerSidePropsContext, NextPage } from 'next';
|
||||
import Add from '@components/connection/Add';
|
||||
import CreateConnection from '@components/connection/CreateConnection';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
const NewConnection: NextPage = () => {
|
||||
return <Add />;
|
||||
return <CreateConnection />;
|
||||
};
|
||||
|
||||
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson, { GetIDPEntityIDBody } from '@lib/jackson';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
@ -9,17 +9,19 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
return handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
const idpEntityID = await connectionAPIController.getIDPEntityID({
|
||||
|
||||
const idpEntityID = connectionAPIController.getIDPEntityID({
|
||||
tenant: req.body.tenant,
|
||||
product: req.body.product,
|
||||
} as GetIDPEntityIDBody);
|
||||
return res.json({ data: idpEntityID, error: null });
|
||||
});
|
||||
|
||||
return res.json({ data: { idpEntityID } });
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { DirectoryType } from '@boxyhq/saml-jackson';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
|
@ -20,7 +20,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const providers = directorySyncController.providers();
|
||||
|
||||
return res.status(200).json({ data: providers });
|
||||
return res.json({ data: providers });
|
||||
};
|
||||
|
||||
export default checkSession(handler);
|
||||
|
|
|
@ -5,12 +5,18 @@ import jackson from '@lib/jackson';
|
|||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,15 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
try {
|
||||
const hasValidLicense = await checkLicense();
|
||||
const hasValidLicense = await checkLicense();
|
||||
|
||||
res.status(200).json({ data: { status: hasValidLicense } });
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: { message },
|
||||
});
|
||||
}
|
||||
return res.status(200).json({ data: { status: hasValidLicense } });
|
||||
};
|
||||
|
|
|
@ -2,26 +2,25 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { checkSession } from '@lib/middleware';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const { method } = req;
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
await handlePOST(req, res);
|
||||
return;
|
||||
return await handlePOST(req, res);
|
||||
case 'GET':
|
||||
await handleGET(req, res);
|
||||
return;
|
||||
return await handleGET(req, res);
|
||||
case 'DELETE':
|
||||
await handleDELETE(req, res);
|
||||
return;
|
||||
return await handleDELETE(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'POST, GET, DELETE');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ data: null, error: { message: error.message || 'Unknown error' } });
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -29,48 +28,55 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
|
||||
const { tenant, product, type: service, regenerate } = req.body;
|
||||
const { tenant, product, service, regenerate } = req.body;
|
||||
|
||||
const { data, error } = await setupLinkController.create({
|
||||
const setupLink = await setupLinkController.create({
|
||||
tenant,
|
||||
product,
|
||||
service,
|
||||
regenerate,
|
||||
});
|
||||
|
||||
return res.status(error ? error.code : 201).json({ data, error });
|
||||
return res.status(201).json({ data: setupLink });
|
||||
};
|
||||
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
|
||||
const { setupID } = req.query;
|
||||
const { setupID } = req.query as { setupID: string };
|
||||
|
||||
const { data, error } = await setupLinkController.remove(setupID as string);
|
||||
await setupLinkController.remove(setupID);
|
||||
|
||||
return res.status(error ? error.code : 200).json({ data, error });
|
||||
return res.json({ data: {} });
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
const service = req.query.service;
|
||||
if (token) {
|
||||
const { data, error } = await setupLinkController.getByToken(req.query.token);
|
||||
|
||||
return res.status(error ? error.code : 200).json({ data, error });
|
||||
} else if (service) {
|
||||
const { data, error } = await setupLinkController.getByService(req.query.service);
|
||||
return res.status(error ? error.code : 200).json({ data, error });
|
||||
} else {
|
||||
const { token, service } = req.query as { token: string; service: string };
|
||||
|
||||
if (!token && !service) {
|
||||
return res.status(404).json({
|
||||
data: undefined,
|
||||
error: {
|
||||
message: 'Setup link is invalid',
|
||||
code: 404,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Get a setup link by token
|
||||
if (token) {
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
return res.json({ data: setupLink });
|
||||
}
|
||||
|
||||
// Get a setup link by service
|
||||
if (service) {
|
||||
const setupLink = await setupLinkController.getByService(service);
|
||||
|
||||
return res.json({ data: setupLink });
|
||||
}
|
||||
};
|
||||
|
||||
export default checkSession(handler);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson, { GetConnectionsQuery } from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', ['GET']);
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController, connectionAPIController } = await jackson();
|
||||
const { token, id } = req.query;
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err || !setup) {
|
||||
return res.status(err ? err.code : 401).json({ err });
|
||||
}
|
||||
|
||||
const list = await connectionAPIController.getConnections({
|
||||
tenant: setup.tenant,
|
||||
product: setup.product,
|
||||
} as GetConnectionsQuery);
|
||||
return res.json({ data: list.filter((l) => l.clientID === id)[0] });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,55 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson, { GetIDPEntityIDBody } from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
let data, error;
|
||||
if (!token) {
|
||||
data = undefined;
|
||||
error = {
|
||||
code: 404,
|
||||
message: 'Invalid setup token!',
|
||||
};
|
||||
res.status(error ? error.code : 201).json({ data, error });
|
||||
} else {
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err) {
|
||||
res.status(err ? err.code : 201).json({ err });
|
||||
} else if (!setup) {
|
||||
data = undefined;
|
||||
error = {
|
||||
code: 404,
|
||||
message: 'Invalid setup token!',
|
||||
};
|
||||
res.status(error ? error.code : 201).json({ data, error });
|
||||
} else if (setup?.validTill < +new Date()) {
|
||||
data = undefined;
|
||||
error = {
|
||||
code: 400,
|
||||
message: 'Setup Link expired!',
|
||||
};
|
||||
return res.status(error ? error.code : 201).json({ data, error });
|
||||
} else {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGET(res, setup);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (res: NextApiResponse, setup: any) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
const idpEntityID = await connectionAPIController.getIDPEntityID({
|
||||
tenant: setup.tenant,
|
||||
product: setup.product,
|
||||
} as GetIDPEntityIDBody);
|
||||
return res.json({ data: idpEntityID, error: null });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,84 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson, { GetConnectionsQuery } from '@lib/jackson';
|
||||
import { strategyChecker } from '@lib/utils';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err || !setup) {
|
||||
res.status(err ? err.code : 401).json({ err });
|
||||
} else {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGET(res, setup);
|
||||
case 'POST':
|
||||
return handlePOST(req, res, setup);
|
||||
case 'PATCH':
|
||||
return handlePATCH(req, res, setup);
|
||||
case 'DELETE':
|
||||
return handleDELETE(req, res, setup);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET, POST, PATCH, DELETE');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (res: NextApiResponse, setup: any) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
return res.json({
|
||||
data: await connectionAPIController.getConnections({
|
||||
tenant: setup.tenant,
|
||||
product: setup.product,
|
||||
} as GetConnectionsQuery),
|
||||
});
|
||||
};
|
||||
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setup: any) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setup?.tenant,
|
||||
product: setup?.product,
|
||||
};
|
||||
const { isSAML, isOIDC } = strategyChecker(req);
|
||||
if (isSAML) {
|
||||
return res.status(201).json({ data: await connectionAPIController.createSAMLConnection(body) });
|
||||
} else if (isOIDC) {
|
||||
return res.status(201).json({ data: await connectionAPIController.createOIDCConnection(body) });
|
||||
} else {
|
||||
throw { message: 'Missing SSO connection params', statusCode: 400 };
|
||||
}
|
||||
};
|
||||
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse, setup: any) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setup?.tenant,
|
||||
product: setup?.product,
|
||||
};
|
||||
await await connectionAPIController.deleteConnections(body);
|
||||
res.status(200).json({ data: null });
|
||||
};
|
||||
|
||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse, setup: any) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setup?.tenant,
|
||||
product: setup?.product,
|
||||
};
|
||||
const { isSAML, isOIDC } = strategyChecker(req);
|
||||
if (isSAML) {
|
||||
res.status(200).json({ data: await connectionAPIController.updateSAMLConnection(body) });
|
||||
} else if (isOIDC) {
|
||||
res.status(200).json({ data: await connectionAPIController.updateOIDCConnection(body) });
|
||||
} else {
|
||||
throw { message: 'Missing SSO connection params', statusCode: 400 };
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,29 +1,37 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err || !setup) {
|
||||
res.status(err ? err.code : 401).json({ err });
|
||||
} else {
|
||||
|
||||
const { method } = req;
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
try {
|
||||
await setupLinkController.getByToken(token);
|
||||
|
||||
switch (method) {
|
||||
case 'PUT':
|
||||
return handlePUT(req, res);
|
||||
return await handlePUT(req, res);
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'PUT');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
res.setHeader('Allow', 'PUT, GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
// Update a directory configuration
|
||||
const handlePUT = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { directoryId } = req.query;
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { directoryId } = req.query as { directoryId: string };
|
||||
|
||||
const { name, webhook_url, webhook_secret, log_webhook_events } = req.body;
|
||||
|
||||
const { data, error } = await directorySyncController.directories.update(directoryId as string, {
|
||||
|
@ -35,7 +43,30 @@ const handlePUT = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
},
|
||||
});
|
||||
|
||||
return res.status(error ? error.code : 201).json({ data, error });
|
||||
if (data) {
|
||||
return res.status(201).json({ data });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return res.status(error.code).json({ error });
|
||||
}
|
||||
};
|
||||
|
||||
// Get a directory configuration
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { directoryId } = req.query as { directoryId: string };
|
||||
|
||||
const { data, error } = await directorySyncController.directories.get(directoryId);
|
||||
|
||||
if (data) {
|
||||
return res.json({ data });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return res.status(error.code).json({ error });
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { DirectoryType } from '@lib/jackson';
|
||||
import type { DirectoryType, SetupLink } from '@boxyhq/saml-jackson';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err || !setup) {
|
||||
res.status(err ? err.code : 401).json({ err });
|
||||
} else {
|
||||
|
||||
const { method } = req;
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
try {
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return handlePOST(req, res, setup);
|
||||
return await handlePOST(req, res, setupLink);
|
||||
case 'GET':
|
||||
return await handleGET(req, res, setupLink);
|
||||
default:
|
||||
res.setHeader('Allow', 'POST');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
res.setHeader('Allow', 'PUT');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new configuration
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setup: any) => {
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { name, type, webhook_url, webhook_secret } = req.body;
|
||||
|
||||
const { data, error } = await directorySyncController.directories.create({
|
||||
name,
|
||||
tenant: setup.tenant,
|
||||
product: setup.product,
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
type: type as DirectoryType,
|
||||
webhook_url,
|
||||
webhook_secret,
|
||||
|
@ -44,4 +51,22 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setup: any)
|
|||
}
|
||||
};
|
||||
|
||||
// Get all configurations
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { data, error } = await directorySyncController.directories.getByTenantAndProduct(
|
||||
setupLink.tenant,
|
||||
setupLink.product
|
||||
);
|
||||
|
||||
if (data) {
|
||||
return res.status(200).json({ data: [data] });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return res.status(error.code).json({ error });
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
};
|
||||
|
||||
// Get the directory providers
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const providers = directorySyncController.providers();
|
||||
|
||||
return res.json({ data: providers });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,40 +1,39 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
// Get a setup link by token
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
const token = req.query.token;
|
||||
if (!token) {
|
||||
return res.status(404).json({
|
||||
data: undefined,
|
||||
error: {
|
||||
message: 'Setup link is invalid',
|
||||
code: 404,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { data, error } = await setupLinkController.getByToken(req.query.token);
|
||||
return res.status(error ? error.code : 200).json({
|
||||
data: {
|
||||
...data,
|
||||
tenant: undefined,
|
||||
product: undefined,
|
||||
},
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
return res.json({
|
||||
data: {
|
||||
...setupLink,
|
||||
tenant: undefined,
|
||||
product: undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import type { SetupLink } from '@boxyhq/saml-jackson';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
|
||||
const { method } = req;
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
try {
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res, setupLink);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const connections = await connectionAPIController.getConnections({
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
});
|
||||
|
||||
return res.json({ data: connections.filter((l) => l.clientID === id)[0] });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1,39 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import type { SetupLink } from '@boxyhq/saml-jackson';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
|
||||
const { method } = req;
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
try {
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res, setupLink);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const idpEntityID = connectionAPIController.getIDPEntityID({
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
});
|
||||
|
||||
return res.json({ data: { idpEntityID } });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1,100 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import { strategyChecker } from '@lib/utils';
|
||||
import type { SetupLink } from '@boxyhq/saml-jackson';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { setupLinkController } = await jackson();
|
||||
|
||||
const { method } = req;
|
||||
const { token } = req.query as { token: string };
|
||||
|
||||
try {
|
||||
const setupLink = await setupLinkController.getByToken(token);
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res, setupLink);
|
||||
case 'POST':
|
||||
return await handlePOST(req, res, setupLink);
|
||||
case 'PATCH':
|
||||
return await handlePATCH(req, res, setupLink);
|
||||
case 'DELETE':
|
||||
return await handleDELETE(req, res, setupLink);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET, POST, PATCH, DELETE');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const connections = await connectionAPIController.getConnections({
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
});
|
||||
|
||||
return res.json({ data: connections });
|
||||
};
|
||||
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
};
|
||||
|
||||
const { isSAML, isOIDC } = strategyChecker(req);
|
||||
|
||||
if (isSAML) {
|
||||
return res.status(201).json({ data: await connectionAPIController.createSAMLConnection(body) });
|
||||
} else if (isOIDC) {
|
||||
return res.status(201).json({ data: await connectionAPIController.createOIDCConnection(body) });
|
||||
} else {
|
||||
throw { message: 'Missing SSO connection params', statusCode: 400 };
|
||||
}
|
||||
};
|
||||
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
};
|
||||
|
||||
await connectionAPIController.deleteConnections(body);
|
||||
|
||||
return res.json({ data: null });
|
||||
};
|
||||
|
||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
|
||||
const { connectionAPIController } = await jackson();
|
||||
|
||||
const body = {
|
||||
...req.body,
|
||||
tenant: setupLink.tenant,
|
||||
product: setupLink.product,
|
||||
};
|
||||
|
||||
const { isSAML, isOIDC } = strategyChecker(req);
|
||||
|
||||
if (isSAML) {
|
||||
res.json({ data: await connectionAPIController.updateSAMLConnection(body) });
|
||||
} else if (isOIDC) {
|
||||
res.json({ data: await connectionAPIController.updateOIDCConnection(body) });
|
||||
} else {
|
||||
throw { message: 'Missing SSO connection params', statusCode: 400 };
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,153 +1,25 @@
|
|||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import jackson from '@lib/jackson';
|
||||
import { inferSSRProps } from '@lib/inferSSRProps';
|
||||
import { LinkBack } from '@components/LinkBack';
|
||||
import { ButtonPrimary } from '@components/ButtonPrimary';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import React from 'react';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import EditDirectory from '@components/dsync/EditDirectory';
|
||||
|
||||
const Edit: NextPage<inferSSRProps<typeof getServerSideProps>> = ({
|
||||
directory: { id, name, log_webhook_events, webhook },
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const DirectoryEditPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
const [directory, setDirectory] = React.useState({
|
||||
name,
|
||||
log_webhook_events,
|
||||
webhook_url: webhook.endpoint,
|
||||
webhook_secret: webhook.secret,
|
||||
});
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { token, directoryId } = router.query as { token: string; directoryId: string };
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(`/api/setup/${token}/directory-sync/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(directory),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const { data, error } = await rawResponse.json();
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
successToast('Directory updated successfully');
|
||||
router.replace(`/setup/${token}/directory-sync`);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
|
||||
setDirectory({
|
||||
...directory,
|
||||
[target.id]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LinkBack href={`/setup/${token}/directory-sync`} />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>Update Directory</h2>
|
||||
<div className='min-w-[28rem] rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800 md:w-3/4 md:max-w-lg'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>Directory name</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input w-full'
|
||||
required
|
||||
onChange={onChange}
|
||||
value={directory.name}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>Webhook URL</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_url'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_url}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>Webhook secret</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='webhook_secret'
|
||||
className='input-bordered input w-full'
|
||||
onChange={onChange}
|
||||
value={directory.webhook_secret}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-control w-full py-2'>
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
id='log_webhook_events'
|
||||
type='checkbox'
|
||||
checked={directory.log_webhook_events}
|
||||
onChange={onChange}
|
||||
className='h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600'
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Enable Webhook events logging
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('save_changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <EditDirectory directoryId={directoryId} setupLinkToken={token} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { directoryId } = context.query;
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { data: directory } = await directorySyncController.directories.get(directoryId as string);
|
||||
|
||||
if (!directory) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const { locale } = context;
|
||||
|
||||
return {
|
||||
props: {
|
||||
directory,
|
||||
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
export default DirectoryEditPage;
|
||||
|
|
|
@ -1,33 +1,25 @@
|
|||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import React from 'react';
|
||||
import jackson from '@lib/jackson';
|
||||
import { inferSSRProps } from '@lib/inferSSRProps';
|
||||
import { useRouter } from 'next/router';
|
||||
import DirectoryInfo from '@components/dsync/DirectoryInfo';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
const Info: NextPage<inferSSRProps<typeof getServerSideProps>> = ({ directory }) => {
|
||||
const DirectoryDetailsPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
return <DirectoryInfo directory={directory} token={token as string} />;
|
||||
|
||||
const { token, directoryId } = router.query as { token: string; directoryId: string };
|
||||
|
||||
return <DirectoryInfo directoryId={directoryId} setupLinkToken={token} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { directoryId } = context.query;
|
||||
const { directorySyncController } = await jackson();
|
||||
|
||||
const { data: directory } = await directorySyncController.directories.get(directoryId as string);
|
||||
|
||||
if (!directory) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const { locale } = context;
|
||||
|
||||
return {
|
||||
props: {
|
||||
directory,
|
||||
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Info;
|
||||
export default DirectoryDetailsPage;
|
||||
|
|
|
@ -1,55 +1,24 @@
|
|||
import type { InferGetServerSidePropsType, GetServerSidePropsContext } from 'next';
|
||||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useRouter } from 'next/router';
|
||||
import jackson from '@lib/jackson';
|
||||
import DirectoryList from '@components/dsync/DirectoryList';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
|
||||
const Index = ({ directories }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||
const DirectoryIndexPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
|
||||
const { token } = router.query as { token: string };
|
||||
|
||||
return (
|
||||
<DirectoryList directories={directories} token={token} paginate={paginate} setPaginate={setPaginate} />
|
||||
);
|
||||
return <DirectoryList setupLinkToken={token} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { offset = 0, token } = context.query;
|
||||
const { directorySyncController, setupLinkController } = await jackson();
|
||||
const { locale }: GetServerSidePropsContext = context;
|
||||
let directories;
|
||||
if (!token) {
|
||||
directories = [];
|
||||
} else {
|
||||
const { data: setup, error: err } = await setupLinkController.getByToken(token);
|
||||
if (err) {
|
||||
directories = [];
|
||||
} else if (!setup) {
|
||||
directories = [];
|
||||
} else if (setup?.validTill < +new Date()) {
|
||||
directories = [];
|
||||
} else {
|
||||
const { data } = await directorySyncController.directories.getByTenantAndProduct(
|
||||
setup.tenant,
|
||||
setup.product
|
||||
);
|
||||
directories = data ? [data] : [];
|
||||
}
|
||||
}
|
||||
const pageOffset = parseInt(offset as string);
|
||||
const pageLimit = 25;
|
||||
const { locale } = context;
|
||||
|
||||
return {
|
||||
props: {
|
||||
providers: directorySyncController.providers(),
|
||||
directories,
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default DirectoryIndexPage;
|
||||
|
|
|
@ -4,10 +4,12 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|||
import { useRouter } from 'next/router';
|
||||
import CreateDirectory from '@components/dsync/CreateDirectory';
|
||||
|
||||
const New: NextPage = () => {
|
||||
const DirectoryCreatePage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
return <CreateDirectory token={token as string} />;
|
||||
|
||||
const { token } = router.query as { token: string };
|
||||
|
||||
return <CreateDirectory setupLinkToken={token} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
|
@ -18,4 +20,4 @@ export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default New;
|
||||
export default DirectoryCreatePage;
|
||||
|
|
|
@ -2,35 +2,47 @@ import type { NextPage } from 'next';
|
|||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ApiError, ApiSuccess } from 'types';
|
||||
import type { SetupLink } from '@boxyhq/saml-jackson';
|
||||
import Loading from '@components/Loading';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
|
||||
const Setup: NextPage = () => {
|
||||
const SetupLinksIndexPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
const { data, error } = useSWR<any>(token ? `/api/setup/${token}` : null, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const setup = data?.data;
|
||||
if (error) {
|
||||
return (
|
||||
<div className='rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700'>
|
||||
{error.info ? JSON.stringify(error.info.error) : error.status ? error.status : error.message}
|
||||
</div>
|
||||
);
|
||||
} else if (!token || !setup) {
|
||||
return null;
|
||||
} else {
|
||||
switch (setup.service) {
|
||||
case 'sso':
|
||||
router.replace(`/setup/${token}/sso-connection`);
|
||||
return null;
|
||||
case 'dsync':
|
||||
router.replace(`/setup/${token}/directory-sync`);
|
||||
return null;
|
||||
default:
|
||||
router.replace(`/`);
|
||||
return null;
|
||||
|
||||
const { token } = router.query as { token: string };
|
||||
|
||||
const { data, error } = useSWR<ApiSuccess<SetupLink>, ApiError>(
|
||||
token ? `/api/setup/${token}` : null,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const setupLink = data.data;
|
||||
|
||||
switch (setupLink.service) {
|
||||
case 'sso':
|
||||
router.replace(`/setup/${token}/sso-connection`);
|
||||
break;
|
||||
case 'dsync':
|
||||
router.replace(`/setup/${token}/directory-sync`);
|
||||
break;
|
||||
default:
|
||||
router.replace('/');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
export default SetupLinksIndexPage;
|
||||
|
|
|
@ -1,44 +1,38 @@
|
|||
import type { NextPage, GetServerSidePropsContext, GetStaticPaths } from 'next';
|
||||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import useSWR from 'swr';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import Edit from '@components/connection/Edit';
|
||||
import EditConnection from '@components/connection/EditConnection';
|
||||
import Loading from '@components/Loading';
|
||||
import { errorToast } from '@components/Toaster';
|
||||
|
||||
const EditConnection: NextPage = () => {
|
||||
const ConnectionEditPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { id, token } = router.query;
|
||||
const { data } = useSWR<any>(token ? `/api/setup/${token}` : null, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const setup = data?.data;
|
||||
const { id, token } = router.query as { id: string; token: string };
|
||||
|
||||
const { data: connectionData, error } = useSWR(
|
||||
token ? (id ? `/api/setup/${token}/connections/${id}` : null) : null,
|
||||
const { data, error } = useSWR(
|
||||
token ? (id ? `/api/setup/${token}/sso-connection/${id}` : null) : null,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const connection = connectionData?.data;
|
||||
if (error) {
|
||||
return (
|
||||
<div className='rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700'>
|
||||
{error.info ? JSON.stringify(error.info) : error.status}
|
||||
</div>
|
||||
);
|
||||
if (!data && !error) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!token || !setup || !connection) {
|
||||
if (error) {
|
||||
errorToast(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Edit connection={connection} setupToken={token as string} />;
|
||||
};
|
||||
const connection = data.data;
|
||||
|
||||
export default EditConnection;
|
||||
return <EditConnection connection={connection} setupLinkToken={token} />;
|
||||
};
|
||||
|
||||
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
||||
return {
|
||||
|
@ -48,9 +42,11 @@ export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
|||
};
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [], //indicates that no page needs be created at build time
|
||||
fallback: 'blocking', //indicates the type of fallback
|
||||
paths: [],
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default ConnectionEditPage;
|
||||
|
|
|
@ -1,42 +1,17 @@
|
|||
import type { GetServerSidePropsContext, NextPage, GetStaticPaths } from 'next';
|
||||
import type { GetServerSidePropsContext, NextPage } from 'next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import ConnectionList from '@components/connection/ConnectionList';
|
||||
import { useRouter } from 'next/router';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import useSWR from 'swr';
|
||||
import { pageLimit } from '@components/Pagination';
|
||||
import usePaginate from '@lib/ui/hooks/usePaginate';
|
||||
import useIdpEntityID from '@lib/ui/hooks/useIdpEntityID';
|
||||
|
||||
const Connections: NextPage = () => {
|
||||
const ConnectionsIndexPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { paginate, setPaginate } = usePaginate();
|
||||
|
||||
const { token } = router.query as { token: string };
|
||||
|
||||
const { data, error, isValidating } = useSWR(
|
||||
[`/api/setup/${token}/connections`, `?pageOffset=${paginate.offset}&pageLimit=${pageLimit}`],
|
||||
fetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
const { idpEntityID } = useIdpEntityID(token);
|
||||
|
||||
const connections = data?.data;
|
||||
const { data: idpEntityIDData } = useSWR(
|
||||
token ? `/api/setup/${token}/connections/idp-entityid` : null,
|
||||
fetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const idpEntityID = idpEntityIDData?.data;
|
||||
|
||||
return token && connections && !error && !isValidating ? (
|
||||
<ConnectionList
|
||||
setupToken={token}
|
||||
paginate={paginate}
|
||||
setPaginate={setPaginate}
|
||||
connections={connections}
|
||||
idpEntityID={idpEntityID}
|
||||
/>
|
||||
) : null;
|
||||
return <ConnectionList setupLinkToken={token} idpEntityID={idpEntityID} />;
|
||||
};
|
||||
|
||||
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
||||
|
@ -47,11 +22,11 @@ export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
|||
};
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [], //indicates that no page needs be created at build time
|
||||
fallback: 'blocking', //indicates the type of fallback
|
||||
paths: [],
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default Connections;
|
||||
export default ConnectionsIndexPage;
|
||||
|
|
|
@ -1,45 +1,19 @@
|
|||
import type { NextPage, GetServerSidePropsContext, GetStaticPaths } from 'next';
|
||||
import Add from '@components/connection/Add';
|
||||
import type { NextPage, GetServerSidePropsContext } from 'next';
|
||||
import CreateConnection from '@components/connection/CreateConnection';
|
||||
import { useRouter } from 'next/router';
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
//import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import useIdpEntityID from '@lib/ui/hooks/useIdpEntityID';
|
||||
|
||||
const NewConnection: NextPage = () => {
|
||||
//const { t } = useTranslation('common');
|
||||
const ConnectionCreatePage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
const { data, error } = useSWR<any>(token ? `/api/setup/${token}` : null, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const setup = data?.data;
|
||||
|
||||
const { data: idpEntityIDData } = useSWR<any>(
|
||||
token ? `/api/setup/${token}/connections/idp-entityid` : null,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
const { token } = router.query as { token: string };
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className='rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700'>
|
||||
{error.info ? JSON.stringify(error.info.error) : error.status ? error.status : error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const idpEntityID = idpEntityIDData?.data;
|
||||
if (!token || !setup) {
|
||||
return null;
|
||||
} else {
|
||||
return <Add setupToken={token as string} idpEntityID={idpEntityID} />;
|
||||
}
|
||||
const { idpEntityID } = useIdpEntityID(token);
|
||||
|
||||
return <CreateConnection setupLinkToken={token} idpEntityID={idpEntityID} />;
|
||||
};
|
||||
|
||||
export default NewConnection;
|
||||
|
||||
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
|
@ -48,9 +22,11 @@ export async function getStaticProps({ locale }: GetServerSidePropsContext) {
|
|||
};
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [], //indicates that no page needs be created at build time
|
||||
fallback: 'blocking', //indicates the type of fallback
|
||||
paths: [],
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default ConnectionCreatePage;
|
||||
|
|
Loading…
Reference in New Issue