mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
feat(*): allow to select auth providers + add multiworkspace with subdomain management (#8656)
## Summary Add support for multi-workspace feature and adjust configurations and states accordingly. - Introduced new state isMultiWorkspaceEnabledState. - Updated ClientConfigProviderEffect component to handle multi-workspace. - Modified GraphQL schema and queries to include multi-workspace related configurations. - Adjusted server environment variables and their respective documentation to support multi-workspace toggle. - Updated server-side logic to handle new multi-workspace configurations and conditions.
This commit is contained in:
parent
9a65e80566
commit
7943141d03
@ -4,7 +4,6 @@ import path from 'path';
|
||||
export const envVariables = (variables: string) => {
|
||||
let payload = `
|
||||
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
|
||||
FRONT_BASE_URL=http://localhost:3001
|
||||
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
||||
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
|
||||
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
||||
|
@ -32,6 +32,12 @@ export type ActivateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ActivateWorkspaceOutput = {
|
||||
__typename?: 'ActivateWorkspaceOutput';
|
||||
loginToken: AuthToken;
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export type Analytics = {
|
||||
__typename?: 'Analytics';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -81,7 +87,7 @@ export type AuthProviders = {
|
||||
magicLink: Scalars['Boolean']['output'];
|
||||
microsoft: Scalars['Boolean']['output'];
|
||||
password: Scalars['Boolean']['output'];
|
||||
sso: Scalars['Boolean']['output'];
|
||||
sso: Array<SsoIdentityProvider>;
|
||||
};
|
||||
|
||||
export type AuthToken = {
|
||||
@ -106,6 +112,15 @@ export type AuthorizeApp = {
|
||||
redirectUrl: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AvailableWorkspaceOutput = {
|
||||
__typename?: 'AvailableWorkspaceOutput';
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
sso: Array<SsoConnection>;
|
||||
subdomain: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
||||
@ -161,14 +176,16 @@ export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
analyticsEnabled: Scalars['Boolean']['output'];
|
||||
api: ApiConfig;
|
||||
authProviders: AuthProviders;
|
||||
billing: Billing;
|
||||
captcha: Captcha;
|
||||
chromeExtensionId?: Maybe<Scalars['String']['output']>;
|
||||
debugMode: Scalars['Boolean']['output'];
|
||||
defaultSubdomain?: Maybe<Scalars['String']['output']>;
|
||||
frontDomain: Scalars['String']['output'];
|
||||
isMultiWorkspaceEnabled: Scalars['Boolean']['output'];
|
||||
isSSOEnabled: Scalars['Boolean']['output'];
|
||||
sentry: Sentry;
|
||||
signInPrefilled: Scalars['Boolean']['output'];
|
||||
signUpDisabled: Scalars['Boolean']['output'];
|
||||
support: Support;
|
||||
};
|
||||
|
||||
@ -332,7 +349,7 @@ export type EditSsoOutput = {
|
||||
issuer: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdpType;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export type EmailPasswordResetLink = {
|
||||
@ -424,17 +441,13 @@ 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;
|
||||
type: IdentityProviderType;
|
||||
workspace: WorkspaceNameAndId;
|
||||
};
|
||||
|
||||
@ -451,22 +464,6 @@ 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'];
|
||||
};
|
||||
@ -485,7 +482,7 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export enum IdpType {
|
||||
export enum IdentityProviderType {
|
||||
Oidc = 'OIDC',
|
||||
Saml = 'SAML'
|
||||
}
|
||||
@ -553,7 +550,7 @@ export enum MessageChannelVisibility {
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkflowVersion: Scalars['Boolean']['output'];
|
||||
activateWorkspace: Workspace;
|
||||
activateWorkspace: ActivateWorkspaceOutput;
|
||||
addUserToWorkspace: User;
|
||||
addUserToWorkspaceByInviteToken: User;
|
||||
authorizeApp: AuthorizeApp;
|
||||
@ -578,7 +575,7 @@ export type Mutation = {
|
||||
deleteOneServerlessFunction: ServerlessFunction;
|
||||
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||
deleteUser: User;
|
||||
deleteWorkflowVersionStep: Scalars['Boolean']['output'];
|
||||
deleteWorkflowVersionStep: WorkflowAction;
|
||||
deleteWorkspaceInvitation: Scalars['String']['output'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
@ -586,9 +583,7 @@ export type Mutation = {
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: GenerateJwt;
|
||||
generateTransientToken: TransientToken;
|
||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||
impersonate: Verify;
|
||||
@ -599,6 +594,7 @@ export type Mutation = {
|
||||
sendInvitations: SendInvitationsOutput;
|
||||
signUp: LoginToken;
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||
switchWorkspace: PublicWorkspaceDataOutput;
|
||||
syncRemoteTable: RemoteTable;
|
||||
syncRemoteTableSchemaChanges: RemoteTable;
|
||||
track: Analytics;
|
||||
@ -609,7 +605,7 @@ export type Mutation = {
|
||||
updateOneRemoteServer: RemoteServer;
|
||||
updateOneServerlessFunction: ServerlessFunction;
|
||||
updatePasswordViaResetToken: InvalidatePassword;
|
||||
updateWorkflowVersionStep: Scalars['Boolean']['output'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean']['output'];
|
||||
uploadFile: Scalars['String']['output'];
|
||||
@ -778,22 +774,12 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||
input: FindAvailableSsoidpInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyTokenArgs = {
|
||||
apiKeyId: Scalars['String']['input'];
|
||||
expiresAt: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateJwtArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGetAuthorizationUrlArgs = {
|
||||
input: GetAuthorizationUrlInput;
|
||||
};
|
||||
@ -838,6 +824,11 @@ export type MutationSignUpArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSwitchWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSyncRemoteTableArgs = {
|
||||
input: RemoteTableInput;
|
||||
};
|
||||
@ -1007,6 +998,15 @@ export type ProductPricesEntity = {
|
||||
totalNumberOfPrices: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type PublicWorkspaceDataOutput = {
|
||||
__typename?: 'PublicWorkspaceDataOutput';
|
||||
authProviders: AuthProviders;
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
subdomain: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type PublishServerlessFunctionInput = {
|
||||
/** The id of the function. */
|
||||
id: Scalars['ID']['input'];
|
||||
@ -1015,13 +1015,14 @@ export type PublishServerlessFunctionInput = {
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
billingPortalSession: SessionEntity;
|
||||
checkUserExists: UserExists;
|
||||
checkUserExists: UserExistsOutput;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
field: Field;
|
||||
fields: FieldConnection;
|
||||
findAvailableWorkspacesByEmail: Array<AvailableWorkspaceOutput>;
|
||||
findDistantTablesWithStatus: Array<RemoteTable>;
|
||||
findManyRemoteServersByType: Array<RemoteServer>;
|
||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||
@ -1032,6 +1033,7 @@ export type Query = {
|
||||
getAvailablePackages: Scalars['JSON']['output'];
|
||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||
getProductPrices: ProductPricesEntity;
|
||||
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
|
||||
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>;
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||
@ -1075,6 +1077,11 @@ export type QueryFieldsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindAvailableWorkspacesByEmailArgs = {
|
||||
email: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindDistantTablesWithStatusArgs = {
|
||||
input: FindManyRemoteTablesInput;
|
||||
};
|
||||
@ -1262,6 +1269,24 @@ export type RunWorkflowVersionInput = {
|
||||
workflowVersionId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type SsoConnection = {
|
||||
__typename?: 'SSOConnection';
|
||||
id: Scalars['String']['output'];
|
||||
issuer: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export type SsoIdentityProvider = {
|
||||
__typename?: 'SSOIdentityProvider';
|
||||
id: Scalars['String']['output'];
|
||||
issuer: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export enum SsoIdentityProviderStatus {
|
||||
Active = 'Active',
|
||||
Error = 'Error',
|
||||
@ -1353,7 +1378,7 @@ export type SetupSsoOutput = {
|
||||
issuer: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdpType;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
/** Sort Directions */
|
||||
@ -1555,8 +1580,12 @@ export type UpdateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||
domainName?: InputMaybe<Scalars['String']['input']>;
|
||||
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
isMicrosoftAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
isPasswordAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
logo?: InputMaybe<Scalars['String']['input']>;
|
||||
subdomain?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
@ -1594,9 +1623,12 @@ export type UserEdge = {
|
||||
|
||||
export type UserExists = {
|
||||
__typename?: 'UserExists';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
exists: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type UserExistsOutput = UserExists | UserNotExists;
|
||||
|
||||
export type UserInfo = {
|
||||
__typename?: 'UserInfo';
|
||||
email: Scalars['String']['output'];
|
||||
@ -1626,6 +1658,11 @@ export type UserMappingOptionsUser = {
|
||||
user?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type UserNotExists = {
|
||||
__typename?: 'UserNotExists';
|
||||
exists: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type UserWorkspace = {
|
||||
__typename?: 'UserWorkspace';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@ -1653,6 +1690,10 @@ export type Verify = {
|
||||
export type WorkflowAction = {
|
||||
__typename?: 'WorkflowAction';
|
||||
id: Scalars['UUID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
settings: Scalars['JSON']['output'];
|
||||
type: Scalars['String']['output'];
|
||||
valid: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type WorkflowRun = {
|
||||
@ -1677,9 +1718,13 @@ export type Workspace = {
|
||||
hasValidEntrepriseKey: Scalars['Boolean']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||
isGoogleAuthEnabled: Scalars['Boolean']['output'];
|
||||
isMicrosoftAuthEnabled: Scalars['Boolean']['output'];
|
||||
isPasswordAuthEnabled: Scalars['Boolean']['output'];
|
||||
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
metadataVersion: Scalars['Float']['output'];
|
||||
subdomain: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
workspaceMembersCount?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as Apollo from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
@ -25,6 +25,12 @@ export type ActivateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ActivateWorkspaceOutput = {
|
||||
__typename?: 'ActivateWorkspaceOutput';
|
||||
loginToken: AuthToken;
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export type Analytics = {
|
||||
__typename?: 'Analytics';
|
||||
/** Boolean that confirms query was dispatched */
|
||||
@ -74,7 +80,7 @@ export type AuthProviders = {
|
||||
magicLink: Scalars['Boolean'];
|
||||
microsoft: Scalars['Boolean'];
|
||||
password: Scalars['Boolean'];
|
||||
sso: Scalars['Boolean'];
|
||||
sso: Array<SsoIdentityProvider>;
|
||||
};
|
||||
|
||||
export type AuthToken = {
|
||||
@ -99,6 +105,15 @@ export type AuthorizeApp = {
|
||||
redirectUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AvailableWorkspaceOutput = {
|
||||
__typename?: 'AvailableWorkspaceOutput';
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
sso: Array<SsoConnection>;
|
||||
subdomain: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']>;
|
||||
@ -154,14 +169,16 @@ export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
analyticsEnabled: Scalars['Boolean'];
|
||||
api: ApiConfig;
|
||||
authProviders: AuthProviders;
|
||||
billing: Billing;
|
||||
captcha: Captcha;
|
||||
chromeExtensionId?: Maybe<Scalars['String']>;
|
||||
debugMode: Scalars['Boolean'];
|
||||
defaultSubdomain?: Maybe<Scalars['String']>;
|
||||
frontDomain: Scalars['String'];
|
||||
isMultiWorkspaceEnabled: Scalars['Boolean'];
|
||||
isSSOEnabled: Scalars['Boolean'];
|
||||
sentry: Sentry;
|
||||
signInPrefilled: Scalars['Boolean'];
|
||||
signUpDisabled: Scalars['Boolean'];
|
||||
support: Support;
|
||||
};
|
||||
|
||||
@ -233,7 +250,7 @@ export type EditSsoOutput = {
|
||||
issuer: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdpType;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export type EmailPasswordResetLink = {
|
||||
@ -325,17 +342,13 @@ 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;
|
||||
type: IdentityProviderType;
|
||||
workspace: WorkspaceNameAndId;
|
||||
};
|
||||
|
||||
@ -345,22 +358,6 @@ export type FullName = {
|
||||
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'];
|
||||
};
|
||||
@ -379,7 +376,7 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum IdpType {
|
||||
export enum IdentityProviderType {
|
||||
Oidc = 'OIDC',
|
||||
Saml = 'SAML'
|
||||
}
|
||||
@ -447,7 +444,7 @@ export enum MessageChannelVisibility {
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkflowVersion: Scalars['Boolean'];
|
||||
activateWorkspace: Workspace;
|
||||
activateWorkspace: ActivateWorkspaceOutput;
|
||||
addUserToWorkspace: User;
|
||||
addUserToWorkspaceByInviteToken: User;
|
||||
authorizeApp: AuthorizeApp;
|
||||
@ -474,9 +471,7 @@ export type Mutation = {
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: GenerateJwt;
|
||||
generateTransientToken: TransientToken;
|
||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||
impersonate: Verify;
|
||||
@ -487,6 +482,7 @@ export type Mutation = {
|
||||
sendInvitations: SendInvitationsOutput;
|
||||
signUp: LoginToken;
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||
switchWorkspace: PublicWorkspaceDataOutput;
|
||||
track: Analytics;
|
||||
updateBillingSubscription: UpdateBillingEntity;
|
||||
updateOneObject: Object;
|
||||
@ -621,22 +617,12 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
||||
input: FindAvailableSsoidpInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyTokenArgs = {
|
||||
apiKeyId: Scalars['String'];
|
||||
expiresAt: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateJwtArgs = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationGetAuthorizationUrlArgs = {
|
||||
input: GetAuthorizationUrlInput;
|
||||
};
|
||||
@ -681,6 +667,11 @@ export type MutationSignUpArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSwitchWorkspaceArgs = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationTrackArgs = {
|
||||
action: Scalars['String'];
|
||||
payload: Scalars['JSON'];
|
||||
@ -825,6 +816,15 @@ export type ProductPricesEntity = {
|
||||
totalNumberOfPrices: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type PublicWorkspaceDataOutput = {
|
||||
__typename?: 'PublicWorkspaceDataOutput';
|
||||
authProviders: AuthProviders;
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
subdomain: Scalars['String'];
|
||||
};
|
||||
|
||||
export type PublishServerlessFunctionInput = {
|
||||
/** The id of the function. */
|
||||
id: Scalars['ID'];
|
||||
@ -833,11 +833,12 @@ export type PublishServerlessFunctionInput = {
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
billingPortalSession: SessionEntity;
|
||||
checkUserExists: UserExists;
|
||||
checkUserExists: UserExistsOutput;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
findAvailableWorkspacesByEmail: Array<AvailableWorkspaceOutput>;
|
||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
@ -845,6 +846,7 @@ export type Query = {
|
||||
getAvailablePackages: Scalars['JSON'];
|
||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||
getProductPrices: ProductPricesEntity;
|
||||
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
|
||||
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||
@ -875,6 +877,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindAvailableWorkspacesByEmailArgs = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindOneServerlessFunctionArgs = {
|
||||
input: ServerlessFunctionIdInput;
|
||||
};
|
||||
@ -1001,6 +1008,24 @@ export type RunWorkflowVersionInput = {
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type SsoConnection = {
|
||||
__typename?: 'SSOConnection';
|
||||
id: Scalars['String'];
|
||||
issuer: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export type SsoIdentityProvider = {
|
||||
__typename?: 'SSOIdentityProvider';
|
||||
id: Scalars['String'];
|
||||
issuer: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
export enum SsoIdentityProviderStatus {
|
||||
Active = 'Active',
|
||||
Error = 'Error',
|
||||
@ -1092,7 +1117,7 @@ export type SetupSsoOutput = {
|
||||
issuer: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
type: IdpType;
|
||||
type: IdentityProviderType;
|
||||
};
|
||||
|
||||
/** Sort Directions */
|
||||
@ -1263,8 +1288,12 @@ export type UpdateWorkspaceInput = {
|
||||
displayName?: InputMaybe<Scalars['String']>;
|
||||
domainName?: InputMaybe<Scalars['String']>;
|
||||
inviteHash?: InputMaybe<Scalars['String']>;
|
||||
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
isMicrosoftAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
isPasswordAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
logo?: InputMaybe<Scalars['String']>;
|
||||
subdomain?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
@ -1302,9 +1331,12 @@ export type UserEdge = {
|
||||
|
||||
export type UserExists = {
|
||||
__typename?: 'UserExists';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
exists: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UserExistsOutput = UserExists | UserNotExists;
|
||||
|
||||
export type UserInfo = {
|
||||
__typename?: 'UserInfo';
|
||||
email: Scalars['String'];
|
||||
@ -1324,6 +1356,11 @@ export type UserMappingOptionsUser = {
|
||||
user?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UserNotExists = {
|
||||
__typename?: 'UserNotExists';
|
||||
exists: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UserWorkspace = {
|
||||
__typename?: 'UserWorkspace';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -1379,9 +1416,13 @@ export type Workspace = {
|
||||
hasValidEntrepriseKey: Scalars['Boolean'];
|
||||
id: Scalars['UUID'];
|
||||
inviteHash?: Maybe<Scalars['String']>;
|
||||
isGoogleAuthEnabled: Scalars['Boolean'];
|
||||
isMicrosoftAuthEnabled: Scalars['Boolean'];
|
||||
isPasswordAuthEnabled: Scalars['Boolean'];
|
||||
isPublicInviteLinkEnabled: Scalars['Boolean'];
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
metadataVersion: Scalars['Float'];
|
||||
subdomain: Scalars['String'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
workspaceMembersCount?: Maybe<Scalars['Float']>;
|
||||
};
|
||||
@ -1784,13 +1825,6 @@ 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'];
|
||||
@ -1799,13 +1833,6 @@ export type GenerateApiKeyTokenMutationVariables = Exact<{
|
||||
|
||||
export type GenerateApiKeyTokenMutation = { __typename?: 'Mutation', generateApiKeyToken: { __typename?: 'ApiKeyToken', token: string } };
|
||||
|
||||
export type GenerateJwtMutationVariables = Exact<{
|
||||
workspaceId: Scalars['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; }>;
|
||||
|
||||
|
||||
@ -1823,7 +1850,7 @@ export type ImpersonateMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, hasValidEntrepriseKey: 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 ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: 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, subdomain: string } | 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'];
|
||||
@ -1843,6 +1870,13 @@ export type SignUpMutationVariables = Exact<{
|
||||
|
||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
||||
|
||||
export type SwitchWorkspaceMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SwitchWorkspaceMutation = { __typename?: 'Mutation', switchWorkspace: { __typename?: 'PublicWorkspaceDataOutput', id: string, subdomain: string, authProviders: { __typename?: 'AuthProviders', google: boolean, magicLink: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> } } };
|
||||
|
||||
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
|
||||
token: Scalars['String'];
|
||||
newPassword: Scalars['String'];
|
||||
@ -1856,7 +1890,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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, hasValidEntrepriseKey: 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 VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: 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, subdomain: string } | 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'];
|
||||
@ -1864,7 +1898,12 @@ export type CheckUserExistsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename?: 'UserExists', exists: boolean } };
|
||||
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
|
||||
|
||||
export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetPublicWorkspaceDataBySubdomainQuery = { __typename?: 'Query', getPublicWorkspaceDataBySubdomain: { __typename?: 'PublicWorkspaceDataOutput', id: string, logo?: string | null, displayName?: string | null, subdomain: string, authProviders: { __typename?: 'AuthProviders', google: boolean, magicLink: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> } } };
|
||||
|
||||
export type ValidatePasswordResetTokenQueryVariables = Exact<{
|
||||
token: Scalars['String'];
|
||||
@ -1903,7 +1942,7 @@ 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, 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 GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, 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; }>;
|
||||
|
||||
@ -1931,14 +1970,14 @@ export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateOidcIdentityProviderMutation = { __typename?: 'Mutation', createOIDCIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||
export type CreateOidcIdentityProviderMutation = { __typename?: 'Mutation', createOIDCIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdentityProviderType, 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 CreateSamlIdentityProviderMutation = { __typename?: 'Mutation', createSAMLIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdentityProviderType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||
|
||||
export type DeleteSsoIdentityProviderMutationVariables = Exact<{
|
||||
input: DeleteSsoInput;
|
||||
@ -1952,14 +1991,14 @@ export type EditSsoIdentityProviderMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||
export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdentityProviderType, 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 ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, 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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, hasValidEntrepriseKey: 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 UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: 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, subdomain: string } | null }> };
|
||||
|
||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -1976,7 +2015,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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, hasValidEntrepriseKey: 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 GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: 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, subdomain: string } | null }> } };
|
||||
|
||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@ -2074,7 +2113,7 @@ export type ActivateWorkspaceMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: any } };
|
||||
export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'ActivateWorkspaceOutput', workspace: { __typename?: 'Workspace', id: any, subdomain: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
||||
|
||||
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -2086,7 +2125,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
|
||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, domainName?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean } };
|
||||
|
||||
export type UploadWorkspaceLogoMutationVariables = Exact<{
|
||||
file: Scalars['Upload'];
|
||||
@ -2248,6 +2287,10 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
allowImpersonation
|
||||
activationStatus
|
||||
isPublicInviteLinkEnabled
|
||||
isGoogleAuthEnabled
|
||||
isMicrosoftAuthEnabled
|
||||
isPasswordAuthEnabled
|
||||
subdomain
|
||||
hasValidEntrepriseKey
|
||||
featureFlags {
|
||||
id
|
||||
@ -2269,6 +2312,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
logo
|
||||
displayName
|
||||
domainName
|
||||
subdomain
|
||||
}
|
||||
}
|
||||
userVars
|
||||
@ -2645,39 +2689,6 @@ 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) {
|
||||
@ -2712,55 +2723,6 @@ export function useGenerateApiKeyTokenMutation(baseOptions?: Apollo.MutationHook
|
||||
export type GenerateApiKeyTokenMutationHookResult = ReturnType<typeof useGenerateApiKeyTokenMutation>;
|
||||
export type GenerateApiKeyTokenMutationResult = Apollo.MutationResult<GenerateApiKeyTokenMutation>;
|
||||
export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions<GenerateApiKeyTokenMutation, GenerateApiKeyTokenMutationVariables>;
|
||||
export const GenerateJwtDocument = gql`
|
||||
mutation GenerateJWT($workspaceId: String!) {
|
||||
generateJWT(workspaceId: $workspaceId) {
|
||||
... on GenerateJWTOutputWithAuthTokens {
|
||||
success
|
||||
reason
|
||||
authTokens {
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
... on GenerateJWTOutputWithSSOAUTH {
|
||||
success
|
||||
reason
|
||||
availableSSOIDPs {
|
||||
...AvailableSSOIdentityProvidersFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokensFragmentFragmentDoc}
|
||||
${AvailableSsoIdentityProvidersFragmentFragmentDoc}`;
|
||||
export type GenerateJwtMutationFn = Apollo.MutationFunction<GenerateJwtMutation, GenerateJwtMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useGenerateJwtMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useGenerateJwtMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useGenerateJwtMutation` 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 [generateJwtMutation, { data, loading, error }] = useGenerateJwtMutation({
|
||||
* variables: {
|
||||
* workspaceId: // value for 'workspaceId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGenerateJwtMutation(baseOptions?: Apollo.MutationHookOptions<GenerateJwtMutation, GenerateJwtMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<GenerateJwtMutation, GenerateJwtMutationVariables>(GenerateJwtDocument, options);
|
||||
}
|
||||
export type GenerateJwtMutationHookResult = ReturnType<typeof useGenerateJwtMutation>;
|
||||
export type GenerateJwtMutationResult = Apollo.MutationResult<GenerateJwtMutation>;
|
||||
export type GenerateJwtMutationOptions = Apollo.BaseMutationOptions<GenerateJwtMutation, GenerateJwtMutationVariables>;
|
||||
export const GenerateTransientTokenDocument = gql`
|
||||
mutation generateTransientToken {
|
||||
generateTransientToken {
|
||||
@ -2949,6 +2911,53 @@ export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignU
|
||||
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
||||
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
||||
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
||||
export const SwitchWorkspaceDocument = gql`
|
||||
mutation SwitchWorkspace($workspaceId: String!) {
|
||||
switchWorkspace(workspaceId: $workspaceId) {
|
||||
id
|
||||
subdomain
|
||||
authProviders {
|
||||
sso {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
issuer
|
||||
}
|
||||
google
|
||||
magicLink
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SwitchWorkspaceMutationFn = Apollo.MutationFunction<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSwitchWorkspaceMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSwitchWorkspaceMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSwitchWorkspaceMutation` 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 [switchWorkspaceMutation, { data, loading, error }] = useSwitchWorkspaceMutation({
|
||||
* variables: {
|
||||
* workspaceId: // value for 'workspaceId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSwitchWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>(SwitchWorkspaceDocument, options);
|
||||
}
|
||||
export type SwitchWorkspaceMutationHookResult = ReturnType<typeof useSwitchWorkspaceMutation>;
|
||||
export type SwitchWorkspaceMutationResult = Apollo.MutationResult<SwitchWorkspaceMutation>;
|
||||
export type SwitchWorkspaceMutationOptions = Apollo.BaseMutationOptions<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>;
|
||||
export const UpdatePasswordViaResetTokenDocument = gql`
|
||||
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
||||
updatePasswordViaResetToken(
|
||||
@ -3028,7 +3037,26 @@ export type VerifyMutationOptions = Apollo.BaseMutationOptions<VerifyMutation, V
|
||||
export const CheckUserExistsDocument = gql`
|
||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
||||
exists
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
subdomain
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UserNotExists {
|
||||
exists
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -3061,6 +3089,56 @@ export function useCheckUserExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOp
|
||||
export type CheckUserExistsQueryHookResult = ReturnType<typeof useCheckUserExistsQuery>;
|
||||
export type CheckUserExistsLazyQueryHookResult = ReturnType<typeof useCheckUserExistsLazyQuery>;
|
||||
export type CheckUserExistsQueryResult = Apollo.QueryResult<CheckUserExistsQuery, CheckUserExistsQueryVariables>;
|
||||
export const GetPublicWorkspaceDataBySubdomainDocument = gql`
|
||||
query GetPublicWorkspaceDataBySubdomain {
|
||||
getPublicWorkspaceDataBySubdomain {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
subdomain
|
||||
authProviders {
|
||||
sso {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
issuer
|
||||
}
|
||||
google
|
||||
magicLink
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetPublicWorkspaceDataBySubdomainQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetPublicWorkspaceDataBySubdomainQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetPublicWorkspaceDataBySubdomainQuery` 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 } = useGetPublicWorkspaceDataBySubdomainQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetPublicWorkspaceDataBySubdomainQuery(baseOptions?: Apollo.QueryHookOptions<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>(GetPublicWorkspaceDataBySubdomainDocument, options);
|
||||
}
|
||||
export function useGetPublicWorkspaceDataBySubdomainLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>(GetPublicWorkspaceDataBySubdomainDocument, options);
|
||||
}
|
||||
export type GetPublicWorkspaceDataBySubdomainQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataBySubdomainQuery>;
|
||||
export type GetPublicWorkspaceDataBySubdomainLazyQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataBySubdomainLazyQuery>;
|
||||
export type GetPublicWorkspaceDataBySubdomainQueryResult = Apollo.QueryResult<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>;
|
||||
export const ValidatePasswordResetTokenDocument = gql`
|
||||
query ValidatePasswordResetToken($token: String!) {
|
||||
validatePasswordResetToken(passwordResetToken: $token) {
|
||||
@ -3244,19 +3322,16 @@ export type UpdateBillingSubscriptionMutationOptions = Apollo.BaseMutationOption
|
||||
export const GetClientConfigDocument = gql`
|
||||
query GetClientConfig {
|
||||
clientConfig {
|
||||
authProviders {
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
sso
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
billingUrl
|
||||
billingFreeTrialDurationInDays
|
||||
}
|
||||
signInPrefilled
|
||||
signUpDisabled
|
||||
isMultiWorkspaceEnabled
|
||||
isSSOEnabled
|
||||
defaultSubdomain
|
||||
frontDomain
|
||||
debugMode
|
||||
analyticsEnabled
|
||||
support {
|
||||
@ -4163,10 +4238,16 @@ export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutation
|
||||
export const ActivateWorkspaceDocument = gql`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
id
|
||||
workspace {
|
||||
id
|
||||
subdomain
|
||||
}
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
|
||||
|
||||
/**
|
||||
@ -4230,9 +4311,14 @@ export const UpdateWorkspaceDocument = gql`
|
||||
updateWorkspace(data: $input) {
|
||||
id
|
||||
domainName
|
||||
subdomain
|
||||
displayName
|
||||
logo
|
||||
allowImpersonation
|
||||
isPublicInviteLinkEnabled
|
||||
isGoogleAuthEnabled
|
||||
isMicrosoftAuthEnabled
|
||||
isPasswordAuthEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -18,6 +18,7 @@ import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||
import { UserProvider } from '@/users/components/UserProvider';
|
||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||
import { StrictMode } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
@ -34,6 +35,7 @@ export const AppRouterProviders = () => {
|
||||
<ChromeExtensionSidecarEffect />
|
||||
<ChromeExtensionSidecarProvider>
|
||||
<UserProviderEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<UserProvider>
|
||||
<AuthProvider>
|
||||
<ApolloMetadataClientProvider>
|
||||
|
@ -105,6 +105,12 @@ const SettingsWorkspace = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsDomain = lazy(() =>
|
||||
import('~/pages/settings/workspace/SettingsDomain').then((module) => ({
|
||||
default: module.SettingsDomain,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsWorkspaceMembers = lazy(() =>
|
||||
import('~/pages/settings/SettingsWorkspaceMembers').then((module) => ({
|
||||
default: module.SettingsWorkspaceMembers,
|
||||
@ -288,6 +294,8 @@ export const SettingsRoutes = ({
|
||||
{isBillingEnabled && (
|
||||
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
||||
)}
|
||||
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
|
||||
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
|
||||
<Route
|
||||
path={SettingsPath.WorkspaceMembersPage}
|
||||
element={<SettingsWorkspaceMembers />}
|
||||
@ -382,14 +390,12 @@ export const SettingsRoutes = ({
|
||||
element={<SettingsObjectFieldEdit />}
|
||||
/>
|
||||
<Route path={SettingsPath.Releases} element={<Releases />} />
|
||||
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
||||
{isSSOEnabled && (
|
||||
<>
|
||||
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
||||
<Route
|
||||
path={SettingsPath.NewSSOIdentityProvider}
|
||||
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||
/>
|
||||
</>
|
||||
<Route
|
||||
path={SettingsPath.NewSSOIdentityProvider}
|
||||
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||
/>
|
||||
)}
|
||||
{isAdminPageEnabled && (
|
||||
<>
|
||||
|
@ -1,13 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_AVAILABLE_SSO_IDENTITY_PROVIDERS = gql`
|
||||
mutation FindAvailableSSOIdentityProviders(
|
||||
$input: FindAvailableSSOIDPInput!
|
||||
) {
|
||||
findAvailableSSOIdentityProviders(input: $input) {
|
||||
...AvailableSSOIdentityProvidersFragment
|
||||
}
|
||||
}
|
||||
`;
|
@ -1,24 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GENERATE_JWT = gql`
|
||||
mutation GenerateJWT($workspaceId: String!) {
|
||||
generateJWT(workspaceId: $workspaceId) {
|
||||
... on GenerateJWTOutputWithAuthTokens {
|
||||
success
|
||||
reason
|
||||
authTokens {
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
... on GenerateJWTOutputWithSSOAUTH {
|
||||
success
|
||||
reason
|
||||
availableSSOIDPs {
|
||||
...AvailableSSOIdentityProvidersFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -0,0 +1,23 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SWITCH_WORKSPACE = gql`
|
||||
mutation SwitchWorkspace($workspaceId: String!) {
|
||||
switchWorkspace(workspaceId: $workspaceId) {
|
||||
id
|
||||
subdomain
|
||||
authProviders {
|
||||
sso {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
issuer
|
||||
}
|
||||
google
|
||||
magicLink
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -3,7 +3,26 @@ import { gql } from '@apollo/client';
|
||||
export const CHECK_USER_EXISTS = gql`
|
||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
||||
exists
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
subdomain
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UserNotExists {
|
||||
exists
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN = gql`
|
||||
query GetPublicWorkspaceDataBySubdomain {
|
||||
getPublicWorkspaceDataBySubdomain {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
subdomain
|
||||
authProviders {
|
||||
sso {
|
||||
id
|
||||
name
|
||||
type
|
||||
status
|
||||
issuer
|
||||
}
|
||||
google
|
||||
magicLink
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -114,11 +114,11 @@ describe('useAuth', () => {
|
||||
|
||||
expect(state.icons).toEqual({});
|
||||
expect(state.authProviders).toEqual({
|
||||
google: false,
|
||||
google: true,
|
||||
microsoft: false,
|
||||
magicLink: false,
|
||||
password: false,
|
||||
sso: false,
|
||||
password: true,
|
||||
sso: [],
|
||||
});
|
||||
expect(state.billing).toBeNull();
|
||||
expect(state.isDeveloperDefaultSignInPrefilled).toBe(false);
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
snapshot_UNSTABLE,
|
||||
useGotoRecoilSnapshot,
|
||||
useRecoilCallback,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import { iconsState } from 'twenty-ui';
|
||||
@ -42,10 +42,18 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
import { lastAuthenticateWorkspaceState } from '@/auth/states/lastAuthenticateWorkspaceState';
|
||||
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [, setTokenPair] = useRecoilState(tokenPairState);
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const urlManager = useRecoilValue(urlManagerState);
|
||||
const setLastAuthenticateWorkspaceState = useSetRecoilState(
|
||||
lastAuthenticateWorkspaceState,
|
||||
);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -60,6 +68,7 @@ export const useAuth = () => {
|
||||
const [challenge] = useChallengeMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [verify] = useVerifyMutation();
|
||||
const { isTwentyWorkspaceSubdomain, getWorkspaceSubdomain } = useUrlManager();
|
||||
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
||||
useCheckUserExistsLazyQuery();
|
||||
|
||||
@ -203,6 +212,15 @@ export const useAuth = () => {
|
||||
const workspace = user.defaultWorkspace ?? null;
|
||||
|
||||
setCurrentWorkspace(workspace);
|
||||
if (isDefined(workspace) && isTwentyWorkspaceSubdomain) {
|
||||
setLastAuthenticateWorkspaceState({
|
||||
id: workspace.id,
|
||||
subdomain: workspace.subdomain,
|
||||
cookieAttributes: {
|
||||
domain: `.${urlManager.frontDomain}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
|
||||
const validWorkspaces = verifyResult.data?.verify.user.workspaces
|
||||
@ -227,9 +245,12 @@ export const useAuth = () => {
|
||||
setTokenPair,
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
isTwentyWorkspaceSubdomain,
|
||||
setCurrentWorkspaceMembers,
|
||||
setCurrentWorkspaceMember,
|
||||
setDateTimeFormat,
|
||||
setLastAuthenticateWorkspaceState,
|
||||
urlManager.frontDomain,
|
||||
setWorkspaces,
|
||||
],
|
||||
);
|
||||
@ -301,23 +322,34 @@ export const useAuth = () => {
|
||||
[setIsVerifyPendingState, signUp, handleVerify],
|
||||
);
|
||||
|
||||
const buildRedirectUrl = (
|
||||
path: string,
|
||||
params: {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
const buildRedirectUrl = useCallback(
|
||||
(
|
||||
path: string,
|
||||
params: {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
},
|
||||
) => {
|
||||
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
|
||||
if (isDefined(params.workspaceInviteHash)) {
|
||||
url.searchParams.set('inviteHash', params.workspaceInviteHash);
|
||||
}
|
||||
if (isDefined(params.workspacePersonalInviteToken)) {
|
||||
url.searchParams.set(
|
||||
'inviteToken',
|
||||
params.workspacePersonalInviteToken,
|
||||
);
|
||||
}
|
||||
const subdomain = getWorkspaceSubdomain;
|
||||
|
||||
if (isDefined(subdomain)) {
|
||||
url.searchParams.set('workspaceSubdomain', subdomain);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
},
|
||||
) => {
|
||||
const authServerUrl = REACT_APP_SERVER_BASE_URL;
|
||||
const url = new URL(`${authServerUrl}${path}`);
|
||||
if (isDefined(params.workspaceInviteHash)) {
|
||||
url.searchParams.set('inviteHash', params.workspaceInviteHash);
|
||||
}
|
||||
if (isDefined(params.workspacePersonalInviteToken)) {
|
||||
url.searchParams.set('inviteToken', params.workspacePersonalInviteToken);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
[getWorkspaceSubdomain],
|
||||
);
|
||||
|
||||
const handleGoogleLogin = useCallback(
|
||||
(params: {
|
||||
@ -326,7 +358,7 @@ export const useAuth = () => {
|
||||
}) => {
|
||||
window.location.href = buildRedirectUrl('/auth/google', params);
|
||||
},
|
||||
[],
|
||||
[buildRedirectUrl],
|
||||
);
|
||||
|
||||
const handleMicrosoftLogin = useCallback(
|
||||
@ -336,7 +368,7 @@ export const useAuth = () => {
|
||||
}) => {
|
||||
window.location.href = buildRedirectUrl('/auth/microsoft', params);
|
||||
},
|
||||
[],
|
||||
[buildRedirectUrl],
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -0,0 +1,60 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
|
||||
const StyledFullWidthMotionDiv = styled(motion.div)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const SignInUpEmailField = ({
|
||||
showErrors,
|
||||
onChange: onChangeFromProps,
|
||||
}: {
|
||||
showErrors: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}) => {
|
||||
const form = useFormContext<Form>();
|
||||
|
||||
return (
|
||||
<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 (isDefined(onChangeFromProps)) onChangeFromProps(value);
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
);
|
||||
};
|
@ -1,393 +0,0 @@
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import {
|
||||
useSignInUpForm,
|
||||
validationSchema,
|
||||
} 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';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
ActionLink,
|
||||
HorizontalSeparator,
|
||||
IconGoogle,
|
||||
IconKey,
|
||||
IconMicrosoft,
|
||||
Loader,
|
||||
MainButton,
|
||||
StyledText,
|
||||
} from 'twenty-ui';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledForm = styled.form`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledFullWidthMotionDiv = styled(motion.div)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const SignInUpForm = () => {
|
||||
const captchaProvider = useRecoilValue(captchaProviderState);
|
||||
const isRequestingCaptchaToken = useRecoilValue(
|
||||
isRequestingCaptchaTokenState,
|
||||
);
|
||||
const [authProviders] = useRecoilState(authProvidersState);
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
const { signInWithMicrosoft } = useSignInWithMicrosoft();
|
||||
const { form } = useSignInUpForm();
|
||||
const { handleResetPassword } = useHandleResetPassword();
|
||||
|
||||
const {
|
||||
signInUpStep,
|
||||
signInUpMode,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
continueWithSSO,
|
||||
submitCredentials,
|
||||
submitSSOEmail,
|
||||
} = useSignInUp(form);
|
||||
|
||||
if (
|
||||
signInUpStep === SignInUpStep.Init &&
|
||||
!authProviders.google &&
|
||||
!authProviders.microsoft &&
|
||||
!authProviders.sso
|
||||
) {
|
||||
continueWithEmail();
|
||||
}
|
||||
|
||||
const toggleSSOMode = () => {
|
||||
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
continueWithEmail();
|
||||
} else {
|
||||
continueWithSSO();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = async (
|
||||
event: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (event.key === Key.Enter) {
|
||||
event.preventDefault();
|
||||
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
} else if (signInUpStep === SignInUpStep.Email) {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
}
|
||||
continueWithCredentials();
|
||||
} else if (signInUpStep === SignInUpStep.Password) {
|
||||
if (!form.formState.isSubmitting) {
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}
|
||||
} else if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
submitSSOEmail(form.getValues('email'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buttonTitle = useMemo(() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
return 'Continue With Email';
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
return 'Continue';
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.SSOEmail) {
|
||||
return 'Continue with SSO';
|
||||
}
|
||||
|
||||
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const shouldWaitForCaptchaToken =
|
||||
signInUpStep !== SignInUpStep.Init &&
|
||||
isDefined(captchaProvider?.provider) &&
|
||||
isRequestingCaptchaToken;
|
||||
|
||||
const isEmailStepSubmitButtonDisabledCondition =
|
||||
signInUpStep === SignInUpStep.Email &&
|
||||
(!validationSchema.shape.email.safeParse(form.watch('email')).success ||
|
||||
shouldWaitForCaptchaToken);
|
||||
|
||||
// TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders
|
||||
// We make the isValid check synchronous and update a reactState to make sure this does not happen
|
||||
const isPasswordStepSubmitButtonDisabledCondition =
|
||||
signInUpStep === SignInUpStep.Password &&
|
||||
(!form.formState.isValid ||
|
||||
form.formState.isSubmitting ||
|
||||
shouldWaitForCaptchaToken);
|
||||
|
||||
const isSubmitButtonDisabled =
|
||||
isEmailStepSubmitButtonDisabledCondition ||
|
||||
isPasswordStepSubmitButtonDisabledCondition;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconGoogle size={theme.icon.size.lg} />}
|
||||
title="Continue with Google"
|
||||
onClick={signInWithGoogle}
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? undefined : 'secondary'
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{authProviders.microsoft && (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.lg} />}
|
||||
title="Continue with Microsoft"
|
||||
onClick={signInWithMicrosoft}
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? undefined : 'secondary'
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
)}
|
||||
{authProviders.sso && (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconKey size={theme.icon.size.lg} />}
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? undefined : 'secondary'
|
||||
}
|
||||
title={
|
||||
signInUpStep === SignInUpStep.SSOEmail
|
||||
? 'Continue with email'
|
||||
: 'Single sign-on (SSO)'
|
||||
}
|
||||
onClick={toggleSSOMode}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{(authProviders.google ||
|
||||
authProviders.microsoft ||
|
||||
authProviders.sso) && <HorizontalSeparator visible />}
|
||||
|
||||
{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}
|
||||
/>
|
||||
{signInUpMode === SignInUpMode.SignUp && (
|
||||
<StyledText
|
||||
text={'At least 8 characters long.'}
|
||||
color={theme.font.color.secondary}
|
||||
/>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
<MainButton
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
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 /> : null)}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
)}
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep === SignInUpStep.SSOEmail && (
|
||||
<>
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="Email"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
setShowErrors(true);
|
||||
submitSSOEmail(form.getValues('email'));
|
||||
}}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
fullWidth
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledForm>
|
||||
</StyledContentContainer>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||
Forgot your password?
|
||||
</ActionLink>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Init && <FooterNote />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,164 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
IconGoogle,
|
||||
IconMicrosoft,
|
||||
Loader,
|
||||
MainButton,
|
||||
HorizontalSeparator,
|
||||
} from 'twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||
|
||||
const StyledContentContainer = styled(motion.div)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledForm = styled.form`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SignInUpGlobalScopeForm = () => {
|
||||
const theme = useTheme();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
const { signInWithMicrosoft } = useSignInWithMicrosoft();
|
||||
const { checkUserExists } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { redirectToWorkspace } = useUrlManager();
|
||||
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
||||
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { submitCredentials } = useSignInUp(form);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
await submitCredentials(form.getValues());
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await readCaptchaToken();
|
||||
await checkUserExists.checkUserExistsQuery({
|
||||
variables: {
|
||||
email: form.getValues('email'),
|
||||
captchaToken: token,
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
if (data.checkUserExists.__typename === 'UserExists') {
|
||||
if (
|
||||
isDefined(data?.checkUserExists.availableWorkspaces) &&
|
||||
data.checkUserExists.availableWorkspaces.length >= 1
|
||||
) {
|
||||
return redirectToWorkspace(
|
||||
data?.checkUserExists.availableWorkspaces[0].subdomain,
|
||||
pathname,
|
||||
{
|
||||
email: form.getValues('email'),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.checkUserExists.__typename === 'UserNotExists') {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconGoogle size={theme.icon.size.lg} />}
|
||||
title="Continue with Google"
|
||||
onClick={signInWithGoogle}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.lg} />}
|
||||
title="Continue with Microsoft"
|
||||
onClick={signInWithMicrosoft}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
<HorizontalSeparator visible />
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<SignInUpEmailField showErrors={showErrors} />
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MainButton
|
||||
title={
|
||||
signInUpStep === SignInUpStep.Password ? 'Sign Up' : 'Continue'
|
||||
}
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</StyledContentContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { StyledText } from 'twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||
|
||||
const StyledFullWidthMotionDiv = styled(motion.div)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const SignInUpPasswordField = ({
|
||||
showErrors,
|
||||
signInUpMode,
|
||||
}: {
|
||||
showErrors: boolean;
|
||||
signInUpMode: SignInUpMode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const form = useFormContext<Form>();
|
||||
|
||||
return (
|
||||
<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
|
||||
/>
|
||||
{signInUpMode === SignInUpMode.SignUp && (
|
||||
<StyledText
|
||||
text={'At least 8 characters long.'}
|
||||
color={theme.font.color.secondary}
|
||||
/>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MainButton, HorizontalSeparator } from 'twenty-ui';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SignInUpSSOIdentityProviderSelection = () => {
|
||||
const authProviders = useRecoilValue(authProvidersState);
|
||||
|
||||
const { redirectToSSOLoginPage } = useSSO();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{isDefined(authProviders?.sso) &&
|
||||
authProviders?.sso.map((idp) => (
|
||||
<>
|
||||
<MainButton
|
||||
key={idp.id}
|
||||
title={idp.name}
|
||||
onClick={() => redirectToSSOLoginPage(idp.id)}
|
||||
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
))}
|
||||
</StyledContentContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,142 @@
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { Loader, MainButton } from 'twenty-ui';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import styled from '@emotion/styled';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||
|
||||
const StyledForm = styled.form`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SignInUpWithCredentials = () => {
|
||||
const { form, validationSchema } = useSignInUpForm();
|
||||
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
const captchaProvider = useRecoilValue(captchaProviderState);
|
||||
const isRequestingCaptchaToken = useRecoilValue(
|
||||
isRequestingCaptchaTokenState,
|
||||
);
|
||||
|
||||
const {
|
||||
signInUpMode,
|
||||
continueWithEmail,
|
||||
continueWithCredentials,
|
||||
submitCredentials,
|
||||
} = useSignInUp(form);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (isSubmitButtonDisabled) return;
|
||||
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
} else if (signInUpStep === SignInUpStep.Email) {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
setShowErrors(true);
|
||||
return;
|
||||
}
|
||||
continueWithCredentials();
|
||||
} else if (signInUpStep === SignInUpStep.Password) {
|
||||
if (!form.formState.isSubmitting) {
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buttonTitle = useMemo(() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
return 'Continue With Email';
|
||||
}
|
||||
|
||||
if (
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
signInUpStep === SignInUpStep.Password
|
||||
) {
|
||||
return 'Sign in';
|
||||
}
|
||||
|
||||
if (
|
||||
signInUpMode === SignInUpMode.SignUp &&
|
||||
signInUpStep === SignInUpStep.Password
|
||||
) {
|
||||
return 'Sign up';
|
||||
}
|
||||
|
||||
return 'Continue';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
const shouldWaitForCaptchaToken =
|
||||
signInUpStep !== SignInUpStep.Init &&
|
||||
isDefined(captchaProvider?.provider) &&
|
||||
isRequestingCaptchaToken;
|
||||
|
||||
const isEmailStepSubmitButtonDisabledCondition =
|
||||
signInUpStep === SignInUpStep.Email &&
|
||||
(!validationSchema.shape.email.safeParse(form.watch('email')).success ||
|
||||
shouldWaitForCaptchaToken);
|
||||
|
||||
// TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders
|
||||
// We make the isValid check synchronous and update a reactState to make sure this does not happen
|
||||
const isPasswordStepSubmitButtonDisabledCondition =
|
||||
signInUpStep === SignInUpStep.Password &&
|
||||
(!form.formState.isValid ||
|
||||
form.formState.isSubmitting ||
|
||||
shouldWaitForCaptchaToken);
|
||||
|
||||
const isSubmitButtonDisabled =
|
||||
isEmailStepSubmitButtonDisabledCondition ||
|
||||
isPasswordStepSubmitButtonDisabledCondition;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(signInUpStep === SignInUpStep.Password ||
|
||||
signInUpStep === SignInUpStep.Email ||
|
||||
signInUpStep === SignInUpStep.Init) && (
|
||||
<>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<SignInUpEmailField showErrors={showErrors} />
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { IconGoogle, MainButton, HorizontalSeparator } from 'twenty-ui';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||
import { memo } from 'react';
|
||||
|
||||
const GoogleIcon = memo(() => {
|
||||
const theme = useTheme();
|
||||
return <IconGoogle size={theme.icon.size.md} />;
|
||||
});
|
||||
|
||||
export const SignInUpWithGoogle = () => {
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={GoogleIcon}
|
||||
title="Continue with Google"
|
||||
onClick={signInWithGoogle}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { IconMicrosoft, MainButton, HorizontalSeparator } from 'twenty-ui';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const SignInUpWithMicrosoft = () => {
|
||||
const theme = useTheme();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { signInWithMicrosoft } = useSignInWithMicrosoft();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
|
||||
title="Continue with Microsoft"
|
||||
onClick={signInWithMicrosoft}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import { IconLock, MainButton, HorizontalSeparator } from 'twenty-ui';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
|
||||
export const SignInUpWithSSO = () => {
|
||||
const theme = useTheme();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const authProviders = useRecoilValue(authProvidersState);
|
||||
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
|
||||
const { redirectToSSOLoginPage } = useSSO();
|
||||
|
||||
const signInWithSSO = () => {
|
||||
if (authProviders.sso.length === 1) {
|
||||
return redirectToSSOLoginPage(authProviders.sso[0].id);
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.SSOIdentityProviderSelection);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconLock size={theme.icon.size.md} />}
|
||||
title="Single sign-on (SSO)"
|
||||
onClick={signInWithSSO}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={false} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { ActionLink, HorizontalSeparator } from 'twenty-ui';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
|
||||
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
|
||||
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SignInUpWorkspaceScopeForm = () => {
|
||||
const [authProviders] = useRecoilState(authProvidersState);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { handleResetPassword } = useHandleResetPassword();
|
||||
|
||||
const { signInUpStep, continueWithEmail, continueWithCredentials } =
|
||||
useSignInUp(form);
|
||||
const location = useLocation();
|
||||
|
||||
const checkAuthProviders = useCallback(() => {
|
||||
if (
|
||||
signInUpStep === SignInUpStep.Init &&
|
||||
!authProviders.google &&
|
||||
!authProviders.microsoft &&
|
||||
!authProviders.sso
|
||||
) {
|
||||
return continueWithEmail();
|
||||
}
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const email = searchParams.get('email');
|
||||
if (isDefined(email) && authProviders.password) {
|
||||
return continueWithCredentials();
|
||||
}
|
||||
}, [
|
||||
continueWithCredentials,
|
||||
location.search,
|
||||
authProviders.google,
|
||||
authProviders.microsoft,
|
||||
authProviders.password,
|
||||
authProviders.sso,
|
||||
continueWithEmail,
|
||||
signInUpStep,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthProviders();
|
||||
}, [checkAuthProviders]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && <SignInUpWithGoogle />}
|
||||
|
||||
{authProviders.microsoft && <SignInUpWithMicrosoft />}
|
||||
|
||||
{authProviders.sso.length > 0 && <SignInUpWithSSO />}
|
||||
|
||||
{(authProviders.google ||
|
||||
authProviders.microsoft ||
|
||||
authProviders.sso.length > 0) &&
|
||||
authProviders.password ? (
|
||||
<HorizontalSeparator visible />
|
||||
) : null}
|
||||
|
||||
{authProviders.password && <SignInUpWithCredentials />}
|
||||
</StyledContentContainer>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||
Forgot your password?
|
||||
</ActionLink>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,9 +3,7 @@
|
||||
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';
|
||||
@ -13,20 +11,8 @@ 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']) => {
|
||||
@ -63,6 +49,5 @@ export const useSSO = () => {
|
||||
return {
|
||||
redirectToSSOLoginPage,
|
||||
getAuthorizationUrlForSSO,
|
||||
findAvailableSSOProviderByEmail,
|
||||
};
|
||||
};
|
||||
|
@ -7,36 +7,25 @@ import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
|
||||
export enum SignInUpMode {
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
}
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const { redirectToSSOLoginPage, findAvailableSSOProviderByEmail } = useSSO();
|
||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
@ -44,12 +33,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
|
||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||
|
||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
||||
return isMatchingLocation(AppPath.SignInUp)
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp;
|
||||
});
|
||||
|
||||
const {
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
@ -67,7 +50,12 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
}, [isMatchingLocation, requestFreshCaptchaToken, setSignInUpStep]);
|
||||
}, [
|
||||
isMatchingLocation,
|
||||
requestFreshCaptchaToken,
|
||||
setSignInUpMode,
|
||||
setSignInUpStep,
|
||||
]);
|
||||
|
||||
const continueWithCredentials = useCallback(async () => {
|
||||
const token = await readCaptchaToken();
|
||||
@ -101,47 +89,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
enqueueSnackBar,
|
||||
requestFreshCaptchaToken,
|
||||
setSignInUpStep,
|
||||
setSignInUpMode,
|
||||
]);
|
||||
|
||||
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();
|
||||
@ -150,19 +100,21 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
signInUpMode === SignInUpMode.SignIn && !isInviteMode
|
||||
? await signInWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
)
|
||||
: await signUpWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
token,
|
||||
);
|
||||
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
|
||||
await signInWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
} else {
|
||||
await signUpWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
token,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
@ -189,8 +141,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
signInUpMode,
|
||||
continueWithCredentials,
|
||||
continueWithEmail,
|
||||
continueWithSSO,
|
||||
submitSSOEmail,
|
||||
submitCredentials,
|
||||
};
|
||||
};
|
||||
|
@ -3,33 +3,50 @@ import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { z } from 'zod';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
|
||||
export const validationSchema = z
|
||||
.object({
|
||||
exist: z.boolean(),
|
||||
email: z.string().trim().email('Email must be a valid email'),
|
||||
password: z
|
||||
.string()
|
||||
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
||||
captchaToken: z.string().default(''),
|
||||
})
|
||||
.required();
|
||||
const makeValidationSchema = (signInUpStep: SignInUpStep) =>
|
||||
z
|
||||
.object({
|
||||
exist: z.boolean(),
|
||||
email: z.string().trim().email('Email must be a valid email'),
|
||||
password:
|
||||
signInUpStep === SignInUpStep.Password
|
||||
? z
|
||||
.string()
|
||||
.regex(
|
||||
PASSWORD_REGEX,
|
||||
'Password must contain at least 8 characters',
|
||||
)
|
||||
: z.string().optional(),
|
||||
captchaToken: z.string().default(''),
|
||||
})
|
||||
.required();
|
||||
|
||||
export type Form = z.infer<typeof validationSchema>;
|
||||
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
|
||||
export const useSignInUpForm = () => {
|
||||
const location = useLocation();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
|
||||
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
|
||||
|
||||
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
|
||||
isDeveloperDefaultSignInPrefilledState,
|
||||
);
|
||||
const [searchParams] = useSearchParams();
|
||||
const invitationPrefilledEmail = searchParams.get('email');
|
||||
const prefilledEmail = searchParams.get('email');
|
||||
|
||||
const form = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
mode: 'onSubmit',
|
||||
defaultValues: {
|
||||
exist: false,
|
||||
email: '',
|
||||
@ -40,12 +57,12 @@ export const useSignInUpForm = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(invitationPrefilledEmail)) {
|
||||
form.setValue('email', invitationPrefilledEmail);
|
||||
if (isDefined(prefilledEmail)) {
|
||||
form.setValue('email', prefilledEmail);
|
||||
} else if (isDeveloperDefaultSignInPrefilled === true) {
|
||||
form.setValue('email', 'tim@apple.dev');
|
||||
form.setValue('password', 'Applecar2025');
|
||||
}
|
||||
}, [form, isDeveloperDefaultSignInPrefilled, invitationPrefilledEmail]);
|
||||
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail, location.search]);
|
||||
return { form: form };
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { UserExists } from '~/generated/graphql';
|
||||
|
||||
export const availableSSOIdentityProvidersForAuthState = createState<
|
||||
NonNullable<UserExists['availableWorkspaces']>[0]['sso']
|
||||
>({
|
||||
key: 'availableSSOIdentityProvidersForAuth',
|
||||
defaultValue: [],
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { FindAvailableSsoIdentityProvidersMutationResult } from '~/generated/graphql';
|
||||
|
||||
export const availableSSOIdentityProvidersState = createState<
|
||||
NonNullable<
|
||||
FindAvailableSsoIdentityProvidersMutationResult['data']
|
||||
>['findAvailableSSOIdentityProviders']
|
||||
>({
|
||||
key: 'availableSSOIdentityProviders',
|
||||
defaultValue: [],
|
||||
});
|
@ -14,7 +14,11 @@ export type CurrentWorkspace = Pick<
|
||||
| 'currentBillingSubscription'
|
||||
| 'workspaceMembersCount'
|
||||
| 'isPublicInviteLinkEnabled'
|
||||
| 'isGoogleAuthEnabled'
|
||||
| 'isMicrosoftAuthEnabled'
|
||||
| 'isPasswordAuthEnabled'
|
||||
| 'hasValidEntrepriseKey'
|
||||
| 'subdomain'
|
||||
| 'metadataVersion'
|
||||
>;
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { cookieStorageEffect } from '~/utils/recoil-effects';
|
||||
import { Workspace } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const lastAuthenticateWorkspaceState = createState<
|
||||
| (Pick<Workspace, 'id' | 'subdomain'> & {
|
||||
cookieAttributes?: Cookies.CookieAttributes;
|
||||
})
|
||||
| null
|
||||
>({
|
||||
key: 'lastAuthenticateWorkspaceState',
|
||||
defaultValue: null,
|
||||
effects: [
|
||||
cookieStorageEffect('lastAuthenticateWorkspace', {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year
|
||||
}),
|
||||
],
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||
|
||||
export const signInUpModeState = createState<SignInUpMode>({
|
||||
key: 'signInUpModeState',
|
||||
defaultValue: SignInUpMode.SignIn,
|
||||
});
|
@ -4,8 +4,8 @@ export enum SignInUpStep {
|
||||
Init = 'init',
|
||||
Email = 'email',
|
||||
Password = 'password',
|
||||
SSOEmail = 'SSOEmail',
|
||||
SSOWorkspaceSelection = 'SSOWorkspaceSelection',
|
||||
WorkspaceSelection = 'workspaceSelection',
|
||||
SSOIdentityProviderSelection = 'SSOIdentityProviderSelection',
|
||||
}
|
||||
|
||||
export const signInUpStepState = createState<SignInUpStep>({
|
||||
|
@ -2,9 +2,17 @@ import { createState } from 'twenty-ui';
|
||||
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
import { cookieStorageEffect } from '~/utils/recoil-effects';
|
||||
|
||||
export const tokenPairState = createState<AuthTokenPair | null>({
|
||||
key: 'tokenPairState',
|
||||
defaultValue: null,
|
||||
effects: [cookieStorageEffect('tokenPair')],
|
||||
effects: [
|
||||
cookieStorageEffect(
|
||||
'tokenPair',
|
||||
{},
|
||||
{
|
||||
validateInitFn: (payload: AuthTokenPair) =>
|
||||
Boolean(payload['accessToken']),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
|
||||
|
||||
export const workspacePublicDataState =
|
||||
createState<PublicWorkspaceDataOutput | null>({
|
||||
key: 'workspacePublicDataState',
|
||||
defaultValue: null,
|
||||
});
|
@ -2,7 +2,10 @@ import { createState } from 'twenty-ui';
|
||||
|
||||
import { Workspace } from '~/generated/graphql';
|
||||
|
||||
export type Workspaces = Pick<Workspace, 'id' | 'logo' | 'displayName'>;
|
||||
export type Workspaces = Pick<
|
||||
Workspace,
|
||||
'id' | 'logo' | 'displayName' | 'subdomain'
|
||||
>;
|
||||
|
||||
export const workspacesState = createState<Workspaces[] | null>({
|
||||
key: 'workspacesState',
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum SignInUpMode {
|
||||
SignIn = 'sign-in',
|
||||
SignUp = 'sign-up',
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||
@ -7,23 +6,28 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA
|
||||
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
|
||||
|
||||
export const ClientConfigProviderEffect = () => {
|
||||
const setAuthProviders = useSetRecoilState(authProvidersState);
|
||||
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
||||
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
||||
const setUrlManager = useSetRecoilState(urlManagerState);
|
||||
|
||||
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
||||
isDeveloperDefaultSignInPrefilledState,
|
||||
);
|
||||
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
|
||||
const setIsMultiWorkspaceEnabled = useSetRecoilState(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
const setIsSSOEnabledState = useSetRecoilState(isSSOEnabledState);
|
||||
|
||||
const setBilling = useSetRecoilState(billingState);
|
||||
const setSupportChat = useSetRecoilState(supportChatState);
|
||||
@ -69,17 +73,10 @@ export const ClientConfigProviderEffect = () => {
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
setAuthProviders({
|
||||
google: data?.clientConfig.authProviders.google,
|
||||
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);
|
||||
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
|
||||
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
|
||||
|
||||
setBilling(data?.clientConfig.billing);
|
||||
setSupportChat(data?.clientConfig.support);
|
||||
@ -97,12 +94,16 @@ export const ClientConfigProviderEffect = () => {
|
||||
|
||||
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
||||
setApiConfig(data?.clientConfig?.api);
|
||||
setIsSSOEnabledState(data?.clientConfig?.isSSOEnabled);
|
||||
setUrlManager({
|
||||
defaultSubdomain: data?.clientConfig?.defaultSubdomain,
|
||||
frontDomain: data?.clientConfig?.frontDomain,
|
||||
});
|
||||
}, [
|
||||
data,
|
||||
setAuthProviders,
|
||||
setIsDebugMode,
|
||||
setIsDeveloperDefaultSignInPrefilled,
|
||||
setIsSignUpDisabled,
|
||||
setIsMultiWorkspaceEnabled,
|
||||
setSupportChat,
|
||||
setBilling,
|
||||
setSentryConfig,
|
||||
@ -113,6 +114,8 @@ export const ClientConfigProviderEffect = () => {
|
||||
setApiConfig,
|
||||
setIsAnalyticsEnabled,
|
||||
error,
|
||||
setUrlManager,
|
||||
setIsSSOEnabledState,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
@ -3,19 +3,16 @@ import { gql } from '@apollo/client';
|
||||
export const GET_CLIENT_CONFIG = gql`
|
||||
query GetClientConfig {
|
||||
clientConfig {
|
||||
authProviders {
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
sso
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
billingUrl
|
||||
billingFreeTrialDurationInDays
|
||||
}
|
||||
signInPrefilled
|
||||
signUpDisabled
|
||||
isMultiWorkspaceEnabled
|
||||
isSSOEnabled
|
||||
defaultSubdomain
|
||||
frontDomain
|
||||
debugMode
|
||||
analyticsEnabled
|
||||
support {
|
||||
|
@ -5,10 +5,10 @@ import { AuthProviders } from '~/generated/graphql';
|
||||
export const authProvidersState = createState<AuthProviders>({
|
||||
key: 'authProvidersState',
|
||||
defaultValue: {
|
||||
google: false,
|
||||
google: true,
|
||||
magicLink: false,
|
||||
password: false,
|
||||
password: true,
|
||||
microsoft: false,
|
||||
sso: false,
|
||||
sso: [],
|
||||
},
|
||||
});
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isMultiWorkspaceEnabledState = createState<boolean>({
|
||||
key: 'isMultiWorkspaceEnabled',
|
||||
defaultValue: false,
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isSSOEnabledState = createState<boolean>({
|
||||
key: 'isSSOEnabledState',
|
||||
defaultValue: false,
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isSignUpDisabledState = createState<boolean>({
|
||||
key: 'isSignUpDisabledState',
|
||||
defaultValue: false,
|
||||
});
|
@ -15,10 +15,14 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
id: '1',
|
||||
featureFlags: [],
|
||||
allowImpersonation: false,
|
||||
subdomain: 'test',
|
||||
activationStatus: WorkspaceActivationStatus.Active,
|
||||
hasValidEntrepriseKey: false,
|
||||
metadataVersion: 1,
|
||||
isPublicInviteLinkEnabled: false,
|
||||
isGoogleAuthEnabled: true,
|
||||
isMicrosoftAuthEnabled: false,
|
||||
isPasswordAuthEnabled: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -58,7 +58,7 @@ const StyledIconContainer = styled.div`
|
||||
height: 75%;
|
||||
`;
|
||||
|
||||
const StyledDeveloperSection = styled.div`
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
@ -82,7 +82,6 @@ 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;
|
||||
|
||||
@ -192,14 +191,20 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
Icon={IconCode}
|
||||
/>
|
||||
)}
|
||||
{isSSOEnabled && (
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Security"
|
||||
path={SettingsPath.Security}
|
||||
Icon={IconKey}
|
||||
/>
|
||||
{isAdvancedModeEnabled && (
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||
</StyledIconContainer>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Security"
|
||||
path={SettingsPath.Security}
|
||||
Icon={IconKey}
|
||||
/>
|
||||
</StyledContainer>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
|
||||
<AnimatePresence>
|
||||
{isAdvancedModeEnabled && (
|
||||
<motion.div
|
||||
@ -209,7 +214,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
exit="exit"
|
||||
variants={motionAnimationVariants}
|
||||
>
|
||||
<StyledDeveloperSection>
|
||||
<StyledContainer>
|
||||
<StyledIconContainer>
|
||||
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||
</StyledIconContainer>
|
||||
@ -228,7 +233,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
/>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
</StyledDeveloperSection>
|
||||
</StyledContainer>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
@ -10,7 +10,7 @@ import styled from '@emotion/styled';
|
||||
import { ReactElement } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { H2Title, IconComponent, IconKey, Section } from 'twenty-ui';
|
||||
import { IdpType } from '~/generated/graphql';
|
||||
import { IdentityProviderType } from '~/generated/graphql';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: grid;
|
||||
@ -30,8 +30,8 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
||||
const { control, getValues } =
|
||||
useFormContext<SettingSecurityNewSSOIdentityFormValues>();
|
||||
|
||||
const IdpMap: Record<
|
||||
IdpType,
|
||||
const IdentitiesProvidersMap: Record<
|
||||
IdentityProviderType,
|
||||
{
|
||||
form: ReactElement;
|
||||
option: {
|
||||
@ -62,12 +62,12 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const getFormByType = (type: Uppercase<IdpType> | undefined) => {
|
||||
const getFormByType = (type: Uppercase<IdentityProviderType> | undefined) => {
|
||||
switch (type) {
|
||||
case IdpType.Oidc:
|
||||
return IdpMap.OIDC.form;
|
||||
case IdpType.Saml:
|
||||
return IdpMap.SAML.form;
|
||||
case IdentityProviderType.Oidc:
|
||||
return IdentitiesProvidersMap.OIDC.form;
|
||||
case IdentityProviderType.Saml:
|
||||
return IdentitiesProvidersMap.SAML.form;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -106,7 +106,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsRadioCardContainer
|
||||
value={value}
|
||||
options={Object.values(IdpMap).map(
|
||||
options={Object.values(IdentitiesProvidersMap).map(
|
||||
(identityProviderType) => identityProviderType.option,
|
||||
)}
|
||||
onChange={onChange}
|
||||
|
@ -8,11 +8,14 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCardWrapper';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { IconKey } from 'twenty-ui';
|
||||
import { useListSsoIdentityProvidersByWorkspaceIdQuery } from '~/generated/graphql';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
const StyledLink = styled(Link, {
|
||||
shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'isDisabled',
|
||||
@ -22,11 +25,29 @@ const StyledLink = styled(Link, {
|
||||
`;
|
||||
|
||||
export const SettingsSSOIdentitiesProvidersListCard = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
|
||||
const [SSOIdentitiesProviders, setSSOIdentitiesProviders] = useRecoilState(
|
||||
SSOIdentitiesProvidersState,
|
||||
);
|
||||
|
||||
return !SSOIdentitiesProviders.length ? (
|
||||
const { loading } = useListSsoIdentityProvidersByWorkspaceIdQuery({
|
||||
skip: currentWorkspace?.hasValidEntrepriseKey === false,
|
||||
onCompleted: (data) => {
|
||||
setSSOIdentitiesProviders(
|
||||
data?.listSSOIdentityProvidersByWorkspaceId ?? [],
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return loading || !SSOIdentitiesProviders.length ? (
|
||||
<StyledLink
|
||||
to={getSettingsPagePath(SettingsPath.NewSSOIdentityProvider)}
|
||||
isDisabled={currentWorkspace?.hasValidEntrepriseKey !== true}
|
||||
|
@ -5,33 +5,14 @@ import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/securit
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { useListSsoIdentityProvidersByWorkspaceIdQuery } from '~/generated/graphql';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [SSOIdentitiesProviders, setSSOIdentitiesProviders] = useRecoilState(
|
||||
SSOIdentitiesProvidersState,
|
||||
);
|
||||
|
||||
const { loading } = useListSsoIdentityProvidersByWorkspaceIdQuery({
|
||||
onCompleted: (data) => {
|
||||
setSSOIdentitiesProviders(
|
||||
data?.listSSOIdentityProvidersByWorkspaceId ?? [],
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
|
||||
|
||||
return (
|
||||
<SettingsListCard
|
||||
@ -39,7 +20,6 @@ export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {
|
||||
getItemLabel={(SSOIdentityProvider) =>
|
||||
`${SSOIdentityProvider.name} - ${SSOIdentityProvider.type}`
|
||||
}
|
||||
isLoading={loading}
|
||||
RowIconFn={(SSOIdentityProvider) =>
|
||||
guessSSOIdentityProviderIconByUrl(SSOIdentityProvider.issuer)
|
||||
}
|
||||
|
@ -2,24 +2,98 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
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 { Card, IconLink, isDefined } from 'twenty-ui';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import {
|
||||
IconLink,
|
||||
Card,
|
||||
IconGoogle,
|
||||
IconMicrosoft,
|
||||
IconPassword,
|
||||
} from 'twenty-ui';
|
||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||
import { AuthProviders } from '~/generated-metadata/graphql';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||
|
||||
const StyledSettingsSecurityOptionsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsSecurityOptionsList = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
|
||||
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
if (!isDefined(currentWorkspace)) {
|
||||
throw new Error(
|
||||
'The current workspace must be defined to edit its security options.',
|
||||
);
|
||||
}
|
||||
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
|
||||
const isValidAuthProvider = (
|
||||
key: string,
|
||||
): key is Exclude<keyof typeof currentWorkspace, '__typename'> => {
|
||||
if (!currentWorkspace) return false;
|
||||
return Reflect.has(currentWorkspace, key);
|
||||
};
|
||||
|
||||
const toggleAuthMethod = async (
|
||||
authProvider: keyof Omit<AuthProviders, '__typename' | 'magicLink' | 'sso'>,
|
||||
) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
|
||||
const key = `is${capitalize(authProvider)}AuthEnabled`;
|
||||
|
||||
if (!isValidAuthProvider(key)) {
|
||||
throw new Error('Invalid auth provider');
|
||||
}
|
||||
|
||||
const allAuthProvidersEnabled = [
|
||||
currentWorkspace.isGoogleAuthEnabled,
|
||||
currentWorkspace.isMicrosoftAuthEnabled,
|
||||
currentWorkspace.isPasswordAuthEnabled,
|
||||
(SSOIdentitiesProviders?.length ?? 0) > 0,
|
||||
];
|
||||
|
||||
if (
|
||||
currentWorkspace[key] === true &&
|
||||
allAuthProvidersEnabled.filter((isAuthEnable) => isAuthEnable).length <= 1
|
||||
) {
|
||||
return enqueueSnackBar(
|
||||
'At least one authentication method must be enabled',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
[key]: !currentWorkspace[key],
|
||||
});
|
||||
|
||||
updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
[key]: !currentWorkspace[key],
|
||||
},
|
||||
},
|
||||
}).catch((err) => {
|
||||
// rollback optimistic update if err
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
[key]: !currentWorkspace[key],
|
||||
});
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) {
|
||||
@ -44,17 +118,49 @@ export const SettingsSecurityOptionsList = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconLink}
|
||||
title="Invite by Link"
|
||||
description="Allow the invitation of new users by sharing an invite link."
|
||||
checked={currentWorkspace.isPublicInviteLinkEnabled}
|
||||
advancedMode
|
||||
onChange={() =>
|
||||
handleChange(!currentWorkspace.isPublicInviteLinkEnabled)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<StyledSettingsSecurityOptionsList>
|
||||
{currentWorkspace && (
|
||||
<>
|
||||
<Card>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconGoogle}
|
||||
title="Google"
|
||||
description="Allow logins through Google's single sign-on functionality."
|
||||
checked={currentWorkspace.isGoogleAuthEnabled}
|
||||
advancedMode
|
||||
onChange={() => toggleAuthMethod('google')}
|
||||
/>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconMicrosoft}
|
||||
title="Microsoft"
|
||||
description="Allow logins through Microsoft's single sign-on functionality."
|
||||
checked={currentWorkspace.isMicrosoftAuthEnabled}
|
||||
advancedMode
|
||||
onChange={() => toggleAuthMethod('microsoft')}
|
||||
/>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconPassword}
|
||||
title="Password"
|
||||
description="Allow users to sign in with an email and password."
|
||||
checked={currentWorkspace.isPasswordAuthEnabled}
|
||||
advancedMode
|
||||
onChange={() => toggleAuthMethod('password')}
|
||||
/>
|
||||
</Card>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconLink}
|
||||
title="Invite by Link"
|
||||
description="Allow the invitation of new users by sharing an invite link."
|
||||
checked={currentWorkspace.isPublicInviteLinkEnabled}
|
||||
advancedMode
|
||||
onChange={() =>
|
||||
handleChange(!currentWorkspace.isPublicInviteLinkEnabled)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</StyledSettingsSecurityOptionsList>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,4 @@
|
||||
export type AuthProvidersKeys =
|
||||
| 'isGoogleAuthEnabled'
|
||||
| 'isMicrosoftAuthEnabled'
|
||||
| 'isPasswordAuthEnabled';
|
@ -2,12 +2,15 @@
|
||||
|
||||
import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema';
|
||||
import { z } from 'zod';
|
||||
import { IdpType, SsoIdentityProviderStatus } from '~/generated/graphql';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
SsoIdentityProviderStatus,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type SSOIdentityProvider = {
|
||||
__typename: 'SSOIdentityProvider';
|
||||
id: string;
|
||||
type: IdpType;
|
||||
type: IdentityProviderType;
|
||||
issuer: string;
|
||||
name?: string | null;
|
||||
status: SsoIdentityProviderStatus;
|
||||
|
@ -16,7 +16,6 @@ export const parseSAMLMetadataFromXMLFile = (
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
|
||||
|
||||
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
||||
throw new Error('Error parsing XML');
|
||||
}
|
||||
@ -28,10 +27,10 @@ export const parseSAMLMetadataFromXMLFile = (
|
||||
'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 keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0];
|
||||
const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0];
|
||||
const x509Certificate = x509Data
|
||||
.getElementsByTagName('ds:X509Certificate')?.[0]
|
||||
?.getElementsByTagName('ds:X509Certificate')?.[0]
|
||||
.textContent?.trim();
|
||||
|
||||
const singleSignOnServices = Array.from(
|
||||
|
@ -1,17 +1,25 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||
import { IdpType } from '~/generated/graphql';
|
||||
import { IdentityProviderType } from '~/generated/graphql';
|
||||
|
||||
export const sSOIdentityProviderDefaultValues: Record<
|
||||
IdpType,
|
||||
IdentityProviderType,
|
||||
() => SettingSecurityNewSSOIdentityFormValues
|
||||
> = {
|
||||
SAML: () => ({
|
||||
type: 'SAML',
|
||||
ssoURL: '',
|
||||
name: '',
|
||||
id: crypto.randomUUID(),
|
||||
id:
|
||||
window.location.protocol === 'https:'
|
||||
? crypto.randomUUID()
|
||||
: '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
|
||||
(
|
||||
+c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
|
||||
).toString(16),
|
||||
),
|
||||
certificate: '',
|
||||
issuer: '',
|
||||
}),
|
||||
|
@ -10,7 +10,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const WorkspaceLogoUploader = () => {
|
||||
const [uploadLogo] = useUploadWorkspaceLogoMutation();
|
||||
const [updateWorkspce] = useUpdateWorkspaceMutation();
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
@ -39,7 +39,7 @@ export const WorkspaceLogoUploader = () => {
|
||||
if (!currentWorkspace?.id) {
|
||||
throw new Error('Workspace id not found');
|
||||
}
|
||||
await updateWorkspce({
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
logo: null,
|
||||
|
@ -19,6 +19,7 @@ export enum SettingsPath {
|
||||
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
||||
WorkspaceMembersPage = 'workspace-members',
|
||||
Workspace = 'workspace',
|
||||
Domain = 'domain',
|
||||
CRMMigration = 'crm-migration',
|
||||
Developers = 'developers',
|
||||
ServerlessFunctions = 'functions',
|
||||
|
@ -15,6 +15,13 @@ import { useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
|
||||
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLogo = styled.div<{ logo: string }>`
|
||||
background: url(${({ logo }) => logo});
|
||||
@ -72,6 +79,7 @@ export const MultiWorkspaceDropdownButton = ({
|
||||
useState(false);
|
||||
|
||||
const { switchWorkspace } = useWorkspaceSwitching();
|
||||
const { buildWorkspaceUrl } = useUrlManager();
|
||||
|
||||
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
||||
|
||||
@ -96,13 +104,9 @@ export const MultiWorkspaceDropdownButton = ({
|
||||
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
||||
>
|
||||
<StyledLogo
|
||||
logo={
|
||||
getImageAbsoluteURI(
|
||||
currentWorkspace?.logo === null
|
||||
? DEFAULT_WORKSPACE_LOGO
|
||||
: currentWorkspace?.logo,
|
||||
) ?? ''
|
||||
}
|
||||
logo={getImageAbsoluteURI(
|
||||
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
|
||||
@ -118,23 +122,26 @@ export const MultiWorkspaceDropdownButton = ({
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{workspaces.map((workspace) => (
|
||||
<MenuItemSelectAvatar
|
||||
<StyledLink
|
||||
key={workspace.id}
|
||||
text={workspace.displayName ?? ''}
|
||||
avatar={
|
||||
<StyledLogo
|
||||
logo={
|
||||
getImageAbsoluteURI(
|
||||
workspace.logo === null
|
||||
? DEFAULT_WORKSPACE_LOGO
|
||||
: workspace.logo,
|
||||
) ?? ''
|
||||
}
|
||||
/>
|
||||
}
|
||||
selected={currentWorkspace?.id === workspace.id}
|
||||
onClick={() => handleChange(workspace.id)}
|
||||
/>
|
||||
to={buildWorkspaceUrl(workspace.subdomain)}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={workspace.displayName ?? ''}
|
||||
avatar={
|
||||
<StyledLogo
|
||||
logo={getImageAbsoluteURI(
|
||||
workspace.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
selected={currentWorkspace?.id === workspace.id}
|
||||
onClick={(event) => {
|
||||
event?.preventDefault();
|
||||
handleChange(workspace.id);
|
||||
}}
|
||||
/>
|
||||
</StyledLink>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -60,14 +61,17 @@ export const NavigationDrawerHeader = ({
|
||||
}: NavigationDrawerHeaderProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const workspaces = useRecoilValue(workspacesState);
|
||||
const isMultiWorkspace = workspaces !== null && workspaces.length > 1;
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
|
||||
const isNavigationDrawerExpanded = useRecoilValue(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{isMultiWorkspace ? (
|
||||
{isMultiWorkspaceEnabled &&
|
||||
workspaces !== null &&
|
||||
workspaces.length > 1 ? (
|
||||
<MultiWorkspaceDropdownButton workspaces={workspaces} />
|
||||
) : (
|
||||
<StyledSingleWorkspaceContainer>
|
||||
|
@ -1,74 +1,44 @@
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useGenerateJwtMutation } from '~/generated/graphql';
|
||||
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSwitchWorkspaceMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
export const useWorkspaceSwitching = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const [generateJWT] = useGenerateJwtMutation();
|
||||
const { redirectToSSOLoginPage } = useSSO();
|
||||
const [switchWorkspaceMutation] = useSwitchWorkspaceMutation();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const { clearSession } = useAuth();
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { redirectToHome, redirectToWorkspace } = useUrlManager();
|
||||
|
||||
const switchWorkspace = async (workspaceId: string) => {
|
||||
if (currentWorkspace?.id === workspaceId) return;
|
||||
const jwt = await generateJWT({
|
||||
|
||||
if (!isMultiWorkspaceEnabled) {
|
||||
return enqueueSnackBar(
|
||||
'Switching workspace is not available in single workspace mode',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { data, errors } = await switchWorkspaceMutation({
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(jwt.errors)) {
|
||||
throw jwt.errors;
|
||||
if (isDefined(errors) || !isDefined(data?.switchWorkspace.subdomain)) {
|
||||
return redirectToHome();
|
||||
}
|
||||
|
||||
if (!isDefined(jwt.data?.generateJWT)) {
|
||||
throw new Error('could not create token');
|
||||
}
|
||||
|
||||
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 clearSession();
|
||||
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;
|
||||
}
|
||||
redirectToWorkspace(data.switchWorkspace.subdomain);
|
||||
};
|
||||
|
||||
return { switchWorkspace };
|
||||
|
@ -0,0 +1,110 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
|
||||
export const useUrlManager = () => {
|
||||
const urlManager = useRecoilValue(urlManagerState);
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
|
||||
const homePageDomain = useMemo(() => {
|
||||
return isMultiWorkspaceEnabled
|
||||
? `${urlManager.defaultSubdomain}.${urlManager.frontDomain}`
|
||||
: urlManager.frontDomain;
|
||||
}, [
|
||||
isMultiWorkspaceEnabled,
|
||||
urlManager.defaultSubdomain,
|
||||
urlManager.frontDomain,
|
||||
]);
|
||||
|
||||
const isTwentyHomePage = useMemo(() => {
|
||||
if (!isMultiWorkspaceEnabled) return true;
|
||||
return window.location.hostname === homePageDomain;
|
||||
}, [homePageDomain, isMultiWorkspaceEnabled]);
|
||||
|
||||
const isTwentyWorkspaceSubdomain = useMemo(() => {
|
||||
if (!isMultiWorkspaceEnabled) return false;
|
||||
|
||||
if (
|
||||
!isDefined(urlManager.frontDomain) ||
|
||||
!isDefined(urlManager.defaultSubdomain)
|
||||
) {
|
||||
throw new Error('frontDomain and defaultSubdomain are required');
|
||||
}
|
||||
|
||||
return window.location.hostname !== homePageDomain;
|
||||
}, [
|
||||
homePageDomain,
|
||||
isMultiWorkspaceEnabled,
|
||||
urlManager.defaultSubdomain,
|
||||
urlManager.frontDomain,
|
||||
]);
|
||||
|
||||
const getWorkspaceSubdomain = useMemo(() => {
|
||||
if (!isDefined(urlManager.frontDomain)) {
|
||||
throw new Error('frontDomain is not defined');
|
||||
}
|
||||
|
||||
return isTwentyWorkspaceSubdomain
|
||||
? window.location.hostname.replace(`.${urlManager.frontDomain}`, '')
|
||||
: null;
|
||||
}, [isTwentyWorkspaceSubdomain, urlManager.frontDomain]);
|
||||
|
||||
const buildWorkspaceUrl = useCallback(
|
||||
(
|
||||
subdomain?: string,
|
||||
onPage?: string,
|
||||
searchParams?: Record<string, string>,
|
||||
) => {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
if (isDefined(subdomain) && subdomain.length !== 0) {
|
||||
url.hostname = `${subdomain}.${urlManager.frontDomain}`;
|
||||
}
|
||||
|
||||
if (isDefined(onPage)) {
|
||||
url.pathname = onPage;
|
||||
}
|
||||
|
||||
if (isDefined(searchParams)) {
|
||||
Object.entries(searchParams).forEach(([key, value]) =>
|
||||
url.searchParams.set(key, value),
|
||||
);
|
||||
}
|
||||
return url.toString();
|
||||
},
|
||||
[urlManager.frontDomain],
|
||||
);
|
||||
|
||||
const redirectToWorkspace = useCallback(
|
||||
(
|
||||
subdomain: string,
|
||||
onPage?: string,
|
||||
searchParams?: Record<string, string>,
|
||||
) => {
|
||||
if (!isMultiWorkspaceEnabled) return;
|
||||
window.location.href = buildWorkspaceUrl(subdomain, onPage, searchParams);
|
||||
},
|
||||
[buildWorkspaceUrl, isMultiWorkspaceEnabled],
|
||||
);
|
||||
|
||||
const redirectToHome = useCallback(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.hostname !== homePageDomain) {
|
||||
url.hostname = homePageDomain;
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
}, [homePageDomain]);
|
||||
|
||||
return {
|
||||
redirectToHome,
|
||||
redirectToWorkspace,
|
||||
homePageDomain,
|
||||
isTwentyHomePage,
|
||||
buildWorkspaceUrl,
|
||||
isTwentyWorkspaceSubdomain,
|
||||
getWorkspaceSubdomain,
|
||||
};
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { ClientConfig } from '~/generated/graphql';
|
||||
|
||||
export const urlManagerState = createState<
|
||||
Pick<ClientConfig, 'frontDomain' | 'defaultSubdomain'>
|
||||
>({
|
||||
key: 'urlManager',
|
||||
defaultValue: {
|
||||
frontDomain: '',
|
||||
defaultSubdomain: undefined,
|
||||
},
|
||||
});
|
@ -32,6 +32,10 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
allowImpersonation
|
||||
activationStatus
|
||||
isPublicInviteLinkEnabled
|
||||
isGoogleAuthEnabled
|
||||
isMicrosoftAuthEnabled
|
||||
isPasswordAuthEnabled
|
||||
subdomain
|
||||
hasValidEntrepriseKey
|
||||
featureFlags {
|
||||
id
|
||||
@ -53,6 +57,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
logo
|
||||
displayName
|
||||
domainName
|
||||
subdomain
|
||||
}
|
||||
}
|
||||
userVars
|
||||
|
@ -0,0 +1,96 @@
|
||||
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
|
||||
|
||||
import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql';
|
||||
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { lastAuthenticateWorkspaceState } from '@/auth/states/lastAuthenticateWorkspaceState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
export const WorkspaceProviderEffect = () => {
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
|
||||
const setAuthProviders = useSetRecoilState(authProvidersState);
|
||||
const setWorkspacePublicDataState = useSetRecoilState(
|
||||
workspacePublicDataState,
|
||||
);
|
||||
|
||||
const [lastAuthenticateWorkspace, setLastAuthenticateWorkspace] =
|
||||
useRecoilState(lastAuthenticateWorkspaceState);
|
||||
|
||||
const {
|
||||
redirectToHome,
|
||||
getWorkspaceSubdomain,
|
||||
redirectToWorkspace,
|
||||
isTwentyHomePage,
|
||||
} = useUrlManager();
|
||||
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
|
||||
useGetPublicWorkspaceDataBySubdomainQuery({
|
||||
skip:
|
||||
(isMultiWorkspaceEnabled && isTwentyHomePage) ||
|
||||
isDefined(workspacePublicData),
|
||||
onCompleted: (data) => {
|
||||
setAuthProviders(data.getPublicWorkspaceDataBySubdomain.authProviders);
|
||||
setWorkspacePublicDataState(data.getPublicWorkspaceDataBySubdomain);
|
||||
},
|
||||
onError: (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
setLastAuthenticateWorkspace(null);
|
||||
redirectToHome();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isMultiWorkspaceEnabled &&
|
||||
isDefined(workspacePublicData?.subdomain) &&
|
||||
workspacePublicData.subdomain !== getWorkspaceSubdomain
|
||||
) {
|
||||
redirectToWorkspace(workspacePublicData.subdomain);
|
||||
}
|
||||
}, [
|
||||
getWorkspaceSubdomain,
|
||||
isMultiWorkspaceEnabled,
|
||||
redirectToWorkspace,
|
||||
workspacePublicData,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isMultiWorkspaceEnabled &&
|
||||
isDefined(lastAuthenticateWorkspace?.subdomain) &&
|
||||
isTwentyHomePage
|
||||
) {
|
||||
redirectToWorkspace(lastAuthenticateWorkspace.subdomain);
|
||||
}
|
||||
}, [
|
||||
isMultiWorkspaceEnabled,
|
||||
isTwentyHomePage,
|
||||
lastAuthenticateWorkspace,
|
||||
redirectToWorkspace,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (isDefined(workspacePublicData?.logo)) {
|
||||
const link: HTMLLinkElement =
|
||||
document.querySelector("link[rel*='icon']") ||
|
||||
document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
link.href = workspacePublicData.logo;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}, [workspacePublicData]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -3,7 +3,13 @@ import { gql } from '@apollo/client';
|
||||
export const ACTIVATE_WORKSPACE = gql`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
id
|
||||
workspace {
|
||||
id
|
||||
subdomain
|
||||
}
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -5,9 +5,14 @@ export const UPDATE_WORKSPACE = gql`
|
||||
updateWorkspace(data: $input) {
|
||||
id
|
||||
domainName
|
||||
subdomain
|
||||
displayName
|
||||
logo
|
||||
allowImpersonation
|
||||
isPublicInviteLinkEnabled
|
||||
isGoogleAuthEnabled
|
||||
isMicrosoftAuthEnabled
|
||||
isPasswordAuthEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@ -16,6 +16,7 @@ import {
|
||||
useAddUserToWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -28,6 +29,7 @@ export const Invite = () => {
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
||||
const [addUserToWorkspaceByInviteToken] =
|
||||
useAddUserToWorkspaceByInviteTokenMutation();
|
||||
@ -77,7 +79,7 @@ export const Invite = () => {
|
||||
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
{isDefined(currentWorkspace) ? (
|
||||
{isDefined(currentUser) ? (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
<MainButton
|
||||
@ -91,7 +93,7 @@ export const Invite = () => {
|
||||
<FooterNote />
|
||||
</>
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
<SignInUpWorkspaceScopeForm />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { AnimatedEaseIn, MainButton } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
@ -27,6 +27,7 @@ import {
|
||||
useValidatePasswordResetTokenQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { logError } from '~/utils/logError';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
@ -71,6 +72,8 @@ const StyledInputContainer = styled.div`
|
||||
export const PasswordReset = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
@ -163,7 +166,7 @@ export const PasswordReset = () => {
|
||||
isTokenValid && (
|
||||
<StyledMainContainer>
|
||||
<AnimatedEaseIn>
|
||||
<Logo />
|
||||
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>Reset Password</Title>
|
||||
<StyledContentContainer>
|
||||
|
@ -1,68 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { HorizontalSeparator, MainButton } from 'twenty-ui';
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
|
||||
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,58 +1,70 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
|
||||
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { AnimatedEaseIn } from 'twenty-ui';
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
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 { SignInUpStep } from '@/auth/states/signInUpStepState';
|
||||
import { IconLockCustom } from '@ui/display/icon/components/IconLock';
|
||||
import { AnimatedEaseIn } from 'twenty-ui';
|
||||
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { SSOWorkspaceSelection } from './SSOWorkspaceSelection';
|
||||
|
||||
export const SignInUp = () => {
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const { signInUpStep } = useSignInUp(form);
|
||||
const { isTwentyHomePage, isTwentyWorkspaceSubdomain } = useUrlManager();
|
||||
|
||||
const { signInUpStep, signInUpMode } = useSignInUp(form);
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
|
||||
const signInUpForm = useMemo(() => {
|
||||
if (isTwentyHomePage && isMultiWorkspaceEnabled) {
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
}
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (
|
||||
signInUpStep === SignInUpStep.Init ||
|
||||
signInUpStep === SignInUpStep.Email
|
||||
(!isMultiWorkspaceEnabled ||
|
||||
(isMultiWorkspaceEnabled && isTwentyWorkspaceSubdomain)) &&
|
||||
signInUpStep === SignInUpStep.SSOIdentityProviderSelection
|
||||
) {
|
||||
return 'Welcome to Twenty';
|
||||
return <SignInUpSSOIdentityProviderSelection />;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) {
|
||||
return 'Choose SSO connection';
|
||||
}
|
||||
return signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
if (isDefined(currentWorkspace)) {
|
||||
return <></>;
|
||||
}
|
||||
if (
|
||||
isDefined(workspacePublicData) &&
|
||||
(!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain)
|
||||
) {
|
||||
return <SignInUpWorkspaceScopeForm />;
|
||||
}
|
||||
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
}, [
|
||||
isTwentyHomePage,
|
||||
isMultiWorkspaceEnabled,
|
||||
isTwentyWorkspaceSubdomain,
|
||||
signInUpStep,
|
||||
workspacePublicData,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<IconLockCustom size={40} />
|
||||
) : (
|
||||
<Logo />
|
||||
)}
|
||||
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
||||
<SSOWorkspaceSelection />
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
)}
|
||||
<Title animate>
|
||||
{`Welcome to ${workspacePublicData?.displayName ?? DEFAULT_WORKSPACE_NAME}`}
|
||||
</Title>
|
||||
{signInUpForm}
|
||||
{signInUpStep !== SignInUpStep.Password && <FooterNote />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
@ -22,6 +22,9 @@ import {
|
||||
useActivateWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -47,6 +50,8 @@ type Form = z.infer<typeof validationSchema>;
|
||||
export const CreateWorkspace = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
const { redirectToWorkspace } = useUrlManager();
|
||||
|
||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
@ -75,8 +80,19 @@ export const CreateWorkspace = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setIsCurrentUserLoaded(false);
|
||||
|
||||
if (isDefined(result.data) && isMultiWorkspaceEnabled) {
|
||||
return redirectToWorkspace(
|
||||
result.data.activateWorkspace.workspace.subdomain,
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: result.data.activateWorkspace.loginToken.token,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await apolloMetadataClient?.refetchQueries({
|
||||
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
||||
});
|
||||
@ -93,7 +109,9 @@ export const CreateWorkspace = () => {
|
||||
[
|
||||
activateWorkspace,
|
||||
setIsCurrentUserLoaded,
|
||||
isMultiWorkspaceEnabled,
|
||||
apolloMetadataClient,
|
||||
redirectToWorkspace,
|
||||
enqueueSnackBar,
|
||||
],
|
||||
);
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { GithubVersionLink, H2Title, Section } from 'twenty-ui';
|
||||
import { GithubVersionLink, H2Title, Section, IconWorld } from 'twenty-ui';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
||||
@ -9,39 +13,61 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import packageJson from '../../../package.json';
|
||||
export const SettingsWorkspace = () => (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'General' },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="Picture" />
|
||||
<WorkspaceLogoUploader />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your workspace" />
|
||||
<NameField />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Support"
|
||||
adornment={<ToggleImpersonate />}
|
||||
description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time."
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<DeleteWorkspace />
|
||||
</Section>
|
||||
<Section>
|
||||
<GithubVersionLink version={packageJson.version} />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const SettingsWorkspace = () => {
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'General' },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="Picture" />
|
||||
<WorkspaceLogoUploader />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your workspace" />
|
||||
<NameField />
|
||||
</Section>
|
||||
{isMultiWorkspaceEnabled && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain"
|
||||
description="Edit your subdomain name or set a custom domain."
|
||||
/>
|
||||
<StyledLink to={getSettingsPagePath(SettingsPath.Domain)}>
|
||||
<SettingsCard title="Customize Domain" Icon={<IconWorld />} />
|
||||
</StyledLink>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Support"
|
||||
adornment={<ToggleImpersonate />}
|
||||
description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time."
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<DeleteWorkspace />
|
||||
</Section>
|
||||
<Section>
|
||||
<GithubVersionLink version={packageJson.version} />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
@ -9,6 +9,9 @@ import { SettingsSecurityOptionsList } from '@/settings/security/components/Sett
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -26,6 +29,10 @@ const StyledSSOSection = styled(Section)`
|
||||
`;
|
||||
|
||||
export const SettingsSecurity = () => {
|
||||
const isSSOEnabled = useRecoilValue(isSSOEnabledState);
|
||||
const isSSOSectionDisplay =
|
||||
useIsFeatureEnabled('IS_SSO_ENABLED') && isSSOEnabled;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="Security"
|
||||
@ -40,26 +47,28 @@ export const SettingsSecurity = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledMainContent>
|
||||
<StyledSSOSection>
|
||||
<H2Title
|
||||
title="SSO"
|
||||
description="Configure an SSO connection"
|
||||
adornment={
|
||||
<Tag
|
||||
text={'Enterprise'}
|
||||
color={'transparent'}
|
||||
Icon={IconLock}
|
||||
variant={'border'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</StyledSSOSection>
|
||||
{isSSOSectionDisplay && (
|
||||
<StyledSSOSection>
|
||||
<H2Title
|
||||
title="SSO"
|
||||
description="Configure an SSO connection"
|
||||
adornment={
|
||||
<Tag
|
||||
text={'Enterprise'}
|
||||
color={'transparent'}
|
||||
Icon={IconLock}
|
||||
variant={'border'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</StyledSSOSection>
|
||||
)}
|
||||
<Section>
|
||||
<AdvancedSettingsWrapper>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title="Other"
|
||||
title="Authentication"
|
||||
description="Customize your workspace security"
|
||||
/>
|
||||
<SettingsSecurityOptionsList />
|
||||
|
@ -0,0 +1,154 @@
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import styled from '@emotion/styled';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
subdomain: z
|
||||
.string()
|
||||
.min(1, { message: 'Subdomain can not be empty' })
|
||||
.max(63, { message: 'Subdomain can not be longer than 63 characters' }),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
|
||||
const StyledDomainFromWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledDomain = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export const SettingsDomain = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const urlManager = useRecoilValue(urlManagerState);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
const { buildWorkspaceUrl } = useUrlManager();
|
||||
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = getValues();
|
||||
|
||||
if (!values || !isValid || !currentWorkspace) {
|
||||
throw new Error('Invalid form values');
|
||||
}
|
||||
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
subdomain: values.subdomain,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
subdomain: values.subdomain,
|
||||
});
|
||||
|
||||
window.location.href = buildWorkspaceUrl(values.subdomain);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
formState: { isValid },
|
||||
} = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
subdomain: currentWorkspace?.subdomain ?? '',
|
||||
},
|
||||
resolver: zodResolver(validationSchema),
|
||||
});
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="General"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'General',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'Domain' },
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!isValid}
|
||||
onCancel={() => navigate(getSettingsPagePath(SettingsPath.Workspace))}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain"
|
||||
description="Set the name of your subdomain"
|
||||
/>
|
||||
{currentWorkspace?.subdomain && (
|
||||
<StyledDomainFromWrapper>
|
||||
<Controller
|
||||
name="subdomain"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isDefined(urlManager) && isDefined(urlManager.frontDomain) && (
|
||||
<StyledDomain>.{urlManager.frontDomain}</StyledDomain>
|
||||
)}
|
||||
</StyledDomainFromWrapper>
|
||||
)}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
@ -25,6 +25,7 @@ import { ObjectMetadataItemsProvider } from '@/object-metadata/components/Object
|
||||
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||
import { IconsProvider } from 'twenty-ui';
|
||||
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
|
||||
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||
|
||||
export type PageDecoratorArgs = {
|
||||
routePath: string;
|
||||
@ -72,6 +73,7 @@ const Providers = () => {
|
||||
<ClientConfigProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<UserProviderEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<UserProvider>
|
||||
<ApolloMetadataClientMockedProvider>
|
||||
<ObjectMetadataItemsProvider>
|
||||
|
@ -23,6 +23,7 @@ import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/gen
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
import { mockedRemoteServers } from './mock-data/remote-servers';
|
||||
import { mockedViewFieldsData } from './mock-data/view-fields';
|
||||
import { GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataBySubdomain';
|
||||
|
||||
const peopleMock = getPeopleMock();
|
||||
const companiesMock = getCompaniesMock();
|
||||
@ -41,6 +42,28 @@ export const graphqlMocks = {
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query(
|
||||
getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN) ?? '',
|
||||
() => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getPublicWorkspaceDataBySubdomain: {
|
||||
id: 'id',
|
||||
logo: 'logo',
|
||||
displayName: 'displayName',
|
||||
subdomain: 'subdomain',
|
||||
authProviders: {
|
||||
google: true,
|
||||
microsoft: false,
|
||||
password: true,
|
||||
magicLink: false,
|
||||
sso: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
graphql.mutation(getOperationName(TRACK) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
|
@ -3,18 +3,13 @@ import { CaptchaDriverType } from '~/generated/graphql';
|
||||
|
||||
export const mockedClientConfig: ClientConfig = {
|
||||
signInPrefilled: true,
|
||||
signUpDisabled: false,
|
||||
isMultiWorkspaceEnabled: false,
|
||||
isSSOEnabled: false,
|
||||
frontDomain: 'localhost',
|
||||
defaultSubdomain: 'app',
|
||||
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
||||
debugMode: false,
|
||||
analyticsEnabled: true,
|
||||
authProviders: {
|
||||
sso: false,
|
||||
google: true,
|
||||
password: true,
|
||||
magicLink: false,
|
||||
microsoft: false,
|
||||
__typename: 'AuthProviders',
|
||||
},
|
||||
support: {
|
||||
supportDriver: 'front',
|
||||
supportFrontChatId: null,
|
||||
|
@ -36,6 +36,7 @@ export const workspaceLogoUrl =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||
|
||||
export const mockDefaultWorkspace: Workspace = {
|
||||
subdomain: 'acme.twenty.com',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w',
|
||||
displayName: 'Twenty',
|
||||
domainName: 'twenty.com',
|
||||
@ -45,6 +46,9 @@ export const mockDefaultWorkspace: Workspace = {
|
||||
allowImpersonation: true,
|
||||
activationStatus: WorkspaceActivationStatus.Active,
|
||||
hasValidEntrepriseKey: false,
|
||||
isGoogleAuthEnabled: true,
|
||||
isPasswordAuthEnabled: true,
|
||||
isMicrosoftAuthEnabled: false,
|
||||
featureFlags: [
|
||||
{
|
||||
id: '1492de61-5018-4368-8923-4f1eeaf988c4',
|
||||
|
@ -16,9 +16,9 @@ class CookieStorage {
|
||||
Cookies.set(key, value, attributes);
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
removeItem(key: string, attributes?: Cookies.CookieAttributes): void {
|
||||
this.keys.delete(key);
|
||||
Cookies.remove(key);
|
||||
Cookies.remove(key, attributes);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AtomEffect } from 'recoil';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
|
||||
@ -20,25 +21,50 @@ export const localStorageEffect =
|
||||
};
|
||||
|
||||
export const cookieStorageEffect =
|
||||
<T>(key: string): AtomEffect<T | null> =>
|
||||
<T>(
|
||||
key: string,
|
||||
attributes?: Cookies.CookieAttributes,
|
||||
hooks?: {
|
||||
validateInitFn?: (payload: T) => boolean;
|
||||
},
|
||||
): AtomEffect<T | null> =>
|
||||
({ setSelf, onSet }) => {
|
||||
const savedValue = cookieStorage.getItem(key);
|
||||
|
||||
if (
|
||||
isDefined(savedValue) &&
|
||||
isDefined(JSON.parse(savedValue)['accessToken'])
|
||||
(!isDefined(hooks?.validateInitFn) ||
|
||||
hooks.validateInitFn(JSON.parse(savedValue)))
|
||||
) {
|
||||
setSelf(JSON.parse(savedValue));
|
||||
}
|
||||
|
||||
const defaultAttributes = {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
...(attributes ?? {}),
|
||||
};
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
if (!newValue) {
|
||||
cookieStorage.removeItem(key);
|
||||
cookieStorage.removeItem(key, defaultAttributes);
|
||||
return;
|
||||
}
|
||||
|
||||
const cookieAttributes = {
|
||||
...defaultAttributes,
|
||||
...(typeof newValue === 'object' &&
|
||||
'cookieAttributes' in newValue &&
|
||||
typeof newValue.cookieAttributes === 'object'
|
||||
? newValue.cookieAttributes
|
||||
: {}),
|
||||
};
|
||||
|
||||
isReset
|
||||
? cookieStorage.removeItem(key)
|
||||
: cookieStorage.setItem(key, JSON.stringify(newValue), {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
});
|
||||
? cookieStorage.removeItem(key, defaultAttributes)
|
||||
: cookieStorage.setItem(
|
||||
key,
|
||||
JSON.stringify(omit(newValue, ['cookieAttributes'])),
|
||||
cookieAttributes,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
||||
# IS_BILLING_ENABLED=false
|
||||
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
||||
# AUTH_PASSWORD_ENABLED=false
|
||||
# IS_SIGN_UP_DISABLED=false
|
||||
# IS_MULTIWORKSPACE_ENABLED=false
|
||||
# AUTH_MICROSOFT_ENABLED=false
|
||||
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
|
||||
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
|
||||
|
@ -0,0 +1,123 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository, In } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
|
||||
// For DX only
|
||||
type WorkspaceId = string;
|
||||
|
||||
type Subdomain = string;
|
||||
|
||||
@Command({
|
||||
name: 'feat-0.34:add-subdomain-to-workspace',
|
||||
description: 'Add a default subdomain to each workspace',
|
||||
})
|
||||
export class GenerateDefaultSubdomainCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
private generatePayloadForQuery({
|
||||
id,
|
||||
subdomain,
|
||||
domainName,
|
||||
displayName,
|
||||
}: Workspace) {
|
||||
const result = { id, subdomain };
|
||||
|
||||
if (domainName) {
|
||||
const subdomain = domainName.split('.')[0];
|
||||
|
||||
if (subdomain.length > 0) {
|
||||
result.subdomain = subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
if (!domainName && displayName) {
|
||||
const displayNameWords = displayName.match(/(\w| |\d)+/);
|
||||
|
||||
if (displayNameWords) {
|
||||
result.subdomain = displayNameWords
|
||||
.join('-')
|
||||
.replace(/ /g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private groupBySubdomainName(
|
||||
acc: Record<Subdomain, Array<WorkspaceId>>,
|
||||
workspace: Workspace,
|
||||
) {
|
||||
const payload = this.generatePayloadForQuery(workspace);
|
||||
|
||||
acc[payload.subdomain] = acc[payload.subdomain]
|
||||
? acc[payload.subdomain].concat([payload.id])
|
||||
: [payload.id];
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
private async deduplicateAndSave(
|
||||
subdomain: Subdomain,
|
||||
workspaceIds: Array<WorkspaceId>,
|
||||
options: BaseCommandOptions,
|
||||
) {
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
const subdomainDeduplicated =
|
||||
index === 0 ? subdomain : `${subdomain}-${index}`;
|
||||
|
||||
this.logger.log(
|
||||
`Updating workspace ${workspaceId} with subdomain ${subdomainDeduplicated}`,
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
subdomain: subdomainDeduplicated,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
activeWorkspaceIds: string[],
|
||||
): Promise<void> {
|
||||
const workspaces = await this.workspaceRepository.find(
|
||||
activeWorkspaceIds.length > 0
|
||||
? {
|
||||
where: {
|
||||
id: In(activeWorkspaceIds),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
this.logger.log('No workspaces found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceBySubdomain = Object.entries(
|
||||
workspaces.reduce(
|
||||
(acc, workspace) => this.groupBySubdomainName(acc, workspace),
|
||||
{} as ReturnType<typeof this.groupBySubdomainName>,
|
||||
),
|
||||
);
|
||||
|
||||
for (const [subdomain, workspaceIds] of workspaceBySubdomain) {
|
||||
await this.deduplicateAndSave(subdomain, workspaceIds, options);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GenerateDefaultSubdomainCommand } from 'src/database/commands/upgrade-version/0-34/0-34-generate-subdomain.command';
|
||||
|
||||
interface UpdateTo0_34CommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.34',
|
||||
description: 'Upgrade to 0.34',
|
||||
})
|
||||
export class UpgradeTo0_34Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly generateDefaultSubdomainCommand: GenerateDefaultSubdomainCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: UpdateTo0_34CommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
await this.generateDefaultSubdomainCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
||||
import { UpgradeTo0_34Command } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceSyncMetadataCommandsModule,
|
||||
SearchModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
providers: [UpgradeTo0_34Command],
|
||||
})
|
||||
export class UpgradeTo0_33CommandModule {}
|
@ -23,6 +23,7 @@ export const seedWorkspaces = async (
|
||||
| 'domainName'
|
||||
| 'inviteHash'
|
||||
| 'logo'
|
||||
| 'subdomain'
|
||||
| 'activationStatus'
|
||||
>;
|
||||
} = {
|
||||
@ -30,6 +31,7 @@ export const seedWorkspaces = async (
|
||||
id: workspaceId,
|
||||
displayName: 'Apple',
|
||||
domainName: 'apple.dev',
|
||||
subdomain: 'apple',
|
||||
inviteHash: 'apple.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
@ -38,6 +40,7 @@ export const seedWorkspaces = async (
|
||||
id: workspaceId,
|
||||
displayName: 'Acme',
|
||||
domainName: 'acme.dev',
|
||||
subdomain: 'acme',
|
||||
inviteHash: 'acme.dev-invite-hash',
|
||||
logo: 'https://logos-world.net/wp-content/uploads/2022/05/Acme-Logo-700x394.png',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
@ -51,6 +54,7 @@ export const seedWorkspaces = async (
|
||||
'id',
|
||||
'displayName',
|
||||
'domainName',
|
||||
'subdomain',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddSubdomainToWorkspace1730137590546
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddSubdomainToWorkspace1730137590546';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "subdomain" varchar NULL`,
|
||||
);
|
||||
await queryRunner.query(`UPDATE "core"."workspace" SET "subdomain" = "id"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "subdomain" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX workspace_subdomain_unique_index ON "core"."workspace" (subdomain)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "subdomain"`,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddAuthProvidersColumnsToWorkspace1730298416367
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddAuthProvidersColumnsToWorkspace1730298416367';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "isMicrosoftAuthEnabled" BOOLEAN DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "isGoogleAuthEnabled" BOOLEAN DEFAULT true`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ADD "isPasswordAuthEnabled" BOOLEAN DEFAULT true`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "isMicrosoftAuthEnabled"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "isGoogleAuthEnabled"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" DROP COLUMN "isPasswordAuthEnabled"`,
|
||||
);
|
||||
}
|
||||
}
|
@ -19,9 +19,9 @@ import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Injectable()
|
||||
export class CoreQueryBuilderFactory {
|
||||
@ -40,7 +40,7 @@ export class CoreQueryBuilderFactory {
|
||||
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
async getObjectMetadata(
|
||||
@ -50,16 +50,20 @@ export class CoreQueryBuilderFactory {
|
||||
objectMetadataItems: ObjectMetadataEntity[];
|
||||
objectMetadataItem: ObjectMetadataEntity;
|
||||
}> {
|
||||
const { workspace } = await this.accessTokenService.validateToken(request);
|
||||
const { workspace } =
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
|
||||
const objectMetadataItems =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
||||
|
||||
if (!objectMetadataItems.length) {
|
||||
throw new BadRequestException(
|
||||
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.environmentService.get(
|
||||
'FRONT_BASE_URL',
|
||||
)}/settings/developers`,
|
||||
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: '/settings/developers',
|
||||
})
|
||||
.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,10 @@ import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/
|
||||
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [ObjectMetadataModule, AuthModule],
|
||||
imports: [ObjectMetadataModule, AuthModule, DomainManagerModule],
|
||||
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
||||
exports: [CoreQueryBuilderFactory],
|
||||
})
|
||||
|
@ -18,7 +18,7 @@ export class RestApiMetadataService {
|
||||
) {}
|
||||
|
||||
async get(request: Request) {
|
||||
await this.accessTokenService.validateToken(request);
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
const data = await this.metadataQueryBuilderFactory.get(request);
|
||||
|
||||
return await this.restApiService.call(
|
||||
@ -29,7 +29,7 @@ export class RestApiMetadataService {
|
||||
}
|
||||
|
||||
async create(request: Request) {
|
||||
await this.accessTokenService.validateToken(request);
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
const data = await this.metadataQueryBuilderFactory.create(request);
|
||||
|
||||
return await this.restApiService.call(
|
||||
@ -40,7 +40,7 @@ export class RestApiMetadataService {
|
||||
}
|
||||
|
||||
async update(request: Request) {
|
||||
await this.accessTokenService.validateToken(request);
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
const data = await this.metadataQueryBuilderFactory.update(request);
|
||||
|
||||
return await this.restApiService.call(
|
||||
@ -51,7 +51,7 @@ export class RestApiMetadataService {
|
||||
}
|
||||
|
||||
async delete(request: Request) {
|
||||
await this.accessTokenService.validateToken(request);
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
const data = await this.metadataQueryBuilderFactory.delete(request);
|
||||
|
||||
return await this.restApiService.call(
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
@ -11,7 +11,6 @@ import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/g
|
||||
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
|
||||
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
|
||||
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
|
||||
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
|
||||
@ -24,7 +23,6 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
@ -36,13 +34,15 @@ import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/worksp
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -54,7 +54,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
JwtModule,
|
||||
FileUploadModule,
|
||||
DataSourceModule,
|
||||
forwardRef(() => UserModule),
|
||||
DomainManagerModule,
|
||||
TokenModule,
|
||||
UserModule,
|
||||
WorkspaceManagerModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature(
|
||||
@ -69,22 +71,20 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
'core',
|
||||
),
|
||||
HttpModule,
|
||||
TokenModule,
|
||||
UserWorkspaceModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceInvitationModule,
|
||||
ConnectedAccountModule,
|
||||
WorkspaceSSOModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceInvitationModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
MicrosoftAuthController,
|
||||
GoogleAPIsAuthController,
|
||||
MicrosoftAPIsAuthController,
|
||||
VerifyAuthController,
|
||||
SSOAuthController,
|
||||
],
|
||||
providers: [
|
||||
|
@ -7,6 +7,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -43,6 +44,14 @@ describe('AuthResolver', () => {
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
buildWorkspaceURL: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new URL('http://localhost:3001')),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {},
|
||||
|
@ -9,12 +9,6 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p
|
||||
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
|
||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||
import { GenerateJwtInput } from 'src/engine/core-modules/auth/dto/generate-jwt.input';
|
||||
import {
|
||||
GenerateJWTOutput,
|
||||
GenerateJWTOutputWithAuthTokens,
|
||||
GenerateJWTOutputWithSSOAUTH,
|
||||
} from 'src/engine/core-modules/auth/dto/generateJWT.output';
|
||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||
import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token.entity';
|
||||
import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input';
|
||||
@ -36,12 +30,22 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SwitchWorkspaceInput } from 'src/engine/core-modules/auth/dto/switch-workspace.input';
|
||||
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { ChallengeInput } from './dto/challenge.input';
|
||||
import { LoginToken } from './dto/login-token.entity';
|
||||
import { SignUpInput } from './dto/sign-up.input';
|
||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||
import { UserExists } from './dto/user-exists.entity';
|
||||
import { UserExistsOutput } from './dto/user-exists.entity';
|
||||
import { CheckUserExistsInput } from './dto/user-exists.input';
|
||||
import { Verify } from './dto/verify.entity';
|
||||
import { VerifyInput } from './dto/verify.input';
|
||||
@ -62,18 +66,15 @@ export class AuthResolver {
|
||||
private switchWorkspaceService: SwitchWorkspaceService,
|
||||
private transientTokenService: TransientTokenService,
|
||||
private oauthService: OAuthService,
|
||||
private domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Query(() => UserExists)
|
||||
@Query(() => UserExistsOutput)
|
||||
async checkUserExists(
|
||||
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||
): Promise<UserExists> {
|
||||
const { exists } = await this.authService.checkUserExists(
|
||||
checkUserExistsInput.email,
|
||||
);
|
||||
|
||||
return { exists };
|
||||
): Promise<typeof UserExistsOutput> {
|
||||
return await this.authService.checkUserExists(checkUserExistsInput.email);
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceInviteHashValid)
|
||||
@ -96,8 +97,20 @@ export class AuthResolver {
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||
const user = await this.authService.challenge(challengeInput);
|
||||
async challenge(
|
||||
@Args() challengeInput: ChallengeInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const user = await this.authService.challenge(challengeInput, workspace);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
@ -107,10 +120,22 @@ export class AuthResolver {
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
async signUp(@Args() signUpInput: SignUpInput): Promise<LoginToken> {
|
||||
async signUp(
|
||||
@Args() signUpInput: SignUpInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const user = await this.authService.signInUp({
|
||||
...signUpInput,
|
||||
targetWorkspaceSubdomain:
|
||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||
fromSSO: false,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'password',
|
||||
new AuthException(
|
||||
'Password auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
@ -124,11 +149,9 @@ export class AuthResolver {
|
||||
async exchangeAuthorizationCode(
|
||||
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
) {
|
||||
const tokens = await this.oauthService.verifyAuthorizationCode(
|
||||
return await this.oauthService.verifyAuthorizationCode(
|
||||
exchangeAuthCodeInput,
|
||||
);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Mutation(() => TransientToken)
|
||||
@ -156,14 +179,18 @@ export class AuthResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Verify)
|
||||
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.loginTokenService.verifyLoginToken(
|
||||
async verify(
|
||||
@Args() verifyInput: VerifyInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<Verify> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
|
||||
const { sub: email } = await this.loginTokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
return await this.authService.verify(email, workspace?.id);
|
||||
}
|
||||
|
||||
@Mutation(() => AuthorizeApp)
|
||||
@ -172,50 +199,22 @@ export class AuthResolver {
|
||||
@Args() authorizeAppInput: AuthorizeAppInput,
|
||||
@AuthUser() user: User,
|
||||
): Promise<AuthorizeApp> {
|
||||
const authorizedApp = await this.authService.generateAuthorizationCode(
|
||||
return await this.authService.generateAuthorizationCode(
|
||||
authorizeAppInput,
|
||||
user,
|
||||
);
|
||||
|
||||
return authorizedApp;
|
||||
}
|
||||
|
||||
@Mutation(() => GenerateJWTOutput)
|
||||
@Mutation(() => PublicWorkspaceDataOutput)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
async generateJWT(
|
||||
async switchWorkspace(
|
||||
@AuthUser() user: User,
|
||||
@Args() args: GenerateJwtInput,
|
||||
): Promise<GenerateJWTOutputWithAuthTokens | GenerateJWTOutputWithSSOAUTH> {
|
||||
const result = await this.switchWorkspaceService.switchWorkspace(
|
||||
@Args() args: SwitchWorkspaceInput,
|
||||
): Promise<PublicWorkspaceDataOutput> {
|
||||
return await this.switchWorkspaceService.switchWorkspace(
|
||||
user,
|
||||
args.workspaceId,
|
||||
);
|
||||
|
||||
if (result.useSSOAuth) {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'WORKSPACE_USE_SSO_AUTH',
|
||||
availableSSOIDPs: result.availableSSOIdentityProviders.map(
|
||||
(identityProvider) => ({
|
||||
...identityProvider,
|
||||
workspace: {
|
||||
id: result.workspace.id,
|
||||
displayName: result.workspace.displayName,
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH',
|
||||
authTokens:
|
||||
await this.switchWorkspaceService.generateSwitchWorkspaceToken(
|
||||
user,
|
||||
result.workspace,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => AuthTokens)
|
||||
@ -278,4 +277,11 @@ export class AuthResolver {
|
||||
args.passwordResetToken,
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => [AvailableWorkspaceOutput])
|
||||
async findAvailableWorkspacesByEmail(
|
||||
@Args('email') email: string,
|
||||
): Promise<AvailableWorkspaceOutput[]> {
|
||||
return this.authService.findAvailableWorkspacesByEmail(email);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -21,6 +23,8 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -30,6 +34,9 @@ export class GoogleAPIsAuthController {
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -96,10 +103,24 @@ export class GoogleAPIsAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
||||
redirectLocation || '/settings/accounts'
|
||||
}`,
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
@ -16,6 +18,14 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Controller('auth/google')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -23,6 +33,10 @@ export class GoogleAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -36,29 +50,81 @@ export class GoogleAuthController {
|
||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||
@UseFilters(AuthOAuthExceptionFilter)
|
||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
} = req.user;
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
} = req.user;
|
||||
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
fromSSO: true,
|
||||
});
|
||||
const signInUpParams = {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
fromSSO: true,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'google',
|
||||
new AuthException(
|
||||
'Google auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
targetWorkspaceSubdomain ===
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN')
|
||||
) {
|
||||
const workspaceWithGoogleAuthActive =
|
||||
await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
isGoogleAuthEnabled: true,
|
||||
workspaceUsers: {
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
||||
});
|
||||
|
||||
return res.redirect(this.authService.computeRedirectURI(loginToken.token));
|
||||
if (workspaceWithGoogleAuthActive) {
|
||||
signInUpParams.targetWorkspaceSubdomain =
|
||||
workspaceWithGoogleAuthActive.subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.authService.signInUp(signInUpParams);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -21,6 +23,9 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
|
||||
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/microsoft-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -29,7 +34,11 @@ export class MicrosoftAPIsAuthController {
|
||||
private readonly microsoftAPIsService: MicrosoftAPIsService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -96,10 +105,24 @@ export class MicrosoftAPIsAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
||||
redirectLocation || '/settings/accounts'
|
||||
}`,
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,13 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -22,6 +29,8 @@ export class MicrosoftAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -37,29 +46,55 @@ export class MicrosoftAuthController {
|
||||
@Req() req: MicrosoftRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
} = req.user;
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
} = req.user;
|
||||
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
fromSSO: true,
|
||||
});
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
fromSSO: true,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'microsoft',
|
||||
new AuthException(
|
||||
'Microsoft auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return res.redirect(this.authService.computeRedirectURI(loginToken.token));
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,14 @@ import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.gua
|
||||
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Controller('auth')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -40,9 +40,9 @@ export class SSOAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly ssoService: SSOService,
|
||||
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||
@ -50,7 +50,7 @@ export class SSOAuthController {
|
||||
|
||||
@Get('saml/metadata/:identityProviderId')
|
||||
@UseGuards(SSOProviderEnabledGuard)
|
||||
async generateMetadata(@Req() req: any): Promise<string> {
|
||||
async generateMetadata(@Req() req: any): Promise<string | void> {
|
||||
return generateServiceProviderMetadata({
|
||||
wantAssertionsSigned: false,
|
||||
issuer: this.ssoService.buildIssuerURL({
|
||||
@ -81,14 +81,26 @@ export class SSOAuthController {
|
||||
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||
try {
|
||||
const loginToken = await this.generateLoginToken(req.user);
|
||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||
req.user,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(loginToken.token),
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
// TODO: improve error management
|
||||
res.status(403).send(err.message);
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,16 +108,26 @@ export class SSOAuthController {
|
||||
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||
try {
|
||||
const loginToken = await this.generateLoginToken(req.user);
|
||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||
req.user,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(loginToken.token),
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
// TODO: improve error management
|
||||
res
|
||||
.status(403)
|
||||
.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`);
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +138,13 @@ export class SSOAuthController {
|
||||
identityProviderId?: string;
|
||||
user: { email: string } & Record<string, string>;
|
||||
}) {
|
||||
if (!identityProviderId) {
|
||||
throw new AuthException(
|
||||
'Identity provider ID is required',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const identityProvider =
|
||||
await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||
where: { id: identityProviderId },
|
||||
@ -129,20 +158,15 @@ export class SSOAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
const invitation =
|
||||
await this.workspaceInvitationService.getOneWorkspaceInvitation(
|
||||
identityProvider.workspaceId,
|
||||
user.email,
|
||||
);
|
||||
|
||||
if (invitation) {
|
||||
await this.authService.signInUp({
|
||||
...user,
|
||||
workspacePersonalInviteToken: invitation.value,
|
||||
workspaceInviteHash: identityProvider.workspace.inviteHash,
|
||||
fromSSO: true,
|
||||
});
|
||||
}
|
||||
await this.authService.signInUp({
|
||||
...user,
|
||||
...(this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||
? {
|
||||
targetWorkspaceSubdomain: identityProvider.workspace.subdomain,
|
||||
}
|
||||
: {}),
|
||||
fromSSO: true,
|
||||
});
|
||||
|
||||
const isUserExistInWorkspace =
|
||||
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
||||
@ -157,6 +181,9 @@ export class SSOAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
return this.loginTokenService.generateLoginToken(user.email);
|
||||
return {
|
||||
identityProvider,
|
||||
loginToken: await this.loginTokenService.generateLoginToken(user.email),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
import { VerifyAuthController } from './verify-auth.controller';
|
||||
|
||||
describe('VerifyAuthController', () => {
|
||||
let controller: VerifyAuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [VerifyAuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<VerifyAuthController>(VerifyAuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
import { Body, Controller, Post, UseFilters } from '@nestjs/common';
|
||||
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
@Controller('auth/verify')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
export class VerifyAuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.loginTokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
SSOIdentityProviderStatus,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
|
||||
@ObjectType()
|
||||
class SSOConnection {
|
||||
@Field(() => IdentityProviderType)
|
||||
type: SSOConfiguration['type'];
|
||||
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
issuer: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => SSOIdentityProviderStatus)
|
||||
status: SSOConfiguration['status'];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AvailableWorkspaceOutput {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
displayName?: string;
|
||||
|
||||
@Field(() => String)
|
||||
subdomain: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@Field(() => [SSOConnection])
|
||||
sso: SSOConnection[];
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
|
||||
|
||||
@ObjectType()
|
||||
export class GenerateJWTOutputWithAuthTokens {
|
||||
@Field(() => Boolean)
|
||||
success: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH';
|
||||
|
||||
@Field(() => AuthTokens)
|
||||
authTokens: AuthTokens;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class GenerateJWTOutputWithSSOAUTH {
|
||||
@Field(() => Boolean)
|
||||
success: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
reason: 'WORKSPACE_USE_SSO_AUTH';
|
||||
|
||||
@Field(() => [FindAvailableSSOIDPOutput])
|
||||
availableSSOIDPs: Array<FindAvailableSSOIDPOutput>;
|
||||
}
|
||||
|
||||
export const GenerateJWTOutput = createUnionType({
|
||||
name: 'GenerateJWT',
|
||||
types: () => [GenerateJWTOutputWithAuthTokens, GenerateJWTOutputWithSSOAUTH],
|
||||
resolveType(value) {
|
||||
if (value.reason === 'WORKSPACE_AVAILABLE_FOR_SWITCH') {
|
||||
return GenerateJWTOutputWithAuthTokens;
|
||||
}
|
||||
if (value.reason === 'WORKSPACE_USE_SSO_AUTH') {
|
||||
return GenerateJWTOutputWithSSOAUTH;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class GenerateJwtInput {
|
||||
export class SwitchWorkspaceInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
@ -1,7 +1,30 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
|
||||
@ObjectType()
|
||||
export class UserExists {
|
||||
@Field(() => Boolean)
|
||||
exists: boolean;
|
||||
exists: true;
|
||||
|
||||
@Field(() => [AvailableWorkspaceOutput])
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class UserNotExists {
|
||||
@Field(() => Boolean)
|
||||
exists: false;
|
||||
}
|
||||
|
||||
export const UserExistsOutput = createUnionType({
|
||||
name: 'UserExistsOutput',
|
||||
types: () => [UserExists, UserNotExists] as const,
|
||||
resolveType(value) {
|
||||
if (value.exists === true) {
|
||||
return UserExists;
|
||||
}
|
||||
|
||||
return UserNotExists;
|
||||
},
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user