mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +00:00
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:
49
LICENSE
49
LICENSE
@@ -1,3 +1,8 @@
|
|||||||
|
|
||||||
|
This project is mostly licensed under the GNU General Public License (GPL) as described below. However, certain files within this project are licensed under a different commercial license. These files are clearly marked with the following comment at the top of the file: /* @license Enterprise */
|
||||||
|
Files with this comment are not licensed under the aGPL v3, but instead are subject to the commercial license terms defined later in this file.
|
||||||
|
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
@@ -659,3 +664,47 @@ specific requirements.
|
|||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The Twenty.com Commercial License (the “Commercial License”)
|
||||||
|
Copyright (c) 2023-present Twenty.com, PBC
|
||||||
|
|
||||||
|
With regard to Twenty's Software:
|
||||||
|
|
||||||
|
This part of the software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, the Terms available
|
||||||
|
at https://twenty.com/legal/terms, or other agreements governing
|
||||||
|
the use of the Software, as mutually agreed by you and Twenty.com, PBC ("Twenty"),
|
||||||
|
and otherwise have a valid Twenty Enterprise Edition subscription
|
||||||
|
for the correct number of hosts and seats as defined in the Commercial Terms.
|
||||||
|
Subject to the foregoing sentence,
|
||||||
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
|
that Twenty and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
and to all such modifications and/or patches, and all such modifications and/or
|
||||||
|
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||||
|
exploited with a valid Commercial Subscription for the correct number of hosts and seats.
|
||||||
|
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||||
|
and testing purposes, without requiring a subscription. You agree that Twenty.Com and/or
|
||||||
|
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||||
|
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||||
|
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell the Software.
|
||||||
|
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Twenty Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json",
|
"$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json",
|
||||||
"regexParameters": {
|
"regexParameters": {
|
||||||
"camelCase": "^[a-z]+([A-Za-z0-9]+)+"
|
"camelCase": "^[a-z]+[A-Za-z0-9]+"
|
||||||
},
|
},
|
||||||
"structure": [
|
"structure": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export type AuthProviders = {
|
|||||||
magicLink: Scalars['Boolean']['output'];
|
magicLink: Scalars['Boolean']['output'];
|
||||||
microsoft: Scalars['Boolean']['output'];
|
microsoft: Scalars['Boolean']['output'];
|
||||||
password: Scalars['Boolean']['output'];
|
password: Scalars['Boolean']['output'];
|
||||||
|
sso: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthToken = {
|
export type AuthToken = {
|
||||||
@@ -148,6 +149,7 @@ export enum CaptchaDriverType {
|
|||||||
|
|
||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
|
analyticsEnabled: Scalars['Boolean']['output'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
@@ -275,6 +277,15 @@ export type DeleteServerlessFunctionInput = {
|
|||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteSsoInput = {
|
||||||
|
identityProviderId: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSsoOutput = {
|
||||||
|
__typename?: 'DeleteSsoOutput';
|
||||||
|
identityProviderId: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
/** Schema update on a table */
|
/** Schema update on a table */
|
||||||
export enum DistantTableUpdate {
|
export enum DistantTableUpdate {
|
||||||
ColumnsAdded = 'COLUMNS_ADDED',
|
ColumnsAdded = 'COLUMNS_ADDED',
|
||||||
@@ -283,6 +294,20 @@ export enum DistantTableUpdate {
|
|||||||
TableDeleted = 'TABLE_DELETED'
|
TableDeleted = 'TABLE_DELETED'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EditSsoInput = {
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditSsoOutput = {
|
||||||
|
__typename?: 'EditSsoOutput';
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
issuer: Scalars['String']['output'];
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
__typename?: 'EmailPasswordResetLink';
|
__typename?: 'EmailPasswordResetLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@@ -372,6 +397,20 @@ export enum FileFolder {
|
|||||||
WorkspaceLogo = 'WorkspaceLogo'
|
WorkspaceLogo = 'WorkspaceLogo'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FindAvailableSsoidpInput = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindAvailableSsoidpOutput = {
|
||||||
|
__typename?: 'FindAvailableSSOIDPOutput';
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
issuer: Scalars['String']['output'];
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
workspace: WorkspaceNameAndId;
|
||||||
|
};
|
||||||
|
|
||||||
export type FindManyRemoteTablesInput = {
|
export type FindManyRemoteTablesInput = {
|
||||||
/** The id of the remote server. */
|
/** The id of the remote server. */
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
@@ -385,6 +424,33 @@ export type FullName = {
|
|||||||
lastName: Scalars['String']['output'];
|
lastName: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
||||||
|
|
||||||
|
export type GenerateJwtOutputWithAuthTokens = {
|
||||||
|
__typename?: 'GenerateJWTOutputWithAuthTokens';
|
||||||
|
authTokens: AuthTokens;
|
||||||
|
reason: Scalars['String']['output'];
|
||||||
|
success: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateJwtOutputWithSsoauth = {
|
||||||
|
__typename?: 'GenerateJWTOutputWithSSOAUTH';
|
||||||
|
availableSSOIDPs: Array<FindAvailableSsoidpOutput>;
|
||||||
|
reason: Scalars['String']['output'];
|
||||||
|
success: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlInput = {
|
||||||
|
identityProviderId: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlOutput = {
|
||||||
|
__typename?: 'GetAuthorizationUrlOutput';
|
||||||
|
authorizationURL: Scalars['String']['output'];
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
type: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type GetServerlessFunctionSourceCodeInput = {
|
export type GetServerlessFunctionSourceCodeInput = {
|
||||||
/** The id of the function. */
|
/** The id of the function. */
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
@@ -392,6 +458,11 @@ export type GetServerlessFunctionSourceCodeInput = {
|
|||||||
version?: Scalars['String']['input'];
|
version?: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum IdpType {
|
||||||
|
Oidc = 'OIDC',
|
||||||
|
Saml = 'SAML'
|
||||||
|
}
|
||||||
|
|
||||||
export type IndexConnection = {
|
export type IndexConnection = {
|
||||||
__typename?: 'IndexConnection';
|
__typename?: 'IndexConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@@ -461,12 +532,14 @@ export type Mutation = {
|
|||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
challenge: LoginToken;
|
challenge: LoginToken;
|
||||||
checkoutSession: SessionEntity;
|
checkoutSession: SessionEntity;
|
||||||
|
createOIDCIdentityProvider: SetupSsoOutput;
|
||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
createOneRelation: Relation;
|
createOneRelation: Relation;
|
||||||
createOneRemoteServer: RemoteServer;
|
createOneRemoteServer: RemoteServer;
|
||||||
createOneServerlessFunction: ServerlessFunction;
|
createOneServerlessFunction: ServerlessFunction;
|
||||||
|
createSAMLIdentityProvider: SetupSsoOutput;
|
||||||
deactivateWorkflowVersion: Scalars['Boolean']['output'];
|
deactivateWorkflowVersion: Scalars['Boolean']['output'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteOneField: Field;
|
deleteOneField: Field;
|
||||||
@@ -474,16 +547,20 @@ export type Mutation = {
|
|||||||
deleteOneRelation: Relation;
|
deleteOneRelation: Relation;
|
||||||
deleteOneRemoteServer: RemoteServer;
|
deleteOneRemoteServer: RemoteServer;
|
||||||
deleteOneServerlessFunction: ServerlessFunction;
|
deleteOneServerlessFunction: ServerlessFunction;
|
||||||
|
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||||
deleteUser: User;
|
deleteUser: User;
|
||||||
deleteWorkspaceInvitation: Scalars['String']['output'];
|
deleteWorkspaceInvitation: Scalars['String']['output'];
|
||||||
disablePostgresProxy: PostgresCredentials;
|
disablePostgresProxy: PostgresCredentials;
|
||||||
|
editSSOIdentityProvider: EditSsoOutput;
|
||||||
emailPasswordResetLink: EmailPasswordResetLink;
|
emailPasswordResetLink: EmailPasswordResetLink;
|
||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
|
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateJWT: AuthTokens;
|
generateJWT: GenerateJwt;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
publishServerlessFunction: ServerlessFunction;
|
publishServerlessFunction: ServerlessFunction;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
@@ -551,6 +628,11 @@ export type MutationCheckoutSessionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOidcIdentityProviderArgs = {
|
||||||
|
input: SetupOidcSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneAppTokenArgs = {
|
export type MutationCreateOneAppTokenArgs = {
|
||||||
input: CreateOneAppTokenInput;
|
input: CreateOneAppTokenInput;
|
||||||
};
|
};
|
||||||
@@ -581,6 +663,11 @@ export type MutationCreateOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateSamlIdentityProviderArgs = {
|
||||||
|
input: SetupSamlSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeactivateWorkflowVersionArgs = {
|
export type MutationDeactivateWorkflowVersionArgs = {
|
||||||
workflowVersionId: Scalars['String']['input'];
|
workflowVersionId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@@ -611,11 +698,21 @@ export type MutationDeleteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteSsoIdentityProviderArgs = {
|
||||||
|
input: DeleteSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||||
appTokenId: Scalars['String']['input'];
|
appTokenId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationEditSsoIdentityProviderArgs = {
|
||||||
|
input: EditSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationEmailPasswordResetLinkArgs = {
|
export type MutationEmailPasswordResetLinkArgs = {
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@@ -633,6 +730,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||||
|
input: FindAvailableSsoidpInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateApiKeyTokenArgs = {
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
apiKeyId: Scalars['String']['input'];
|
apiKeyId: Scalars['String']['input'];
|
||||||
expiresAt: Scalars['String']['input'];
|
expiresAt: Scalars['String']['input'];
|
||||||
@@ -644,6 +746,11 @@ export type MutationGenerateJwtArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationGetAuthorizationUrlArgs = {
|
||||||
|
input: GetAuthorizationUrlInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationImpersonateArgs = {
|
export type MutationImpersonateArgs = {
|
||||||
userId: Scalars['String']['input'];
|
userId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@@ -865,6 +972,7 @@ export type Query = {
|
|||||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||||
index: Index;
|
index: Index;
|
||||||
indexMetadatas: IndexConnection;
|
indexMetadatas: IndexConnection;
|
||||||
|
listSSOIdentityProvidersByWorkspaceId: Array<FindAvailableSsoidpOutput>;
|
||||||
object: Object;
|
object: Object;
|
||||||
objects: ObjectConnection;
|
objects: ObjectConnection;
|
||||||
relation: Relation;
|
relation: Relation;
|
||||||
@@ -1091,6 +1199,12 @@ export type RunWorkflowVersionInput = {
|
|||||||
workflowVersionId: Scalars['String']['input'];
|
workflowVersionId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SsoIdentityProviderStatus {
|
||||||
|
Active = 'Active',
|
||||||
|
Error = 'Error',
|
||||||
|
Inactive = 'Inactive'
|
||||||
|
}
|
||||||
|
|
||||||
export type SendInvitationsOutput = {
|
export type SendInvitationsOutput = {
|
||||||
__typename?: 'SendInvitationsOutput';
|
__typename?: 'SendInvitationsOutput';
|
||||||
errors: Array<Scalars['String']['output']>;
|
errors: Array<Scalars['String']['output']>;
|
||||||
@@ -1179,6 +1293,31 @@ export type SessionEntity = {
|
|||||||
url?: Maybe<Scalars['String']['output']>;
|
url?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SetupOidcSsoInput = {
|
||||||
|
clientID: Scalars['String']['input'];
|
||||||
|
clientSecret: Scalars['String']['input'];
|
||||||
|
issuer: Scalars['String']['input'];
|
||||||
|
name: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetupSamlSsoInput = {
|
||||||
|
certificate: Scalars['String']['input'];
|
||||||
|
fingerprint?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
issuer: Scalars['String']['input'];
|
||||||
|
name: Scalars['String']['input'];
|
||||||
|
ssoURL: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetupSsoOutput = {
|
||||||
|
__typename?: 'SetupSsoOutput';
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
issuer: Scalars['String']['output'];
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
};
|
||||||
|
|
||||||
/** Sort Directions */
|
/** Sort Directions */
|
||||||
export enum SortDirection {
|
export enum SortDirection {
|
||||||
Asc = 'ASC',
|
Asc = 'ASC',
|
||||||
@@ -1368,11 +1507,13 @@ export type UpdateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||||
domainName?: InputMaybe<Scalars['String']['input']>;
|
domainName?: InputMaybe<Scalars['String']['input']>;
|
||||||
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
logo?: InputMaybe<Scalars['String']['input']>;
|
logo?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
|
analyticsTinybirdJwt?: Maybe<Scalars['String']['output']>;
|
||||||
canImpersonate: Scalars['Boolean']['output'];
|
canImpersonate: Scalars['Boolean']['output'];
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
||||||
@@ -1467,6 +1608,7 @@ export type Workspace = {
|
|||||||
featureFlags?: Maybe<Array<FeatureFlag>>;
|
featureFlags?: Maybe<Array<FeatureFlag>>;
|
||||||
id: Scalars['UUID']['output'];
|
id: Scalars['UUID']['output'];
|
||||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||||
|
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
|
||||||
logo?: Maybe<Scalars['String']['output']>;
|
logo?: Maybe<Scalars['String']['output']>;
|
||||||
metadataVersion: Scalars['Float']['output'];
|
metadataVersion: Scalars['Float']['output'];
|
||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
@@ -1539,6 +1681,12 @@ export enum WorkspaceMemberTimeFormatEnum {
|
|||||||
System = 'SYSTEM'
|
System = 'SYSTEM'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceNameAndId = {
|
||||||
|
__typename?: 'WorkspaceNameAndId';
|
||||||
|
displayName?: Maybe<Scalars['String']['output']>;
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
__typename?: 'field';
|
__typename?: 'field';
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export type AuthProviders = {
|
|||||||
magicLink: Scalars['Boolean'];
|
magicLink: Scalars['Boolean'];
|
||||||
microsoft: Scalars['Boolean'];
|
microsoft: Scalars['Boolean'];
|
||||||
password: Scalars['Boolean'];
|
password: Scalars['Boolean'];
|
||||||
|
sso: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthToken = {
|
export type AuthToken = {
|
||||||
@@ -180,6 +181,15 @@ export type DeleteServerlessFunctionInput = {
|
|||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteSsoInput = {
|
||||||
|
identityProviderId: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteSsoOutput = {
|
||||||
|
__typename?: 'DeleteSsoOutput';
|
||||||
|
identityProviderId: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
/** Schema update on a table */
|
/** Schema update on a table */
|
||||||
export enum DistantTableUpdate {
|
export enum DistantTableUpdate {
|
||||||
ColumnsAdded = 'COLUMNS_ADDED',
|
ColumnsAdded = 'COLUMNS_ADDED',
|
||||||
@@ -188,6 +198,20 @@ export enum DistantTableUpdate {
|
|||||||
TableDeleted = 'TABLE_DELETED'
|
TableDeleted = 'TABLE_DELETED'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EditSsoInput = {
|
||||||
|
id: Scalars['String'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditSsoOutput = {
|
||||||
|
__typename?: 'EditSsoOutput';
|
||||||
|
id: Scalars['String'];
|
||||||
|
issuer: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
__typename?: 'EmailPasswordResetLink';
|
__typename?: 'EmailPasswordResetLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@@ -277,12 +301,53 @@ export enum FileFolder {
|
|||||||
WorkspaceLogo = 'WorkspaceLogo'
|
WorkspaceLogo = 'WorkspaceLogo'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FindAvailableSsoidpInput = {
|
||||||
|
email: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindAvailableSsoidpOutput = {
|
||||||
|
__typename?: 'FindAvailableSSOIDPOutput';
|
||||||
|
id: Scalars['String'];
|
||||||
|
issuer: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
workspace: WorkspaceNameAndId;
|
||||||
|
};
|
||||||
|
|
||||||
export type FullName = {
|
export type FullName = {
|
||||||
__typename?: 'FullName';
|
__typename?: 'FullName';
|
||||||
firstName: Scalars['String'];
|
firstName: Scalars['String'];
|
||||||
lastName: Scalars['String'];
|
lastName: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
||||||
|
|
||||||
|
export type GenerateJwtOutputWithAuthTokens = {
|
||||||
|
__typename?: 'GenerateJWTOutputWithAuthTokens';
|
||||||
|
authTokens: AuthTokens;
|
||||||
|
reason: Scalars['String'];
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateJwtOutputWithSsoauth = {
|
||||||
|
__typename?: 'GenerateJWTOutputWithSSOAUTH';
|
||||||
|
availableSSOIDPs: Array<FindAvailableSsoidpOutput>;
|
||||||
|
reason: Scalars['String'];
|
||||||
|
success: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlInput = {
|
||||||
|
identityProviderId: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlOutput = {
|
||||||
|
__typename?: 'GetAuthorizationUrlOutput';
|
||||||
|
authorizationURL: Scalars['String'];
|
||||||
|
id: Scalars['String'];
|
||||||
|
type: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type GetServerlessFunctionSourceCodeInput = {
|
export type GetServerlessFunctionSourceCodeInput = {
|
||||||
/** The id of the function. */
|
/** The id of the function. */
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
@@ -290,6 +355,11 @@ export type GetServerlessFunctionSourceCodeInput = {
|
|||||||
version?: Scalars['String'];
|
version?: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum IdpType {
|
||||||
|
Oidc = 'OIDC',
|
||||||
|
Saml = 'SAML'
|
||||||
|
}
|
||||||
|
|
||||||
export type IndexConnection = {
|
export type IndexConnection = {
|
||||||
__typename?: 'IndexConnection';
|
__typename?: 'IndexConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@@ -359,23 +429,29 @@ export type Mutation = {
|
|||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
challenge: LoginToken;
|
challenge: LoginToken;
|
||||||
checkoutSession: SessionEntity;
|
checkoutSession: SessionEntity;
|
||||||
|
createOIDCIdentityProvider: SetupSsoOutput;
|
||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
createOneServerlessFunction: ServerlessFunction;
|
createOneServerlessFunction: ServerlessFunction;
|
||||||
|
createSAMLIdentityProvider: SetupSsoOutput;
|
||||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
deleteOneServerlessFunction: ServerlessFunction;
|
deleteOneServerlessFunction: ServerlessFunction;
|
||||||
|
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||||
deleteUser: User;
|
deleteUser: User;
|
||||||
deleteWorkspaceInvitation: Scalars['String'];
|
deleteWorkspaceInvitation: Scalars['String'];
|
||||||
disablePostgresProxy: PostgresCredentials;
|
disablePostgresProxy: PostgresCredentials;
|
||||||
|
editSSOIdentityProvider: EditSsoOutput;
|
||||||
emailPasswordResetLink: EmailPasswordResetLink;
|
emailPasswordResetLink: EmailPasswordResetLink;
|
||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
|
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateJWT: AuthTokens;
|
generateJWT: GenerateJwt;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
publishServerlessFunction: ServerlessFunction;
|
publishServerlessFunction: ServerlessFunction;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
@@ -438,11 +514,21 @@ export type MutationCheckoutSessionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOidcIdentityProviderArgs = {
|
||||||
|
input: SetupOidcSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneServerlessFunctionArgs = {
|
export type MutationCreateOneServerlessFunctionArgs = {
|
||||||
input: CreateServerlessFunctionInput;
|
input: CreateServerlessFunctionInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateSamlIdentityProviderArgs = {
|
||||||
|
input: SetupSamlSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeactivateWorkflowVersionArgs = {
|
export type MutationDeactivateWorkflowVersionArgs = {
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -458,11 +544,21 @@ export type MutationDeleteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteSsoIdentityProviderArgs = {
|
||||||
|
input: DeleteSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||||
appTokenId: Scalars['String'];
|
appTokenId: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationEditSsoIdentityProviderArgs = {
|
||||||
|
input: EditSsoInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationEmailPasswordResetLinkArgs = {
|
export type MutationEmailPasswordResetLinkArgs = {
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -480,6 +576,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||||
|
input: FindAvailableSsoidpInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateApiKeyTokenArgs = {
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
apiKeyId: Scalars['String'];
|
apiKeyId: Scalars['String'];
|
||||||
expiresAt: Scalars['String'];
|
expiresAt: Scalars['String'];
|
||||||
@@ -491,6 +592,11 @@ export type MutationGenerateJwtArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationGetAuthorizationUrlArgs = {
|
||||||
|
input: GetAuthorizationUrlInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationImpersonateArgs = {
|
export type MutationImpersonateArgs = {
|
||||||
userId: Scalars['String'];
|
userId: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -682,6 +788,7 @@ export type Query = {
|
|||||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||||
index: Index;
|
index: Index;
|
||||||
indexMetadatas: IndexConnection;
|
indexMetadatas: IndexConnection;
|
||||||
|
listSSOIdentityProvidersByWorkspaceId: Array<FindAvailableSsoidpOutput>;
|
||||||
object: Object;
|
object: Object;
|
||||||
objects: ObjectConnection;
|
objects: ObjectConnection;
|
||||||
serverlessFunction: ServerlessFunction;
|
serverlessFunction: ServerlessFunction;
|
||||||
@@ -822,6 +929,12 @@ export type RunWorkflowVersionInput = {
|
|||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SsoIdentityProviderStatus {
|
||||||
|
Active = 'Active',
|
||||||
|
Error = 'Error',
|
||||||
|
Inactive = 'Inactive'
|
||||||
|
}
|
||||||
|
|
||||||
export type SendInvitationsOutput = {
|
export type SendInvitationsOutput = {
|
||||||
__typename?: 'SendInvitationsOutput';
|
__typename?: 'SendInvitationsOutput';
|
||||||
errors: Array<Scalars['String']>;
|
errors: Array<Scalars['String']>;
|
||||||
@@ -894,6 +1007,31 @@ export type SessionEntity = {
|
|||||||
url?: Maybe<Scalars['String']>;
|
url?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SetupOidcSsoInput = {
|
||||||
|
clientID: Scalars['String'];
|
||||||
|
clientSecret: Scalars['String'];
|
||||||
|
issuer: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetupSamlSsoInput = {
|
||||||
|
certificate: Scalars['String'];
|
||||||
|
fingerprint?: InputMaybe<Scalars['String']>;
|
||||||
|
id: Scalars['String'];
|
||||||
|
issuer: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
ssoURL: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetupSsoOutput = {
|
||||||
|
__typename?: 'SetupSsoOutput';
|
||||||
|
id: Scalars['String'];
|
||||||
|
issuer: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
type: IdpType;
|
||||||
|
};
|
||||||
|
|
||||||
/** Sort Directions */
|
/** Sort Directions */
|
||||||
export enum SortDirection {
|
export enum SortDirection {
|
||||||
Asc = 'ASC',
|
Asc = 'ASC',
|
||||||
@@ -1053,6 +1191,7 @@ export type UpdateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
domainName?: InputMaybe<Scalars['String']>;
|
domainName?: InputMaybe<Scalars['String']>;
|
||||||
inviteHash?: InputMaybe<Scalars['String']>;
|
inviteHash?: InputMaybe<Scalars['String']>;
|
||||||
|
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
logo?: InputMaybe<Scalars['String']>;
|
logo?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1143,6 +1282,7 @@ export type Workspace = {
|
|||||||
featureFlags?: Maybe<Array<FeatureFlag>>;
|
featureFlags?: Maybe<Array<FeatureFlag>>;
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
inviteHash?: Maybe<Scalars['String']>;
|
inviteHash?: Maybe<Scalars['String']>;
|
||||||
|
isPublicInviteLinkEnabled: Scalars['Boolean'];
|
||||||
logo?: Maybe<Scalars['String']>;
|
logo?: Maybe<Scalars['String']>;
|
||||||
metadataVersion: Scalars['Float'];
|
metadataVersion: Scalars['Float'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
@@ -1215,6 +1355,12 @@ export enum WorkspaceMemberTimeFormatEnum {
|
|||||||
System = 'SYSTEM'
|
System = 'SYSTEM'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceNameAndId = {
|
||||||
|
__typename?: 'WorkspaceNameAndId';
|
||||||
|
displayName?: Maybe<Scalars['String']>;
|
||||||
|
id: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
__typename?: 'field';
|
__typename?: 'field';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
@@ -1471,6 +1617,8 @@ export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: strin
|
|||||||
|
|
||||||
export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } };
|
export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } };
|
||||||
|
|
||||||
|
export type AvailableSsoIdentityProvidersFragmentFragment = { __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } };
|
||||||
|
|
||||||
export type AuthorizeAppMutationVariables = Exact<{
|
export type AuthorizeAppMutationVariables = Exact<{
|
||||||
clientId: Scalars['String'];
|
clientId: Scalars['String'];
|
||||||
codeChallenge: Scalars['String'];
|
codeChallenge: Scalars['String'];
|
||||||
@@ -1496,6 +1644,13 @@ export type EmailPasswordResetLinkMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
||||||
|
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutationVariables = Exact<{
|
||||||
|
input: FindAvailableSsoidpInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutation = { __typename?: 'Mutation', findAvailableSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }> };
|
||||||
|
|
||||||
export type GenerateApiKeyTokenMutationVariables = Exact<{
|
export type GenerateApiKeyTokenMutationVariables = Exact<{
|
||||||
apiKeyId: Scalars['String'];
|
apiKeyId: Scalars['String'];
|
||||||
expiresAt: Scalars['String'];
|
expiresAt: Scalars['String'];
|
||||||
@@ -1509,19 +1664,26 @@ export type GenerateJwtMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'GenerateJWTOutputWithAuthTokens', success: boolean, reason: string, authTokens: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } } | { __typename?: 'GenerateJWTOutputWithSSOAUTH', success: boolean, reason: string, availableSSOIDPs: Array<{ __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }> } };
|
||||||
|
|
||||||
export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: never; }>;
|
export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GenerateTransientTokenMutation = { __typename?: 'Mutation', generateTransientToken: { __typename?: 'TransientToken', transientToken: { __typename?: 'AuthToken', token: string } } };
|
export type GenerateTransientTokenMutation = { __typename?: 'Mutation', generateTransientToken: { __typename?: 'TransientToken', transientToken: { __typename?: 'AuthToken', token: string } } };
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlMutationVariables = Exact<{
|
||||||
|
input: GetAuthorizationUrlInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetAuthorizationUrlMutation = { __typename?: 'Mutation', getAuthorizationUrl: { __typename?: 'GetAuthorizationUrlOutput', id: string, type: string, authorizationURL: string } };
|
||||||
|
|
||||||
export type ImpersonateMutationVariables = Exact<{
|
export type ImpersonateMutationVariables = Exact<{
|
||||||
userId: Scalars['String'];
|
userId: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
appToken: Scalars['String'];
|
||||||
@@ -1554,7 +1716,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CheckUserExistsQueryVariables = Exact<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@@ -1601,14 +1763,47 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
|
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
|
||||||
|
|
||||||
export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
|
export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
||||||
|
input: SetupOidcSsoInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateOidcIdentityProviderMutation = { __typename?: 'Mutation', createOIDCIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||||
|
|
||||||
|
export type CreateSamlIdentityProviderMutationVariables = Exact<{
|
||||||
|
input: SetupSamlSsoInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateSamlIdentityProviderMutation = { __typename?: 'Mutation', createSAMLIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||||
|
|
||||||
|
export type DeleteSsoIdentityProviderMutationVariables = Exact<{
|
||||||
|
input: DeleteSsoInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DeleteSsoIdentityProviderMutation = { __typename?: 'Mutation', deleteSSOIdentityProvider: { __typename?: 'DeleteSsoOutput', identityProviderId: string } };
|
||||||
|
|
||||||
|
export type EditSsoIdentityProviderMutationVariables = Exact<{
|
||||||
|
input: EditSsoInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||||
|
|
||||||
|
export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
||||||
|
|
||||||
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@@ -1625,7 +1820,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||||
|
|
||||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
@@ -1803,6 +1998,18 @@ export const AuthTokensFragmentFragmentDoc = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
${AuthTokenFragmentFragmentDoc}`;
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
|
export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
|
||||||
|
fragment AvailableSSOIdentityProvidersFragment on FindAvailableSSOIDPOutput {
|
||||||
|
id
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
||||||
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||||
id
|
id
|
||||||
@@ -1842,6 +2049,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
inviteHash
|
inviteHash
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
activationStatus
|
activationStatus
|
||||||
|
isPublicInviteLinkEnabled
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
key
|
key
|
||||||
@@ -2238,6 +2446,39 @@ export function useEmailPasswordResetLinkMutation(baseOptions?: Apollo.MutationH
|
|||||||
export type EmailPasswordResetLinkMutationHookResult = ReturnType<typeof useEmailPasswordResetLinkMutation>;
|
export type EmailPasswordResetLinkMutationHookResult = ReturnType<typeof useEmailPasswordResetLinkMutation>;
|
||||||
export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult<EmailPasswordResetLinkMutation>;
|
export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult<EmailPasswordResetLinkMutation>;
|
||||||
export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions<EmailPasswordResetLinkMutation, EmailPasswordResetLinkMutationVariables>;
|
export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions<EmailPasswordResetLinkMutation, EmailPasswordResetLinkMutationVariables>;
|
||||||
|
export const FindAvailableSsoIdentityProvidersDocument = gql`
|
||||||
|
mutation FindAvailableSSOIdentityProviders($input: FindAvailableSSOIDPInput!) {
|
||||||
|
findAvailableSSOIdentityProviders(input: $input) {
|
||||||
|
...AvailableSSOIdentityProvidersFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${AvailableSsoIdentityProvidersFragmentFragmentDoc}`;
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutationFn = Apollo.MutationFunction<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useFindAvailableSsoIdentityProvidersMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useFindAvailableSsoIdentityProvidersMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useFindAvailableSsoIdentityProvidersMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [findAvailableSsoIdentityProvidersMutation, { data, loading, error }] = useFindAvailableSsoIdentityProvidersMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useFindAvailableSsoIdentityProvidersMutation(baseOptions?: Apollo.MutationHookOptions<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>(FindAvailableSsoIdentityProvidersDocument, options);
|
||||||
|
}
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutationHookResult = ReturnType<typeof useFindAvailableSsoIdentityProvidersMutation>;
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutationResult = Apollo.MutationResult<FindAvailableSsoIdentityProvidersMutation>;
|
||||||
|
export type FindAvailableSsoIdentityProvidersMutationOptions = Apollo.BaseMutationOptions<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>;
|
||||||
export const GenerateApiKeyTokenDocument = gql`
|
export const GenerateApiKeyTokenDocument = gql`
|
||||||
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
|
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
|
||||||
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
|
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
|
||||||
@@ -2275,12 +2516,26 @@ export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions<Gene
|
|||||||
export const GenerateJwtDocument = gql`
|
export const GenerateJwtDocument = gql`
|
||||||
mutation GenerateJWT($workspaceId: String!) {
|
mutation GenerateJWT($workspaceId: String!) {
|
||||||
generateJWT(workspaceId: $workspaceId) {
|
generateJWT(workspaceId: $workspaceId) {
|
||||||
|
... on GenerateJWTOutputWithAuthTokens {
|
||||||
|
success
|
||||||
|
reason
|
||||||
|
authTokens {
|
||||||
tokens {
|
tokens {
|
||||||
...AuthTokensFragment
|
...AuthTokensFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${AuthTokensFragmentFragmentDoc}`;
|
... on GenerateJWTOutputWithSSOAUTH {
|
||||||
|
success
|
||||||
|
reason
|
||||||
|
availableSSOIDPs {
|
||||||
|
...AvailableSSOIdentityProvidersFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${AuthTokensFragmentFragmentDoc}
|
||||||
|
${AvailableSsoIdentityProvidersFragmentFragmentDoc}`;
|
||||||
export type GenerateJwtMutationFn = Apollo.MutationFunction<GenerateJwtMutation, GenerateJwtMutationVariables>;
|
export type GenerateJwtMutationFn = Apollo.MutationFunction<GenerateJwtMutation, GenerateJwtMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2341,6 +2596,41 @@ export function useGenerateTransientTokenMutation(baseOptions?: Apollo.MutationH
|
|||||||
export type GenerateTransientTokenMutationHookResult = ReturnType<typeof useGenerateTransientTokenMutation>;
|
export type GenerateTransientTokenMutationHookResult = ReturnType<typeof useGenerateTransientTokenMutation>;
|
||||||
export type GenerateTransientTokenMutationResult = Apollo.MutationResult<GenerateTransientTokenMutation>;
|
export type GenerateTransientTokenMutationResult = Apollo.MutationResult<GenerateTransientTokenMutation>;
|
||||||
export type GenerateTransientTokenMutationOptions = Apollo.BaseMutationOptions<GenerateTransientTokenMutation, GenerateTransientTokenMutationVariables>;
|
export type GenerateTransientTokenMutationOptions = Apollo.BaseMutationOptions<GenerateTransientTokenMutation, GenerateTransientTokenMutationVariables>;
|
||||||
|
export const GetAuthorizationUrlDocument = gql`
|
||||||
|
mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) {
|
||||||
|
getAuthorizationUrl(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
authorizationURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type GetAuthorizationUrlMutationFn = Apollo.MutationFunction<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetAuthorizationUrlMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useGetAuthorizationUrlMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetAuthorizationUrlMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [getAuthorizationUrlMutation, { data, loading, error }] = useGetAuthorizationUrlMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetAuthorizationUrlMutation(baseOptions?: Apollo.MutationHookOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>(GetAuthorizationUrlDocument, options);
|
||||||
|
}
|
||||||
|
export type GetAuthorizationUrlMutationHookResult = ReturnType<typeof useGetAuthorizationUrlMutation>;
|
||||||
|
export type GetAuthorizationUrlMutationResult = Apollo.MutationResult<GetAuthorizationUrlMutation>;
|
||||||
|
export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
||||||
export const ImpersonateDocument = gql`
|
export const ImpersonateDocument = gql`
|
||||||
mutation Impersonate($userId: String!) {
|
mutation Impersonate($userId: String!) {
|
||||||
impersonate(userId: $userId) {
|
impersonate(userId: $userId) {
|
||||||
@@ -2759,6 +3049,7 @@ export const GetClientConfigDocument = gql`
|
|||||||
google
|
google
|
||||||
password
|
password
|
||||||
microsoft
|
microsoft
|
||||||
|
sso
|
||||||
}
|
}
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
@@ -2848,6 +3139,188 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
|
|||||||
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
||||||
|
export const CreateOidcIdentityProviderDocument = gql`
|
||||||
|
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
|
||||||
|
createOIDCIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateOidcIdentityProviderMutationFn = Apollo.MutationFunction<CreateOidcIdentityProviderMutation, CreateOidcIdentityProviderMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateOidcIdentityProviderMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateOidcIdentityProviderMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateOidcIdentityProviderMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createOidcIdentityProviderMutation, { data, loading, error }] = useCreateOidcIdentityProviderMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateOidcIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions<CreateOidcIdentityProviderMutation, CreateOidcIdentityProviderMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateOidcIdentityProviderMutation, CreateOidcIdentityProviderMutationVariables>(CreateOidcIdentityProviderDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateOidcIdentityProviderMutationHookResult = ReturnType<typeof useCreateOidcIdentityProviderMutation>;
|
||||||
|
export type CreateOidcIdentityProviderMutationResult = Apollo.MutationResult<CreateOidcIdentityProviderMutation>;
|
||||||
|
export type CreateOidcIdentityProviderMutationOptions = Apollo.BaseMutationOptions<CreateOidcIdentityProviderMutation, CreateOidcIdentityProviderMutationVariables>;
|
||||||
|
export const CreateSamlIdentityProviderDocument = gql`
|
||||||
|
mutation CreateSAMLIdentityProvider($input: SetupSAMLSsoInput!) {
|
||||||
|
createSAMLIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateSamlIdentityProviderMutationFn = Apollo.MutationFunction<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateSamlIdentityProviderMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateSamlIdentityProviderMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateSamlIdentityProviderMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createSamlIdentityProviderMutation, { data, loading, error }] = useCreateSamlIdentityProviderMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateSamlIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>(CreateSamlIdentityProviderDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateSamlIdentityProviderMutationHookResult = ReturnType<typeof useCreateSamlIdentityProviderMutation>;
|
||||||
|
export type CreateSamlIdentityProviderMutationResult = Apollo.MutationResult<CreateSamlIdentityProviderMutation>;
|
||||||
|
export type CreateSamlIdentityProviderMutationOptions = Apollo.BaseMutationOptions<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>;
|
||||||
|
export const DeleteSsoIdentityProviderDocument = gql`
|
||||||
|
mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) {
|
||||||
|
deleteSSOIdentityProvider(input: $input) {
|
||||||
|
identityProviderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type DeleteSsoIdentityProviderMutationFn = Apollo.MutationFunction<DeleteSsoIdentityProviderMutation, DeleteSsoIdentityProviderMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDeleteSsoIdentityProviderMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDeleteSsoIdentityProviderMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDeleteSsoIdentityProviderMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [deleteSsoIdentityProviderMutation, { data, loading, error }] = useDeleteSsoIdentityProviderMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDeleteSsoIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions<DeleteSsoIdentityProviderMutation, DeleteSsoIdentityProviderMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<DeleteSsoIdentityProviderMutation, DeleteSsoIdentityProviderMutationVariables>(DeleteSsoIdentityProviderDocument, options);
|
||||||
|
}
|
||||||
|
export type DeleteSsoIdentityProviderMutationHookResult = ReturnType<typeof useDeleteSsoIdentityProviderMutation>;
|
||||||
|
export type DeleteSsoIdentityProviderMutationResult = Apollo.MutationResult<DeleteSsoIdentityProviderMutation>;
|
||||||
|
export type DeleteSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions<DeleteSsoIdentityProviderMutation, DeleteSsoIdentityProviderMutationVariables>;
|
||||||
|
export const EditSsoIdentityProviderDocument = gql`
|
||||||
|
mutation EditSSOIdentityProvider($input: EditSsoInput!) {
|
||||||
|
editSSOIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type EditSsoIdentityProviderMutationFn = Apollo.MutationFunction<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useEditSsoIdentityProviderMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useEditSsoIdentityProviderMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useEditSsoIdentityProviderMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [editSsoIdentityProviderMutation, { data, loading, error }] = useEditSsoIdentityProviderMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useEditSsoIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>(EditSsoIdentityProviderDocument, options);
|
||||||
|
}
|
||||||
|
export type EditSsoIdentityProviderMutationHookResult = ReturnType<typeof useEditSsoIdentityProviderMutation>;
|
||||||
|
export type EditSsoIdentityProviderMutationResult = Apollo.MutationResult<EditSsoIdentityProviderMutation>;
|
||||||
|
export type EditSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>;
|
||||||
|
export const ListSsoIdentityProvidersByWorkspaceIdDocument = gql`
|
||||||
|
query ListSSOIdentityProvidersByWorkspaceId {
|
||||||
|
listSSOIdentityProvidersByWorkspaceId {
|
||||||
|
type
|
||||||
|
id
|
||||||
|
name
|
||||||
|
issuer
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useListSsoIdentityProvidersByWorkspaceIdQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useListSsoIdentityProvidersByWorkspaceIdQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useListSsoIdentityProvidersByWorkspaceIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useListSsoIdentityProvidersByWorkspaceIdQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useListSsoIdentityProvidersByWorkspaceIdQuery(baseOptions?: Apollo.QueryHookOptions<ListSsoIdentityProvidersByWorkspaceIdQuery, ListSsoIdentityProvidersByWorkspaceIdQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ListSsoIdentityProvidersByWorkspaceIdQuery, ListSsoIdentityProvidersByWorkspaceIdQueryVariables>(ListSsoIdentityProvidersByWorkspaceIdDocument, options);
|
||||||
|
}
|
||||||
|
export function useListSsoIdentityProvidersByWorkspaceIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ListSsoIdentityProvidersByWorkspaceIdQuery, ListSsoIdentityProvidersByWorkspaceIdQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ListSsoIdentityProvidersByWorkspaceIdQuery, ListSsoIdentityProvidersByWorkspaceIdQueryVariables>(ListSsoIdentityProvidersByWorkspaceIdDocument, options);
|
||||||
|
}
|
||||||
|
export type ListSsoIdentityProvidersByWorkspaceIdQueryHookResult = ReturnType<typeof useListSsoIdentityProvidersByWorkspaceIdQuery>;
|
||||||
|
export type ListSsoIdentityProvidersByWorkspaceIdLazyQueryHookResult = ReturnType<typeof useListSsoIdentityProvidersByWorkspaceIdLazyQuery>;
|
||||||
|
export type ListSsoIdentityProvidersByWorkspaceIdQueryResult = Apollo.QueryResult<ListSsoIdentityProvidersByWorkspaceIdQuery, ListSsoIdentityProvidersByWorkspaceIdQueryVariables>;
|
||||||
export const DeleteUserAccountDocument = gql`
|
export const DeleteUserAccountDocument = gql`
|
||||||
mutation DeleteUserAccount {
|
mutation DeleteUserAccount {
|
||||||
deleteUser {
|
deleteUser {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const AppRouter = () => {
|
|||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
||||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||||
|
const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED');
|
||||||
const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
|
const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
|
||||||
'IS_FUNCTION_SETTINGS_ENABLED',
|
'IS_FUNCTION_SETTINGS_ENABLED',
|
||||||
);
|
);
|
||||||
@@ -21,6 +22,7 @@ export const AppRouter = () => {
|
|||||||
isBillingPageEnabled,
|
isBillingPageEnabled,
|
||||||
isCRMMigrationEnabled,
|
isCRMMigrationEnabled,
|
||||||
isServerlessFunctionSettingsEnabled,
|
isServerlessFunctionSettingsEnabled,
|
||||||
|
isSSOEnabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -234,16 +234,32 @@ const SettingsCRMMigration = lazy(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SettingsSecurity = lazy(() =>
|
||||||
|
import('~/pages/settings/security/SettingsSecurity').then((module) => ({
|
||||||
|
default: module.SettingsSecurity,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingsSecuritySSOIdentifyProvider = lazy(() =>
|
||||||
|
import('~/pages/settings/security/SettingsSecuritySSOIdentifyProvider').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.SettingsSecuritySSOIdentifyProvider,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
type SettingsRoutesProps = {
|
type SettingsRoutesProps = {
|
||||||
isBillingEnabled?: boolean;
|
isBillingEnabled?: boolean;
|
||||||
isCRMMigrationEnabled?: boolean;
|
isCRMMigrationEnabled?: boolean;
|
||||||
isServerlessFunctionSettingsEnabled?: boolean;
|
isServerlessFunctionSettingsEnabled?: boolean;
|
||||||
|
isSSOEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsRoutes = ({
|
export const SettingsRoutes = ({
|
||||||
isBillingEnabled,
|
isBillingEnabled,
|
||||||
isCRMMigrationEnabled,
|
isCRMMigrationEnabled,
|
||||||
isServerlessFunctionSettingsEnabled,
|
isServerlessFunctionSettingsEnabled,
|
||||||
|
isSSOEnabled,
|
||||||
}: SettingsRoutesProps) => (
|
}: SettingsRoutesProps) => (
|
||||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -357,6 +373,15 @@ export const SettingsRoutes = ({
|
|||||||
element={<SettingsObjectFieldEdit />}
|
element={<SettingsObjectFieldEdit />}
|
||||||
/>
|
/>
|
||||||
<Route path={SettingsPath.Releases} element={<Releases />} />
|
<Route path={SettingsPath.Releases} element={<Releases />} />
|
||||||
|
{isSSOEnabled && (
|
||||||
|
<>
|
||||||
|
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.NewSSOIdentityProvider}
|
||||||
|
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const useCreateAppRouter = (
|
|||||||
isBillingEnabled?: boolean,
|
isBillingEnabled?: boolean,
|
||||||
isCRMMigrationEnabled?: boolean,
|
isCRMMigrationEnabled?: boolean,
|
||||||
isServerlessFunctionSettingsEnabled?: boolean,
|
isServerlessFunctionSettingsEnabled?: boolean,
|
||||||
|
isSSOEnabled?: boolean,
|
||||||
) =>
|
) =>
|
||||||
createBrowserRouter(
|
createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
@@ -65,6 +66,7 @@ export const useCreateAppRouter = (
|
|||||||
isServerlessFunctionSettingsEnabled={
|
isServerlessFunctionSettingsEnabled={
|
||||||
isServerlessFunctionSettingsEnabled
|
isServerlessFunctionSettingsEnabled
|
||||||
}
|
}
|
||||||
|
isSSOEnabled={isSSOEnabled}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const AVAILABLE_SSO_IDENTITY_PROVIDERS_FRAGMENT = gql`
|
||||||
|
fragment AvailableSSOIdentityProvidersFragment on FindAvailableSSOIDPOutput {
|
||||||
|
id
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const FIND_AVAILABLE_SSO_IDENTITY_PROVIDERS = gql`
|
||||||
|
mutation FindAvailableSSOIdentityProviders(
|
||||||
|
$input: FindAvailableSSOIDPInput!
|
||||||
|
) {
|
||||||
|
findAvailableSSOIdentityProviders(input: $input) {
|
||||||
|
...AvailableSSOIdentityProvidersFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -3,9 +3,22 @@ import { gql } from '@apollo/client';
|
|||||||
export const GENERATE_JWT = gql`
|
export const GENERATE_JWT = gql`
|
||||||
mutation GenerateJWT($workspaceId: String!) {
|
mutation GenerateJWT($workspaceId: String!) {
|
||||||
generateJWT(workspaceId: $workspaceId) {
|
generateJWT(workspaceId: $workspaceId) {
|
||||||
|
... on GenerateJWTOutputWithAuthTokens {
|
||||||
|
success
|
||||||
|
reason
|
||||||
|
authTokens {
|
||||||
tokens {
|
tokens {
|
||||||
...AuthTokensFragment
|
...AuthTokensFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on GenerateJWTOutputWithSSOAUTH {
|
||||||
|
success
|
||||||
|
reason
|
||||||
|
availableSSOIDPs {
|
||||||
|
...AvailableSSOIdentityProvidersFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_AUTHORIZATION_URL = gql`
|
||||||
|
mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) {
|
||||||
|
getAuthorizationUrl(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
authorizationURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -116,6 +116,7 @@ describe('useAuth', () => {
|
|||||||
microsoft: false,
|
microsoft: false,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
password: false,
|
password: false,
|
||||||
|
sso: false,
|
||||||
});
|
});
|
||||||
expect(state.billing).toBeNull();
|
expect(state.billing).toBeNull();
|
||||||
expect(state.isSignInPrefilled).toBe(false);
|
expect(state.isSignInPrefilled).toBe(false);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type FooterNoteProps = { children: React.ReactNode };
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@@ -20,6 +18,24 @@ const StyledContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FooterNote = ({ children }: FooterNoteProps) => (
|
export const FooterNote = () => (
|
||||||
<StyledContainer>{children}</StyledContainer>
|
<StyledContainer>
|
||||||
|
By using Twenty, you agree to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://twenty.com/legal/terms"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Terms of Service
|
||||||
|
</a>{' '}
|
||||||
|
and{' '}
|
||||||
|
<a
|
||||||
|
href="https://twenty.com/legal/privacy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
type HorizontalSeparatorProps = {
|
type HorizontalSeparatorProps = {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
text?: string;
|
||||||
};
|
};
|
||||||
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
||||||
background-color: ${({ theme }) => theme.border.color.medium};
|
background-color: ${({ theme }) => theme.border.color.medium};
|
||||||
@@ -12,8 +13,39 @@ const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSeparatorContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLine = styled.div<HorizontalSeparatorProps>`
|
||||||
|
background-color: ${({ theme }) => theme.border.color.medium};
|
||||||
|
height: ${({ visible }) => (visible ? '1px' : 0)};
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledText = styled.span`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
margin: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
export const HorizontalSeparator = ({
|
export const HorizontalSeparator = ({
|
||||||
visible = true,
|
visible = true,
|
||||||
|
text = '',
|
||||||
}: HorizontalSeparatorProps): JSX.Element => (
|
}: HorizontalSeparatorProps): JSX.Element => (
|
||||||
|
<>
|
||||||
|
{text ? (
|
||||||
|
<StyledSeparatorContainer>
|
||||||
|
<StyledLine visible={visible} />
|
||||||
|
{text && <StyledText>{text}</StyledText>}
|
||||||
|
<StyledLine visible={visible} />
|
||||||
|
</StyledSeparatorContainer>
|
||||||
|
) : (
|
||||||
<StyledSeparator visible={visible} />
|
<StyledSeparator visible={visible} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { IconGoogle, IconMicrosoft } from 'twenty-ui';
|
import { IconGoogle, IconMicrosoft, IconKey } from 'twenty-ui';
|
||||||
|
|
||||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||||
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
|
||||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||||
import {
|
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||||
SignInUpMode,
|
|
||||||
SignInUpStep,
|
|
||||||
useSignInUp,
|
|
||||||
} from '@/auth/sign-in-up/hooks/useSignInUp';
|
|
||||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||||
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
||||||
@@ -26,6 +22,7 @@ import { MainButton } from '@/ui/input/button/components/MainButton';
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||||
@@ -64,9 +61,19 @@ export const SignInUpForm = () => {
|
|||||||
signInUpMode,
|
signInUpMode,
|
||||||
continueWithCredentials,
|
continueWithCredentials,
|
||||||
continueWithEmail,
|
continueWithEmail,
|
||||||
|
continueWithSSO,
|
||||||
submitCredentials,
|
submitCredentials,
|
||||||
|
submitSSOEmail,
|
||||||
} = useSignInUp(form);
|
} = useSignInUp(form);
|
||||||
|
|
||||||
|
const toggleSSOMode = () => {
|
||||||
|
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||||
|
continueWithEmail();
|
||||||
|
} else {
|
||||||
|
continueWithSSO();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = async (
|
const handleKeyDown = async (
|
||||||
event: React.KeyboardEvent<HTMLInputElement>,
|
event: React.KeyboardEvent<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -86,6 +93,8 @@ export const SignInUpForm = () => {
|
|||||||
setShowErrors(true);
|
setShowErrors(true);
|
||||||
form.handleSubmit(submitCredentials)();
|
form.handleSubmit(submitCredentials)();
|
||||||
}
|
}
|
||||||
|
} else if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||||
|
submitSSOEmail(form.getValues('email'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -99,6 +108,10 @@ export const SignInUpForm = () => {
|
|||||||
return 'Continue';
|
return 'Continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||||
|
return 'Continue with SSO';
|
||||||
|
}
|
||||||
|
|
||||||
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||||
}, [signInUpMode, signInUpStep]);
|
}, [signInUpMode, signInUpStep]);
|
||||||
|
|
||||||
@@ -136,7 +149,7 @@ export const SignInUpForm = () => {
|
|||||||
onClick={signInWithGoogle}
|
onClick={signInWithGoogle}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<HorizontalSeparator visible={!authProviders.microsoft} />
|
<HorizontalSeparator visible={false} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -148,11 +161,31 @@ export const SignInUpForm = () => {
|
|||||||
onClick={signInWithMicrosoft}
|
onClick={signInWithMicrosoft}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<HorizontalSeparator visible={authProviders.password} />
|
<HorizontalSeparator visible={false} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{authProviders.sso && (
|
||||||
|
<>
|
||||||
|
<MainButton
|
||||||
|
Icon={() => <IconKey size={theme.icon.size.lg} />}
|
||||||
|
title={
|
||||||
|
signInUpStep === SignInUpStep.SSOEmail
|
||||||
|
? 'Continue with email'
|
||||||
|
: 'Single sign-on (SSO)'
|
||||||
|
}
|
||||||
|
onClick={toggleSSOMode}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<HorizontalSeparator visible={false} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authProviders.password && (
|
<HorizontalSeparator visible={true} />
|
||||||
|
|
||||||
|
{authProviders.password &&
|
||||||
|
(signInUpStep === SignInUpStep.Password ||
|
||||||
|
signInUpStep === SignInUpStep.Email ||
|
||||||
|
signInUpStep === SignInUpStep.Init) && (
|
||||||
<StyledForm
|
<StyledForm
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -258,33 +291,67 @@ export const SignInUpForm = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
)}
|
)}
|
||||||
|
<StyledForm
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{signInUpStep === SignInUpStep.SSOEmail && (
|
||||||
|
<>
|
||||||
|
<StyledFullWidthMotionDiv
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 800,
|
||||||
|
damping: 35,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={form.control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, onBlur, value },
|
||||||
|
fieldState: { error },
|
||||||
|
}) => (
|
||||||
|
<StyledInputContainer>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
placeholder="Email"
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
error={showErrors ? error?.message : undefined}
|
||||||
|
fullWidth
|
||||||
|
disableHotkeys
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFullWidthMotionDiv>
|
||||||
|
<MainButton
|
||||||
|
variant="secondary"
|
||||||
|
title={buttonTitle}
|
||||||
|
type="submit"
|
||||||
|
onClick={async () => {
|
||||||
|
setShowErrors(true);
|
||||||
|
submitSSOEmail(form.getValues('email'));
|
||||||
|
}}
|
||||||
|
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||||
|
disabled={isSubmitButtonDisabled}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledForm>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
{signInUpStep === SignInUpStep.Password && (
|
{signInUpStep === SignInUpStep.Password && (
|
||||||
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
)}
|
)}
|
||||||
{signInUpStep === SignInUpStep.Init && (
|
{signInUpStep === SignInUpStep.Init && <FooterNote />}
|
||||||
<FooterNote>
|
|
||||||
By using Twenty, you agree to the{' '}
|
|
||||||
<a
|
|
||||||
href="https://twenty.com/legal/terms"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</a>{' '}
|
|
||||||
and{' '}
|
|
||||||
<a
|
|
||||||
href="https://twenty.com/legal/privacy"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</FooterNote>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import {
|
||||||
|
FindAvailableSsoIdentityProvidersMutationVariables,
|
||||||
|
GetAuthorizationUrlMutationVariables,
|
||||||
|
useFindAvailableSsoIdentityProvidersMutation,
|
||||||
|
useGetAuthorizationUrlMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const useSSO = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const [findAvailableSSOProviderByEmailMutation] =
|
||||||
|
useFindAvailableSsoIdentityProvidersMutation();
|
||||||
|
const [getAuthorizationUrlMutation] = useGetAuthorizationUrlMutation();
|
||||||
|
|
||||||
|
const findAvailableSSOProviderByEmail = async ({
|
||||||
|
email,
|
||||||
|
}: FindAvailableSsoIdentityProvidersMutationVariables['input']) => {
|
||||||
|
return await findAvailableSSOProviderByEmailMutation({
|
||||||
|
variables: {
|
||||||
|
input: { email },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthorizationUrlForSSO = async ({
|
||||||
|
identityProviderId,
|
||||||
|
}: GetAuthorizationUrlMutationVariables['input']) => {
|
||||||
|
return await getAuthorizationUrlMutation({
|
||||||
|
variables: {
|
||||||
|
input: { identityProviderId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToSSOLoginPage = async (identityProviderId: string) => {
|
||||||
|
const authorizationUrlForSSOResult = await getAuthorizationUrlForSSO({
|
||||||
|
identityProviderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(authorizationUrlForSSOResult.errors) ||
|
||||||
|
!authorizationUrlForSSOResult.data ||
|
||||||
|
!authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL
|
||||||
|
) {
|
||||||
|
return enqueueSnackBar(
|
||||||
|
authorizationUrlForSSOResult.errors?.[0]?.message ?? 'Unknown error',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href =
|
||||||
|
authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirectToSSOLoginPage,
|
||||||
|
getAuthorizationUrlForSSO,
|
||||||
|
findAvailableSSOProviderByEmail,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -9,25 +9,34 @@ import { AppPath } from '@/types/AppPath';
|
|||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import {
|
||||||
|
SignInUpStep,
|
||||||
|
signInUpStepState,
|
||||||
|
} from '@/auth/states/signInUpStepState';
|
||||||
|
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||||
|
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||||
|
|
||||||
export enum SignInUpMode {
|
export enum SignInUpMode {
|
||||||
SignIn = 'sign-in',
|
SignIn = 'sign-in',
|
||||||
SignUp = 'sign-up',
|
SignUp = 'sign-up',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SignInUpStep {
|
|
||||||
Init = 'init',
|
|
||||||
Email = 'email',
|
|
||||||
Password = 'password',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||||
|
|
||||||
const isMatchingLocation = useIsMatchingLocation();
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
|
||||||
|
const { redirectToSSOLoginPage, findAvailableSSOProviderByEmail } = useSSO();
|
||||||
|
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||||
|
availableSSOIdentityProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const workspacePersonalInviteToken =
|
const workspacePersonalInviteToken =
|
||||||
@@ -35,10 +44,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
|
|
||||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||||
|
|
||||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
|
||||||
SignInUpStep.Init,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
||||||
return isMatchingLocation(AppPath.SignInUp)
|
return isMatchingLocation(AppPath.SignInUp)
|
||||||
? SignInUpMode.SignIn
|
? SignInUpMode.SignIn
|
||||||
@@ -62,7 +67,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
? SignInUpMode.SignIn
|
? SignInUpMode.SignIn
|
||||||
: SignInUpMode.SignUp,
|
: SignInUpMode.SignUp,
|
||||||
);
|
);
|
||||||
}, [isMatchingLocation, requestFreshCaptchaToken]);
|
}, [isMatchingLocation, requestFreshCaptchaToken, setSignInUpStep]);
|
||||||
|
|
||||||
const continueWithCredentials = useCallback(async () => {
|
const continueWithCredentials = useCallback(async () => {
|
||||||
const token = await readCaptchaToken();
|
const token = await readCaptchaToken();
|
||||||
@@ -95,8 +100,48 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
checkUserExistsQuery,
|
checkUserExistsQuery,
|
||||||
enqueueSnackBar,
|
enqueueSnackBar,
|
||||||
requestFreshCaptchaToken,
|
requestFreshCaptchaToken,
|
||||||
|
setSignInUpStep,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const continueWithSSO = () => {
|
||||||
|
setSignInUpStep(SignInUpStep.SSOEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSSOEmail = async (email: string) => {
|
||||||
|
const result = await findAvailableSSOProviderByEmail({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDefined(result.errors)) {
|
||||||
|
return enqueueSnackBar(result.errors[0].message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!result.data?.findAvailableSSOIdentityProviders ||
|
||||||
|
result.data?.findAvailableSSOIdentityProviders.length === 0
|
||||||
|
) {
|
||||||
|
enqueueSnackBar('No workspaces with SSO found', {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If only one workspace, redirect to SSO
|
||||||
|
if (result.data?.findAvailableSSOIdentityProviders.length === 1) {
|
||||||
|
return redirectToSSOLoginPage(
|
||||||
|
result.data.findAvailableSSOIdentityProviders[0].id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data?.findAvailableSSOIdentityProviders.length > 1) {
|
||||||
|
setAvailableWorkspacesForSSOState(
|
||||||
|
result.data.findAvailableSSOIdentityProviders,
|
||||||
|
);
|
||||||
|
setSignInUpStep(SignInUpStep.SSOWorkspaceSelection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitCredentials: SubmitHandler<Form> = useCallback(
|
const submitCredentials: SubmitHandler<Form> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
const token = await readCaptchaToken();
|
const token = await readCaptchaToken();
|
||||||
@@ -144,6 +189,8 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
signInUpMode,
|
signInUpMode,
|
||||||
continueWithCredentials,
|
continueWithCredentials,
|
||||||
continueWithEmail,
|
continueWithEmail,
|
||||||
|
continueWithSSO,
|
||||||
|
submitSSOEmail,
|
||||||
submitCredentials,
|
submitCredentials,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
import { FindAvailableSsoIdentityProvidersMutationResult } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const availableSSOIdentityProvidersState = createState<
|
||||||
|
NonNullable<
|
||||||
|
FindAvailableSsoIdentityProvidersMutationResult['data']
|
||||||
|
>['findAvailableSSOIdentityProviders']
|
||||||
|
>({
|
||||||
|
key: 'availableSSOIdentityProviders',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ export type CurrentWorkspace = Pick<
|
|||||||
| 'activationStatus'
|
| 'activationStatus'
|
||||||
| 'currentBillingSubscription'
|
| 'currentBillingSubscription'
|
||||||
| 'workspaceMembersCount'
|
| 'workspaceMembersCount'
|
||||||
|
| 'isPublicInviteLinkEnabled'
|
||||||
| 'metadataVersion'
|
| 'metadataVersion'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export enum SignInUpStep {
|
||||||
|
Init = 'init',
|
||||||
|
Email = 'email',
|
||||||
|
Password = 'password',
|
||||||
|
SSOEmail = 'SSOEmail',
|
||||||
|
SSOWorkspaceSelection = 'SSOWorkspaceSelection',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const signInUpStepState = createState<SignInUpStep>({
|
||||||
|
key: 'signInUpStepState',
|
||||||
|
defaultValue: SignInUpStep.Init,
|
||||||
|
});
|
||||||
@@ -49,6 +49,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
microsoft: data?.clientConfig.authProviders.microsoft,
|
microsoft: data?.clientConfig.authProviders.microsoft,
|
||||||
password: data?.clientConfig.authProviders.password,
|
password: data?.clientConfig.authProviders.password,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
|
sso: data?.clientConfig.authProviders.sso,
|
||||||
});
|
});
|
||||||
setIsDebugMode(data?.clientConfig.debugMode);
|
setIsDebugMode(data?.clientConfig.debugMode);
|
||||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
google
|
google
|
||||||
password
|
password
|
||||||
microsoft
|
microsoft
|
||||||
|
sso
|
||||||
}
|
}
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export const authProvidersState = createState<AuthProviders>({
|
|||||||
magicLink: false,
|
magicLink: false,
|
||||||
password: false,
|
password: false,
|
||||||
microsoft: false,
|
microsoft: false,
|
||||||
|
sso: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
|||||||
allowImpersonation: false,
|
allowImpersonation: false,
|
||||||
activationStatus: WorkspaceActivationStatus.Active,
|
activationStatus: WorkspaceActivationStatus.Active,
|
||||||
metadataVersion: 1,
|
metadataVersion: 1,
|
||||||
|
isPublicInviteLinkEnabled: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { CalendarChannel } from '@/accounts/types/CalendarChannel';
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard';
|
import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard';
|
||||||
import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
import { H2Title } from 'twenty-ui';
|
import { H2Title } from 'twenty-ui';
|
||||||
import { CalendarChannelVisibility } from '~/generated-metadata/graphql';
|
import { CalendarChannelVisibility } from '~/generated-metadata/graphql';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent';
|
||||||
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
|
|
||||||
const StyledDetailsContainer = styled.div`
|
const StyledDetailsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -21,6 +23,10 @@ type SettingsAccountsCalendarChannelDetailsProps = {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledToggle = styled(Toggle)`
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsCalendarChannelDetails = ({
|
export const SettingsAccountsCalendarChannelDetails = ({
|
||||||
calendarChannel,
|
calendarChannel,
|
||||||
}: SettingsAccountsCalendarChannelDetailsProps) => {
|
}: SettingsAccountsCalendarChannelDetailsProps) => {
|
||||||
@@ -63,16 +69,21 @@ export const SettingsAccountsCalendarChannelDetails = ({
|
|||||||
title="Contact auto-creation"
|
title="Contact auto-creation"
|
||||||
description="Automatically create contacts for people you've participated in an event with."
|
description="Automatically create contacts for people you've participated in an event with."
|
||||||
/>
|
/>
|
||||||
<SettingsAccountsToggleSettingCard
|
<Card>
|
||||||
parameters={[
|
<SettingsOptionCardContent
|
||||||
{
|
title="Auto-creation"
|
||||||
value: !!calendarChannel.isContactAutoCreationEnabled,
|
description="Automatically create contacts for people."
|
||||||
title: 'Auto-creation',
|
onClick={() =>
|
||||||
description: 'Automatically create contacts for people.',
|
handleContactAutoCreationToggle(
|
||||||
onToggle: handleContactAutoCreationToggle,
|
!calendarChannel.isContactAutoCreationEnabled,
|
||||||
},
|
)
|
||||||
]}
|
}
|
||||||
|
>
|
||||||
|
<StyledToggle
|
||||||
|
value={calendarChannel.isContactAutoCreationEnabled}
|
||||||
/>
|
/>
|
||||||
|
</SettingsOptionCardContent>
|
||||||
|
</Card>
|
||||||
</Section>
|
</Section>
|
||||||
</StyledDetailsContainer>
|
</StyledDetailsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
|
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
|
||||||
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
|
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
|
||||||
import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard';
|
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { MessageChannelVisibility } from '~/generated-metadata/graphql';
|
import { MessageChannelVisibility } from '~/generated-metadata/graphql';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent';
|
||||||
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
|
|
||||||
type SettingsAccountsMessageChannelDetailsProps = {
|
type SettingsAccountsMessageChannelDetailsProps = {
|
||||||
messageChannel: Pick<
|
messageChannel: Pick<
|
||||||
@@ -31,6 +33,10 @@ const StyledDetailsContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(6)};
|
gap: ${({ theme }) => theme.spacing(6)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledToggle = styled(Toggle)`
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsMessageChannelDetails = ({
|
export const SettingsAccountsMessageChannelDetails = ({
|
||||||
messageChannel,
|
messageChannel,
|
||||||
}: SettingsAccountsMessageChannelDetailsProps) => {
|
}: SettingsAccountsMessageChannelDetailsProps) => {
|
||||||
@@ -99,23 +105,31 @@ export const SettingsAccountsMessageChannelDetails = ({
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<SettingsAccountsToggleSettingCard
|
<Card>
|
||||||
parameters={[
|
<SettingsOptionCardContent
|
||||||
{
|
title="Exclude non-professional emails"
|
||||||
title: 'Exclude non-professional emails',
|
description="Don’t create contacts from/to Gmail, Outlook emails"
|
||||||
description:
|
divider
|
||||||
'Don’t create contacts from/to Gmail, Outlook emails',
|
onClick={() =>
|
||||||
value: !!messageChannel.excludeNonProfessionalEmails,
|
handleIsNonProfessionalEmailExcludedToggle(
|
||||||
onToggle: handleIsNonProfessionalEmailExcludedToggle,
|
!messageChannel.excludeNonProfessionalEmails,
|
||||||
},
|
)
|
||||||
{
|
}
|
||||||
title: 'Exclude group emails',
|
>
|
||||||
description: 'Don’t sync emails from team@ support@ noreply@...',
|
<StyledToggle value={messageChannel.excludeNonProfessionalEmails} />
|
||||||
value: !!messageChannel.excludeGroupEmails,
|
</SettingsOptionCardContent>
|
||||||
onToggle: handleIsGroupEmailExcludedToggle,
|
<SettingsOptionCardContent
|
||||||
},
|
title="Exclude group emails"
|
||||||
]}
|
description="Don’t sync emails from team@ support@ noreply@..."
|
||||||
/>
|
onClick={() =>
|
||||||
|
handleIsGroupEmailExcludedToggle(
|
||||||
|
!messageChannel.excludeGroupEmails,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledToggle value={messageChannel.excludeGroupEmails} />
|
||||||
|
</SettingsOptionCardContent>
|
||||||
|
</Card>
|
||||||
</Section>
|
</Section>
|
||||||
</StyledDetailsContainer>
|
</StyledDetailsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { Toggle } from '@/ui/input/components/Toggle';
|
|
||||||
import { Card } from '@/ui/layout/card/components/Card';
|
|
||||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
|
||||||
|
|
||||||
type Parameter = {
|
|
||||||
value: boolean;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
onToggle: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SettingsAccountsToggleSettingCardProps = {
|
|
||||||
parameters: Parameter[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledCardContent = styled(CardContent)`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTitle = styled.div`
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDescription = styled.div`
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledToggle = styled(Toggle)`
|
|
||||||
margin-left: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAccountsToggleSettingCard = ({
|
|
||||||
parameters,
|
|
||||||
}: SettingsAccountsToggleSettingCardProps) => (
|
|
||||||
<Card rounded>
|
|
||||||
{parameters.map((parameter, index) => (
|
|
||||||
<StyledCardContent
|
|
||||||
key={index}
|
|
||||||
divider={index < parameters.length - 1}
|
|
||||||
onClick={() => parameter.onToggle(!parameter.value)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<StyledTitle>{parameter.title}</StyledTitle>
|
|
||||||
<StyledDescription>{parameter.description}</StyledDescription>
|
|
||||||
</div>
|
|
||||||
<StyledToggle value={parameter.value} onChange={parameter.onToggle} />
|
|
||||||
</StyledCardContent>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
@@ -42,6 +42,7 @@ type SettingsListCardProps<ListItem extends { id: string }> = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onRowClick?: (item: ListItem) => void;
|
onRowClick?: (item: ListItem) => void;
|
||||||
RowIcon?: IconComponent;
|
RowIcon?: IconComponent;
|
||||||
|
RowIconFn?: (item: ListItem) => IconComponent;
|
||||||
RowRightComponent: ComponentType<{ item: ListItem }>;
|
RowRightComponent: ComponentType<{ item: ListItem }>;
|
||||||
footerButtonLabel?: string;
|
footerButtonLabel?: string;
|
||||||
onFooterButtonClick?: () => void;
|
onFooterButtonClick?: () => void;
|
||||||
@@ -58,6 +59,7 @@ export const SettingsListCard = <
|
|||||||
isLoading,
|
isLoading,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
RowIcon,
|
RowIcon,
|
||||||
|
RowIconFn,
|
||||||
RowRightComponent,
|
RowRightComponent,
|
||||||
onFooterButtonClick,
|
onFooterButtonClick,
|
||||||
footerButtonLabel,
|
footerButtonLabel,
|
||||||
@@ -71,7 +73,7 @@ export const SettingsListCard = <
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<SettingsListItemCardContent
|
<SettingsListItemCardContent
|
||||||
key={item.id}
|
key={item.id}
|
||||||
LeftIcon={RowIcon}
|
LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon}
|
||||||
label={getItemLabel(item)}
|
label={getItemLabel(item)}
|
||||||
rightComponent={<RowRightComponent item={item} />}
|
rightComponent={<RowRightComponent item={item} />}
|
||||||
divider={index < items.length - 1}
|
divider={index < items.length - 1}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
IconTool,
|
IconTool,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
|
IconKey,
|
||||||
MAIN_COLORS,
|
MAIN_COLORS,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
);
|
);
|
||||||
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
||||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||||
|
const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED');
|
||||||
const isBillingPageEnabled =
|
const isBillingPageEnabled =
|
||||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||||
|
|
||||||
@@ -186,6 +188,13 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
Icon={IconCode}
|
Icon={IconCode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isSSOEnabled && (
|
||||||
|
<SettingsNavigationDrawerItem
|
||||||
|
label="Security"
|
||||||
|
path={SettingsPath.Security}
|
||||||
|
Icon={IconKey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</NavigationDrawerSection>
|
</NavigationDrawerSection>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isAdvancedModeEnabled && (
|
{isAdvancedModeEnabled && (
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type SettingsOptionCardContentProps = {
|
||||||
|
Icon?: IconComponent;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
onClick: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
divider?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledCardContent = styled(CardContent)`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDescription = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
|
display: flex;
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
justify-content: center;
|
||||||
|
width: ${({ theme }) => theme.spacing(8)};
|
||||||
|
min-width: ${({ theme }) => theme.icon.size.md};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsOptionCardContent = ({
|
||||||
|
Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
divider,
|
||||||
|
}: SettingsOptionCardContentProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCardContent onClick={onClick} divider={divider}>
|
||||||
|
{Icon && (
|
||||||
|
<StyledIcon>
|
||||||
|
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.md} />
|
||||||
|
</StyledIcon>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<StyledTitle>{title}</StyledTitle>
|
||||||
|
<StyledDescription>{description}</StyledDescription>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</StyledCardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Radio } from '@/ui/input/components/Radio';
|
||||||
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
const StyledRadioCardContent = styled(CardContent)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRadio = styled(Radio)`
|
||||||
|
margin-left: auto;
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDescription = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SettingsRadioCardProps = {
|
||||||
|
value: string;
|
||||||
|
handleClick: (value: string) => void;
|
||||||
|
isSelected: boolean;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
Icon?: IconComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsRadioCard = ({
|
||||||
|
value,
|
||||||
|
handleClick,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
isSelected,
|
||||||
|
Icon,
|
||||||
|
}: SettingsRadioCardProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledRadioCardContent onClick={() => handleClick(value)}>
|
||||||
|
{Icon && <Icon size={theme.icon.size.xl} color={theme.color.gray50} />}
|
||||||
|
<span>
|
||||||
|
{title && <StyledTitle>{title}</StyledTitle>}
|
||||||
|
{description && <StyledDescription>{description}</StyledDescription>}
|
||||||
|
</span>
|
||||||
|
<StyledRadio value={value} checked={isSelected} />
|
||||||
|
</StyledRadioCardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
import { SettingsRadioCard } from '@/settings/components/SettingsRadioCard';
|
||||||
|
|
||||||
|
const StyledRadioCardContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SettingsRadioCardContainerProps = {
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
value: string;
|
||||||
|
options: Array<{
|
||||||
|
value: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
Icon?: IconComponent;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsRadioCardContainer = ({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: SettingsRadioCardContainerProps) => {
|
||||||
|
return (
|
||||||
|
<StyledRadioCardContainer>
|
||||||
|
{options.map((option) => (
|
||||||
|
<SettingsRadioCard
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
isSelected={value === option.value}
|
||||||
|
handleClick={onChange}
|
||||||
|
title={option.title}
|
||||||
|
description={option.description}
|
||||||
|
Icon={option.Icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledRadioCardContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SettingsRadioCardContainer } from '@/settings/components/SettingsRadioCardContainer';
|
||||||
|
import { SettingsSSOOIDCForm } from '@/settings/security/components/SettingsSSOOIDCForm';
|
||||||
|
import { SettingsSSOSAMLForm } from '@/settings/security/components/SettingsSSOSAMLForm';
|
||||||
|
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { H2Title, IconComponent, IconKey } from 'twenty-ui';
|
||||||
|
import { IdpType } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: grid;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2, 4)};
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
'input-1 input-1'
|
||||||
|
'input-2 input-3'
|
||||||
|
'input-4 input-5';
|
||||||
|
|
||||||
|
& :first-of-type {
|
||||||
|
grid-area: input-1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSSOIdentitiesProvidersForm = () => {
|
||||||
|
const { control, getValues } =
|
||||||
|
useFormContext<SettingSecurityNewSSOIdentityFormValues>();
|
||||||
|
|
||||||
|
const IdpMap: Record<
|
||||||
|
IdpType,
|
||||||
|
{
|
||||||
|
form: ReactElement;
|
||||||
|
option: {
|
||||||
|
Icon: IconComponent;
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
OIDC: {
|
||||||
|
option: {
|
||||||
|
Icon: IconKey,
|
||||||
|
title: 'OIDC',
|
||||||
|
value: 'OIDC',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
form: <SettingsSSOOIDCForm />,
|
||||||
|
},
|
||||||
|
SAML: {
|
||||||
|
option: {
|
||||||
|
Icon: IconKey,
|
||||||
|
title: 'SAML',
|
||||||
|
value: 'SAML',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
form: <SettingsSSOSAMLForm />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFormByType = (type: Uppercase<IdpType> | undefined) => {
|
||||||
|
switch (type) {
|
||||||
|
case IdpType.Oidc:
|
||||||
|
return IdpMap.OIDC.form;
|
||||||
|
case IdpType.Saml:
|
||||||
|
return IdpMap.SAML.form;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<Section>
|
||||||
|
<H2Title title="Name" description="The name of your connection" />
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
autoComplete="off"
|
||||||
|
label="Name"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Google OIDC"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Type"
|
||||||
|
description="Choose between OIDC and SAML protocols"
|
||||||
|
/>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<SettingsRadioCardContainer
|
||||||
|
value={value}
|
||||||
|
options={Object.values(IdpMap).map(
|
||||||
|
(identityProviderType) => identityProviderType.option,
|
||||||
|
)}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
{getFormByType(getValues().type)}
|
||||||
|
</SettingsPageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsSSOIdentitiesProvidersForm;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
|
||||||
|
import { SettingsSSOIdentitiesProvidersListEmptyStateCard } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListEmptyStateCard';
|
||||||
|
import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/security/components/SettingsSSOIdentityProviderRowRightContainer';
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { useListSsoIdentityProvidersByWorkspaceIdQuery } from '~/generated/graphql';
|
||||||
|
import { SettingsListCard } from '../../components/SettingsListCard';
|
||||||
|
import { guessSSOIdentityProviderIconByUrl } from '../utils/guessSSOIdentityProviderIconByUrl';
|
||||||
|
|
||||||
|
export const SettingsSSOIdentitiesProvidersListCard = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const [SSOIdentitiesProviders, setSSOIdentitiesProviders] = useRecoilState(
|
||||||
|
SSOIdentitiesProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loading } = useListSsoIdentityProvidersByWorkspaceIdQuery({
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSSOIdentitiesProviders(
|
||||||
|
data?.listSSOIdentityProvidersByWorkspaceId ?? [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
enqueueSnackBar(error.message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !SSOIdentitiesProviders.length && !loading ? (
|
||||||
|
<SettingsSSOIdentitiesProvidersListEmptyStateCard />
|
||||||
|
) : (
|
||||||
|
<SettingsListCard
|
||||||
|
items={SSOIdentitiesProviders}
|
||||||
|
getItemLabel={(SSOIdentityProvider) =>
|
||||||
|
`${SSOIdentityProvider.name} - ${SSOIdentityProvider.type}`
|
||||||
|
}
|
||||||
|
isLoading={loading}
|
||||||
|
RowIconFn={(SSOIdentityProvider) =>
|
||||||
|
guessSSOIdentityProviderIconByUrl(SSOIdentityProvider.issuer)
|
||||||
|
}
|
||||||
|
RowRightComponent={({ item: SSOIdp }) => (
|
||||||
|
<SettingsSSOIdentityProviderRowRightContainer SSOIdp={SSOIdp} />
|
||||||
|
)}
|
||||||
|
hasFooter
|
||||||
|
footerButtonLabel="Add SSO Identity Provider"
|
||||||
|
onFooterButtonClick={() =>
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.NewSSOIdentityProvider))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
import { CardHeader } from '@/ui/layout/card/components/CardHeader';
|
||||||
|
import { IconKey } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledHeader = styled(CardHeader)`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBody = styled(CardContent)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSSOIdentitiesProvidersListEmptyStateCard = () => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<StyledHeader>{'No SSO Identity Providers Configured'}</StyledHeader>
|
||||||
|
<StyledBody>
|
||||||
|
<Button
|
||||||
|
Icon={IconKey}
|
||||||
|
title="Add SSO Identity Provider"
|
||||||
|
variant="secondary"
|
||||||
|
to={getSettingsPagePath(SettingsPath.NewSSOIdentityProvider)}
|
||||||
|
/>
|
||||||
|
</StyledBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SettingsSecuritySSORowDropdownMenu';
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { getColorBySSOIdentityProviderStatus } from '@/settings/security/utils/getColorBySSOIdentityProviderStatus';
|
||||||
|
import { Status } from '@/ui/display/status/components/Status';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { UnwrapRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
const StyledRowRightContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSSOIdentityProviderRowRightContainer = ({
|
||||||
|
SSOIdp,
|
||||||
|
}: {
|
||||||
|
SSOIdp: UnwrapRecoilValue<typeof SSOIdentitiesProvidersState>[0];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledRowRightContainer>
|
||||||
|
<Status
|
||||||
|
color={getColorBySSOIdentityProviderStatus[SSOIdp.status]}
|
||||||
|
text={SSOIdp.status}
|
||||||
|
weight="medium"
|
||||||
|
/>
|
||||||
|
<SettingsSecuritySSORowDropdownMenu SSOIdp={SSOIdp} />
|
||||||
|
</StyledRowRightContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { H2Title, IconCopy } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2, 4)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLinkContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButtonCopy = styled.div`
|
||||||
|
align-items: end;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSSOOIDCForm = () => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const authorizedUrl = window.location.origin;
|
||||||
|
const redirectionUrl = `${window.location.origin}/auth/oidc/callback`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Client Settings"
|
||||||
|
description="Provide your OIDC provider details"
|
||||||
|
/>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledLinkContainer>
|
||||||
|
<TextInput
|
||||||
|
readOnly={true}
|
||||||
|
label="Authorized URI"
|
||||||
|
value={authorizedUrl}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledLinkContainer>
|
||||||
|
<StyledButtonCopy>
|
||||||
|
<Button
|
||||||
|
Icon={IconCopy}
|
||||||
|
title="Copy"
|
||||||
|
onClick={() => {
|
||||||
|
enqueueSnackBar('Authorized Url copied to clipboard', {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(authorizedUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledButtonCopy>
|
||||||
|
</StyledContainer>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledLinkContainer>
|
||||||
|
<TextInput
|
||||||
|
readOnly={true}
|
||||||
|
label="Redirection URI"
|
||||||
|
value={redirectionUrl}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledLinkContainer>
|
||||||
|
<StyledButtonCopy>
|
||||||
|
<Button
|
||||||
|
Icon={IconCopy}
|
||||||
|
title="Copy"
|
||||||
|
onClick={() => {
|
||||||
|
enqueueSnackBar('Redirect Url copied to clipboard', {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(redirectionUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledButtonCopy>
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Identity Provider"
|
||||||
|
description="Enter the credentials to set the connection"
|
||||||
|
/>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="clientID"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
autoComplete="off"
|
||||||
|
label="Client ID"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
fullWidth
|
||||||
|
placeholder="900960562328-36306ohbk8e3.apps.googleusercontent.com"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="clientSecret"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
autoComplete="off"
|
||||||
|
type="password"
|
||||||
|
label="Client Secret"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
fullWidth
|
||||||
|
placeholder="****************************"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="issuer"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
autoComplete="off"
|
||||||
|
label="Issuer URI"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
fullWidth
|
||||||
|
placeholder="https://accounts.google.com"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
|
||||||
|
import { parseSAMLMetadataFromXMLFile } from '@/settings/security/utils/parseSAMLMetadataFromXMLFile';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ChangeEvent, useRef } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
H2Title,
|
||||||
|
IconCheck,
|
||||||
|
IconCopy,
|
||||||
|
IconDownload,
|
||||||
|
IconUpload,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledUploadFileContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFileInput = styled.input`
|
||||||
|
display: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2, 4)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLinkContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButtonCopy = styled.div`
|
||||||
|
align-items: end;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSSOSAMLForm = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { setValue, getValues, watch } = useFormContext();
|
||||||
|
|
||||||
|
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (isDefined(e.target.files)) {
|
||||||
|
const text = await e.target.files[0].text();
|
||||||
|
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
|
||||||
|
if (!samlMetadataParsed.success) {
|
||||||
|
enqueueSnackBar('Invalid File', {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue('ssoURL', samlMetadataParsed.data.ssoUrl);
|
||||||
|
setValue('certificate', samlMetadataParsed.data.certificate);
|
||||||
|
setValue('issuer', samlMetadataParsed.data.entityID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const entityID = `${REACT_APP_SERVER_BASE_URL}/auth/saml/login/${getValues('id')}`;
|
||||||
|
const acsUrl = `${REACT_APP_SERVER_BASE_URL}/auth/saml/callback`;
|
||||||
|
|
||||||
|
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleUploadFileClick = () => {
|
||||||
|
inputFileRef?.current?.click?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ssoURL = watch('ssoURL');
|
||||||
|
const certificate = watch('certificate');
|
||||||
|
const issuer = watch('issuer');
|
||||||
|
|
||||||
|
const isXMLMetadataValid = () => {
|
||||||
|
return [ssoURL, certificate, issuer].every(
|
||||||
|
(field) => isDefined(field) && field.length > 0,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadMetadata = async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${REACT_APP_SERVER_BASE_URL}/auth/saml/metadata/${getValues('id')}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
return enqueueSnackBar('Metadata file generation failed', {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
const blob = new Blob([text], { type: 'application/xml' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'metadata.xml';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Identity Provider Metadata XML"
|
||||||
|
description="Upload the XML file with your connection infos"
|
||||||
|
/>
|
||||||
|
<StyledUploadFileContainer>
|
||||||
|
<StyledFileInput
|
||||||
|
ref={inputFileRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
type="file"
|
||||||
|
accept=".xml"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
Icon={IconUpload}
|
||||||
|
onClick={handleUploadFileClick}
|
||||||
|
title="Upload file"
|
||||||
|
></Button>
|
||||||
|
{isXMLMetadataValid() && (
|
||||||
|
<IconCheck
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.lg}
|
||||||
|
color={theme.color.blue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledUploadFileContainer>
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Service Provider Details"
|
||||||
|
description="Enter the infos to set the connection"
|
||||||
|
/>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<StyledContainer>
|
||||||
|
<Button
|
||||||
|
Icon={IconDownload}
|
||||||
|
onClick={downloadMetadata}
|
||||||
|
title="Download file"
|
||||||
|
></Button>
|
||||||
|
</StyledContainer>
|
||||||
|
<HorizontalSeparator visible={true} text={'Or'} />
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledLinkContainer>
|
||||||
|
<TextInput
|
||||||
|
disabled={true}
|
||||||
|
label="ACS Url"
|
||||||
|
value={acsUrl}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledLinkContainer>
|
||||||
|
<StyledButtonCopy>
|
||||||
|
<Button
|
||||||
|
Icon={IconCopy}
|
||||||
|
title="Copy"
|
||||||
|
onClick={() => {
|
||||||
|
enqueueSnackBar('ACS Url copied to clipboard', {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(acsUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledButtonCopy>
|
||||||
|
</StyledContainer>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledLinkContainer>
|
||||||
|
<TextInput
|
||||||
|
disabled={true}
|
||||||
|
label="Entity ID"
|
||||||
|
value={entityID}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledLinkContainer>
|
||||||
|
<StyledButtonCopy>
|
||||||
|
<Button
|
||||||
|
Icon={IconCopy}
|
||||||
|
title="Copy"
|
||||||
|
onClick={() => {
|
||||||
|
enqueueSnackBar('Entity ID copied to clipboard', {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(entityID);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledButtonCopy>
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { IconLink } from 'twenty-ui';
|
||||||
|
import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
|
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
|
||||||
|
const StyledToggle = styled(Toggle)`
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsSecurityOptionsList = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
|
currentWorkspaceState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||||
|
|
||||||
|
const handleChange = async (value: boolean) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.id) {
|
||||||
|
throw new Error('User is not logged in');
|
||||||
|
}
|
||||||
|
await updateWorkspace({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
isPublicInviteLinkEnabled: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCurrentWorkspace({
|
||||||
|
...currentWorkspace,
|
||||||
|
isPublicInviteLinkEnabled: value,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
enqueueSnackBar(err?.message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SettingsOptionCardContent
|
||||||
|
Icon={IconLink}
|
||||||
|
title="Invite by Link"
|
||||||
|
description="Allow the invitation of new users by sharing an invite link."
|
||||||
|
onClick={() =>
|
||||||
|
handleChange(!currentWorkspace?.isPublicInviteLinkEnabled)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledToggle value={currentWorkspace?.isPublicInviteLinkEnabled} />
|
||||||
|
</SettingsOptionCardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { IconArchive, IconDotsVertical, IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider';
|
||||||
|
import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider';
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { UnwrapRecoilValue } from 'recoil';
|
||||||
|
import { SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type SettingsSecuritySSORowDropdownMenuProps = {
|
||||||
|
SSOIdp: UnwrapRecoilValue<typeof SSOIdentitiesProvidersState>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsSecuritySSORowDropdownMenu = ({
|
||||||
|
SSOIdp,
|
||||||
|
}: SettingsSecuritySSORowDropdownMenuProps) => {
|
||||||
|
const dropdownId = `settings-account-row-${SSOIdp.id}`;
|
||||||
|
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider();
|
||||||
|
const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider();
|
||||||
|
|
||||||
|
const handleDeleteSSOIdentityProvider = async (
|
||||||
|
identityProviderId: string,
|
||||||
|
) => {
|
||||||
|
const result = await deleteSSOIdentityProvider({
|
||||||
|
identityProviderId,
|
||||||
|
});
|
||||||
|
if (isDefined(result.errors)) {
|
||||||
|
enqueueSnackBar('Error deleting SSO Identity Provider', {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSSOIdentityProviderStatus = async (
|
||||||
|
identityProviderId: string,
|
||||||
|
) => {
|
||||||
|
const result = await updateSSOIdentityProvider({
|
||||||
|
id: identityProviderId,
|
||||||
|
status:
|
||||||
|
SSOIdp.status === 'Active'
|
||||||
|
? SsoIdentityProviderStatus.Inactive
|
||||||
|
: SsoIdentityProviderStatus.Active,
|
||||||
|
});
|
||||||
|
if (isDefined(result.errors)) {
|
||||||
|
enqueueSnackBar('Error editing SSO Identity Provider', {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
dropdownPlacement="right-start"
|
||||||
|
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||||
|
clickableComponent={
|
||||||
|
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem
|
||||||
|
accent="default"
|
||||||
|
LeftIcon={IconArchive}
|
||||||
|
text={SSOIdp.status === 'Active' ? 'Deactivate' : 'Activate'}
|
||||||
|
onClick={() => {
|
||||||
|
toggleSSOIdentityProviderStatus(SSOIdp.id);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
accent="danger"
|
||||||
|
LeftIcon={IconTrash}
|
||||||
|
text="Delete"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteSSOIdentityProvider(SSOIdp.id);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_OIDC_SSO_IDENTITY_PROVIDER = gql`
|
||||||
|
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
|
||||||
|
createOIDCIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_SAML_SSO_IDENTITY_PROVIDER = gql`
|
||||||
|
mutation CreateSAMLIdentityProvider($input: SetupSAMLSsoInput!) {
|
||||||
|
createSAMLIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const DELETE_SSO_IDENTITY_PROVIDER = gql`
|
||||||
|
mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) {
|
||||||
|
deleteSSOIdentityProvider(input: $input) {
|
||||||
|
identityProviderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const EDIT_SSO_IDENTITY_PROVIDER = gql`
|
||||||
|
mutation EditSSOIdentityProvider($input: EditSsoInput!) {
|
||||||
|
editSSOIdentityProvider(input: $input) {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
issuer
|
||||||
|
name
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const LIST_WORKSPACE_SSO_IDENTITY_PROVIDERS = gql`
|
||||||
|
query ListSSOIdentityProvidersByWorkspaceId {
|
||||||
|
listSSOIdentityProvidersByWorkspaceId {
|
||||||
|
type
|
||||||
|
id
|
||||||
|
name
|
||||||
|
issuer
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider';
|
||||||
|
|
||||||
|
const mutationOIDCCallSpy = jest.fn();
|
||||||
|
const mutationSAMLCallSpy = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('~/generated/graphql', () => ({
|
||||||
|
useCreateOidcIdentityProviderMutation: () => [mutationOIDCCallSpy],
|
||||||
|
useCreateSamlIdentityProviderMutation: () => [mutationSAMLCallSpy],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useCreateSSOIdentityProvider', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create OIDC sso identity provider', async () => {
|
||||||
|
const OIDCParams = {
|
||||||
|
type: 'OIDC' as const,
|
||||||
|
name: 'test',
|
||||||
|
clientID: 'test',
|
||||||
|
clientSecret: 'test',
|
||||||
|
issuer: 'test',
|
||||||
|
};
|
||||||
|
renderHook(
|
||||||
|
() => {
|
||||||
|
const { createSSOIdentityProvider } = useCreateSSOIdentityProvider();
|
||||||
|
createSSOIdentityProvider(OIDCParams);
|
||||||
|
},
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
const { type, ...input } = OIDCParams;
|
||||||
|
expect(mutationOIDCCallSpy).toHaveBeenCalledWith({
|
||||||
|
onCompleted: expect.any(Function),
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('create SAML sso identity provider', async () => {
|
||||||
|
const SAMLParams = {
|
||||||
|
type: 'SAML' as const,
|
||||||
|
name: 'test',
|
||||||
|
metadata: 'test',
|
||||||
|
certificate: 'test',
|
||||||
|
id: 'test',
|
||||||
|
issuer: 'test',
|
||||||
|
ssoURL: 'test',
|
||||||
|
};
|
||||||
|
renderHook(
|
||||||
|
() => {
|
||||||
|
const { createSSOIdentityProvider } = useCreateSSOIdentityProvider();
|
||||||
|
createSSOIdentityProvider(SAMLParams);
|
||||||
|
},
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
const { type, ...input } = SAMLParams;
|
||||||
|
expect(mutationOIDCCallSpy).not.toHaveBeenCalled();
|
||||||
|
expect(mutationSAMLCallSpy).toHaveBeenCalledWith({
|
||||||
|
onCompleted: expect.any(Function),
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throw error if provider is not SAML or OIDC', async () => {
|
||||||
|
const OTHERParams = {
|
||||||
|
type: 'OTHER' as const,
|
||||||
|
};
|
||||||
|
renderHook(
|
||||||
|
async () => {
|
||||||
|
const { createSSOIdentityProvider } = useCreateSSOIdentityProvider();
|
||||||
|
await expect(
|
||||||
|
// @ts-expect-error - It's expected to throw an error
|
||||||
|
createSSOIdentityProvider(OTHERParams),
|
||||||
|
).rejects.toThrowError();
|
||||||
|
},
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider';
|
||||||
|
|
||||||
|
const mutationDeleteSSOIDPCallSpy = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('~/generated/graphql', () => ({
|
||||||
|
useDeleteSsoIdentityProviderMutation: () => [mutationDeleteSSOIDPCallSpy],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useDeleteSsoIdentityProvider', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delete SSO identity provider', async () => {
|
||||||
|
renderHook(
|
||||||
|
() => {
|
||||||
|
const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider();
|
||||||
|
deleteSSOIdentityProvider({ identityProviderId: 'test' });
|
||||||
|
},
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mutationDeleteSSOIDPCallSpy).toHaveBeenCalledWith({
|
||||||
|
onCompleted: expect.any(Function),
|
||||||
|
variables: {
|
||||||
|
input: { identityProviderId: 'test' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider';
|
||||||
|
import { SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const mutationEditSSOIDPCallSpy = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('~/generated/graphql', () => {
|
||||||
|
const actual = jest.requireActual('~/generated/graphql');
|
||||||
|
return {
|
||||||
|
useEditSsoIdentityProviderMutation: () => [mutationEditSSOIDPCallSpy],
|
||||||
|
SsoIdentityProviderStatus: actual.SsoIdentityProviderStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useEditSsoIdentityProvider', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Deactivate SSO identity provider', async () => {
|
||||||
|
const params = {
|
||||||
|
id: 'test',
|
||||||
|
status: SsoIdentityProviderStatus.Inactive,
|
||||||
|
};
|
||||||
|
renderHook(
|
||||||
|
() => {
|
||||||
|
const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider();
|
||||||
|
updateSSOIdentityProvider(params);
|
||||||
|
},
|
||||||
|
{ wrapper: Wrapper },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mutationEditSSOIDPCallSpy).toHaveBeenCalledWith({
|
||||||
|
onCompleted: expect.any(Function),
|
||||||
|
variables: {
|
||||||
|
input: params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import {
|
||||||
|
CreateOidcIdentityProviderMutationVariables,
|
||||||
|
CreateSamlIdentityProviderMutationVariables,
|
||||||
|
useCreateOidcIdentityProviderMutation,
|
||||||
|
useCreateSamlIdentityProviderMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useCreateSSOIdentityProvider = () => {
|
||||||
|
const [createOidcIdentityProviderMutation] =
|
||||||
|
useCreateOidcIdentityProviderMutation();
|
||||||
|
const [createSamlIdentityProviderMutation] =
|
||||||
|
useCreateSamlIdentityProviderMutation();
|
||||||
|
|
||||||
|
const setSSOIdentitiesProviders = useSetRecoilState(
|
||||||
|
SSOIdentitiesProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const createSSOIdentityProvider = async (
|
||||||
|
input:
|
||||||
|
| ({
|
||||||
|
type: 'OIDC';
|
||||||
|
} & CreateOidcIdentityProviderMutationVariables['input'])
|
||||||
|
| ({
|
||||||
|
type: 'SAML';
|
||||||
|
} & CreateSamlIdentityProviderMutationVariables['input']),
|
||||||
|
) => {
|
||||||
|
if (input.type === 'OIDC') {
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
const { type, ...params } = input;
|
||||||
|
return await createOidcIdentityProviderMutation({
|
||||||
|
variables: { input: params },
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSSOIdentitiesProviders((existingProvider) => [
|
||||||
|
...existingProvider,
|
||||||
|
data.createOIDCIdentityProvider,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (input.type === 'SAML') {
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
const { type, ...params } = input;
|
||||||
|
return await createSamlIdentityProviderMutation({
|
||||||
|
variables: { input: params },
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSSOIdentitiesProviders((existingProvider) => [
|
||||||
|
...existingProvider,
|
||||||
|
data.createSAMLIdentityProvider,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid IdpType');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSSOIdentityProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import {
|
||||||
|
DeleteSsoIdentityProviderMutationVariables,
|
||||||
|
useDeleteSsoIdentityProviderMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useDeleteSSOIdentityProvider = () => {
|
||||||
|
const [deleteSsoIdentityProviderMutation] =
|
||||||
|
useDeleteSsoIdentityProviderMutation();
|
||||||
|
|
||||||
|
const setSSOIdentitiesProviders = useSetRecoilState(
|
||||||
|
SSOIdentitiesProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteSSOIdentityProvider = async ({
|
||||||
|
identityProviderId,
|
||||||
|
}: DeleteSsoIdentityProviderMutationVariables['input']) => {
|
||||||
|
return await deleteSsoIdentityProviderMutation({
|
||||||
|
variables: {
|
||||||
|
input: { identityProviderId },
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSSOIdentitiesProviders((SSOIdentitiesProviders) =>
|
||||||
|
SSOIdentitiesProviders.filter(
|
||||||
|
(identityProvider) =>
|
||||||
|
identityProvider.id !==
|
||||||
|
data.deleteSSOIdentityProvider.identityProviderId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteSSOIdentityProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import {
|
||||||
|
EditSsoIdentityProviderMutationVariables,
|
||||||
|
useEditSsoIdentityProviderMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useUpdateSSOIdentityProvider = () => {
|
||||||
|
const [editSsoIdentityProviderMutation] =
|
||||||
|
useEditSsoIdentityProviderMutation();
|
||||||
|
|
||||||
|
const setSSOIdentitiesProviders = useSetRecoilState(
|
||||||
|
SSOIdentitiesProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSSOIdentityProvider = async (
|
||||||
|
payload: EditSsoIdentityProviderMutationVariables['input'],
|
||||||
|
) => {
|
||||||
|
return await editSsoIdentityProviderMutation({
|
||||||
|
variables: {
|
||||||
|
input: payload,
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setSSOIdentitiesProviders((SSOIdentitiesProviders) =>
|
||||||
|
SSOIdentitiesProviders.map((identityProvider) =>
|
||||||
|
identityProvider.id === data.editSSOIdentityProvider.id
|
||||||
|
? data.editSSOIdentityProvider
|
||||||
|
: identityProvider,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateSSOIdentityProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SSOIdentityProvider } from '@/settings/security/types/SSOIdentityProvider';
|
||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const SSOIdentitiesProvidersState = createState<
|
||||||
|
Omit<SSOIdentityProvider, '__typename'>[]
|
||||||
|
>({
|
||||||
|
key: 'SSOIdentitiesProvidersState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { IdpType, SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export type SSOIdentityProvider = {
|
||||||
|
__typename: 'SSOIdentityProvider';
|
||||||
|
id: string;
|
||||||
|
type: IdpType;
|
||||||
|
issuer: string;
|
||||||
|
name?: string | null;
|
||||||
|
status: SsoIdentityProviderStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingSecurityNewSSOIdentityFormValues = z.infer<
|
||||||
|
typeof SSOIdentitiesProvidersParamsSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { parseSAMLMetadataFromXMLFile } from '../parseSAMLMetadataFromXMLFile';
|
||||||
|
|
||||||
|
describe('parseSAMLMetadataFromXMLFile', () => {
|
||||||
|
it('should parse SAML metadata from XML file', () => {
|
||||||
|
const xmlString = `<?xml version="1.0" encoding="UTF-8"?><md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://test.com" validUntil="2026-02-04T17:46:23.000Z">
|
||||||
|
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<md:KeyDescriptor use="signing">
|
||||||
|
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
|
<ds:X509Data>
|
||||||
|
<ds:X509Certificate>test</ds:X509Certificate>
|
||||||
|
</ds:X509Data>
|
||||||
|
</ds:KeyInfo>
|
||||||
|
</md:KeyDescriptor>
|
||||||
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
||||||
|
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test.com"/>
|
||||||
|
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.com"/>
|
||||||
|
</md:IDPSSODescriptor>
|
||||||
|
</md:EntityDescriptor>`;
|
||||||
|
const result = parseSAMLMetadataFromXMLFile(xmlString);
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
entityID: 'https://test.com',
|
||||||
|
ssoUrl: 'https://test.com',
|
||||||
|
certificate: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return error if XML is invalid', () => {
|
||||||
|
const xmlString = 'invalid xml';
|
||||||
|
const result = parseSAMLMetadataFromXMLFile(xmlString);
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new Error('Error parsing XML'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { ThemeColor } from 'twenty-ui';
|
||||||
|
import { SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const getColorBySSOIdentityProviderStatus: Record<
|
||||||
|
SsoIdentityProviderStatus,
|
||||||
|
ThemeColor
|
||||||
|
> = {
|
||||||
|
Active: 'green',
|
||||||
|
Inactive: 'gray',
|
||||||
|
Error: 'red',
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { IconComponent, IconGoogle, IconKey } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const guessSSOIdentityProviderIconByUrl = (
|
||||||
|
url: string,
|
||||||
|
): IconComponent => {
|
||||||
|
if (url.includes('google')) {
|
||||||
|
return IconGoogle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconKey;
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const validator = z.object({
|
||||||
|
entityID: z.string().url(),
|
||||||
|
ssoUrl: z.string().url(),
|
||||||
|
certificate: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const parseSAMLMetadataFromXMLFile = (
|
||||||
|
xmlString: string,
|
||||||
|
):
|
||||||
|
| { success: true; data: z.infer<typeof validator> }
|
||||||
|
| { success: false; error: unknown } => {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
|
||||||
|
|
||||||
|
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
||||||
|
throw new Error('Error parsing XML');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityDescriptor = xmlDoc.getElementsByTagName(
|
||||||
|
'md:EntityDescriptor',
|
||||||
|
)?.[0];
|
||||||
|
const idpSSODescriptor = xmlDoc.getElementsByTagName(
|
||||||
|
'md:IDPSSODescriptor',
|
||||||
|
)?.[0];
|
||||||
|
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
|
||||||
|
const keyInfo = keyDescriptor.getElementsByTagName('ds:KeyInfo')[0];
|
||||||
|
const x509Data = keyInfo.getElementsByTagName('ds:X509Data')[0];
|
||||||
|
const x509Certificate = x509Data
|
||||||
|
.getElementsByTagName('ds:X509Certificate')?.[0]
|
||||||
|
.textContent?.trim();
|
||||||
|
|
||||||
|
const singleSignOnServices = Array.from(
|
||||||
|
idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'),
|
||||||
|
).map((service) => ({
|
||||||
|
Binding: service.getAttribute('Binding'),
|
||||||
|
Location: service.getAttribute('Location'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
ssoUrl: singleSignOnServices.find((singleSignOnService) => {
|
||||||
|
return (
|
||||||
|
singleSignOnService.Binding ===
|
||||||
|
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||||
|
);
|
||||||
|
})?.Location,
|
||||||
|
certificate: x509Certificate,
|
||||||
|
entityID: entityDescriptor?.getAttribute('entityID'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, data: validator.parse(result) };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||||
|
import { IdpType } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const sSOIdentityProviderDefaultValues: Record<
|
||||||
|
IdpType,
|
||||||
|
() => SettingSecurityNewSSOIdentityFormValues
|
||||||
|
> = {
|
||||||
|
SAML: () => ({
|
||||||
|
type: 'SAML',
|
||||||
|
ssoURL: '',
|
||||||
|
name: '',
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
certificate: '',
|
||||||
|
issuer: '',
|
||||||
|
}),
|
||||||
|
OIDC: () => ({
|
||||||
|
type: 'OIDC',
|
||||||
|
name: '',
|
||||||
|
clientID: '',
|
||||||
|
clientSecret: '',
|
||||||
|
issuer: '',
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const SSOIdentitiesProvidersOIDCParamsSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.literal('OIDC'),
|
||||||
|
clientID: z.string().optional(),
|
||||||
|
clientSecret: z.string().optional(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const SSOIdentitiesProvidersSAMLParamsSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.literal('SAML'),
|
||||||
|
id: z.string().optional(),
|
||||||
|
ssoURL: z.string().url().optional(),
|
||||||
|
certificate: z.string().optional(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const SSOIdentitiesProvidersParamsSchema = z
|
||||||
|
.discriminatedUnion('type', [
|
||||||
|
SSOIdentitiesProvidersOIDCParamsSchema,
|
||||||
|
SSOIdentitiesProvidersSAMLParamsSchema,
|
||||||
|
])
|
||||||
|
.and(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
issuer: z.string().url().optional(),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
);
|
||||||
@@ -30,6 +30,9 @@ export enum SettingsPath {
|
|||||||
IntegrationDatabaseConnection = 'integrations/:databaseKey/:connectionId',
|
IntegrationDatabaseConnection = 'integrations/:databaseKey/:connectionId',
|
||||||
IntegrationEditDatabaseConnection = 'integrations/:databaseKey/:connectionId/edit',
|
IntegrationEditDatabaseConnection = 'integrations/:databaseKey/:connectionId/edit',
|
||||||
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
|
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
|
||||||
|
Security = 'security',
|
||||||
|
NewSSOIdentityProvider = 'security/sso/new',
|
||||||
|
EditSSOIdentityProvider = 'security/sso/:identityProviderId',
|
||||||
DevelopersNewWebhook = 'webhooks/new',
|
DevelopersNewWebhook = 'webhooks/new',
|
||||||
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
|
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
|
||||||
Releases = 'releases',
|
Releases = 'releases',
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const StyledButton = styled.button<
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||||
|
max-height: ${({ theme }) => theme.spacing(8)};
|
||||||
width: ${({ fullWidth, width }) =>
|
width: ${({ fullWidth, width }) =>
|
||||||
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
||||||
${({ theme, variant, disabled }) => {
|
${({ theme, variant, disabled }) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const StyledCircle = styled(motion.span)<{
|
|||||||
export type ToggleProps = {
|
export type ToggleProps = {
|
||||||
id?: string;
|
id?: string;
|
||||||
value?: boolean;
|
value?: boolean;
|
||||||
onChange?: (value: boolean) => void;
|
onChange?: (value: boolean, e?: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
color?: string;
|
color?: string;
|
||||||
toggleSize?: ToggleSize;
|
toggleSize?: ToggleSize;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -6,11 +6,24 @@ import { AppPath } from '@/types/AppPath';
|
|||||||
import { useGenerateJwtMutation } from '~/generated/graphql';
|
import { useGenerateJwtMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { sleep } from '~/utils/sleep';
|
import { sleep } from '~/utils/sleep';
|
||||||
|
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||||
|
import {
|
||||||
|
SignInUpStep,
|
||||||
|
signInUpStepState,
|
||||||
|
} from '@/auth/states/signInUpStepState';
|
||||||
|
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||||
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
|
|
||||||
export const useWorkspaceSwitching = () => {
|
export const useWorkspaceSwitching = () => {
|
||||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
const [generateJWT] = useGenerateJwtMutation();
|
const [generateJWT] = useGenerateJwtMutation();
|
||||||
|
const { redirectToSSOLoginPage } = useSSO();
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
|
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||||
|
availableSSOIdentityProvidersState,
|
||||||
|
);
|
||||||
|
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||||
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
const switchWorkspace = async (workspaceId: string) => {
|
const switchWorkspace = async (workspaceId: string) => {
|
||||||
if (currentWorkspace?.id === workspaceId) return;
|
if (currentWorkspace?.id === workspaceId) return;
|
||||||
@@ -28,10 +41,34 @@ export const useWorkspaceSwitching = () => {
|
|||||||
throw new Error('could not create token');
|
throw new Error('could not create token');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tokens } = jwt.data.generateJWT;
|
if (
|
||||||
|
jwt.data.generateJWT.reason === 'WORKSPACE_USE_SSO_AUTH' &&
|
||||||
|
'availableSSOIDPs' in jwt.data.generateJWT
|
||||||
|
) {
|
||||||
|
if (jwt.data.generateJWT.availableSSOIDPs.length === 1) {
|
||||||
|
redirectToSSOLoginPage(jwt.data.generateJWT.availableSSOIDPs[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwt.data.generateJWT.availableSSOIDPs.length > 1) {
|
||||||
|
await signOut();
|
||||||
|
setAvailableWorkspacesForSSOState(
|
||||||
|
jwt.data.generateJWT.availableSSOIDPs,
|
||||||
|
);
|
||||||
|
setSignInUpStep(SignInUpStep.SSOWorkspaceSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
jwt.data.generateJWT.reason !== 'WORKSPACE_USE_SSO_AUTH' &&
|
||||||
|
'authTokens' in jwt.data.generateJWT
|
||||||
|
) {
|
||||||
|
const { tokens } = jwt.data.generateJWT.authTokens;
|
||||||
setTokenPair(tokens);
|
setTokenPair(tokens);
|
||||||
await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly.
|
await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly.
|
||||||
window.location.href = AppPath.Index;
|
window.location.href = AppPath.Index;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { switchWorkspace };
|
return { switchWorkspace };
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
inviteHash
|
inviteHash
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
activationStatus
|
activationStatus
|
||||||
|
isPublicInviteLinkEnabled
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
key
|
key
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useSendInvitationsMutation } from '~/generated/graphql';
|
import {
|
||||||
import { SendInvitationsMutationVariables } from '../../../generated/graphql';
|
useSendInvitationsMutation,
|
||||||
|
SendInvitationsMutationVariables,
|
||||||
|
} from '~/generated/graphql';
|
||||||
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
||||||
|
|
||||||
export const useCreateWorkspaceInvitation = () => {
|
export const useCreateWorkspaceInvitation = () => {
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ export type FeatureFlagKey =
|
|||||||
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
||||||
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
|
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
|
||||||
| 'IS_ANALYTICS_V2_ENABLED'
|
| 'IS_ANALYTICS_V2_ENABLED'
|
||||||
|
| 'IS_SSO_ENABLED'
|
||||||
| 'IS_UNIQUE_INDEXES_ENABLED'
|
| 'IS_UNIQUE_INDEXES_ENABLED'
|
||||||
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED';
|
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED';
|
||||||
|
|||||||
@@ -91,25 +91,7 @@ export const Invite = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
<FooterNote>
|
<FooterNote />
|
||||||
By using Twenty, you agree to the{' '}
|
|
||||||
<a
|
|
||||||
href="https://twenty.com/legal/terms"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</a>{' '}
|
|
||||||
and{' '}
|
|
||||||
<a
|
|
||||||
href="https://twenty.com/legal/privacy"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</FooterNote>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<SignInUpForm />
|
<SignInUpForm />
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||||
|
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
|
||||||
|
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||||
|
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||||
|
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
|
||||||
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
|
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
const StyledContentContainer = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.h2`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SSOWorkspaceSelection = () => {
|
||||||
|
const availableSSOIdentityProviders = useRecoilValue(
|
||||||
|
availableSSOIdentityProvidersState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { redirectToSSOLoginPage } = useSSO();
|
||||||
|
|
||||||
|
const availableWorkspacesForSSOGroupByWorkspace =
|
||||||
|
availableSSOIdentityProviders.reduce(
|
||||||
|
(acc, idp) => {
|
||||||
|
acc[idp.workspace.id] = [...(acc[idp.workspace.id] ?? []), idp];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, typeof availableSSOIdentityProviders>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledContentContainer>
|
||||||
|
{Object.values(availableWorkspacesForSSOGroupByWorkspace).map(
|
||||||
|
(idps) => (
|
||||||
|
<>
|
||||||
|
<StyledTitle>
|
||||||
|
{idps[0].workspace.displayName ?? DEFAULT_WORKSPACE_NAME}
|
||||||
|
</StyledTitle>
|
||||||
|
<HorizontalSeparator visible={false} />
|
||||||
|
{idps.map((idp) => (
|
||||||
|
<>
|
||||||
|
<MainButton
|
||||||
|
title={idp.name}
|
||||||
|
onClick={() => redirectToSSOLoginPage(idp.id)}
|
||||||
|
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<HorizontalSeparator visible={false} />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</StyledContentContainer>
|
||||||
|
<FooterNote />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,15 +4,14 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { Logo } from '@/auth/components/Logo';
|
import { Logo } from '@/auth/components/Logo';
|
||||||
import { Title } from '@/auth/components/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||||
import {
|
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||||
SignInUpMode,
|
|
||||||
SignInUpStep,
|
|
||||||
useSignInUp,
|
|
||||||
} from '@/auth/sign-in-up/hooks/useSignInUp';
|
|
||||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||||
|
import { IconLockCustom } from '@ui/display/icon/components/IconLock';
|
||||||
|
import { SSOWorkspaceSelection } from './SSOWorkspaceSelection';
|
||||||
|
|
||||||
export const SignInUp = () => {
|
export const SignInUp = () => {
|
||||||
const { form } = useSignInUpForm();
|
const { form } = useSignInUpForm();
|
||||||
@@ -27,6 +26,9 @@ export const SignInUp = () => {
|
|||||||
) {
|
) {
|
||||||
return 'Welcome to Twenty';
|
return 'Welcome to Twenty';
|
||||||
}
|
}
|
||||||
|
if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) {
|
||||||
|
return 'Choose SSO connection';
|
||||||
|
}
|
||||||
return signInUpMode === SignInUpMode.SignIn
|
return signInUpMode === SignInUpMode.SignIn
|
||||||
? 'Sign in to Twenty'
|
? 'Sign in to Twenty'
|
||||||
: 'Sign up to Twenty';
|
: 'Sign up to Twenty';
|
||||||
@@ -39,10 +41,18 @@ export const SignInUp = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatedEaseIn>
|
<AnimatedEaseIn>
|
||||||
|
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||||
|
<IconLockCustom size={40} />
|
||||||
|
) : (
|
||||||
<Logo />
|
<Logo />
|
||||||
|
)}
|
||||||
</AnimatedEaseIn>
|
</AnimatedEaseIn>
|
||||||
<Title animate>{title}</Title>
|
<Title animate>{title}</Title>
|
||||||
|
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||||
|
<SSOWorkspaceSelection />
|
||||||
|
) : (
|
||||||
<SignInUpForm />
|
<SignInUpForm />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
{currentWorkspace?.inviteHash && (
|
{currentWorkspace?.inviteHash &&
|
||||||
|
currentWorkspace?.isPublicInviteLinkEnabled && (
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Invite by link"
|
title="Invite by link"
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { H2Title } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
||||||
|
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCard';
|
||||||
|
import { SettingsSecurityOptionsList } from '@/settings/security/components/SettingsSecurityOptionsList';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
|
||||||
|
export const SettingsSecurity = () => {
|
||||||
|
return (
|
||||||
|
<SubMenuTopBarContainer
|
||||||
|
title="Security"
|
||||||
|
actionButton={<SettingsReadDocumentationButton />}
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Workspace',
|
||||||
|
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||||
|
},
|
||||||
|
{ children: 'Security' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<Section>
|
||||||
|
<H2Title title="SSO" description="Configure an SSO connection" />
|
||||||
|
<SettingsSSOIdentitiesProvidersListCard />
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Other"
|
||||||
|
description="Customize your workspace security"
|
||||||
|
/>
|
||||||
|
<SettingsSecurityOptionsList />
|
||||||
|
</Section>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
|
import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SettingsSSOIdentitiesProvidersForm';
|
||||||
|
import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider';
|
||||||
|
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||||
|
import { sSOIdentityProviderDefaultValues } from '@/settings/security/utils/sSOIdentityProviderDefaultValues';
|
||||||
|
import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const SettingsSecuritySSOIdentifyProvider = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const { createSSOIdentityProvider } = useCreateSSOIdentityProvider();
|
||||||
|
|
||||||
|
const formConfig = useForm<SettingSecurityNewSSOIdentityFormValues>({
|
||||||
|
mode: 'onChange',
|
||||||
|
resolver: zodResolver(SSOIdentitiesProvidersParamsSchema),
|
||||||
|
defaultValues: Object.values(sSOIdentityProviderDefaultValues).reduce(
|
||||||
|
(acc, fn) => ({ ...acc, ...fn() }),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedType = formConfig.watch('type');
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
formConfig.reset({
|
||||||
|
...sSOIdentityProviderDefaultValues[selectedType](),
|
||||||
|
name: formConfig.getValues('name'),
|
||||||
|
}),
|
||||||
|
[formConfig, selectedType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await createSSOIdentityProvider(formConfig.getValues());
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.Security));
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackBar((error as Error).message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubMenuTopBarContainer
|
||||||
|
title="New SSO Configuration"
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!formConfig.formState.isValid}
|
||||||
|
onCancel={() => navigate(getSettingsPagePath(SettingsPath.Security))}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Workspace',
|
||||||
|
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: 'Security',
|
||||||
|
href: getSettingsPagePath(SettingsPath.Security),
|
||||||
|
},
|
||||||
|
{ children: 'New' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormProvider
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...formConfig}
|
||||||
|
>
|
||||||
|
<SettingsSSOIdentitiesProvidersForm />
|
||||||
|
</FormProvider>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,9 @@ export const mockedClientConfig: ClientConfig = {
|
|||||||
signUpDisabled: false,
|
signUpDisabled: false,
|
||||||
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
|
analyticsEnabled: true,
|
||||||
authProviders: {
|
authProviders: {
|
||||||
|
sso: false,
|
||||||
google: true,
|
google: true,
|
||||||
password: true,
|
password: true,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const mockDefaultWorkspace: Workspace = {
|
|||||||
domainName: 'twenty.com',
|
domainName: 'twenty.com',
|
||||||
inviteHash: 'twenty.com-invite-hash',
|
inviteHash: 'twenty.com-invite-hash',
|
||||||
logo: workspaceLogoUrl,
|
logo: workspaceLogoUrl,
|
||||||
|
isPublicInviteLinkEnabled: true,
|
||||||
allowImpersonation: true,
|
allowImpersonation: true,
|
||||||
activationStatus: WorkspaceActivationStatus.Active,
|
activationStatus: WorkspaceActivationStatus.Active,
|
||||||
featureFlags: [
|
featureFlags: [
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ REDIS_URL=redis://localhost:6379
|
|||||||
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
||||||
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
||||||
# AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
# AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
||||||
|
# AUTH_SSO_ENABLED=false
|
||||||
# SERVERLESS_TYPE=local
|
# SERVERLESS_TYPE=local
|
||||||
# STORAGE_TYPE=local
|
# STORAGE_TYPE=local
|
||||||
# STORAGE_LOCAL_PATH=.local-storage
|
# STORAGE_LOCAL_PATH=.local-storage
|
||||||
@@ -74,3 +75,5 @@ REDIS_URL=redis://localhost:6379
|
|||||||
# MUTATION_MAXIMUM_AFFECTED_RECORDS=100
|
# MUTATION_MAXIMUM_AFFECTED_RECORDS=100
|
||||||
# CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp
|
# CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp
|
||||||
# PG_SSL_ALLOW_SELF_SIGNED=true
|
# PG_SSL_ALLOW_SELF_SIGNED=true
|
||||||
|
# SESSION_STORE_SECRET=replace_me_with_a_random_string_session
|
||||||
|
# ENTERPRISE_KEY=replace_me_with_a_valid_enterprise_key
|
||||||
|
|||||||
@@ -23,12 +23,15 @@
|
|||||||
"@nestjs/cache-manager": "^2.2.1",
|
"@nestjs/cache-manager": "^2.2.1",
|
||||||
"@nestjs/devtools-integration": "^0.1.6",
|
"@nestjs/devtools-integration": "^0.1.6",
|
||||||
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
||||||
|
"@node-saml/passport-saml": "^5.0.0",
|
||||||
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
||||||
"@revertdotdev/revert-react": "^0.0.21",
|
"@revertdotdev/revert-react": "^0.0.21",
|
||||||
"@sentry/nestjs": "^8.30.0",
|
"@sentry/nestjs": "^8.30.0",
|
||||||
"cache-manager": "^5.4.0",
|
"cache-manager": "^5.4.0",
|
||||||
"cache-manager-redis-yet": "^4.1.2",
|
"cache-manager-redis-yet": "^4.1.2",
|
||||||
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
|
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
|
||||||
|
"connect-redis": "^7.1.1",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
"graphql-middleware": "^6.1.35",
|
"graphql-middleware": "^6.1.35",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"jsdom": "~22.1.0",
|
"jsdom": "~22.1.0",
|
||||||
@@ -42,8 +45,10 @@
|
|||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"monaco-editor": "^0.51.0",
|
"monaco-editor": "^0.51.0",
|
||||||
"monaco-editor-auto-typings": "^0.4.5",
|
"monaco-editor-auto-typings": "^0.4.5",
|
||||||
|
"openid-client": "^5.7.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"psl": "^1.9.0",
|
"psl": "^1.9.0",
|
||||||
|
"redis": "^4.7.0",
|
||||||
"ts-morph": "^24.0.0",
|
"ts-morph": "^24.0.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch",
|
"typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch",
|
||||||
@@ -53,6 +58,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "10.3.0",
|
"@nestjs/cli": "10.3.0",
|
||||||
"@nx/js": "18.3.3",
|
"@nx/js": "18.3.3",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
"@types/lodash.differencewith": "^4.5.9",
|
"@types/lodash.differencewith": "^4.5.9",
|
||||||
"@types/lodash.isempty": "^4.4.7",
|
"@types/lodash.isempty": "^4.4.7",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
@@ -64,6 +70,7 @@
|
|||||||
"@types/lodash.uniq": "^4.5.9",
|
"@types/lodash.uniq": "^4.5.9",
|
||||||
"@types/lodash.uniqby": "^4.7.9",
|
"@types/lodash.uniqby": "^4.7.9",
|
||||||
"@types/lodash.upperfirst": "^4.3.7",
|
"@types/lodash.upperfirst": "^4.3.7",
|
||||||
|
"@types/openid-client": "^3.7.0",
|
||||||
"@types/react": "^18.2.39",
|
"@types/react": "^18.2.39",
|
||||||
"@types/unzipper": "^0",
|
"@types/unzipper": "^0",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsSSOEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddWorkspaceSSOIdentityProvider1727181198403
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddWorkspaceSSOIdentityProvider1727181198403';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TYPE "core"."idp_type_enum" AS ENUM('OIDC', 'SAML');
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE "core"."workspaceSSOIdentityProvider" (
|
||||||
|
"id" uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||||
|
"name" varchar NULL,
|
||||||
|
"workspaceId" uuid NOT NULL,
|
||||||
|
"createdAt" timestamptz DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamptz DEFAULT now() NOT NULL,
|
||||||
|
"type" "core"."idp_type_enum" DEFAULT 'OIDC' NOT NULL,
|
||||||
|
"issuer" varchar NOT NULL,
|
||||||
|
"ssoURL" varchar NULL,
|
||||||
|
"clientID" varchar NULL,
|
||||||
|
"clientSecret" varchar NULL,
|
||||||
|
"certificate" varchar NULL,
|
||||||
|
"fingerprint" varchar NULL,
|
||||||
|
"status" varchar DEFAULT 'Active' NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "core"."workspaceSSOIdentityProvider"
|
||||||
|
ADD CONSTRAINT "FK_workspaceId"
|
||||||
|
FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id")
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_OIDC" CHECK (
|
||||||
|
("type" = 'OIDC' AND "clientID" IS NOT NULL AND "clientSecret" IS NOT NULL) OR "type" = 'SAML'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_SAML" CHECK (
|
||||||
|
("type" = 'SAML' AND "ssoURL" IS NOT NULL AND "certificate" IS NOT NULL) OR "type" = 'OIDC'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "core"."workspaceSSOIdentityProvider"
|
||||||
|
DROP CONSTRAINT "FK_workspaceId";
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DROP TABLE "core"."workspaceSSOIdentityProvider";
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
DROP TYPE "core"."idp_type_enum";
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddIsPublicInviteLinkEnabledOnWorkspace1728986317196
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddIsPublicInviteLinkEnabledOnWorkspace1728986317196';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" ADD "isPublicInviteLinkEnabled" boolean NOT NULL DEFAULT true`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" DROP COLUMN "isPublicInviteLinkEnabled"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
|
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||||
@@ -36,6 +37,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
|||||||
BillingSubscription,
|
BillingSubscription,
|
||||||
BillingSubscriptionItem,
|
BillingSubscriptionItem,
|
||||||
PostgresCredentials,
|
PostgresCredentials,
|
||||||
|
WorkspaceSSOIdentityProvider,
|
||||||
],
|
],
|
||||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||||
ssl: environmentService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
ssl: environmentService.get('PG_SSL_ALLOW_SELF_SIGNED')
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export enum AppTokenType {
|
|||||||
AuthorizationCode = 'AUTHORIZATION_CODE',
|
AuthorizationCode = 'AUTHORIZATION_CODE',
|
||||||
PasswordResetToken = 'PASSWORD_RESET_TOKEN',
|
PasswordResetToken = 'PASSWORD_RESET_TOKEN',
|
||||||
InvitationToken = 'INVITATION_TOKEN',
|
InvitationToken = 'INVITATION_TOKEN',
|
||||||
|
OIDCCodeVerifier = 'OIDC_CODE_VERIFIER',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity({ name: 'appToken', schema: 'core' })
|
@Entity({ name: 'appToken', schema: 'core' })
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ export enum AuthExceptionCode {
|
|||||||
INVALID_DATA = 'INVALID_DATA',
|
INVALID_DATA = 'INVALID_DATA',
|
||||||
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||||
OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED',
|
OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED',
|
||||||
|
SSO_AUTH_FAILED = 'SSO_AUTH_FAILED',
|
||||||
|
USE_SSO_AUTH = 'USE_SSO_AUTH',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
|
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||||
|
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
|
||||||
|
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
|
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||||
|
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||||
|
|
||||||
import { AuthResolver } from './auth.resolver';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@@ -43,7 +49,14 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
WorkspaceManagerModule,
|
WorkspaceManagerModule,
|
||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[Workspace, User, AppToken, FeatureFlagEntity],
|
[
|
||||||
|
Workspace,
|
||||||
|
User,
|
||||||
|
AppToken,
|
||||||
|
FeatureFlagEntity,
|
||||||
|
WorkspaceSSOIdentityProvider,
|
||||||
|
KeyValuePair,
|
||||||
|
],
|
||||||
'core',
|
'core',
|
||||||
),
|
),
|
||||||
HttpModule,
|
HttpModule,
|
||||||
@@ -52,7 +65,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
OnboardingModule,
|
OnboardingModule,
|
||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
|
WorkspaceInvitationModule,
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
|
WorkspaceSSOModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
@@ -60,11 +75,13 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
MicrosoftAuthController,
|
MicrosoftAuthController,
|
||||||
GoogleAPIsAuthController,
|
GoogleAPIsAuthController,
|
||||||
VerifyAuthController,
|
VerifyAuthController,
|
||||||
|
SSOAuthController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SignInUpService,
|
SignInUpService,
|
||||||
AuthService,
|
AuthService,
|
||||||
JwtAuthStrategy,
|
JwtAuthStrategy,
|
||||||
|
SamlAuthStrategy,
|
||||||
AuthResolver,
|
AuthResolver,
|
||||||
TokenService,
|
TokenService,
|
||||||
GoogleAPIsService,
|
GoogleAPIsService,
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
|||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import {
|
||||||
|
GenerateJWTOutput,
|
||||||
|
GenerateJWTOutputWithAuthTokens,
|
||||||
|
GenerateJWTOutputWithSSOAUTH,
|
||||||
|
} from 'src/engine/core-modules/auth/dto/generateJWT.output';
|
||||||
|
|
||||||
import { ChallengeInput } from './dto/challenge.input';
|
import { ChallengeInput } from './dto/challenge.input';
|
||||||
import { ImpersonateInput } from './dto/impersonate.input';
|
import { ImpersonateInput } from './dto/impersonate.input';
|
||||||
@@ -159,18 +164,41 @@ export class AuthResolver {
|
|||||||
return authorizedApp;
|
return authorizedApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AuthTokens)
|
@Mutation(() => GenerateJWTOutput)
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||||
async generateJWT(
|
async generateJWT(
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@Args() args: GenerateJwtInput,
|
@Args() args: GenerateJwtInput,
|
||||||
): Promise<AuthTokens> {
|
): Promise<GenerateJWTOutputWithAuthTokens | GenerateJWTOutputWithSSOAUTH> {
|
||||||
const token = await this.tokenService.generateSwitchWorkspaceToken(
|
const result = await this.tokenService.switchWorkspace(
|
||||||
user,
|
user,
|
||||||
args.workspaceId,
|
args.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return token;
|
if (result.useSSOAuth) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
reason: 'WORKSPACE_USE_SSO_AUTH',
|
||||||
|
availableSSOIDPs: result.availableSSOIdentityProviders.map(
|
||||||
|
(identityProvider) => ({
|
||||||
|
...identityProvider,
|
||||||
|
workspace: {
|
||||||
|
id: result.workspace.id,
|
||||||
|
displayName: result.workspace.displayName,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH',
|
||||||
|
authTokens: await this.tokenService.generateSwitchWorkspaceToken(
|
||||||
|
user,
|
||||||
|
result.workspace,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AuthTokens)
|
@Mutation(() => AuthTokens)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
UseFilters,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { generateServiceProviderMetadata } from '@node-saml/node-saml';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
|
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
|
||||||
|
import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard';
|
||||||
|
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
|
||||||
|
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
|
import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
import {
|
||||||
|
IdentityProviderType,
|
||||||
|
WorkspaceSSOIdentityProvider,
|
||||||
|
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
|
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
|
export class SSOAuthController {
|
||||||
|
constructor(
|
||||||
|
private readonly tokenService: TokenService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
|
private readonly ssoService: SSOService,
|
||||||
|
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||||
|
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('saml/metadata/:identityProviderId')
|
||||||
|
@UseGuards(SSOProviderEnabledGuard)
|
||||||
|
async generateMetadata(@Req() req: any): Promise<string> {
|
||||||
|
return generateServiceProviderMetadata({
|
||||||
|
wantAssertionsSigned: false,
|
||||||
|
issuer: this.ssoService.buildIssuerURL({
|
||||||
|
id: req.params.identityProviderId,
|
||||||
|
type: IdentityProviderType.SAML,
|
||||||
|
}),
|
||||||
|
callbackUrl: this.ssoService.buildCallbackUrl({
|
||||||
|
type: IdentityProviderType.SAML,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('oidc/login/:identityProviderId')
|
||||||
|
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||||
|
async oidcAuth() {
|
||||||
|
// As this method is protected by OIDC Auth guard, it will trigger OIDC SSO flow
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('saml/login/:identityProviderId')
|
||||||
|
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||||
|
async samlAuth() {
|
||||||
|
// As this method is protected by SAML Auth guard, it will trigger SAML SSO flow
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('oidc/callback')
|
||||||
|
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||||
|
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
|
try {
|
||||||
|
const loginToken = await this.generateLoginToken(req.user);
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
this.tokenService.computeRedirectURI(loginToken.token),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: improve error management
|
||||||
|
res.status(403).send(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('saml/callback')
|
||||||
|
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||||
|
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
|
try {
|
||||||
|
const loginToken = await this.generateLoginToken(req.user);
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
this.tokenService.computeRedirectURI(loginToken.token),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: improve error management
|
||||||
|
res.status(403).send(err.message);
|
||||||
|
res.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateLoginToken({
|
||||||
|
user,
|
||||||
|
identityProviderId,
|
||||||
|
}: {
|
||||||
|
identityProviderId?: string;
|
||||||
|
user: { email: string } & Record<string, string>;
|
||||||
|
}) {
|
||||||
|
const identityProvider =
|
||||||
|
await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||||
|
where: { id: identityProviderId },
|
||||||
|
relations: ['workspace'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!identityProvider) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Identity provider not found',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invitation =
|
||||||
|
await this.workspaceInvitationService.getOneWorkspaceInvitation(
|
||||||
|
identityProvider.workspaceId,
|
||||||
|
user.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invitation) {
|
||||||
|
await this.authService.signInUp({
|
||||||
|
...user,
|
||||||
|
workspacePersonalInviteToken: invitation.value,
|
||||||
|
workspaceInviteHash: identityProvider.workspace.inviteHash,
|
||||||
|
fromSSO: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserExistInWorkspace =
|
||||||
|
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
||||||
|
user.email,
|
||||||
|
identityProvider.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserExistInWorkspace) {
|
||||||
|
throw new AuthException(
|
||||||
|
'User not found in workspace',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tokenService.generateLoginToken(user.email);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||||
|
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class GenerateJWTOutputWithAuthTokens {
|
||||||
|
@Field(() => Boolean)
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH';
|
||||||
|
|
||||||
|
@Field(() => AuthTokens)
|
||||||
|
authTokens: AuthTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class GenerateJWTOutputWithSSOAUTH {
|
||||||
|
@Field(() => Boolean)
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
reason: 'WORKSPACE_USE_SSO_AUTH';
|
||||||
|
|
||||||
|
@Field(() => [FindAvailableSSOIDPOutput])
|
||||||
|
availableSSOIDPs: Array<FindAvailableSSOIDPOutput>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenerateJWTOutput = createUnionType({
|
||||||
|
name: 'GenerateJWT',
|
||||||
|
types: () => [GenerateJWTOutputWithAuthTokens, GenerateJWTOutputWithSSOAUTH],
|
||||||
|
resolveType(value) {
|
||||||
|
if (value.reason === 'WORKSPACE_AVAILABLE_FOR_SWITCH') {
|
||||||
|
return GenerateJWTOutputWithAuthTokens;
|
||||||
|
}
|
||||||
|
if (value.reason === 'WORKSPACE_USE_SSO_AUTH') {
|
||||||
|
return GenerateJWTOutputWithSSOAUTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import { Issuer } from 'openid-client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OIDCAuthGuard extends AuthGuard('openidconnect') {
|
||||||
|
constructor(private readonly ssoService: SSOService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIdentityProviderId(request: any): string {
|
||||||
|
if (request.params.identityProviderId) {
|
||||||
|
return request.params.identityProviderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.query.state &&
|
||||||
|
typeof request.query.state === 'string' &&
|
||||||
|
request.query.state.startsWith('{') &&
|
||||||
|
request.query.state.endsWith('}')
|
||||||
|
) {
|
||||||
|
const state = JSON.parse(request.query.state);
|
||||||
|
|
||||||
|
return state.identityProviderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Invalid OIDC identity provider params');
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const identityProviderId = this.getIdentityProviderId(request);
|
||||||
|
|
||||||
|
const identityProvider =
|
||||||
|
await this.ssoService.findSSOIdentityProviderById(identityProviderId);
|
||||||
|
|
||||||
|
if (!identityProvider) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Identity provider not found',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuer = await Issuer.discover(identityProvider.issuer);
|
||||||
|
|
||||||
|
new OIDCAuthStrategy(
|
||||||
|
this.ssoService.getOIDCClient(identityProvider, issuer),
|
||||||
|
identityProvider.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await super.canActivate(context)) as boolean;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AuthException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO AMOREAUX: trigger sentry error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SAMLAuthGuard extends AuthGuard('saml') {
|
||||||
|
constructor(private readonly sSOService: SSOService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
try {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const RelayState =
|
||||||
|
'RelayState' in request.body ? JSON.parse(request.body.RelayState) : {};
|
||||||
|
|
||||||
|
request.params.identityProviderId =
|
||||||
|
request.params.identityProviderId ?? RelayState.identityProviderId;
|
||||||
|
|
||||||
|
if (!request.params.identityProviderId) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Invalid SAML identity provider',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
new SamlAuthStrategy(this.sSOService);
|
||||||
|
|
||||||
|
return (await super.canActivate(context)) as boolean;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AuthException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO AMOREAUX: trigger sentry error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { CanActivate, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SSOProviderEnabledGuard implements CanActivate {
|
||||||
|
constructor(private readonly environmentService: EnvironmentService) {}
|
||||||
|
|
||||||
|
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!this.environmentService.get('ENTERPRISE_KEY')) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Enterprise key must be defined to use SSO',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,6 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u
|
|||||||
import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service';
|
import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service';
|
||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly signInUpService: SignInUpService,
|
private readonly signInUpService: SignInUpService,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
|||||||
@@ -225,23 +225,45 @@ export class SignInUpService {
|
|||||||
email,
|
email,
|
||||||
}) {
|
}) {
|
||||||
if (!workspacePersonalInviteToken && !workspaceInviteHash) {
|
if (!workspacePersonalInviteToken && !workspaceInviteHash) {
|
||||||
throw new Error('No invite token or hash provided');
|
throw new AuthException(
|
||||||
}
|
'No invite token or hash provided',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
if (!workspacePersonalInviteToken && workspaceInviteHash) {
|
|
||||||
return (
|
|
||||||
(await this.workspaceRepository.findOneBy({
|
|
||||||
inviteHash: workspaceInviteHash,
|
|
||||||
})) ?? undefined
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appToken = await this.userWorkspaceService.validateInvitation(
|
const workspace = await this.workspaceRepository.findOneBy({
|
||||||
|
inviteHash: workspaceInviteHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Workspace not found',
|
||||||
|
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workspacePersonalInviteToken && !workspace.isPublicInviteLinkEnabled) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Workspace does not allow public invites',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspacePersonalInviteToken && workspace.isPublicInviteLinkEnabled) {
|
||||||
|
try {
|
||||||
|
await this.userWorkspaceService.validateInvitation(
|
||||||
workspacePersonalInviteToken,
|
workspacePersonalInviteToken,
|
||||||
email,
|
email,
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new AuthException(
|
||||||
|
err.message,
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return appToken?.workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async activateOnboardingForNewUser(
|
private async activateOnboardingForNewUser(
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Strategy,
|
||||||
|
StrategyOptions,
|
||||||
|
StrategyVerifyCallbackReq,
|
||||||
|
} from 'openid-client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OIDCAuthStrategy extends PassportStrategy(
|
||||||
|
Strategy,
|
||||||
|
'openidconnect',
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
private client: StrategyOptions['client'],
|
||||||
|
sessionKey: string,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
params: {
|
||||||
|
scope: 'openid email profile',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
usePKCE: true,
|
||||||
|
passReqToCallback: true,
|
||||||
|
sessionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate(req: any, options: any) {
|
||||||
|
return super.authenticate(req, {
|
||||||
|
...options,
|
||||||
|
state: JSON.stringify({
|
||||||
|
identityProviderId: req.params.identityProviderId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validate: StrategyVerifyCallbackReq<{
|
||||||
|
identityProviderId: string;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
};
|
||||||
|
}> = async (req, tokenset, done) => {
|
||||||
|
try {
|
||||||
|
const state = JSON.parse(
|
||||||
|
'query' in req &&
|
||||||
|
req.query &&
|
||||||
|
typeof req.query === 'object' &&
|
||||||
|
'state' in req.query &&
|
||||||
|
req.query.state &&
|
||||||
|
typeof req.query.state === 'string'
|
||||||
|
? req.query.state
|
||||||
|
: '{}',
|
||||||
|
);
|
||||||
|
|
||||||
|
const userinfo = await this.client.userinfo(tokenset);
|
||||||
|
|
||||||
|
if (!userinfo || !userinfo.email) {
|
||||||
|
return done(
|
||||||
|
new AuthException('Email not found', AuthExceptionCode.INVALID_DATA),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email: userinfo.email,
|
||||||
|
...(userinfo.given_name ? { firstName: userinfo.given_name } : {}),
|
||||||
|
...(userinfo.family_name ? { lastName: userinfo.family_name } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
done(null, { user, identityProviderId: state.identityProviderId });
|
||||||
|
} catch (err) {
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/* @license Enterprise */
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MultiSamlStrategy,
|
||||||
|
MultiStrategyConfig,
|
||||||
|
PassportSamlConfig,
|
||||||
|
SamlConfig,
|
||||||
|
VerifyWithRequest,
|
||||||
|
} from '@node-saml/passport-saml';
|
||||||
|
import { AuthenticateOptions } from '@node-saml/passport-saml/lib/types';
|
||||||
|
import { isEmail } from 'class-validator';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SamlAuthStrategy extends PassportStrategy(
|
||||||
|
MultiSamlStrategy,
|
||||||
|
'saml',
|
||||||
|
) {
|
||||||
|
constructor(private readonly sSOService: SSOService) {
|
||||||
|
super({
|
||||||
|
getSamlOptions: (req, callback) => {
|
||||||
|
this.sSOService
|
||||||
|
.findSSOIdentityProviderById(req.params.identityProviderId)
|
||||||
|
.then((identityProvider) => {
|
||||||
|
if (
|
||||||
|
identityProvider &&
|
||||||
|
this.sSOService.isSAMLIdentityProvider(identityProvider)
|
||||||
|
) {
|
||||||
|
const config: SamlConfig = {
|
||||||
|
entryPoint: identityProvider.ssoURL,
|
||||||
|
issuer: this.sSOService.buildIssuerURL(identityProvider),
|
||||||
|
callbackUrl: this.sSOService.buildCallbackUrl(identityProvider),
|
||||||
|
idpCert: identityProvider.certificate,
|
||||||
|
wantAssertionsSigned: false,
|
||||||
|
// TODO: Improve the feature by sign the response
|
||||||
|
wantAuthnResponseSigned: false,
|
||||||
|
signatureAlgorithm: 'sha256',
|
||||||
|
};
|
||||||
|
|
||||||
|
return callback(null, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve error management
|
||||||
|
return callback(new Error('Invalid SAML identity provider'));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// TODO: improve error management
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
passReqToCallback: true,
|
||||||
|
} as PassportSamlConfig & MultiStrategyConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate(req: Request, options: AuthenticateOptions) {
|
||||||
|
super.authenticate(req, {
|
||||||
|
...options,
|
||||||
|
additionalParams: {
|
||||||
|
RelayState: JSON.stringify({
|
||||||
|
identityProviderId: req.params.identityProviderId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validate: VerifyWithRequest = async (request, profile, done) => {
|
||||||
|
if (!profile) {
|
||||||
|
return done(new Error('Profile is must be provided'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = profile.email ?? profile.mail ?? profile.nameID;
|
||||||
|
|
||||||
|
if (!isEmail(email)) {
|
||||||
|
return done(new Error('Invalid email'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: {
|
||||||
|
user: Record<string, string>;
|
||||||
|
identityProviderId?: string;
|
||||||
|
} = { user: { email } };
|
||||||
|
|
||||||
|
if (
|
||||||
|
'RelayState' in request.body &&
|
||||||
|
typeof request.body.RelayState === 'string'
|
||||||
|
) {
|
||||||
|
const RelayState = JSON.parse(request.body.RelayState);
|
||||||
|
|
||||||
|
result.identityProviderId = RelayState.identityProviderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
done(null, result);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
@@ -50,6 +51,12 @@ describe('TokenService', () => {
|
|||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SSOService,
|
||||||
|
useValue: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(User, 'core'),
|
provide: getRepositoryToken(User, 'core'),
|
||||||
useValue: {
|
useValue: {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
@@ -60,6 +61,7 @@ export class TokenService {
|
|||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly emailService: EmailService,
|
private readonly emailService: EmailService,
|
||||||
|
private readonly sSSOService: SSOService,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -341,10 +343,7 @@ export class TokenService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateSwitchWorkspaceToken(
|
async switchWorkspace(user: User, workspaceId: string) {
|
||||||
user: User,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<AuthTokens> {
|
|
||||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
const userExists = await this.userRepository.findBy({ id: user.id });
|
||||||
|
|
||||||
if (!userExists) {
|
if (!userExists) {
|
||||||
@@ -356,7 +355,7 @@ export class TokenService {
|
|||||||
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
relations: ['workspaceUsers'],
|
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
@@ -377,12 +376,44 @@ export class TokenService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workspace.workspaceSSOIdentityProviders.length > 0) {
|
||||||
|
return {
|
||||||
|
useSSOAuth: true,
|
||||||
|
workspace,
|
||||||
|
availableSSOIdentityProviders:
|
||||||
|
await this.sSSOService.listSSOIdentityProvidersByWorkspaceId(
|
||||||
|
workspaceId,
|
||||||
|
),
|
||||||
|
} as {
|
||||||
|
useSSOAuth: true;
|
||||||
|
workspace: Workspace;
|
||||||
|
availableSSOIdentityProviders: Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof this.sSSOService.listSSOIdentityProvidersByWorkspaceId
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
useSSOAuth: false,
|
||||||
|
workspace,
|
||||||
|
} as {
|
||||||
|
useSSOAuth: false;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateSwitchWorkspaceToken(
|
||||||
|
user: User,
|
||||||
|
workspace: Workspace,
|
||||||
|
): Promise<AuthTokens> {
|
||||||
await this.userRepository.save({
|
await this.userRepository.save({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
defaultWorkspace: workspace,
|
defaultWorkspace: workspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = await this.generateAccessToken(user.id, workspaceId);
|
const token = await this.generateAccessToken(user.id, workspace.id);
|
||||||
const refreshToken = await this.generateRefreshToken(user.id);
|
const refreshToken = await this.generateRefreshToken(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
|
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -19,6 +20,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
EmailModule,
|
EmailModule,
|
||||||
|
WorkspaceSSOModule,
|
||||||
],
|
],
|
||||||
providers: [TokenService, JwtAuthStrategy],
|
providers: [TokenService, JwtAuthStrategy],
|
||||||
exports: [TokenService],
|
exports: [TokenService],
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class AuthProviders {
|
|||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
microsoft: boolean;
|
microsoft: boolean;
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
sso: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user