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:
@ -71,6 +71,7 @@ export type AuthProviders = {
|
||||
magicLink: Scalars['Boolean']['output'];
|
||||
microsoft: Scalars['Boolean']['output'];
|
||||
password: Scalars['Boolean']['output'];
|
||||
sso: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type AuthToken = {
|
||||
@ -148,6 +149,7 @@ export enum CaptchaDriverType {
|
||||
|
||||
export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
analyticsEnabled: Scalars['Boolean']['output'];
|
||||
api: ApiConfig;
|
||||
authProviders: AuthProviders;
|
||||
billing: Billing;
|
||||
@ -275,6 +277,15 @@ export type DeleteServerlessFunctionInput = {
|
||||
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 */
|
||||
export enum DistantTableUpdate {
|
||||
ColumnsAdded = 'COLUMNS_ADDED',
|
||||
@ -283,6 +294,20 @@ export enum DistantTableUpdate {
|
||||
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 = {
|
||||
__typename?: 'EmailPasswordResetLink';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -372,6 +397,20 @@ export enum FileFolder {
|
||||
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 = {
|
||||
/** The id of the remote server. */
|
||||
id: Scalars['ID']['input'];
|
||||
@ -385,6 +424,33 @@ export type FullName = {
|
||||
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 = {
|
||||
/** The id of the function. */
|
||||
id: Scalars['ID']['input'];
|
||||
@ -392,6 +458,11 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum IdpType {
|
||||
Oidc = 'OIDC',
|
||||
Saml = 'SAML'
|
||||
}
|
||||
|
||||
export type IndexConnection = {
|
||||
__typename?: 'IndexConnection';
|
||||
/** Array of edges. */
|
||||
@ -461,12 +532,14 @@ export type Mutation = {
|
||||
authorizeApp: AuthorizeApp;
|
||||
challenge: LoginToken;
|
||||
checkoutSession: SessionEntity;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createOneAppToken: AppToken;
|
||||
createOneField: Field;
|
||||
createOneObject: Object;
|
||||
createOneRelation: Relation;
|
||||
createOneRemoteServer: RemoteServer;
|
||||
createOneServerlessFunction: ServerlessFunction;
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
deactivateWorkflowVersion: Scalars['Boolean']['output'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteOneField: Field;
|
||||
@ -474,16 +547,20 @@ export type Mutation = {
|
||||
deleteOneRelation: Relation;
|
||||
deleteOneRemoteServer: RemoteServer;
|
||||
deleteOneServerlessFunction: ServerlessFunction;
|
||||
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||
deleteUser: User;
|
||||
deleteWorkspaceInvitation: Scalars['String']['output'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: AuthTokens;
|
||||
generateJWT: GenerateJwt;
|
||||
generateTransientToken: TransientToken;
|
||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||
impersonate: Verify;
|
||||
publishServerlessFunction: ServerlessFunction;
|
||||
renewToken: AuthTokens;
|
||||
@ -551,6 +628,11 @@ export type MutationCheckoutSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOidcIdentityProviderArgs = {
|
||||
input: SetupOidcSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneAppTokenArgs = {
|
||||
input: CreateOneAppTokenInput;
|
||||
};
|
||||
@ -581,6 +663,11 @@ export type MutationCreateOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateSamlIdentityProviderArgs = {
|
||||
input: SetupSamlSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeactivateWorkflowVersionArgs = {
|
||||
workflowVersionId: Scalars['String']['input'];
|
||||
};
|
||||
@ -611,11 +698,21 @@ export type MutationDeleteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteSsoIdentityProviderArgs = {
|
||||
input: DeleteSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||
appTokenId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationEditSsoIdentityProviderArgs = {
|
||||
input: EditSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationEmailPasswordResetLinkArgs = {
|
||||
email: Scalars['String']['input'];
|
||||
};
|
||||
@ -633,6 +730,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||
input: FindAvailableSsoidpInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyTokenArgs = {
|
||||
apiKeyId: Scalars['String']['input'];
|
||||
expiresAt: Scalars['String']['input'];
|
||||
@ -644,6 +746,11 @@ export type MutationGenerateJwtArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationGetAuthorizationUrlArgs = {
|
||||
input: GetAuthorizationUrlInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationImpersonateArgs = {
|
||||
userId: Scalars['String']['input'];
|
||||
};
|
||||
@ -865,6 +972,7 @@ export type Query = {
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
listSSOIdentityProvidersByWorkspaceId: Array<FindAvailableSsoidpOutput>;
|
||||
object: Object;
|
||||
objects: ObjectConnection;
|
||||
relation: Relation;
|
||||
@ -1091,6 +1199,12 @@ export type RunWorkflowVersionInput = {
|
||||
workflowVersionId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum SsoIdentityProviderStatus {
|
||||
Active = 'Active',
|
||||
Error = 'Error',
|
||||
Inactive = 'Inactive'
|
||||
}
|
||||
|
||||
export type SendInvitationsOutput = {
|
||||
__typename?: 'SendInvitationsOutput';
|
||||
errors: Array<Scalars['String']['output']>;
|
||||
@ -1179,6 +1293,31 @@ export type SessionEntity = {
|
||||
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 */
|
||||
export enum SortDirection {
|
||||
Asc = 'ASC',
|
||||
@ -1368,11 +1507,13 @@ export type UpdateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||
domainName?: InputMaybe<Scalars['String']['input']>;
|
||||
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
logo?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
analyticsTinybirdJwt?: Maybe<Scalars['String']['output']>;
|
||||
canImpersonate: Scalars['Boolean']['output'];
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
||||
@ -1467,6 +1608,7 @@ export type Workspace = {
|
||||
featureFlags?: Maybe<Array<FeatureFlag>>;
|
||||
id: Scalars['UUID']['output'];
|
||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
metadataVersion: Scalars['Float']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
@ -1539,6 +1681,12 @@ export enum WorkspaceMemberTimeFormatEnum {
|
||||
System = 'SYSTEM'
|
||||
}
|
||||
|
||||
export type WorkspaceNameAndId = {
|
||||
__typename?: 'WorkspaceNameAndId';
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
__typename?: 'field';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
||||
@ -64,6 +64,7 @@ export type AuthProviders = {
|
||||
magicLink: Scalars['Boolean'];
|
||||
microsoft: Scalars['Boolean'];
|
||||
password: Scalars['Boolean'];
|
||||
sso: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type AuthToken = {
|
||||
@ -180,6 +181,15 @@ export type DeleteServerlessFunctionInput = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type DeleteSsoInput = {
|
||||
identityProviderId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DeleteSsoOutput = {
|
||||
__typename?: 'DeleteSsoOutput';
|
||||
identityProviderId: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Schema update on a table */
|
||||
export enum DistantTableUpdate {
|
||||
ColumnsAdded = 'COLUMNS_ADDED',
|
||||
@ -188,6 +198,20 @@ export enum DistantTableUpdate {
|
||||
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 = {
|
||||
__typename?: 'EmailPasswordResetLink';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -277,12 +301,53 @@ export enum FileFolder {
|
||||
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 = {
|
||||
__typename?: 'FullName';
|
||||
firstName: 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 = {
|
||||
/** The id of the function. */
|
||||
id: Scalars['ID'];
|
||||
@ -290,6 +355,11 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum IdpType {
|
||||
Oidc = 'OIDC',
|
||||
Saml = 'SAML'
|
||||
}
|
||||
|
||||
export type IndexConnection = {
|
||||
__typename?: 'IndexConnection';
|
||||
/** Array of edges. */
|
||||
@ -359,23 +429,29 @@ export type Mutation = {
|
||||
authorizeApp: AuthorizeApp;
|
||||
challenge: LoginToken;
|
||||
checkoutSession: SessionEntity;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createOneAppToken: AppToken;
|
||||
createOneObject: Object;
|
||||
createOneServerlessFunction: ServerlessFunction;
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteOneObject: Object;
|
||||
deleteOneServerlessFunction: ServerlessFunction;
|
||||
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||
deleteUser: User;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: AuthTokens;
|
||||
generateJWT: GenerateJwt;
|
||||
generateTransientToken: TransientToken;
|
||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||
impersonate: Verify;
|
||||
publishServerlessFunction: ServerlessFunction;
|
||||
renewToken: AuthTokens;
|
||||
@ -438,11 +514,21 @@ export type MutationCheckoutSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOidcIdentityProviderArgs = {
|
||||
input: SetupOidcSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneServerlessFunctionArgs = {
|
||||
input: CreateServerlessFunctionInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateSamlIdentityProviderArgs = {
|
||||
input: SetupSamlSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeactivateWorkflowVersionArgs = {
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
@ -458,11 +544,21 @@ export type MutationDeleteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteSsoIdentityProviderArgs = {
|
||||
input: DeleteSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||
appTokenId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationEditSsoIdentityProviderArgs = {
|
||||
input: EditSsoInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationEmailPasswordResetLinkArgs = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
@ -480,6 +576,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||
input: FindAvailableSsoidpInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyTokenArgs = {
|
||||
apiKeyId: Scalars['String'];
|
||||
expiresAt: Scalars['String'];
|
||||
@ -491,6 +592,11 @@ export type MutationGenerateJwtArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationGetAuthorizationUrlArgs = {
|
||||
input: GetAuthorizationUrlInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationImpersonateArgs = {
|
||||
userId: Scalars['String'];
|
||||
};
|
||||
@ -682,6 +788,7 @@ export type Query = {
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
listSSOIdentityProvidersByWorkspaceId: Array<FindAvailableSsoidpOutput>;
|
||||
object: Object;
|
||||
objects: ObjectConnection;
|
||||
serverlessFunction: ServerlessFunction;
|
||||
@ -822,6 +929,12 @@ export type RunWorkflowVersionInput = {
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum SsoIdentityProviderStatus {
|
||||
Active = 'Active',
|
||||
Error = 'Error',
|
||||
Inactive = 'Inactive'
|
||||
}
|
||||
|
||||
export type SendInvitationsOutput = {
|
||||
__typename?: 'SendInvitationsOutput';
|
||||
errors: Array<Scalars['String']>;
|
||||
@ -894,6 +1007,31 @@ export type SessionEntity = {
|
||||
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 */
|
||||
export enum SortDirection {
|
||||
Asc = 'ASC',
|
||||
@ -1053,6 +1191,7 @@ export type UpdateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']>;
|
||||
domainName?: InputMaybe<Scalars['String']>;
|
||||
inviteHash?: InputMaybe<Scalars['String']>;
|
||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
logo?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@ -1143,6 +1282,7 @@ export type Workspace = {
|
||||
featureFlags?: Maybe<Array<FeatureFlag>>;
|
||||
id: Scalars['UUID'];
|
||||
inviteHash?: Maybe<Scalars['String']>;
|
||||
isPublicInviteLinkEnabled: Scalars['Boolean'];
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
metadataVersion: Scalars['Float'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
@ -1215,6 +1355,12 @@ export enum WorkspaceMemberTimeFormatEnum {
|
||||
System = 'SYSTEM'
|
||||
}
|
||||
|
||||
export type WorkspaceNameAndId = {
|
||||
__typename?: 'WorkspaceNameAndId';
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
__typename?: 'field';
|
||||
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 AvailableSsoIdentityProvidersFragmentFragment = { __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } };
|
||||
|
||||
export type AuthorizeAppMutationVariables = Exact<{
|
||||
clientId: 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 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<{
|
||||
apiKeyId: 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 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<{
|
||||
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<{
|
||||
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<{
|
||||
email: Scalars['String'];
|
||||
@ -1601,14 +1763,47 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
||||
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 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; }>;
|
||||
|
||||
@ -1625,7 +1820,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
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<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@ -1803,6 +1998,18 @@ export const AuthTokensFragmentFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
|
||||
fragment AvailableSSOIdentityProvidersFragment on FindAvailableSSOIDPOutput {
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
workspace {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
||||
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||
id
|
||||
@ -1842,6 +2049,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
inviteHash
|
||||
allowImpersonation
|
||||
activationStatus
|
||||
isPublicInviteLinkEnabled
|
||||
featureFlags {
|
||||
id
|
||||
key
|
||||
@ -2238,6 +2446,39 @@ export function useEmailPasswordResetLinkMutation(baseOptions?: Apollo.MutationH
|
||||
export type EmailPasswordResetLinkMutationHookResult = ReturnType<typeof useEmailPasswordResetLinkMutation>;
|
||||
export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult<EmailPasswordResetLinkMutation>;
|
||||
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`
|
||||
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
|
||||
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
|
||||
@ -2275,12 +2516,26 @@ export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions<Gene
|
||||
export const GenerateJwtDocument = gql`
|
||||
mutation GenerateJWT($workspaceId: String!) {
|
||||
generateJWT(workspaceId: $workspaceId) {
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
... on GenerateJWTOutputWithAuthTokens {
|
||||
success
|
||||
reason
|
||||
authTokens {
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
... on GenerateJWTOutputWithSSOAUTH {
|
||||
success
|
||||
reason
|
||||
availableSSOIDPs {
|
||||
...AvailableSSOIdentityProvidersFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokensFragmentFragmentDoc}`;
|
||||
${AuthTokensFragmentFragmentDoc}
|
||||
${AvailableSsoIdentityProvidersFragmentFragmentDoc}`;
|
||||
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 GenerateTransientTokenMutationResult = Apollo.MutationResult<GenerateTransientTokenMutation>;
|
||||
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`
|
||||
mutation Impersonate($userId: String!) {
|
||||
impersonate(userId: $userId) {
|
||||
@ -2759,6 +3049,7 @@ export const GetClientConfigDocument = gql`
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
sso
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
@ -2848,6 +3139,188 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
|
||||
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
||||
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
||||
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`
|
||||
mutation DeleteUserAccount {
|
||||
deleteUser {
|
||||
|
||||
@ -8,6 +8,7 @@ export const AppRouter = () => {
|
||||
const billing = useRecoilValue(billingState);
|
||||
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||
const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED');
|
||||
const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
|
||||
'IS_FUNCTION_SETTINGS_ENABLED',
|
||||
);
|
||||
@ -21,6 +22,7 @@ export const AppRouter = () => {
|
||||
isBillingPageEnabled,
|
||||
isCRMMigrationEnabled,
|
||||
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 = {
|
||||
isBillingEnabled?: boolean;
|
||||
isCRMMigrationEnabled?: boolean;
|
||||
isServerlessFunctionSettingsEnabled?: boolean;
|
||||
isSSOEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRoutes = ({
|
||||
isBillingEnabled,
|
||||
isCRMMigrationEnabled,
|
||||
isServerlessFunctionSettingsEnabled,
|
||||
isSSOEnabled,
|
||||
}: SettingsRoutesProps) => (
|
||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||
<Routes>
|
||||
@ -357,6 +373,15 @@ export const SettingsRoutes = ({
|
||||
element={<SettingsObjectFieldEdit />}
|
||||
/>
|
||||
<Route path={SettingsPath.Releases} element={<Releases />} />
|
||||
{isSSOEnabled && (
|
||||
<>
|
||||
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
||||
<Route
|
||||
path={SettingsPath.NewSSOIdentityProvider}
|
||||
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@ -29,6 +29,7 @@ export const useCreateAppRouter = (
|
||||
isBillingEnabled?: boolean,
|
||||
isCRMMigrationEnabled?: boolean,
|
||||
isServerlessFunctionSettingsEnabled?: boolean,
|
||||
isSSOEnabled?: boolean,
|
||||
) =>
|
||||
createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
@ -65,6 +66,7 @@ export const useCreateAppRouter = (
|
||||
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,8 +3,21 @@ import { gql } from '@apollo/client';
|
||||
export const GENERATE_JWT = gql`
|
||||
mutation GenerateJWT($workspaceId: String!) {
|
||||
generateJWT(workspaceId: $workspaceId) {
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
... on GenerateJWTOutputWithAuthTokens {
|
||||
success
|
||||
reason
|
||||
authTokens {
|
||||
tokens {
|
||||
...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,
|
||||
magicLink: false,
|
||||
password: false,
|
||||
sso: false,
|
||||
});
|
||||
expect(state.billing).toBeNull();
|
||||
expect(state.isSignInPrefilled).toBe(false);
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
|
||||
type FooterNoteProps = { children: React.ReactNode };
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
@ -20,6 +18,24 @@ const StyledContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const FooterNote = ({ children }: FooterNoteProps) => (
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
export const FooterNote = () => (
|
||||
<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 = {
|
||||
visible?: boolean;
|
||||
text?: string;
|
||||
};
|
||||
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
||||
background-color: ${({ theme }) => theme.border.color.medium};
|
||||
@ -12,8 +13,39 @@ const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
||||
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 = ({
|
||||
visible = true,
|
||||
text = '',
|
||||
}: HorizontalSeparatorProps): JSX.Element => (
|
||||
<StyledSeparator visible={visible} />
|
||||
<>
|
||||
{text ? (
|
||||
<StyledSeparatorContainer>
|
||||
<StyledLine visible={visible} />
|
||||
{text && <StyledText>{text}</StyledText>}
|
||||
<StyledLine visible={visible} />
|
||||
</StyledSeparatorContainer>
|
||||
) : (
|
||||
<StyledSeparator visible={visible} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -5,16 +5,12 @@ import { useMemo, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
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 { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import {
|
||||
SignInUpMode,
|
||||
SignInUpStep,
|
||||
useSignInUp,
|
||||
} from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||
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 { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -64,9 +61,19 @@ export const SignInUpForm = () => {
|
||||
signInUpMode,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
continueWithSSO,
|
||||
submitCredentials,
|
||||
submitSSOEmail,
|
||||
} = useSignInUp(form);
|
||||
|
||||
const toggleSSOMode = () => {
|
||||
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
continueWithEmail();
|
||||
} else {
|
||||
continueWithSSO();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = async (
|
||||
event: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
@ -86,6 +93,8 @@ export const SignInUpForm = () => {
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}
|
||||
} else if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
submitSSOEmail(form.getValues('email'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -99,6 +108,10 @@ export const SignInUpForm = () => {
|
||||
return 'Continue';
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
return 'Continue with SSO';
|
||||
}
|
||||
|
||||
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
@ -136,7 +149,7 @@ export const SignInUpForm = () => {
|
||||
onClick={signInWithGoogle}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={!authProviders.microsoft} />
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -148,17 +161,143 @@ export const SignInUpForm = () => {
|
||||
onClick={signInWithMicrosoft}
|
||||
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 && (
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<HorizontalSeparator visible={true} />
|
||||
|
||||
{authProviders.password &&
|
||||
(signInUpStep === SignInUpStep.Password ||
|
||||
signInUpStep === SignInUpStep.Email ||
|
||||
signInUpStep === SignInUpStep.Init) && (
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<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={(value: string) => {
|
||||
onChange(value);
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
continueWithEmail();
|
||||
}
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
return;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
}
|
||||
continueWithCredentials();
|
||||
return;
|
||||
}
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
)}
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep === SignInUpStep.SSOEmail && (
|
||||
<>
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
@ -181,46 +320,6 @@ export const SignInUpForm = () => {
|
||||
value={value}
|
||||
placeholder="Email"
|
||||
onBlur={onBlur}
|
||||
onChange={(value: string) => {
|
||||
onChange(value);
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
continueWithEmail();
|
||||
}
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
@ -231,60 +330,28 @@ export const SignInUpForm = () => {
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
return;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
}
|
||||
continueWithCredentials();
|
||||
return;
|
||||
}
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
)}
|
||||
<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>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||
Forgot your password?
|
||||
</ActionLink>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Init && (
|
||||
<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>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Init && <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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
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 {
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
}
|
||||
|
||||
export enum SignInUpStep {
|
||||
Init = 'init',
|
||||
Email = 'email',
|
||||
Password = 'password',
|
||||
}
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const { redirectToSSOLoginPage, findAvailableSSOProviderByEmail } = useSSO();
|
||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
@ -35,10 +44,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
|
||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||
|
||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||
SignInUpStep.Init,
|
||||
);
|
||||
|
||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
||||
return isMatchingLocation(AppPath.SignInUp)
|
||||
? SignInUpMode.SignIn
|
||||
@ -62,7 +67,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
}, [isMatchingLocation, requestFreshCaptchaToken]);
|
||||
}, [isMatchingLocation, requestFreshCaptchaToken, setSignInUpStep]);
|
||||
|
||||
const continueWithCredentials = useCallback(async () => {
|
||||
const token = await readCaptchaToken();
|
||||
@ -95,8 +100,48 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
checkUserExistsQuery,
|
||||
enqueueSnackBar,
|
||||
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(
|
||||
async (data) => {
|
||||
const token = await readCaptchaToken();
|
||||
@ -144,6 +189,8 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
signInUpMode,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
continueWithSSO,
|
||||
submitSSOEmail,
|
||||
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'
|
||||
| 'currentBillingSubscription'
|
||||
| 'workspaceMembersCount'
|
||||
| 'isPublicInviteLinkEnabled'
|
||||
| '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,
|
||||
password: data?.clientConfig.authProviders.password,
|
||||
magicLink: false,
|
||||
sso: data?.clientConfig.authProviders.sso,
|
||||
});
|
||||
setIsDebugMode(data?.clientConfig.debugMode);
|
||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||
|
||||
@ -7,6 +7,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
sso
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
|
||||
@ -9,5 +9,6 @@ export const authProvidersState = createState<AuthProviders>({
|
||||
magicLink: false,
|
||||
password: false,
|
||||
microsoft: false,
|
||||
sso: false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
allowImpersonation: false,
|
||||
activationStatus: WorkspaceActivationStatus.Active,
|
||||
metadataVersion: 1,
|
||||
isPublicInviteLinkEnabled: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -2,11 +2,13 @@ import { CalendarChannel } from '@/accounts/types/CalendarChannel';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard';
|
||||
import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard';
|
||||
import styled from '@emotion/styled';
|
||||
import { Section } from '@react-email/components';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
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`
|
||||
display: flex;
|
||||
@ -21,6 +23,10 @@ type SettingsAccountsCalendarChannelDetailsProps = {
|
||||
>;
|
||||
};
|
||||
|
||||
const StyledToggle = styled(Toggle)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const SettingsAccountsCalendarChannelDetails = ({
|
||||
calendarChannel,
|
||||
}: SettingsAccountsCalendarChannelDetailsProps) => {
|
||||
@ -63,16 +69,21 @@ export const SettingsAccountsCalendarChannelDetails = ({
|
||||
title="Contact auto-creation"
|
||||
description="Automatically create contacts for people you've participated in an event with."
|
||||
/>
|
||||
<SettingsAccountsToggleSettingCard
|
||||
parameters={[
|
||||
{
|
||||
value: !!calendarChannel.isContactAutoCreationEnabled,
|
||||
title: 'Auto-creation',
|
||||
description: 'Automatically create contacts for people.',
|
||||
onToggle: handleContactAutoCreationToggle,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<SettingsOptionCardContent
|
||||
title="Auto-creation"
|
||||
description="Automatically create contacts for people."
|
||||
onClick={() =>
|
||||
handleContactAutoCreationToggle(
|
||||
!calendarChannel.isContactAutoCreationEnabled,
|
||||
)
|
||||
}
|
||||
>
|
||||
<StyledToggle
|
||||
value={calendarChannel.isContactAutoCreationEnabled}
|
||||
/>
|
||||
</SettingsOptionCardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
</StyledDetailsContainer>
|
||||
);
|
||||
|
||||
@ -9,9 +9,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
|
||||
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
|
||||
import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
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 = {
|
||||
messageChannel: Pick<
|
||||
@ -31,6 +33,10 @@ const StyledDetailsContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledToggle = styled(Toggle)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const SettingsAccountsMessageChannelDetails = ({
|
||||
messageChannel,
|
||||
}: SettingsAccountsMessageChannelDetailsProps) => {
|
||||
@ -99,23 +105,31 @@ export const SettingsAccountsMessageChannelDetails = ({
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<SettingsAccountsToggleSettingCard
|
||||
parameters={[
|
||||
{
|
||||
title: 'Exclude non-professional emails',
|
||||
description:
|
||||
'Don’t create contacts from/to Gmail, Outlook emails',
|
||||
value: !!messageChannel.excludeNonProfessionalEmails,
|
||||
onToggle: handleIsNonProfessionalEmailExcludedToggle,
|
||||
},
|
||||
{
|
||||
title: 'Exclude group emails',
|
||||
description: 'Don’t sync emails from team@ support@ noreply@...',
|
||||
value: !!messageChannel.excludeGroupEmails,
|
||||
onToggle: handleIsGroupEmailExcludedToggle,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card>
|
||||
<SettingsOptionCardContent
|
||||
title="Exclude non-professional emails"
|
||||
description="Don’t create contacts from/to Gmail, Outlook emails"
|
||||
divider
|
||||
onClick={() =>
|
||||
handleIsNonProfessionalEmailExcludedToggle(
|
||||
!messageChannel.excludeNonProfessionalEmails,
|
||||
)
|
||||
}
|
||||
>
|
||||
<StyledToggle value={messageChannel.excludeNonProfessionalEmails} />
|
||||
</SettingsOptionCardContent>
|
||||
<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>
|
||||
</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;
|
||||
onRowClick?: (item: ListItem) => void;
|
||||
RowIcon?: IconComponent;
|
||||
RowIconFn?: (item: ListItem) => IconComponent;
|
||||
RowRightComponent: ComponentType<{ item: ListItem }>;
|
||||
footerButtonLabel?: string;
|
||||
onFooterButtonClick?: () => void;
|
||||
@ -58,6 +59,7 @@ export const SettingsListCard = <
|
||||
isLoading,
|
||||
onRowClick,
|
||||
RowIcon,
|
||||
RowIconFn,
|
||||
RowRightComponent,
|
||||
onFooterButtonClick,
|
||||
footerButtonLabel,
|
||||
@ -71,7 +73,7 @@ export const SettingsListCard = <
|
||||
{items.map((item, index) => (
|
||||
<SettingsListItemCardContent
|
||||
key={item.id}
|
||||
LeftIcon={RowIcon}
|
||||
LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon}
|
||||
label={getItemLabel(item)}
|
||||
rightComponent={<RowRightComponent item={item} />}
|
||||
divider={index < items.length - 1}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
IconTool,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
IconKey,
|
||||
MAIN_COLORS,
|
||||
} from 'twenty-ui';
|
||||
|
||||
@ -79,6 +80,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
);
|
||||
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||
const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED');
|
||||
const isBillingPageEnabled =
|
||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||
|
||||
@ -186,6 +188,13 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
Icon={IconCode}
|
||||
/>
|
||||
)}
|
||||
{isSSOEnabled && (
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Security"
|
||||
path={SettingsPath.Security}
|
||||
Icon={IconKey}
|
||||
/>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
<AnimatePresence>
|
||||
{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',
|
||||
IntegrationEditDatabaseConnection = 'integrations/:databaseKey/:connectionId/edit',
|
||||
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
|
||||
Security = 'security',
|
||||
NewSSOIdentityProvider = 'security/sso/new',
|
||||
EditSSOIdentityProvider = 'security/sso/:identityProviderId',
|
||||
DevelopersNewWebhook = 'webhooks/new',
|
||||
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
|
||||
Releases = 'releases',
|
||||
|
||||
@ -77,6 +77,7 @@ const StyledButton = styled.button<
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||
max-height: ${({ theme }) => theme.spacing(8)};
|
||||
width: ${({ fullWidth, width }) =>
|
||||
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
||||
${({ theme, variant, disabled }) => {
|
||||
|
||||
@ -39,7 +39,7 @@ const StyledCircle = styled(motion.span)<{
|
||||
export type ToggleProps = {
|
||||
id?: string;
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
onChange?: (value: boolean, e?: React.MouseEvent<HTMLDivElement>) => void;
|
||||
color?: string;
|
||||
toggleSize?: ToggleSize;
|
||||
className?: string;
|
||||
|
||||
@ -6,11 +6,24 @@ import { AppPath } from '@/types/AppPath';
|
||||
import { useGenerateJwtMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
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 = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const [generateJWT] = useGenerateJwtMutation();
|
||||
const { redirectToSSOLoginPage } = useSSO();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const switchWorkspace = async (workspaceId: string) => {
|
||||
if (currentWorkspace?.id === workspaceId) return;
|
||||
@ -28,10 +41,34 @@ export const useWorkspaceSwitching = () => {
|
||||
throw new Error('could not create token');
|
||||
}
|
||||
|
||||
const { tokens } = jwt.data.generateJWT;
|
||||
setTokenPair(tokens);
|
||||
await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly.
|
||||
window.location.href = AppPath.Index;
|
||||
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);
|
||||
await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly.
|
||||
window.location.href = AppPath.Index;
|
||||
}
|
||||
};
|
||||
|
||||
return { switchWorkspace };
|
||||
|
||||
@ -24,6 +24,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
inviteHash
|
||||
allowImpersonation
|
||||
activationStatus
|
||||
isPublicInviteLinkEnabled
|
||||
featureFlags {
|
||||
id
|
||||
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 { useSendInvitationsMutation } from '~/generated/graphql';
|
||||
import { SendInvitationsMutationVariables } from '../../../generated/graphql';
|
||||
import {
|
||||
useSendInvitationsMutation,
|
||||
SendInvitationsMutationVariables,
|
||||
} from '~/generated/graphql';
|
||||
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
||||
|
||||
export const useCreateWorkspaceInvitation = () => {
|
||||
|
||||
@ -13,5 +13,6 @@ export type FeatureFlagKey =
|
||||
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
||||
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
|
||||
| 'IS_ANALYTICS_V2_ENABLED'
|
||||
| 'IS_SSO_ENABLED'
|
||||
| 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED';
|
||||
|
||||
@ -91,25 +91,7 @@ export const Invite = () => {
|
||||
fullWidth
|
||||
/>
|
||||
</StyledContentContainer>
|
||||
<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>
|
||||
<FooterNote />
|
||||
</>
|
||||
) : (
|
||||
<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 { Title } from '@/auth/components/Title';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import {
|
||||
SignInUpMode,
|
||||
SignInUpStep,
|
||||
useSignInUp,
|
||||
} from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
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 = () => {
|
||||
const { form } = useSignInUpForm();
|
||||
@ -27,6 +26,9 @@ export const SignInUp = () => {
|
||||
) {
|
||||
return 'Welcome to Twenty';
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) {
|
||||
return 'Choose SSO connection';
|
||||
}
|
||||
return signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty';
|
||||
@ -39,10 +41,18 @@ export const SignInUp = () => {
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<Logo />
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<IconLockCustom size={40} />
|
||||
) : (
|
||||
<Logo />
|
||||
)}
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
<SignInUpForm />
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<SSOWorkspaceSelection />
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -148,17 +148,18 @@ export const SettingsWorkspaceMembers = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{currentWorkspace?.inviteHash && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Invite by link"
|
||||
description="Share this link to invite users to join your workspace"
|
||||
/>
|
||||
<WorkspaceInviteLink
|
||||
inviteLink={`${window.location.origin}/invite/${currentWorkspace?.inviteHash}`}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
{currentWorkspace?.inviteHash &&
|
||||
currentWorkspace?.isPublicInviteLinkEnabled && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Invite by link"
|
||||
description="Share this link to invite users to join your workspace"
|
||||
/>
|
||||
<WorkspaceInviteLink
|
||||
inviteLink={`${window.location.origin}/invite/${currentWorkspace?.inviteHash}`}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Members"
|
||||
|
||||
@ -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,
|
||||
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
||||
debugMode: false,
|
||||
analyticsEnabled: true,
|
||||
authProviders: {
|
||||
sso: false,
|
||||
google: true,
|
||||
password: true,
|
||||
magicLink: false,
|
||||
|
||||
@ -40,6 +40,7 @@ export const mockDefaultWorkspace: Workspace = {
|
||||
domainName: 'twenty.com',
|
||||
inviteHash: 'twenty.com-invite-hash',
|
||||
logo: workspaceLogoUrl,
|
||||
isPublicInviteLinkEnabled: true,
|
||||
allowImpersonation: true,
|
||||
activationStatus: WorkspaceActivationStatus.Active,
|
||||
featureFlags: [
|
||||
|
||||
Reference in New Issue
Block a user