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:
Kiran K 2022-12-29 19:01:50 +05:30 committed by GitHub
parent c0bf71acff
commit 4e10d501ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1182 additions and 1290 deletions

View File

@ -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;

View File

@ -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}`
);
}}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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'>

View File

@ -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'>

View File

@ -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`
);
}}

View File

@ -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;

View File

@ -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;

View File

@ -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())) {

View File

@ -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())) {

View File

@ -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,
};

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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>;

View File

@ -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';

View File

@ -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;

View File

@ -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')} />
) : (
<>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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);

View File

@ -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 } });
};

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;