feat(sso): allow to use OIDC and SAML (#7246)

## What it does
### Backend
- [x] Add a mutation to create OIDC and SAML configuration
- [x] Add a mutation to delete an SSO config
- [x] Add a feature flag to toggle SSO
- [x] Add a mutation to activate/deactivate an SSO config
- [x] Add a mutation to delete an SSO config
- [x] Add strategy to use OIDC or SAML
- [ ] Improve error management

### Frontend
- [x] Add section "security" in settings
- [x] Add page to list SSO configurations
- [x] Add page and forms to create OIDC or SAML configuration
- [x] Add field to "connect with SSO" in the signin/signup process
- [x] Trigger auth when a user switch to a workspace with SSO enable
- [x] Add an option on the security page to activate/deactivate the
global invitation link
- [ ] Add new Icons for SSO Identity Providers (okta, Auth0, Azure,
Microsoft)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Antoine Moreaux
2024-10-21 20:07:08 +02:00
committed by GitHub
parent 11c3f1c399
commit 0f0a7966b1
132 changed files with 5245 additions and 306 deletions

View File

@ -0,0 +1,36 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useCreateWorkspaceInvitation } from '@/workspace-invitation/hooks/useCreateWorkspaceInvitation';
const mutationSendInvitationsCallSpy = jest.fn();
jest.mock('~/generated/graphql', () => ({
useSendInvitationsMutation: () => [mutationSendInvitationsCallSpy],
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useCreateWorkspaceInvitation', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('Send invitations', async () => {
const invitationParams = { emails: ['test@twenty.com'] };
renderHook(
() => {
const { sendInvitation } = useCreateWorkspaceInvitation();
sendInvitation(invitationParams);
},
{ wrapper: Wrapper },
);
expect(mutationSendInvitationsCallSpy).toHaveBeenCalledWith({
onCompleted: expect.any(Function),
variables: invitationParams,
});
});
});

View File

@ -0,0 +1,38 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useDeleteWorkspaceInvitation } from '@/workspace-invitation/hooks/useDeleteWorkspaceInvitation';
const mutationDeleteWorspaceInvitationCallSpy = jest.fn();
jest.mock('~/generated/graphql', () => ({
useDeleteWorkspaceInvitationMutation: () => [
mutationDeleteWorspaceInvitationCallSpy,
],
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useDeleteWorkspaceInvitation', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('Delete Workspace Invitation', async () => {
const params = { appTokenId: 'test' };
renderHook(
() => {
const { deleteWorkspaceInvitation } = useDeleteWorkspaceInvitation();
deleteWorkspaceInvitation(params);
},
{ wrapper: Wrapper },
);
expect(mutationDeleteWorspaceInvitationCallSpy).toHaveBeenCalledWith({
onCompleted: expect.any(Function),
variables: params,
});
});
});

View File

@ -0,0 +1,38 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useResendWorkspaceInvitation } from '@/workspace-invitation/hooks/useResendWorkspaceInvitation';
const mutationResendWorspaceInvitationCallSpy = jest.fn();
jest.mock('~/generated/graphql', () => ({
useResendWorkspaceInvitationMutation: () => [
mutationResendWorspaceInvitationCallSpy,
],
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useResendWorkspaceInvitation', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('Resend Workspace Invitation', async () => {
const params = { appTokenId: 'test' };
renderHook(
() => {
const { resendInvitation } = useResendWorkspaceInvitation();
resendInvitation(params);
},
{ wrapper: Wrapper },
);
expect(mutationResendWorspaceInvitationCallSpy).toHaveBeenCalledWith({
onCompleted: expect.any(Function),
variables: params,
});
});
});

View File

@ -1,6 +1,8 @@
import { useSetRecoilState } from 'recoil';
import { useSendInvitationsMutation } from '~/generated/graphql';
import { SendInvitationsMutationVariables } from '../../../generated/graphql';
import {
useSendInvitationsMutation,
SendInvitationsMutationVariables,
} from '~/generated/graphql';
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
export const useCreateWorkspaceInvitation = () => {