Admin portal sso login (#762)

* env, login button & translations

* added setting in sidebar
Added login with sso button
Added connection create form in settings

* added new pages for Self SSO connection CRUD

* Fixed Self SSO issue

* Use @boxyhq/react-ui component for SSO

* `await` on method instead of class

* Fix import

* Set fields to non-editable for settings view

* Tweak for settings view

* Add link for settings in sidebar

* Take in admin SSO defaults from env

* Tweak edit page for settings view

* Remove `NEXT_PUBLIC` prefix

* Switch back to getSSP from getStaticProps

* Sync lock file

* Set defaults in env

* Filter out admin sso tenant/product

* Load admin SSO tenant/product

* Update heading

* Fix back link

* Use latest published version

* Set `clientId` to dummy in provider init

* Use the defaults from env

* Fix redirectUrl after savingConnection for settingsView

* Use `isLoading` from SWR

* Fix settings view url for mutation and redirect in Edit

* Replace api route path

* Use rewrite instead of router.push and other tweaks

* Reuse `ConnectionList` for settings

* Use pagination query params in settings api

* Import styles from sdk

* Fix failing build

* Use latest version

* - Display badge for system sso connections
- Reuse admin connection for retrieving system sso
connections

* Tweak styling

* Construct profile in updateUser as done previously

* Update react-ui

* Remove extra truthy check

* Hide pagination buttons for settings view

* Install @boxyhq/react-ui as symlink to local

* Tweak badge size

* Rename admin portal sso envs

* Fix the edit redirection for system sso

Co-authored-by: ukrocks007 <ukrocks.mehta@gmail.com>
Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
Co-authored-by: Kiran K <kiran@boxyhq.com>
This commit is contained in:
Aswin V 2023-01-12 20:39:08 +05:30 committed by GitHub
parent 93d7ef1470
commit b14a0f1623
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 4469 additions and 475 deletions

View File

@ -2,6 +2,8 @@
EXTERNAL_URL=
SAML_AUDIENCE=https://saml.boxyhq.com
JACKSON_API_KEYS="secret"
ADMIN_PORTAL_SSO_TENANT="_jackson_boxyhq"
ADMIN_PORTAL_SSO_PRODUCT="_jackson_admin_portal"
IDP_ENABLED=
PRE_LOADED_CONNECTION=
CLIENT_SECRET_VERIFIER=

View File

@ -9,6 +9,7 @@ import { useTranslation } from 'next-i18next';
import SSOLogo from '@components/logo/SSO';
import DSyncLogo from '@components/logo/DSync';
import AuditLogsLogo from '@components/logo/AuditLogs';
import { Cog8ToothIcon } from '@heroicons/react/24/outline';
type SidebarProps = {
isOpen: boolean;
@ -91,6 +92,12 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
},
],
},
{
href: '/admin/settings',
text: t('settings'),
icon: Cog8ToothIcon,
active: asPath.includes('/admin/settings'),
},
];
return (

View File

@ -15,13 +15,16 @@ import { fetcher } from '@lib/ui/utils';
import Loading from '@components/Loading';
import { errorToast } from '@components/Toaster';
import type { ApiError, ApiSuccess } from 'types';
import { Badge } from 'react-daisyui';
const ConnectionList = ({
setupLinkToken,
idpEntityID,
isSettingsView = false,
}: {
setupLinkToken?: string;
idpEntityID?: string;
isSettingsView?: boolean;
}) => {
const { t } = useTranslation('common');
const { paginate, setPaginate } = usePaginate();
@ -30,16 +33,19 @@ const ConnectionList = ({
const displayTenantProduct = setupLinkToken ? false : true;
const getConnectionsUrl = setupLinkToken
? `/api/setup/${setupLinkToken}/sso-connection`
: isSettingsView
? `/api/admin/connections?isSystemSSO`
: `/api/admin/connections?pageOffset=${paginate.offset}&pageLimit=${pageLimit}`;
const createConnectionUrl = setupLinkToken
? `/setup/${setupLinkToken}/sso-connection/new`
: isSettingsView
? `/admin/settings/sso-connection/new`
: '/admin/sso-connection/new';
const { data, error, isLoading } = useSWR<ApiSuccess<(SAMLSSORecord | OIDCSSORecord)[]>, ApiError>(
getConnectionsUrl,
fetcher,
{ revalidateOnFocus: false }
);
const { data, error, isLoading } = useSWR<
ApiSuccess<((SAMLSSORecord | OIDCSSORecord) & { isSystemSSO?: boolean })[]>,
ApiError
>(getConnectionsUrl, fetcher, { revalidateOnFocus: false });
if (isLoading) {
return <Loading />;
@ -62,12 +68,14 @@ const ConnectionList = ({
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>
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>
{t(isSettingsView ? 'admin_portal_sso' : 'enterprise_sso')}
</h2>
<div className='flex gap-2'>
<LinkPrimary Icon={PlusIcon} href={createConnectionUrl} data-test-id='create-connection'>
{t('new_connection')}
</LinkPrimary>
{!setupLinkToken && (
{!setupLinkToken && !isSettingsView && (
<LinkPrimary
Icon={LinkIcon}
href='/admin/sso-connection/setup-link/new'
@ -117,7 +125,7 @@ const ConnectionList = ({
{connections.map((connection) => {
const connectionIsSAML = 'idpMetadata' in connection;
const connectionIsOIDC = 'oidcProvider' in connection;
const isSystemSSO = connection?.isSystemSSO;
return (
<tr
key={connection.clientID}
@ -127,6 +135,16 @@ const ConnectionList = ({
(connectionIsSAML
? connection.idpMetadata?.provider
: connection.oidcProvider?.provider)}
{isSystemSSO && (
<Badge
color='primary'
variant='outline'
size='sm'
className='ml-1 uppercase'
aria-label='is an sso connection for the admin portal'>
System
</Badge>
)}
</td>
{displayTenantProduct && (
<>
@ -151,6 +169,8 @@ const ConnectionList = ({
router.push(
setupLinkToken
? `/setup/${setupLinkToken}/sso-connection/edit/${connection.clientID}`
: isSettingsView || isSystemSSO
? `/admin/settings/sso-connection/edit/${connection.clientID}`
: `/admin/sso-connection/edit/${connection.clientID}`
);
}}
@ -164,20 +184,22 @@ const ConnectionList = ({
</tbody>
</table>
</div>
<Pagination
itemsCount={connections.length}
offset={paginate.offset}
onPrevClick={() => {
setPaginate({
offset: paginate.offset - pageLimit,
});
}}
onNextClick={() => {
setPaginate({
offset: paginate.offset + pageLimit,
});
}}
/>
{!isSettingsView && (
<Pagination
itemsCount={connections.length}
offset={paginate.offset}
onPrevClick={() => {
setPaginate({
offset: paginate.offset - pageLimit,
});
}}
onNextClick={() => {
setPaginate({
offset: paginate.offset + pageLimit,
});
}}
/>
)}
</>
)}
</div>

View File

@ -1,7 +1,12 @@
import { useRouter } from 'next/router';
import { useState } from 'react';
import { getCommonFields } from './fieldCatalog';
import { saveConnection, fieldCatalogFilterByConnection, renderFieldList } from './utils';
import {
saveConnection,
fieldCatalogFilterByConnection,
renderFieldList,
useFieldCatalog,
type AdminPortalSSODefaults,
} from './utils';
import { mutate } from 'swr';
import { ApiResponse } from 'types';
import { errorToast } from '@components/Toaster';
@ -10,15 +15,18 @@ import { LinkBack } from '@components/LinkBack';
import { ButtonPrimary } from '@components/ButtonPrimary';
import { InputWithCopyButton } from '@components/ClipboardButton';
const fieldCatalog = [...getCommonFields()];
const CreateConnection = ({
setupLinkToken,
idpEntityID,
isSettingsView = false,
adminPortalSSODefaults,
}: {
setupLinkToken?: string;
idpEntityID?: string;
isSettingsView?: boolean;
adminPortalSSODefaults?: AdminPortalSSODefaults;
}) => {
const fieldCatalog = useFieldCatalog({ isSettingsView });
const { t } = useTranslation('common');
const router = useRouter();
const [loading, setLoading] = useState(false);
@ -33,10 +41,20 @@ const CreateConnection = ({
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 backUrl = setupLinkToken
? `/setup/${setupLinkToken}`
: isSettingsView
? '/admin/settings/sso-connection'
: '/admin/sso-connection';
const redirectUrl = setupLinkToken
? `/setup/${setupLinkToken}/sso-connection`
: isSettingsView
? '/admin/settings/sso-connection'
: '/admin/sso-connection';
const mutationUrl = setupLinkToken
? `/api/setup/${setupLinkToken}/sso-connection`
: isSettingsView
? '/api/admin/connections?adminSSO'
: '/api/admin/connections';
// FORM LOGIC: SUBMIT
@ -69,7 +87,9 @@ const CreateConnection = ({
};
// STATE: FORM
const [formObj, setFormObj] = useState<Record<string, string>>({});
const [formObj, setFormObj] = useState<Record<string, string>>(
isSettingsView ? { ...adminPortalSSODefaults } : {}
);
return (
<>

View File

@ -3,8 +3,7 @@ import { useEffect, useState } from 'react';
import { mutate } from 'swr';
import ConfirmationModal from '@components/ConfirmationModal';
import { EditViewOnlyFields, getCommonFields } from './fieldCatalog';
import { saveConnection, fieldCatalogFilterByConnection, renderFieldList } from './utils';
import { saveConnection, fieldCatalogFilterByConnection, renderFieldList, useFieldCatalog } from './utils';
import { ApiResponse } from 'types';
import { errorToast, successToast } from '@components/Toaster';
import { useTranslation } from 'next-i18next';
@ -12,9 +11,7 @@ import { LinkBack } from '@components/LinkBack';
import { ButtonPrimary } from '@components/ButtonPrimary';
import { ButtonDanger } from '@components/ButtonDanger';
const fieldCatalog = [...getCommonFields(true), ...EditViewOnlyFields];
function getInitialState(connection) {
function getInitialState(connection, fieldCatalog) {
const _state = {};
fieldCatalog.forEach(({ key, attributes }) => {
@ -37,9 +34,12 @@ function getInitialState(connection) {
type EditProps = {
connection?: Record<string, any>;
setupLinkToken?: string;
isSettingsView?: boolean;
};
const EditConnection = ({ connection, setupLinkToken }: EditProps) => {
const EditConnection = ({ connection, setupLinkToken, isSettingsView = false }: EditProps) => {
const fieldCatalog = useFieldCatalog({ isEditView: true, isSettingsView });
const router = useRouter();
const { t } = useTranslation('common');
@ -101,26 +101,46 @@ const EditConnection = ({ connection, setupLinkToken }: EditProps) => {
}
if (res.ok) {
await mutate(setupLinkToken ? `/api/setup/${setupLinkToken}/connections` : '/api/admin/connections');
router.replace(setupLinkToken ? `/setup/${setupLinkToken}/sso-connection` : '/admin/sso-connection');
await mutate(
setupLinkToken
? `/api/setup/${setupLinkToken}/connections`
: isSettingsView
? `/api/admin/connections?isSystemSSO`
: '/api/admin/connections'
);
router.replace(
setupLinkToken
? `/setup/${setupLinkToken}/sso-connection`
: isSettingsView
? '/admin/settings/sso-connection'
: '/admin/sso-connection'
);
}
};
// STATE: FORM
const [formObj, setFormObj] = useState<Record<string, string>>(() => getInitialState(connection));
const [formObj, setFormObj] = useState<Record<string, string>>(() =>
getInitialState(connection, fieldCatalog)
);
// Resync form state on save
useEffect(() => {
const _state = getInitialState(connection);
const _state = getInitialState(connection, fieldCatalog);
setFormObj(_state);
}, [connection]);
}, [connection, fieldCatalog]);
const filteredFieldsByConnection = fieldCatalog.filter(
fieldCatalogFilterByConnection(connectionIsSAML ? 'saml' : connectionIsOIDC ? 'oidc' : null)
);
const backUrl = setupLinkToken
? `/setup/${setupLinkToken}`
: isSettingsView
? '/admin/settings/sso-connection'
: '/admin/sso-connection';
return (
<>
<LinkBack href={setupLinkToken ? `/setup/${setupLinkToken}` : '/admin/sso-connection'} />
<LinkBack href={backUrl} />
<div>
<h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>
{t('edit_sso_connection')}

View File

@ -5,7 +5,13 @@
* `accessor` only used to set initial state and retrieve saved value. Useful when key is different from retrieved payload.
*/
export const getCommonFields = (isEditView?: boolean) => [
export const getCommonFields = ({
isEditView,
isSettingsView,
}: {
isEditView?: boolean;
isSettingsView?: boolean;
}) => [
{
key: 'name',
label: 'Name',
@ -31,6 +37,7 @@ export const getCommonFields = (isEditView?: boolean) => [
hideInSetupView: true,
}
: {
editable: !isSettingsView,
hideInSetupView: true,
},
},
@ -45,6 +52,7 @@ export const getCommonFields = (isEditView?: boolean) => [
hideInSetupView: true,
}
: {
editable: !isSettingsView,
hideInSetupView: true,
},
},
@ -53,16 +61,15 @@ export const getCommonFields = (isEditView?: boolean) => [
label: 'Allowed redirect URLs (newline separated)',
type: 'textarea',
placeholder: 'http://localhost:3366',
attributes: { isArray: true, rows: 3, hideInSetupView: true },
attributes: { isArray: true, rows: 3, hideInSetupView: true, editable: !isSettingsView },
},
{
key: 'defaultRedirectUrl',
label: 'Default redirect URL',
type: 'url',
placeholder: 'http://localhost:3366/login/saml',
attributes: { hideInSetupView: true },
attributes: { hideInSetupView: true, editable: !isSettingsView },
},
{
key: 'oidcDiscoveryUrl',
label: 'Well-known URL of OpenId Provider',

View File

@ -1,5 +1,6 @@
import { errorToast } from '@components/Toaster';
import { FormEvent, SetStateAction } from 'react';
import { FormEvent, SetStateAction, useMemo } from 'react';
import { EditViewOnlyFields, getCommonFields } from './fieldCatalog';
export const saveConnection = async ({
formObj,
@ -46,6 +47,7 @@ export const saveConnection = async ({
);
callback(res);
};
export function fieldCatalogFilterByConnection(connection) {
return ({ attributes }) =>
attributes.connection && connection !== null ? attributes.connection === connection : true;
@ -78,6 +80,29 @@ type FieldCatalog = {
};
};
export const useFieldCatalog = ({
isEditView,
isSettingsView,
}: {
isEditView?: boolean;
isSettingsView?: boolean;
}) => {
const fieldCatalog = useMemo(() => {
if (isEditView) {
return [...getCommonFields({ isEditView: true, isSettingsView }), ...EditViewOnlyFields];
}
return [...getCommonFields({ isSettingsView })];
}, [isEditView, isSettingsView]);
return fieldCatalog;
};
export type AdminPortalSSODefaults = {
tenant: string;
product: string;
redirectUrl: string;
defaultRedirectUrl: string;
};
export function renderFieldList(args: {
isEditView?: boolean;
formObj: Record<string, string>;
@ -99,7 +124,7 @@ export function renderFieldList(args: {
required = true,
},
}: FieldCatalog) => {
const disabled = args.isEditView && editable === false;
const disabled = editable === false;
const value =
disabled && typeof formatForDisplay === 'function'
? formatForDisplay(args.formObj[key])
@ -134,14 +159,18 @@ export function renderFieldList(args: {
disabled={disabled}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj)}
className={`textarea-bordered textarea h-24 w-full ${isArray ? 'whitespace-pre' : ''}`}
className={`textarea-bordered textarea h-24 w-full ${isArray ? 'whitespace-pre' : ''} ${
isHidden ? (isHidden(args.formObj[key]) == true ? 'hidden' : '') : ''
}`}
rows={rows}
/>
) : type === 'checkbox' ? (
<>
<label
htmlFor={key}
className='inline-block align-middle text-sm font-medium text-gray-900 dark:text-gray-300'>
className={`inline-block align-middle text-sm font-medium text-gray-900 dark:text-gray-300 ${
isHidden ? (isHidden(args.formObj[key]) == true ? 'hidden' : '') : ''
}`}>
{label}
</label>
<input
@ -165,7 +194,9 @@ export function renderFieldList(args: {
disabled={disabled}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj)}
className='input-bordered input w-full'
className={`input-bordered input w-full ${
isHidden ? (isHidden(args.formObj[key]) == true ? 'hidden' : '') : ''
}`}
/>
)}
</div>

View File

@ -64,6 +64,14 @@ const jacksonOptions: JacksonOption = {
process.env.BOXYHQ_NO_ANALYTICS === 'true',
};
const adminPortalSSODefaults = {
tenant: process.env.ADMIN_PORTAL_SSO_TENANT || '_jackson_boxyhq',
product: process.env.ADMIN_PORTAL_SSO_PRODUCT || '_jackson_admin_portal',
redirectUrl: externalUrl,
defaultRedirectUrl: `${externalUrl}/api/auth/callback/boxyhq-saml`,
};
export { adminPortalSSODefaults };
export { retraced as retracedOptions };
export { apiKeys };
export { jacksonOptions };

View File

@ -2,7 +2,6 @@ import { Storable } from '@boxyhq/saml-jackson';
import DB from 'npm/src/db/db';
import { jacksonOptions } from './env';
import type { AdapterUser, VerificationToken } from 'next-auth/adapters';
import { validateEmailWithACL } from './utils';
import defaultDb from 'npm/src/db/defaultDb';
const g = global as any;
@ -27,17 +26,15 @@ export default function Adapter() {
return;
},
async getUserByEmail(email) {
// ?? we already do the validation in signIn callback (see pages/api/auth/[...nextauth].ts)
if (validateEmailWithACL(email)) {
return {
id: email,
name: email.split('@')[0],
email,
role: 'admin',
emailVerified: new Date(),
} as AdapterUser;
}
return null;
return email
? ({
id: email,
name: email.split('@')[0],
email,
role: 'admin',
emailVerified: new Date(),
} as AdapterUser)
: null;
},
async getUserByAccount({ providerAccountId, provider }) {
return;
@ -47,17 +44,13 @@ export default function Adapter() {
return null;
}
const email = user.id;
// ?? we already do the validation in signIn callback (see pages/api/auth/[...nextauth].ts)
if (validateEmailWithACL(email)) {
return {
id: email,
name: email.split('@')[0],
email,
role: 'admin',
emailVerified: new Date(),
} as AdapterUser;
}
return null;
return {
id: email,
name: email.split('@')[0],
email,
role: 'admin',
emailVerified: new Date(),
} as AdapterUser;
},
// will be required in a future release, but are not yet invoked
async deleteUser(userId) {

View File

@ -35,6 +35,7 @@
"idp_type": "IdP Type",
"idp_entity_id": "IdP Entity ID",
"last_name": "Last Name",
"login_with_sso": "Login with SSO",
"login_success_toast": "A sign in link has been sent to your email address.",
"link_generated": "Link Generated",
"link_regenerated": "Link Regenerated",
@ -111,6 +112,8 @@
"view": "View",
"view_idp_configuration": "View IdP Configuration",
"edit": "Edit",
"settings": "Settings",
"admin_portal_sso": "SSO for Admin Portal",
"sp_metadata_description": "The metadata file that your customers who use federated management systems like OpenAthens and Shibboleth will need to configure your service.",
"sp_config_description": "The configuration setup guide that your customers will need to refer to when setting up SAML application with their Identity Provider.",
"saml_public_cert_description": "The SAML Public Certificate if you want to enable encryption with your Identity Provider.",

View File

@ -75,6 +75,10 @@ module.exports = {
source: '/.well-known',
destination: '/well-known',
},
{
source: '/admin/settings',
destination: '/admin/settings/sso-connection',
},
];
},
};

4522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@
"test": "cd npm && npm run test"
},
"dependencies": {
"@boxyhq/react-ui": "file:sdk/ui/react",
"@boxyhq/saml-jackson": "file:./npm",
"@heroicons/react": "2.0.13",
"@opentelemetry/api": "1.3.0",

View File

@ -1,19 +1,18 @@
import type { ReactElement } from 'react';
import { useState, type ReactElement } from 'react';
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import { useSession, getCsrfToken, signIn } from 'next-auth/react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Image from 'next/image';
import { SessionProvider } from 'next-auth/react';
import { useState } from 'react';
import { useSession, getCsrfToken, signIn, SessionProvider } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { errorToast, successToast } from '@components/Toaster';
import { ButtonPrimary } from '@components/ButtonPrimary';
import Link from 'next/link';
import { ButtonOutline } from '@components/ButtonOutline';
import Loading from '@components/Loading';
import { Login as SSOLogin } from '@boxyhq/react-ui';
import { adminPortalSSODefaults } from '@lib/env';
const Login = ({ csrfToken }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const Login = ({ csrfToken, tenant, product }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const { t } = useTranslation('common');
const router = useRouter();
const { status } = useSession();
@ -56,6 +55,10 @@ const Login = ({ csrfToken }: InferGetServerSidePropsType<typeof getServerSidePr
}
};
const onSSOSubmit = async (ssoIdentifier: string) => {
await signIn('boxyhq-saml', undefined, { client_id: ssoIdentifier });
};
return (
<>
<div className='flex min-h-screen flex-col items-center justify-center'>
@ -90,12 +93,21 @@ const Login = ({ csrfToken }: InferGetServerSidePropsType<typeof getServerSidePr
</label>
</div>
<div className='flex items-baseline justify-between'>
<ButtonPrimary type='submit' loading={loading} className='btn-block'>
<ButtonOutline type='submit' loading={loading} className='btn-block'>
{t('send_magic_link')}
</ButtonPrimary>
</ButtonOutline>
</div>
</div>
</form>
<SSOLogin
buttonText={t('login_with_sso')}
ssoIdentifier={`tenant=${tenant}&product=${product}`}
onSubmit={onSSOSubmit}
classNames={{
container: 'mt-2',
button: 'btn-outline btn-block btn',
}}
/>
</div>
</div>
<Link href='/.well-known' className='my-3 text-sm underline' target='_blank'>
@ -112,9 +124,12 @@ Login.getLayout = function getLayout(page: ReactElement) {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { locale }: GetServerSidePropsContext = context;
const { tenant, product } = adminPortalSSODefaults;
return {
props: {
csrfToken: await getCsrfToken(context),
tenant,
product,
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
},
};

View File

@ -0,0 +1,46 @@
import type { GetServerSidePropsContext, NextPage } from 'next';
import useSWR from 'swr';
import { useRouter } from 'next/router';
import { fetcher } from '@lib/ui/utils';
import EditConnection from '@components/connection/EditConnection';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Loading from '@components/Loading';
import { errorToast } from '@components/Toaster';
import type { ApiError, ApiSuccess } from 'types';
import type { OIDCSSORecord, SAMLSSORecord } from '@boxyhq/saml-jackson';
const EditSSOConnection: NextPage = () => {
const router = useRouter();
const { id } = router.query as { id: string };
const { data, error, isLoading } = useSWR<ApiSuccess<SAMLSSORecord | OIDCSSORecord>, ApiError>(
id ? `/api/admin/connections/${id}` : null,
fetcher,
{
revalidateOnFocus: false,
}
);
if (isLoading) {
return <Loading />;
}
if (error) {
errorToast(error.message);
return null;
}
return <EditConnection connection={data?.data} isSettingsView />;
};
export async function getServerSideProps({ locale }: GetServerSidePropsContext) {
return {
props: {
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
},
};
}
export default EditSSOConnection;

View File

@ -0,0 +1,17 @@
import type { GetServerSidePropsContext, NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import ConnectionList from '@components/connection/ConnectionList';
const ConnectionsIndexPageForSettings: NextPage = () => {
return <ConnectionList isSettingsView />;
};
export default ConnectionsIndexPageForSettings;
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
return {
props: {
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
},
};
}

View File

@ -0,0 +1,24 @@
import type { GetServerSidePropsContext, NextPage } from 'next';
import CreateConnection from '@components/connection/CreateConnection';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import type { AdminPortalSSODefaults } from '@components/connection/utils';
import { adminPortalSSODefaults } from '@lib/env';
type Props = {
adminPortalSSODefaults: AdminPortalSSODefaults;
};
const NewSSOConnection: NextPage<Props> = ({ adminPortalSSODefaults }) => {
return <CreateConnection isSettingsView adminPortalSSODefaults={adminPortalSSODefaults} />;
};
export async function getStaticProps({ locale }: GetServerSidePropsContext) {
return {
props: {
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
adminPortalSSODefaults,
},
};
}
export default NewSSOConnection;

View File

@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { strategyChecker } from '@lib/utils';
import { adminPortalSSODefaults } from '@lib/env';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
@ -23,14 +24,26 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Get all connections
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { adminController } = await jackson();
const { adminController, connectionAPIController } = await jackson();
const { pageOffset, pageLimit } = req.query as {
const { pageOffset, pageLimit, isSystemSSO } = req.query as {
pageOffset: string;
pageLimit: string;
isSystemSSO?: string; // if present will be '' else undefined
};
const connections = await adminController.getAllConnection(+(pageOffset || 0), +(pageLimit || 0));
const { tenant: adminPortalSSOTenant, product: adminPortalSSOProduct } = adminPortalSSODefaults;
const connections =
isSystemSSO === undefined
? (await adminController.getAllConnection(+(pageOffset || 0), +(pageLimit || 0)))?.map((conn) => ({
...conn,
isSystemSSO: adminPortalSSOTenant === conn.tenant && adminPortalSSOProduct === conn.product,
}))
: await connectionAPIController.getConnections({
tenant: adminPortalSSOTenant,
product: adminPortalSSOProduct,
});
return res.json({ data: connections });
};

View File

@ -1,7 +1,9 @@
import Adapter from '@lib/nextAuthAdapter';
import NextAuth from 'next-auth';
import EmailProvider from 'next-auth/providers/email';
import BoxyHQSAMLProvider from 'next-auth/providers/boxyhq-saml';
import { validateEmailWithACL } from '@lib/utils';
import { jacksonOptions as env } from '@lib/env';
import { sessionName } from '@lib/constants';
export default NextAuth({
@ -9,6 +11,16 @@ export default NextAuth({
colorScheme: 'light',
},
providers: [
BoxyHQSAMLProvider({
authorization: { params: { scope: '' } },
issuer: env.externalUrl,
clientId: 'dummy',
clientSecret: 'dummy',
httpOptions: {
timeout: 30000,
},
allowDangerousEmailAccountLinking: true,
}),
EmailProvider({
server: {
host: process.env.SMTP_HOST,
@ -40,12 +52,11 @@ export default NextAuth({
},
},
callbacks: {
async signIn({ user }): Promise<boolean> {
async signIn({ user, account }): Promise<boolean> {
if (!user.email) {
return false;
}
return validateEmailWithACL(user.email);
return account?.provider === 'boxyhq-saml' ? true : validateEmailWithACL(user.email);
},
},
pages: {