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:
@ -4,7 +4,6 @@ import path from 'path';
|
|||||||
export const envVariables = (variables: string) => {
|
export const envVariables = (variables: string) => {
|
||||||
let payload = `
|
let payload = `
|
||||||
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
|
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
|
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
||||||
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
|
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
|
||||||
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
||||||
|
|||||||
@ -32,6 +32,12 @@ export type ActivateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActivateWorkspaceOutput = {
|
||||||
|
__typename?: 'ActivateWorkspaceOutput';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
__typename?: 'Analytics';
|
__typename?: 'Analytics';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -81,7 +87,7 @@ export type AuthProviders = {
|
|||||||
magicLink: Scalars['Boolean']['output'];
|
magicLink: Scalars['Boolean']['output'];
|
||||||
microsoft: Scalars['Boolean']['output'];
|
microsoft: Scalars['Boolean']['output'];
|
||||||
password: Scalars['Boolean']['output'];
|
password: Scalars['Boolean']['output'];
|
||||||
sso: Scalars['Boolean']['output'];
|
sso: Array<SsoIdentityProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthToken = {
|
export type AuthToken = {
|
||||||
@ -106,6 +112,15 @@ export type AuthorizeApp = {
|
|||||||
redirectUrl: Scalars['String']['output'];
|
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 = {
|
export type Billing = {
|
||||||
__typename?: 'Billing';
|
__typename?: 'Billing';
|
||||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
||||||
@ -161,14 +176,16 @@ export type ClientConfig = {
|
|||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
analyticsEnabled: Scalars['Boolean']['output'];
|
analyticsEnabled: Scalars['Boolean']['output'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
authProviders: AuthProviders;
|
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
captcha: Captcha;
|
captcha: Captcha;
|
||||||
chromeExtensionId?: Maybe<Scalars['String']['output']>;
|
chromeExtensionId?: Maybe<Scalars['String']['output']>;
|
||||||
debugMode: Scalars['Boolean']['output'];
|
debugMode: Scalars['Boolean']['output'];
|
||||||
|
defaultSubdomain?: Maybe<Scalars['String']['output']>;
|
||||||
|
frontDomain: Scalars['String']['output'];
|
||||||
|
isMultiWorkspaceEnabled: Scalars['Boolean']['output'];
|
||||||
|
isSSOEnabled: Scalars['Boolean']['output'];
|
||||||
sentry: Sentry;
|
sentry: Sentry;
|
||||||
signInPrefilled: Scalars['Boolean']['output'];
|
signInPrefilled: Scalars['Boolean']['output'];
|
||||||
signUpDisabled: Scalars['Boolean']['output'];
|
|
||||||
support: Support;
|
support: Support;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -332,7 +349,7 @@ export type EditSsoOutput = {
|
|||||||
issuer: Scalars['String']['output'];
|
issuer: Scalars['String']['output'];
|
||||||
name: Scalars['String']['output'];
|
name: Scalars['String']['output'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
@ -424,17 +441,13 @@ export enum FileFolder {
|
|||||||
WorkspaceLogo = 'WorkspaceLogo'
|
WorkspaceLogo = 'WorkspaceLogo'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FindAvailableSsoidpInput = {
|
|
||||||
email: Scalars['String']['input'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FindAvailableSsoidpOutput = {
|
export type FindAvailableSsoidpOutput = {
|
||||||
__typename?: 'FindAvailableSSOIDPOutput';
|
__typename?: 'FindAvailableSSOIDPOutput';
|
||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
issuer: Scalars['String']['output'];
|
issuer: Scalars['String']['output'];
|
||||||
name: Scalars['String']['output'];
|
name: Scalars['String']['output'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
workspace: WorkspaceNameAndId;
|
workspace: WorkspaceNameAndId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -451,22 +464,6 @@ export type FullName = {
|
|||||||
lastName: Scalars['String']['output'];
|
lastName: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
|
||||||
|
|
||||||
export type GenerateJwtOutputWithAuthTokens = {
|
|
||||||
__typename?: 'GenerateJWTOutputWithAuthTokens';
|
|
||||||
authTokens: AuthTokens;
|
|
||||||
reason: Scalars['String']['output'];
|
|
||||||
success: Scalars['Boolean']['output'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GenerateJwtOutputWithSsoauth = {
|
|
||||||
__typename?: 'GenerateJWTOutputWithSSOAUTH';
|
|
||||||
availableSSOIDPs: Array<FindAvailableSsoidpOutput>;
|
|
||||||
reason: Scalars['String']['output'];
|
|
||||||
success: Scalars['Boolean']['output'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAuthorizationUrlInput = {
|
export type GetAuthorizationUrlInput = {
|
||||||
identityProviderId: Scalars['String']['input'];
|
identityProviderId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@ -485,7 +482,7 @@ export type GetServerlessFunctionSourceCodeInput = {
|
|||||||
version?: Scalars['String']['input'];
|
version?: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum IdpType {
|
export enum IdentityProviderType {
|
||||||
Oidc = 'OIDC',
|
Oidc = 'OIDC',
|
||||||
Saml = 'SAML'
|
Saml = 'SAML'
|
||||||
}
|
}
|
||||||
@ -553,7 +550,7 @@ export enum MessageChannelVisibility {
|
|||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkflowVersion: Scalars['Boolean']['output'];
|
activateWorkflowVersion: Scalars['Boolean']['output'];
|
||||||
activateWorkspace: Workspace;
|
activateWorkspace: ActivateWorkspaceOutput;
|
||||||
addUserToWorkspace: User;
|
addUserToWorkspace: User;
|
||||||
addUserToWorkspaceByInviteToken: User;
|
addUserToWorkspaceByInviteToken: User;
|
||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
@ -578,7 +575,7 @@ export type Mutation = {
|
|||||||
deleteOneServerlessFunction: ServerlessFunction;
|
deleteOneServerlessFunction: ServerlessFunction;
|
||||||
deleteSSOIdentityProvider: DeleteSsoOutput;
|
deleteSSOIdentityProvider: DeleteSsoOutput;
|
||||||
deleteUser: User;
|
deleteUser: User;
|
||||||
deleteWorkflowVersionStep: Scalars['Boolean']['output'];
|
deleteWorkflowVersionStep: WorkflowAction;
|
||||||
deleteWorkspaceInvitation: Scalars['String']['output'];
|
deleteWorkspaceInvitation: Scalars['String']['output'];
|
||||||
disablePostgresProxy: PostgresCredentials;
|
disablePostgresProxy: PostgresCredentials;
|
||||||
editSSOIdentityProvider: EditSsoOutput;
|
editSSOIdentityProvider: EditSsoOutput;
|
||||||
@ -586,9 +583,7 @@ export type Mutation = {
|
|||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateJWT: GenerateJwt;
|
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
@ -599,6 +594,7 @@ export type Mutation = {
|
|||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signUp: LoginToken;
|
signUp: LoginToken;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
|
switchWorkspace: PublicWorkspaceDataOutput;
|
||||||
syncRemoteTable: RemoteTable;
|
syncRemoteTable: RemoteTable;
|
||||||
syncRemoteTableSchemaChanges: RemoteTable;
|
syncRemoteTableSchemaChanges: RemoteTable;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
@ -609,7 +605,7 @@ export type Mutation = {
|
|||||||
updateOneRemoteServer: RemoteServer;
|
updateOneRemoteServer: RemoteServer;
|
||||||
updateOneServerlessFunction: ServerlessFunction;
|
updateOneServerlessFunction: ServerlessFunction;
|
||||||
updatePasswordViaResetToken: InvalidatePassword;
|
updatePasswordViaResetToken: InvalidatePassword;
|
||||||
updateWorkflowVersionStep: Scalars['Boolean']['output'];
|
updateWorkflowVersionStep: WorkflowAction;
|
||||||
updateWorkspace: Workspace;
|
updateWorkspace: Workspace;
|
||||||
updateWorkspaceFeatureFlag: Scalars['Boolean']['output'];
|
updateWorkspaceFeatureFlag: Scalars['Boolean']['output'];
|
||||||
uploadFile: Scalars['String']['output'];
|
uploadFile: Scalars['String']['output'];
|
||||||
@ -778,22 +774,12 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
|
||||||
input: FindAvailableSsoidpInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateApiKeyTokenArgs = {
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
apiKeyId: Scalars['String']['input'];
|
apiKeyId: Scalars['String']['input'];
|
||||||
expiresAt: Scalars['String']['input'];
|
expiresAt: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateJwtArgs = {
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationGetAuthorizationUrlArgs = {
|
export type MutationGetAuthorizationUrlArgs = {
|
||||||
input: GetAuthorizationUrlInput;
|
input: GetAuthorizationUrlInput;
|
||||||
};
|
};
|
||||||
@ -838,6 +824,11 @@ export type MutationSignUpArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSwitchWorkspaceArgs = {
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSyncRemoteTableArgs = {
|
export type MutationSyncRemoteTableArgs = {
|
||||||
input: RemoteTableInput;
|
input: RemoteTableInput;
|
||||||
};
|
};
|
||||||
@ -1007,6 +998,15 @@ export type ProductPricesEntity = {
|
|||||||
totalNumberOfPrices: Scalars['Int']['output'];
|
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 = {
|
export type PublishServerlessFunctionInput = {
|
||||||
/** The id of the function. */
|
/** The id of the function. */
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
@ -1015,13 +1015,14 @@ export type PublishServerlessFunctionInput = {
|
|||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
billingPortalSession: SessionEntity;
|
billingPortalSession: SessionEntity;
|
||||||
checkUserExists: UserExists;
|
checkUserExists: UserExistsOutput;
|
||||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||||
clientConfig: ClientConfig;
|
clientConfig: ClientConfig;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
field: Field;
|
field: Field;
|
||||||
fields: FieldConnection;
|
fields: FieldConnection;
|
||||||
|
findAvailableWorkspacesByEmail: Array<AvailableWorkspaceOutput>;
|
||||||
findDistantTablesWithStatus: Array<RemoteTable>;
|
findDistantTablesWithStatus: Array<RemoteTable>;
|
||||||
findManyRemoteServersByType: Array<RemoteServer>;
|
findManyRemoteServersByType: Array<RemoteServer>;
|
||||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||||
@ -1032,6 +1033,7 @@ export type Query = {
|
|||||||
getAvailablePackages: Scalars['JSON']['output'];
|
getAvailablePackages: Scalars['JSON']['output'];
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
getProductPrices: ProductPricesEntity;
|
getProductPrices: ProductPricesEntity;
|
||||||
|
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
|
||||||
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>;
|
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>;
|
||||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||||
@ -1075,6 +1077,11 @@ export type QueryFieldsArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryFindAvailableWorkspacesByEmailArgs = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFindDistantTablesWithStatusArgs = {
|
export type QueryFindDistantTablesWithStatusArgs = {
|
||||||
input: FindManyRemoteTablesInput;
|
input: FindManyRemoteTablesInput;
|
||||||
};
|
};
|
||||||
@ -1262,6 +1269,24 @@ export type RunWorkflowVersionInput = {
|
|||||||
workflowVersionId: Scalars['String']['input'];
|
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 {
|
export enum SsoIdentityProviderStatus {
|
||||||
Active = 'Active',
|
Active = 'Active',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
@ -1353,7 +1378,7 @@ export type SetupSsoOutput = {
|
|||||||
issuer: Scalars['String']['output'];
|
issuer: Scalars['String']['output'];
|
||||||
name: Scalars['String']['output'];
|
name: Scalars['String']['output'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Sort Directions */
|
/** Sort Directions */
|
||||||
@ -1555,8 +1580,12 @@ export type UpdateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||||
domainName?: InputMaybe<Scalars['String']['input']>;
|
domainName?: InputMaybe<Scalars['String']['input']>;
|
||||||
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
inviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
isMicrosoftAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
isPasswordAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
logo?: InputMaybe<Scalars['String']['input']>;
|
logo?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
subdomain?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@ -1594,9 +1623,12 @@ export type UserEdge = {
|
|||||||
|
|
||||||
export type UserExists = {
|
export type UserExists = {
|
||||||
__typename?: 'UserExists';
|
__typename?: 'UserExists';
|
||||||
|
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||||
exists: Scalars['Boolean']['output'];
|
exists: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserExistsOutput = UserExists | UserNotExists;
|
||||||
|
|
||||||
export type UserInfo = {
|
export type UserInfo = {
|
||||||
__typename?: 'UserInfo';
|
__typename?: 'UserInfo';
|
||||||
email: Scalars['String']['output'];
|
email: Scalars['String']['output'];
|
||||||
@ -1626,6 +1658,11 @@ export type UserMappingOptionsUser = {
|
|||||||
user?: Maybe<Scalars['String']['output']>;
|
user?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserNotExists = {
|
||||||
|
__typename?: 'UserNotExists';
|
||||||
|
exists: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UserWorkspace = {
|
export type UserWorkspace = {
|
||||||
__typename?: 'UserWorkspace';
|
__typename?: 'UserWorkspace';
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
@ -1653,6 +1690,10 @@ export type Verify = {
|
|||||||
export type WorkflowAction = {
|
export type WorkflowAction = {
|
||||||
__typename?: 'WorkflowAction';
|
__typename?: 'WorkflowAction';
|
||||||
id: Scalars['UUID']['output'];
|
id: Scalars['UUID']['output'];
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
settings: Scalars['JSON']['output'];
|
||||||
|
type: Scalars['String']['output'];
|
||||||
|
valid: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowRun = {
|
export type WorkflowRun = {
|
||||||
@ -1677,9 +1718,13 @@ export type Workspace = {
|
|||||||
hasValidEntrepriseKey: Scalars['Boolean']['output'];
|
hasValidEntrepriseKey: Scalars['Boolean']['output'];
|
||||||
id: Scalars['UUID']['output'];
|
id: Scalars['UUID']['output'];
|
||||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||||
|
isGoogleAuthEnabled: Scalars['Boolean']['output'];
|
||||||
|
isMicrosoftAuthEnabled: Scalars['Boolean']['output'];
|
||||||
|
isPasswordAuthEnabled: Scalars['Boolean']['output'];
|
||||||
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
|
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
|
||||||
logo?: Maybe<Scalars['String']['output']>;
|
logo?: Maybe<Scalars['String']['output']>;
|
||||||
metadataVersion: Scalars['Float']['output'];
|
metadataVersion: Scalars['Float']['output'];
|
||||||
|
subdomain: Scalars['String']['output'];
|
||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
workspaceMembersCount?: Maybe<Scalars['Float']['output']>;
|
workspaceMembersCount?: Maybe<Scalars['Float']['output']>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as Apollo from '@apollo/client';
|
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
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']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActivateWorkspaceOutput = {
|
||||||
|
__typename?: 'ActivateWorkspaceOutput';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
__typename?: 'Analytics';
|
__typename?: 'Analytics';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -74,7 +80,7 @@ export type AuthProviders = {
|
|||||||
magicLink: Scalars['Boolean'];
|
magicLink: Scalars['Boolean'];
|
||||||
microsoft: Scalars['Boolean'];
|
microsoft: Scalars['Boolean'];
|
||||||
password: Scalars['Boolean'];
|
password: Scalars['Boolean'];
|
||||||
sso: Scalars['Boolean'];
|
sso: Array<SsoIdentityProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthToken = {
|
export type AuthToken = {
|
||||||
@ -99,6 +105,15 @@ export type AuthorizeApp = {
|
|||||||
redirectUrl: Scalars['String'];
|
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 = {
|
export type Billing = {
|
||||||
__typename?: 'Billing';
|
__typename?: 'Billing';
|
||||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']>;
|
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']>;
|
||||||
@ -154,14 +169,16 @@ export type ClientConfig = {
|
|||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
analyticsEnabled: Scalars['Boolean'];
|
analyticsEnabled: Scalars['Boolean'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
authProviders: AuthProviders;
|
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
captcha: Captcha;
|
captcha: Captcha;
|
||||||
chromeExtensionId?: Maybe<Scalars['String']>;
|
chromeExtensionId?: Maybe<Scalars['String']>;
|
||||||
debugMode: Scalars['Boolean'];
|
debugMode: Scalars['Boolean'];
|
||||||
|
defaultSubdomain?: Maybe<Scalars['String']>;
|
||||||
|
frontDomain: Scalars['String'];
|
||||||
|
isMultiWorkspaceEnabled: Scalars['Boolean'];
|
||||||
|
isSSOEnabled: Scalars['Boolean'];
|
||||||
sentry: Sentry;
|
sentry: Sentry;
|
||||||
signInPrefilled: Scalars['Boolean'];
|
signInPrefilled: Scalars['Boolean'];
|
||||||
signUpDisabled: Scalars['Boolean'];
|
|
||||||
support: Support;
|
support: Support;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -233,7 +250,7 @@ export type EditSsoOutput = {
|
|||||||
issuer: Scalars['String'];
|
issuer: Scalars['String'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
@ -325,17 +342,13 @@ export enum FileFolder {
|
|||||||
WorkspaceLogo = 'WorkspaceLogo'
|
WorkspaceLogo = 'WorkspaceLogo'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FindAvailableSsoidpInput = {
|
|
||||||
email: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FindAvailableSsoidpOutput = {
|
export type FindAvailableSsoidpOutput = {
|
||||||
__typename?: 'FindAvailableSSOIDPOutput';
|
__typename?: 'FindAvailableSSOIDPOutput';
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
issuer: Scalars['String'];
|
issuer: Scalars['String'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
workspace: WorkspaceNameAndId;
|
workspace: WorkspaceNameAndId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -345,22 +358,6 @@ export type FullName = {
|
|||||||
lastName: Scalars['String'];
|
lastName: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
|
||||||
|
|
||||||
export type GenerateJwtOutputWithAuthTokens = {
|
|
||||||
__typename?: 'GenerateJWTOutputWithAuthTokens';
|
|
||||||
authTokens: AuthTokens;
|
|
||||||
reason: Scalars['String'];
|
|
||||||
success: Scalars['Boolean'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GenerateJwtOutputWithSsoauth = {
|
|
||||||
__typename?: 'GenerateJWTOutputWithSSOAUTH';
|
|
||||||
availableSSOIDPs: Array<FindAvailableSsoidpOutput>;
|
|
||||||
reason: Scalars['String'];
|
|
||||||
success: Scalars['Boolean'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetAuthorizationUrlInput = {
|
export type GetAuthorizationUrlInput = {
|
||||||
identityProviderId: Scalars['String'];
|
identityProviderId: Scalars['String'];
|
||||||
};
|
};
|
||||||
@ -379,7 +376,7 @@ export type GetServerlessFunctionSourceCodeInput = {
|
|||||||
version?: Scalars['String'];
|
version?: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum IdpType {
|
export enum IdentityProviderType {
|
||||||
Oidc = 'OIDC',
|
Oidc = 'OIDC',
|
||||||
Saml = 'SAML'
|
Saml = 'SAML'
|
||||||
}
|
}
|
||||||
@ -447,7 +444,7 @@ export enum MessageChannelVisibility {
|
|||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkflowVersion: Scalars['Boolean'];
|
activateWorkflowVersion: Scalars['Boolean'];
|
||||||
activateWorkspace: Workspace;
|
activateWorkspace: ActivateWorkspaceOutput;
|
||||||
addUserToWorkspace: User;
|
addUserToWorkspace: User;
|
||||||
addUserToWorkspaceByInviteToken: User;
|
addUserToWorkspaceByInviteToken: User;
|
||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
@ -474,9 +471,7 @@ export type Mutation = {
|
|||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateJWT: GenerateJwt;
|
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
@ -487,6 +482,7 @@ export type Mutation = {
|
|||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signUp: LoginToken;
|
signUp: LoginToken;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
|
switchWorkspace: PublicWorkspaceDataOutput;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
updateBillingSubscription: UpdateBillingEntity;
|
updateBillingSubscription: UpdateBillingEntity;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
@ -621,22 +617,12 @@ export type MutationExecuteOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationFindAvailableSsoIdentityProvidersArgs = {
|
|
||||||
input: FindAvailableSsoidpInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateApiKeyTokenArgs = {
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
apiKeyId: Scalars['String'];
|
apiKeyId: Scalars['String'];
|
||||||
expiresAt: Scalars['String'];
|
expiresAt: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateJwtArgs = {
|
|
||||||
workspaceId: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationGetAuthorizationUrlArgs = {
|
export type MutationGetAuthorizationUrlArgs = {
|
||||||
input: GetAuthorizationUrlInput;
|
input: GetAuthorizationUrlInput;
|
||||||
};
|
};
|
||||||
@ -681,6 +667,11 @@ export type MutationSignUpArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSwitchWorkspaceArgs = {
|
||||||
|
workspaceId: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationTrackArgs = {
|
export type MutationTrackArgs = {
|
||||||
action: Scalars['String'];
|
action: Scalars['String'];
|
||||||
payload: Scalars['JSON'];
|
payload: Scalars['JSON'];
|
||||||
@ -825,6 +816,15 @@ export type ProductPricesEntity = {
|
|||||||
totalNumberOfPrices: Scalars['Int'];
|
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 = {
|
export type PublishServerlessFunctionInput = {
|
||||||
/** The id of the function. */
|
/** The id of the function. */
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
@ -833,11 +833,12 @@ export type PublishServerlessFunctionInput = {
|
|||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
billingPortalSession: SessionEntity;
|
billingPortalSession: SessionEntity;
|
||||||
checkUserExists: UserExists;
|
checkUserExists: UserExistsOutput;
|
||||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||||
clientConfig: ClientConfig;
|
clientConfig: ClientConfig;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
|
findAvailableWorkspacesByEmail: Array<AvailableWorkspaceOutput>;
|
||||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||||
findOneServerlessFunction: ServerlessFunction;
|
findOneServerlessFunction: ServerlessFunction;
|
||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
@ -845,6 +846,7 @@ export type Query = {
|
|||||||
getAvailablePackages: Scalars['JSON'];
|
getAvailablePackages: Scalars['JSON'];
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
getProductPrices: ProductPricesEntity;
|
getProductPrices: ProductPricesEntity;
|
||||||
|
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
|
||||||
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
|
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
|
||||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||||
@ -875,6 +877,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryFindAvailableWorkspacesByEmailArgs = {
|
||||||
|
email: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFindOneServerlessFunctionArgs = {
|
export type QueryFindOneServerlessFunctionArgs = {
|
||||||
input: ServerlessFunctionIdInput;
|
input: ServerlessFunctionIdInput;
|
||||||
};
|
};
|
||||||
@ -1001,6 +1008,24 @@ export type RunWorkflowVersionInput = {
|
|||||||
workflowVersionId: Scalars['String'];
|
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 {
|
export enum SsoIdentityProviderStatus {
|
||||||
Active = 'Active',
|
Active = 'Active',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
@ -1092,7 +1117,7 @@ export type SetupSsoOutput = {
|
|||||||
issuer: Scalars['String'];
|
issuer: Scalars['String'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Sort Directions */
|
/** Sort Directions */
|
||||||
@ -1263,8 +1288,12 @@ export type UpdateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
domainName?: InputMaybe<Scalars['String']>;
|
domainName?: InputMaybe<Scalars['String']>;
|
||||||
inviteHash?: InputMaybe<Scalars['String']>;
|
inviteHash?: InputMaybe<Scalars['String']>;
|
||||||
|
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
isMicrosoftAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
isPasswordAuthEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
|
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||||
logo?: InputMaybe<Scalars['String']>;
|
logo?: InputMaybe<Scalars['String']>;
|
||||||
|
subdomain?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
@ -1302,9 +1331,12 @@ export type UserEdge = {
|
|||||||
|
|
||||||
export type UserExists = {
|
export type UserExists = {
|
||||||
__typename?: 'UserExists';
|
__typename?: 'UserExists';
|
||||||
|
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||||
exists: Scalars['Boolean'];
|
exists: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserExistsOutput = UserExists | UserNotExists;
|
||||||
|
|
||||||
export type UserInfo = {
|
export type UserInfo = {
|
||||||
__typename?: 'UserInfo';
|
__typename?: 'UserInfo';
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -1324,6 +1356,11 @@ export type UserMappingOptionsUser = {
|
|||||||
user?: Maybe<Scalars['String']>;
|
user?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserNotExists = {
|
||||||
|
__typename?: 'UserNotExists';
|
||||||
|
exists: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UserWorkspace = {
|
export type UserWorkspace = {
|
||||||
__typename?: 'UserWorkspace';
|
__typename?: 'UserWorkspace';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
@ -1379,9 +1416,13 @@ export type Workspace = {
|
|||||||
hasValidEntrepriseKey: Scalars['Boolean'];
|
hasValidEntrepriseKey: Scalars['Boolean'];
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
inviteHash?: Maybe<Scalars['String']>;
|
inviteHash?: Maybe<Scalars['String']>;
|
||||||
|
isGoogleAuthEnabled: Scalars['Boolean'];
|
||||||
|
isMicrosoftAuthEnabled: Scalars['Boolean'];
|
||||||
|
isPasswordAuthEnabled: Scalars['Boolean'];
|
||||||
isPublicInviteLinkEnabled: Scalars['Boolean'];
|
isPublicInviteLinkEnabled: Scalars['Boolean'];
|
||||||
logo?: Maybe<Scalars['String']>;
|
logo?: Maybe<Scalars['String']>;
|
||||||
metadataVersion: Scalars['Float'];
|
metadataVersion: Scalars['Float'];
|
||||||
|
subdomain: Scalars['String'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
workspaceMembersCount?: Maybe<Scalars['Float']>;
|
workspaceMembersCount?: Maybe<Scalars['Float']>;
|
||||||
};
|
};
|
||||||
@ -1784,13 +1825,6 @@ export type EmailPasswordResetLinkMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
||||||
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutationVariables = Exact<{
|
|
||||||
input: FindAvailableSsoidpInput;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutation = { __typename?: 'Mutation', findAvailableSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }> };
|
|
||||||
|
|
||||||
export type GenerateApiKeyTokenMutationVariables = Exact<{
|
export type GenerateApiKeyTokenMutationVariables = Exact<{
|
||||||
apiKeyId: Scalars['String'];
|
apiKeyId: Scalars['String'];
|
||||||
expiresAt: Scalars['String'];
|
expiresAt: Scalars['String'];
|
||||||
@ -1799,13 +1833,6 @@ export type GenerateApiKeyTokenMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type GenerateApiKeyTokenMutation = { __typename?: 'Mutation', generateApiKeyToken: { __typename?: 'ApiKeyToken', token: string } };
|
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; }>;
|
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<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
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 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<{
|
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
|
||||||
token: Scalars['String'];
|
token: Scalars['String'];
|
||||||
newPassword: 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<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
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<{
|
export type ValidatePasswordResetTokenQueryVariables = Exact<{
|
||||||
token: Scalars['String'];
|
token: Scalars['String'];
|
||||||
@ -1903,7 +1942,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, 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; }>;
|
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<{
|
export type CreateSamlIdentityProviderMutationVariables = Exact<{
|
||||||
input: SetupSamlSsoInput;
|
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<{
|
export type DeleteSsoIdentityProviderMutationVariables = Exact<{
|
||||||
input: DeleteSsoInput;
|
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 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; }>;
|
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 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<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
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; }>;
|
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<{
|
export type UploadWorkspaceLogoMutationVariables = Exact<{
|
||||||
file: Scalars['Upload'];
|
file: Scalars['Upload'];
|
||||||
@ -2248,6 +2287,10 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
allowImpersonation
|
allowImpersonation
|
||||||
activationStatus
|
activationStatus
|
||||||
isPublicInviteLinkEnabled
|
isPublicInviteLinkEnabled
|
||||||
|
isGoogleAuthEnabled
|
||||||
|
isMicrosoftAuthEnabled
|
||||||
|
isPasswordAuthEnabled
|
||||||
|
subdomain
|
||||||
hasValidEntrepriseKey
|
hasValidEntrepriseKey
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
@ -2269,6 +2312,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
logo
|
logo
|
||||||
displayName
|
displayName
|
||||||
domainName
|
domainName
|
||||||
|
subdomain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userVars
|
userVars
|
||||||
@ -2645,39 +2689,6 @@ export function useEmailPasswordResetLinkMutation(baseOptions?: Apollo.MutationH
|
|||||||
export type EmailPasswordResetLinkMutationHookResult = ReturnType<typeof useEmailPasswordResetLinkMutation>;
|
export type EmailPasswordResetLinkMutationHookResult = ReturnType<typeof useEmailPasswordResetLinkMutation>;
|
||||||
export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult<EmailPasswordResetLinkMutation>;
|
export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult<EmailPasswordResetLinkMutation>;
|
||||||
export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions<EmailPasswordResetLinkMutation, EmailPasswordResetLinkMutationVariables>;
|
export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions<EmailPasswordResetLinkMutation, EmailPasswordResetLinkMutationVariables>;
|
||||||
export const FindAvailableSsoIdentityProvidersDocument = gql`
|
|
||||||
mutation FindAvailableSSOIdentityProviders($input: FindAvailableSSOIDPInput!) {
|
|
||||||
findAvailableSSOIdentityProviders(input: $input) {
|
|
||||||
...AvailableSSOIdentityProvidersFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${AvailableSsoIdentityProvidersFragmentFragmentDoc}`;
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutationFn = Apollo.MutationFunction<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useFindAvailableSsoIdentityProvidersMutation__
|
|
||||||
*
|
|
||||||
* To run a mutation, you first call `useFindAvailableSsoIdentityProvidersMutation` within a React component and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useFindAvailableSsoIdentityProvidersMutation` returns a tuple that includes:
|
|
||||||
* - A mutate function that you can call at any time to execute the mutation
|
|
||||||
* - An object with fields that represent the current status of the mutation's execution
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const [findAvailableSsoIdentityProvidersMutation, { data, loading, error }] = useFindAvailableSsoIdentityProvidersMutation({
|
|
||||||
* variables: {
|
|
||||||
* input: // value for 'input'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useFindAvailableSsoIdentityProvidersMutation(baseOptions?: Apollo.MutationHookOptions<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useMutation<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>(FindAvailableSsoIdentityProvidersDocument, options);
|
|
||||||
}
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutationHookResult = ReturnType<typeof useFindAvailableSsoIdentityProvidersMutation>;
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutationResult = Apollo.MutationResult<FindAvailableSsoIdentityProvidersMutation>;
|
|
||||||
export type FindAvailableSsoIdentityProvidersMutationOptions = Apollo.BaseMutationOptions<FindAvailableSsoIdentityProvidersMutation, FindAvailableSsoIdentityProvidersMutationVariables>;
|
|
||||||
export const GenerateApiKeyTokenDocument = gql`
|
export const GenerateApiKeyTokenDocument = gql`
|
||||||
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
|
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
|
||||||
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
|
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
|
||||||
@ -2712,55 +2723,6 @@ export function useGenerateApiKeyTokenMutation(baseOptions?: Apollo.MutationHook
|
|||||||
export type GenerateApiKeyTokenMutationHookResult = ReturnType<typeof useGenerateApiKeyTokenMutation>;
|
export type GenerateApiKeyTokenMutationHookResult = ReturnType<typeof useGenerateApiKeyTokenMutation>;
|
||||||
export type GenerateApiKeyTokenMutationResult = Apollo.MutationResult<GenerateApiKeyTokenMutation>;
|
export type GenerateApiKeyTokenMutationResult = Apollo.MutationResult<GenerateApiKeyTokenMutation>;
|
||||||
export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions<GenerateApiKeyTokenMutation, GenerateApiKeyTokenMutationVariables>;
|
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`
|
export const GenerateTransientTokenDocument = gql`
|
||||||
mutation generateTransientToken {
|
mutation generateTransientToken {
|
||||||
generateTransientToken {
|
generateTransientToken {
|
||||||
@ -2949,6 +2911,53 @@ export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignU
|
|||||||
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
||||||
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
||||||
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
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`
|
export const UpdatePasswordViaResetTokenDocument = gql`
|
||||||
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
||||||
updatePasswordViaResetToken(
|
updatePasswordViaResetToken(
|
||||||
@ -3028,7 +3037,26 @@ export type VerifyMutationOptions = Apollo.BaseMutationOptions<VerifyMutation, V
|
|||||||
export const CheckUserExistsDocument = gql`
|
export const CheckUserExistsDocument = gql`
|
||||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
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 CheckUserExistsQueryHookResult = ReturnType<typeof useCheckUserExistsQuery>;
|
||||||
export type CheckUserExistsLazyQueryHookResult = ReturnType<typeof useCheckUserExistsLazyQuery>;
|
export type CheckUserExistsLazyQueryHookResult = ReturnType<typeof useCheckUserExistsLazyQuery>;
|
||||||
export type CheckUserExistsQueryResult = Apollo.QueryResult<CheckUserExistsQuery, CheckUserExistsQueryVariables>;
|
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`
|
export const ValidatePasswordResetTokenDocument = gql`
|
||||||
query ValidatePasswordResetToken($token: String!) {
|
query ValidatePasswordResetToken($token: String!) {
|
||||||
validatePasswordResetToken(passwordResetToken: $token) {
|
validatePasswordResetToken(passwordResetToken: $token) {
|
||||||
@ -3244,19 +3322,16 @@ export type UpdateBillingSubscriptionMutationOptions = Apollo.BaseMutationOption
|
|||||||
export const GetClientConfigDocument = gql`
|
export const GetClientConfigDocument = gql`
|
||||||
query GetClientConfig {
|
query GetClientConfig {
|
||||||
clientConfig {
|
clientConfig {
|
||||||
authProviders {
|
|
||||||
google
|
|
||||||
password
|
|
||||||
microsoft
|
|
||||||
sso
|
|
||||||
}
|
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
billingUrl
|
billingUrl
|
||||||
billingFreeTrialDurationInDays
|
billingFreeTrialDurationInDays
|
||||||
}
|
}
|
||||||
signInPrefilled
|
signInPrefilled
|
||||||
signUpDisabled
|
isMultiWorkspaceEnabled
|
||||||
|
isSSOEnabled
|
||||||
|
defaultSubdomain
|
||||||
|
frontDomain
|
||||||
debugMode
|
debugMode
|
||||||
analyticsEnabled
|
analyticsEnabled
|
||||||
support {
|
support {
|
||||||
@ -4163,10 +4238,16 @@ export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutation
|
|||||||
export const ActivateWorkspaceDocument = gql`
|
export const ActivateWorkspaceDocument = gql`
|
||||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||||
activateWorkspace(data: $input) {
|
activateWorkspace(data: $input) {
|
||||||
id
|
workspace {
|
||||||
|
id
|
||||||
|
subdomain
|
||||||
|
}
|
||||||
|
loginToken {
|
||||||
|
...AuthTokenFragment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
|
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4230,9 +4311,14 @@ export const UpdateWorkspaceDocument = gql`
|
|||||||
updateWorkspace(data: $input) {
|
updateWorkspace(data: $input) {
|
||||||
id
|
id
|
||||||
domainName
|
domainName
|
||||||
|
subdomain
|
||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
allowImpersonation
|
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 { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||||
import { UserProvider } from '@/users/components/UserProvider';
|
import { UserProvider } from '@/users/components/UserProvider';
|
||||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||||
|
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||||
@ -34,6 +35,7 @@ export const AppRouterProviders = () => {
|
|||||||
<ChromeExtensionSidecarEffect />
|
<ChromeExtensionSidecarEffect />
|
||||||
<ChromeExtensionSidecarProvider>
|
<ChromeExtensionSidecarProvider>
|
||||||
<UserProviderEffect />
|
<UserProviderEffect />
|
||||||
|
<WorkspaceProviderEffect />
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ApolloMetadataClientProvider>
|
<ApolloMetadataClientProvider>
|
||||||
|
|||||||
@ -105,6 +105,12 @@ const SettingsWorkspace = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SettingsDomain = lazy(() =>
|
||||||
|
import('~/pages/settings/workspace/SettingsDomain').then((module) => ({
|
||||||
|
default: module.SettingsDomain,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const SettingsWorkspaceMembers = lazy(() =>
|
const SettingsWorkspaceMembers = lazy(() =>
|
||||||
import('~/pages/settings/SettingsWorkspaceMembers').then((module) => ({
|
import('~/pages/settings/SettingsWorkspaceMembers').then((module) => ({
|
||||||
default: module.SettingsWorkspaceMembers,
|
default: module.SettingsWorkspaceMembers,
|
||||||
@ -288,6 +294,8 @@ export const SettingsRoutes = ({
|
|||||||
{isBillingEnabled && (
|
{isBillingEnabled && (
|
||||||
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
||||||
)}
|
)}
|
||||||
|
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
|
||||||
|
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.WorkspaceMembersPage}
|
path={SettingsPath.WorkspaceMembersPage}
|
||||||
element={<SettingsWorkspaceMembers />}
|
element={<SettingsWorkspaceMembers />}
|
||||||
@ -382,14 +390,12 @@ export const SettingsRoutes = ({
|
|||||||
element={<SettingsObjectFieldEdit />}
|
element={<SettingsObjectFieldEdit />}
|
||||||
/>
|
/>
|
||||||
<Route path={SettingsPath.Releases} element={<Releases />} />
|
<Route path={SettingsPath.Releases} element={<Releases />} />
|
||||||
|
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
||||||
{isSSOEnabled && (
|
{isSSOEnabled && (
|
||||||
<>
|
<Route
|
||||||
<Route path={SettingsPath.Security} element={<SettingsSecurity />} />
|
path={SettingsPath.NewSSOIdentityProvider}
|
||||||
<Route
|
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||||
path={SettingsPath.NewSSOIdentityProvider}
|
/>
|
||||||
element={<SettingsSecuritySSOIdentifyProvider />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{isAdminPageEnabled && (
|
{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`
|
export const CHECK_USER_EXISTS = gql`
|
||||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
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.icons).toEqual({});
|
||||||
expect(state.authProviders).toEqual({
|
expect(state.authProviders).toEqual({
|
||||||
google: false,
|
google: true,
|
||||||
microsoft: false,
|
microsoft: false,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
password: false,
|
password: true,
|
||||||
sso: false,
|
sso: [],
|
||||||
});
|
});
|
||||||
expect(state.billing).toBeNull();
|
expect(state.billing).toBeNull();
|
||||||
expect(state.isDeveloperDefaultSignInPrefilled).toBe(false);
|
expect(state.isDeveloperDefaultSignInPrefilled).toBe(false);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
snapshot_UNSTABLE,
|
snapshot_UNSTABLE,
|
||||||
useGotoRecoilSnapshot,
|
useGotoRecoilSnapshot,
|
||||||
useRecoilCallback,
|
useRecoilCallback,
|
||||||
useRecoilState,
|
useRecoilValue,
|
||||||
useSetRecoilState,
|
useSetRecoilState,
|
||||||
} from 'recoil';
|
} from 'recoil';
|
||||||
import { iconsState } from 'twenty-ui';
|
import { iconsState } from 'twenty-ui';
|
||||||
@ -42,10 +42,18 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
|
|||||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||||
import { currentUserState } from '../states/currentUserState';
|
import { currentUserState } from '../states/currentUserState';
|
||||||
import { tokenPairState } from '../states/tokenPairState';
|
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 = () => {
|
export const useAuth = () => {
|
||||||
const [, setTokenPair] = useRecoilState(tokenPairState);
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||||
|
const urlManager = useRecoilValue(urlManagerState);
|
||||||
|
const setLastAuthenticateWorkspaceState = useSetRecoilState(
|
||||||
|
lastAuthenticateWorkspaceState,
|
||||||
|
);
|
||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
@ -60,6 +68,7 @@ export const useAuth = () => {
|
|||||||
const [challenge] = useChallengeMutation();
|
const [challenge] = useChallengeMutation();
|
||||||
const [signUp] = useSignUpMutation();
|
const [signUp] = useSignUpMutation();
|
||||||
const [verify] = useVerifyMutation();
|
const [verify] = useVerifyMutation();
|
||||||
|
const { isTwentyWorkspaceSubdomain, getWorkspaceSubdomain } = useUrlManager();
|
||||||
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
||||||
useCheckUserExistsLazyQuery();
|
useCheckUserExistsLazyQuery();
|
||||||
|
|
||||||
@ -203,6 +212,15 @@ export const useAuth = () => {
|
|||||||
const workspace = user.defaultWorkspace ?? null;
|
const workspace = user.defaultWorkspace ?? null;
|
||||||
|
|
||||||
setCurrentWorkspace(workspace);
|
setCurrentWorkspace(workspace);
|
||||||
|
if (isDefined(workspace) && isTwentyWorkspaceSubdomain) {
|
||||||
|
setLastAuthenticateWorkspaceState({
|
||||||
|
id: workspace.id,
|
||||||
|
subdomain: workspace.subdomain,
|
||||||
|
cookieAttributes: {
|
||||||
|
domain: `.${urlManager.frontDomain}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
|
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
|
||||||
const validWorkspaces = verifyResult.data?.verify.user.workspaces
|
const validWorkspaces = verifyResult.data?.verify.user.workspaces
|
||||||
@ -227,9 +245,12 @@ export const useAuth = () => {
|
|||||||
setTokenPair,
|
setTokenPair,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setCurrentWorkspace,
|
setCurrentWorkspace,
|
||||||
|
isTwentyWorkspaceSubdomain,
|
||||||
setCurrentWorkspaceMembers,
|
setCurrentWorkspaceMembers,
|
||||||
setCurrentWorkspaceMember,
|
setCurrentWorkspaceMember,
|
||||||
setDateTimeFormat,
|
setDateTimeFormat,
|
||||||
|
setLastAuthenticateWorkspaceState,
|
||||||
|
urlManager.frontDomain,
|
||||||
setWorkspaces,
|
setWorkspaces,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -301,23 +322,34 @@ export const useAuth = () => {
|
|||||||
[setIsVerifyPendingState, signUp, handleVerify],
|
[setIsVerifyPendingState, signUp, handleVerify],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildRedirectUrl = (
|
const buildRedirectUrl = useCallback(
|
||||||
path: string,
|
(
|
||||||
params: {
|
path: string,
|
||||||
workspacePersonalInviteToken?: string;
|
params: {
|
||||||
workspaceInviteHash?: string;
|
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();
|
||||||
},
|
},
|
||||||
) => {
|
[getWorkspaceSubdomain],
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoogleLogin = useCallback(
|
const handleGoogleLogin = useCallback(
|
||||||
(params: {
|
(params: {
|
||||||
@ -326,7 +358,7 @@ export const useAuth = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
window.location.href = buildRedirectUrl('/auth/google', params);
|
window.location.href = buildRedirectUrl('/auth/google', params);
|
||||||
},
|
},
|
||||||
[],
|
[buildRedirectUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMicrosoftLogin = useCallback(
|
const handleMicrosoftLogin = useCallback(
|
||||||
@ -336,7 +368,7 @@ export const useAuth = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
window.location.href = buildRedirectUrl('/auth/microsoft', params);
|
window.location.href = buildRedirectUrl('/auth/microsoft', params);
|
||||||
},
|
},
|
||||||
[],
|
[buildRedirectUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import {
|
import {
|
||||||
FindAvailableSsoIdentityProvidersMutationVariables,
|
|
||||||
GetAuthorizationUrlMutationVariables,
|
GetAuthorizationUrlMutationVariables,
|
||||||
useFindAvailableSsoIdentityProvidersMutation,
|
|
||||||
useGetAuthorizationUrlMutation,
|
useGetAuthorizationUrlMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -13,20 +11,8 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
export const useSSO = () => {
|
export const useSSO = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const [findAvailableSSOProviderByEmailMutation] =
|
|
||||||
useFindAvailableSsoIdentityProvidersMutation();
|
|
||||||
const [getAuthorizationUrlMutation] = useGetAuthorizationUrlMutation();
|
const [getAuthorizationUrlMutation] = useGetAuthorizationUrlMutation();
|
||||||
|
|
||||||
const findAvailableSSOProviderByEmail = async ({
|
|
||||||
email,
|
|
||||||
}: FindAvailableSsoIdentityProvidersMutationVariables['input']) => {
|
|
||||||
return await findAvailableSSOProviderByEmailMutation({
|
|
||||||
variables: {
|
|
||||||
input: { email },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAuthorizationUrlForSSO = async ({
|
const getAuthorizationUrlForSSO = async ({
|
||||||
identityProviderId,
|
identityProviderId,
|
||||||
}: GetAuthorizationUrlMutationVariables['input']) => {
|
}: GetAuthorizationUrlMutationVariables['input']) => {
|
||||||
@ -63,6 +49,5 @@ export const useSSO = () => {
|
|||||||
return {
|
return {
|
||||||
redirectToSSOLoginPage,
|
redirectToSSOLoginPage,
|
||||||
getAuthorizationUrlForSSO,
|
getAuthorizationUrlForSSO,
|
||||||
findAvailableSSOProviderByEmail,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,36 +7,25 @@ import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
|||||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
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 {
|
import {
|
||||||
SignInUpStep,
|
SignInUpStep,
|
||||||
signInUpStepState,
|
signInUpStepState,
|
||||||
} from '@/auth/states/signInUpStepState';
|
} from '@/auth/states/signInUpStepState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||||
export enum SignInUpMode {
|
import { SignInUpMode } from '@/auth/types/signInUpMode.type';
|
||||||
SignIn = 'sign-in',
|
|
||||||
SignUp = 'sign-up',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||||
|
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||||
|
|
||||||
const isMatchingLocation = useIsMatchingLocation();
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
|
||||||
const { redirectToSSOLoginPage, findAvailableSSOProviderByEmail } = useSSO();
|
|
||||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
|
||||||
availableSSOIdentityProvidersState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const workspacePersonalInviteToken =
|
const workspacePersonalInviteToken =
|
||||||
@ -44,12 +33,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
|
|
||||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||||
|
|
||||||
const [signInUpMode, setSignInUpMode] = useState<SignInUpMode>(() => {
|
|
||||||
return isMatchingLocation(AppPath.SignInUp)
|
|
||||||
? SignInUpMode.SignIn
|
|
||||||
: SignInUpMode.SignUp;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
signInWithCredentials,
|
signInWithCredentials,
|
||||||
signUpWithCredentials,
|
signUpWithCredentials,
|
||||||
@ -67,7 +50,12 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
? SignInUpMode.SignIn
|
? SignInUpMode.SignIn
|
||||||
: SignInUpMode.SignUp,
|
: SignInUpMode.SignUp,
|
||||||
);
|
);
|
||||||
}, [isMatchingLocation, requestFreshCaptchaToken, setSignInUpStep]);
|
}, [
|
||||||
|
isMatchingLocation,
|
||||||
|
requestFreshCaptchaToken,
|
||||||
|
setSignInUpMode,
|
||||||
|
setSignInUpStep,
|
||||||
|
]);
|
||||||
|
|
||||||
const continueWithCredentials = useCallback(async () => {
|
const continueWithCredentials = useCallback(async () => {
|
||||||
const token = await readCaptchaToken();
|
const token = await readCaptchaToken();
|
||||||
@ -101,47 +89,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
enqueueSnackBar,
|
enqueueSnackBar,
|
||||||
requestFreshCaptchaToken,
|
requestFreshCaptchaToken,
|
||||||
setSignInUpStep,
|
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(
|
const submitCredentials: SubmitHandler<Form> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
const token = await readCaptchaToken();
|
const token = await readCaptchaToken();
|
||||||
@ -150,19 +100,21 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
throw new Error('Email and password are required');
|
throw new Error('Email and password are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
signInUpMode === SignInUpMode.SignIn && !isInviteMode
|
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
|
||||||
? await signInWithCredentials(
|
await signInWithCredentials(
|
||||||
data.email.toLowerCase().trim(),
|
data.email.toLowerCase().trim(),
|
||||||
data.password,
|
data.password,
|
||||||
token,
|
token,
|
||||||
)
|
);
|
||||||
: await signUpWithCredentials(
|
} else {
|
||||||
data.email.toLowerCase().trim(),
|
await signUpWithCredentials(
|
||||||
data.password,
|
data.email.toLowerCase().trim(),
|
||||||
workspaceInviteHash,
|
data.password,
|
||||||
workspacePersonalInviteToken,
|
workspaceInviteHash,
|
||||||
token,
|
workspacePersonalInviteToken,
|
||||||
);
|
token,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
enqueueSnackBar(err?.message, {
|
enqueueSnackBar(err?.message, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
@ -189,8 +141,6 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
signInUpMode,
|
signInUpMode,
|
||||||
continueWithCredentials,
|
continueWithCredentials,
|
||||||
continueWithEmail,
|
continueWithEmail,
|
||||||
continueWithSSO,
|
|
||||||
submitSSOEmail,
|
|
||||||
submitCredentials,
|
submitCredentials,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,33 +3,50 @@ import { useEffect } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
||||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import {
|
||||||
|
SignInUpStep,
|
||||||
|
signInUpStepState,
|
||||||
|
} from '@/auth/states/signInUpStepState';
|
||||||
|
|
||||||
export const validationSchema = z
|
const makeValidationSchema = (signInUpStep: SignInUpStep) =>
|
||||||
.object({
|
z
|
||||||
exist: z.boolean(),
|
.object({
|
||||||
email: z.string().trim().email('Email must be a valid email'),
|
exist: z.boolean(),
|
||||||
password: z
|
email: z.string().trim().email('Email must be a valid email'),
|
||||||
.string()
|
password:
|
||||||
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
signInUpStep === SignInUpStep.Password
|
||||||
captchaToken: z.string().default(''),
|
? z
|
||||||
})
|
.string()
|
||||||
.required();
|
.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 = () => {
|
export const useSignInUpForm = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||||
|
|
||||||
|
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
|
||||||
|
|
||||||
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
|
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
|
||||||
isDeveloperDefaultSignInPrefilledState,
|
isDeveloperDefaultSignInPrefilledState,
|
||||||
);
|
);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const invitationPrefilledEmail = searchParams.get('email');
|
const prefilledEmail = searchParams.get('email');
|
||||||
|
|
||||||
const form = useForm<Form>({
|
const form = useForm<Form>({
|
||||||
mode: 'onChange',
|
mode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
exist: false,
|
exist: false,
|
||||||
email: '',
|
email: '',
|
||||||
@ -40,12 +57,12 @@ export const useSignInUpForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDefined(invitationPrefilledEmail)) {
|
if (isDefined(prefilledEmail)) {
|
||||||
form.setValue('email', invitationPrefilledEmail);
|
form.setValue('email', prefilledEmail);
|
||||||
} else if (isDeveloperDefaultSignInPrefilled === true) {
|
} else if (isDeveloperDefaultSignInPrefilled === true) {
|
||||||
form.setValue('email', 'tim@apple.dev');
|
form.setValue('email', 'tim@apple.dev');
|
||||||
form.setValue('password', 'Applecar2025');
|
form.setValue('password', 'Applecar2025');
|
||||||
}
|
}
|
||||||
}, [form, isDeveloperDefaultSignInPrefilled, invitationPrefilledEmail]);
|
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail, location.search]);
|
||||||
return { form: form };
|
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'
|
| 'currentBillingSubscription'
|
||||||
| 'workspaceMembersCount'
|
| 'workspaceMembersCount'
|
||||||
| 'isPublicInviteLinkEnabled'
|
| 'isPublicInviteLinkEnabled'
|
||||||
|
| 'isGoogleAuthEnabled'
|
||||||
|
| 'isMicrosoftAuthEnabled'
|
||||||
|
| 'isPasswordAuthEnabled'
|
||||||
| 'hasValidEntrepriseKey'
|
| 'hasValidEntrepriseKey'
|
||||||
|
| 'subdomain'
|
||||||
| 'metadataVersion'
|
| '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',
|
Init = 'init',
|
||||||
Email = 'email',
|
Email = 'email',
|
||||||
Password = 'password',
|
Password = 'password',
|
||||||
SSOEmail = 'SSOEmail',
|
WorkspaceSelection = 'workspaceSelection',
|
||||||
SSOWorkspaceSelection = 'SSOWorkspaceSelection',
|
SSOIdentityProviderSelection = 'SSOIdentityProviderSelection',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signInUpStepState = createState<SignInUpStep>({
|
export const signInUpStepState = createState<SignInUpStep>({
|
||||||
|
|||||||
@ -2,9 +2,17 @@ import { createState } from 'twenty-ui';
|
|||||||
|
|
||||||
import { AuthTokenPair } from '~/generated/graphql';
|
import { AuthTokenPair } from '~/generated/graphql';
|
||||||
import { cookieStorageEffect } from '~/utils/recoil-effects';
|
import { cookieStorageEffect } from '~/utils/recoil-effects';
|
||||||
|
|
||||||
export const tokenPairState = createState<AuthTokenPair | null>({
|
export const tokenPairState = createState<AuthTokenPair | null>({
|
||||||
key: 'tokenPairState',
|
key: 'tokenPairState',
|
||||||
defaultValue: null,
|
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';
|
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>({
|
export const workspacesState = createState<Workspaces[] | null>({
|
||||||
key: 'workspacesState',
|
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 { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
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 { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
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 { sentryConfigState } from '@/client-config/states/sentryConfigState';
|
||||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||||
|
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
|
||||||
|
|
||||||
export const ClientConfigProviderEffect = () => {
|
export const ClientConfigProviderEffect = () => {
|
||||||
const setAuthProviders = useSetRecoilState(authProvidersState);
|
|
||||||
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
||||||
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
|
||||||
|
const setUrlManager = useSetRecoilState(urlManagerState);
|
||||||
|
|
||||||
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
|
||||||
isDeveloperDefaultSignInPrefilledState,
|
isDeveloperDefaultSignInPrefilledState,
|
||||||
);
|
);
|
||||||
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
|
const setIsMultiWorkspaceEnabled = useSetRecoilState(
|
||||||
|
isMultiWorkspaceEnabledState,
|
||||||
|
);
|
||||||
|
const setIsSSOEnabledState = useSetRecoilState(isSSOEnabledState);
|
||||||
|
|
||||||
const setBilling = useSetRecoilState(billingState);
|
const setBilling = useSetRecoilState(billingState);
|
||||||
const setSupportChat = useSetRecoilState(supportChatState);
|
const setSupportChat = useSetRecoilState(supportChatState);
|
||||||
@ -69,17 +73,10 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
error: undefined,
|
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);
|
setIsDebugMode(data?.clientConfig.debugMode);
|
||||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||||
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||||
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
|
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
|
||||||
|
|
||||||
setBilling(data?.clientConfig.billing);
|
setBilling(data?.clientConfig.billing);
|
||||||
setSupportChat(data?.clientConfig.support);
|
setSupportChat(data?.clientConfig.support);
|
||||||
@ -97,12 +94,16 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
|
|
||||||
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
||||||
setApiConfig(data?.clientConfig?.api);
|
setApiConfig(data?.clientConfig?.api);
|
||||||
|
setIsSSOEnabledState(data?.clientConfig?.isSSOEnabled);
|
||||||
|
setUrlManager({
|
||||||
|
defaultSubdomain: data?.clientConfig?.defaultSubdomain,
|
||||||
|
frontDomain: data?.clientConfig?.frontDomain,
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
setAuthProviders,
|
|
||||||
setIsDebugMode,
|
setIsDebugMode,
|
||||||
setIsDeveloperDefaultSignInPrefilled,
|
setIsDeveloperDefaultSignInPrefilled,
|
||||||
setIsSignUpDisabled,
|
setIsMultiWorkspaceEnabled,
|
||||||
setSupportChat,
|
setSupportChat,
|
||||||
setBilling,
|
setBilling,
|
||||||
setSentryConfig,
|
setSentryConfig,
|
||||||
@ -113,6 +114,8 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setApiConfig,
|
setApiConfig,
|
||||||
setIsAnalyticsEnabled,
|
setIsAnalyticsEnabled,
|
||||||
error,
|
error,
|
||||||
|
setUrlManager,
|
||||||
|
setIsSSOEnabledState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -3,19 +3,16 @@ import { gql } from '@apollo/client';
|
|||||||
export const GET_CLIENT_CONFIG = gql`
|
export const GET_CLIENT_CONFIG = gql`
|
||||||
query GetClientConfig {
|
query GetClientConfig {
|
||||||
clientConfig {
|
clientConfig {
|
||||||
authProviders {
|
|
||||||
google
|
|
||||||
password
|
|
||||||
microsoft
|
|
||||||
sso
|
|
||||||
}
|
|
||||||
billing {
|
billing {
|
||||||
isBillingEnabled
|
isBillingEnabled
|
||||||
billingUrl
|
billingUrl
|
||||||
billingFreeTrialDurationInDays
|
billingFreeTrialDurationInDays
|
||||||
}
|
}
|
||||||
signInPrefilled
|
signInPrefilled
|
||||||
signUpDisabled
|
isMultiWorkspaceEnabled
|
||||||
|
isSSOEnabled
|
||||||
|
defaultSubdomain
|
||||||
|
frontDomain
|
||||||
debugMode
|
debugMode
|
||||||
analyticsEnabled
|
analyticsEnabled
|
||||||
support {
|
support {
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { AuthProviders } from '~/generated/graphql';
|
|||||||
export const authProvidersState = createState<AuthProviders>({
|
export const authProvidersState = createState<AuthProviders>({
|
||||||
key: 'authProvidersState',
|
key: 'authProvidersState',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
google: false,
|
google: true,
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
password: false,
|
password: true,
|
||||||
microsoft: false,
|
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',
|
id: '1',
|
||||||
featureFlags: [],
|
featureFlags: [],
|
||||||
allowImpersonation: false,
|
allowImpersonation: false,
|
||||||
|
subdomain: 'test',
|
||||||
activationStatus: WorkspaceActivationStatus.Active,
|
activationStatus: WorkspaceActivationStatus.Active,
|
||||||
hasValidEntrepriseKey: false,
|
hasValidEntrepriseKey: false,
|
||||||
metadataVersion: 1,
|
metadataVersion: 1,
|
||||||
isPublicInviteLinkEnabled: false,
|
isPublicInviteLinkEnabled: false,
|
||||||
|
isGoogleAuthEnabled: true,
|
||||||
|
isMicrosoftAuthEnabled: false,
|
||||||
|
isPasswordAuthEnabled: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const StyledIconContainer = styled.div`
|
|||||||
height: 75%;
|
height: 75%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDeveloperSection = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
@ -82,7 +82,6 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
);
|
);
|
||||||
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED');
|
||||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||||
const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED');
|
|
||||||
const isBillingPageEnabled =
|
const isBillingPageEnabled =
|
||||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||||
|
|
||||||
@ -192,14 +191,20 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
Icon={IconCode}
|
Icon={IconCode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isSSOEnabled && (
|
{isAdvancedModeEnabled && (
|
||||||
<SettingsNavigationDrawerItem
|
<StyledContainer>
|
||||||
label="Security"
|
<StyledIconContainer>
|
||||||
path={SettingsPath.Security}
|
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||||
Icon={IconKey}
|
</StyledIconContainer>
|
||||||
/>
|
<SettingsNavigationDrawerItem
|
||||||
|
label="Security"
|
||||||
|
path={SettingsPath.Security}
|
||||||
|
Icon={IconKey}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
)}
|
)}
|
||||||
</NavigationDrawerSection>
|
</NavigationDrawerSection>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isAdvancedModeEnabled && (
|
{isAdvancedModeEnabled && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -209,7 +214,7 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
exit="exit"
|
exit="exit"
|
||||||
variants={motionAnimationVariants}
|
variants={motionAnimationVariants}
|
||||||
>
|
>
|
||||||
<StyledDeveloperSection>
|
<StyledContainer>
|
||||||
<StyledIconContainer>
|
<StyledIconContainer>
|
||||||
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
@ -228,7 +233,7 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</NavigationDrawerSection>
|
</NavigationDrawerSection>
|
||||||
</StyledDeveloperSection>
|
</StyledContainer>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import styled from '@emotion/styled';
|
|||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { H2Title, IconComponent, IconKey, Section } from 'twenty-ui';
|
import { H2Title, IconComponent, IconKey, Section } from 'twenty-ui';
|
||||||
import { IdpType } from '~/generated/graphql';
|
import { IdentityProviderType } from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
const StyledInputsContainer = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -30,8 +30,8 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
|||||||
const { control, getValues } =
|
const { control, getValues } =
|
||||||
useFormContext<SettingSecurityNewSSOIdentityFormValues>();
|
useFormContext<SettingSecurityNewSSOIdentityFormValues>();
|
||||||
|
|
||||||
const IdpMap: Record<
|
const IdentitiesProvidersMap: Record<
|
||||||
IdpType,
|
IdentityProviderType,
|
||||||
{
|
{
|
||||||
form: ReactElement;
|
form: ReactElement;
|
||||||
option: {
|
option: {
|
||||||
@ -62,12 +62,12 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFormByType = (type: Uppercase<IdpType> | undefined) => {
|
const getFormByType = (type: Uppercase<IdentityProviderType> | undefined) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case IdpType.Oidc:
|
case IdentityProviderType.Oidc:
|
||||||
return IdpMap.OIDC.form;
|
return IdentitiesProvidersMap.OIDC.form;
|
||||||
case IdpType.Saml:
|
case IdentityProviderType.Saml:
|
||||||
return IdpMap.SAML.form;
|
return IdentitiesProvidersMap.SAML.form;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
|
|||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<SettingsRadioCardContainer
|
<SettingsRadioCardContainer
|
||||||
value={value}
|
value={value}
|
||||||
options={Object.values(IdpMap).map(
|
options={Object.values(IdentitiesProvidersMap).map(
|
||||||
(identityProviderType) => identityProviderType.option,
|
(identityProviderType) => identityProviderType.option,
|
||||||
)}
|
)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@ -8,11 +8,14 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||||
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCardWrapper';
|
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCardWrapper';
|
||||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
|
||||||
import isPropValid from '@emotion/is-prop-valid';
|
import isPropValid from '@emotion/is-prop-valid';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||||
import { IconKey } from 'twenty-ui';
|
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, {
|
const StyledLink = styled(Link, {
|
||||||
shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'isDisabled',
|
shouldForwardProp: (prop) => isPropValid(prop) && prop !== 'isDisabled',
|
||||||
@ -22,11 +25,29 @@ const StyledLink = styled(Link, {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsSSOIdentitiesProvidersListCard = () => {
|
export const SettingsSSOIdentitiesProvidersListCard = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
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
|
<StyledLink
|
||||||
to={getSettingsPagePath(SettingsPath.NewSSOIdentityProvider)}
|
to={getSettingsPagePath(SettingsPath.NewSSOIdentityProvider)}
|
||||||
isDisabled={currentWorkspace?.hasValidEntrepriseKey !== true}
|
isDisabled={currentWorkspace?.hasValidEntrepriseKey !== true}
|
||||||
|
|||||||
@ -5,33 +5,14 @@ import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/securit
|
|||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
|
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {
|
export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [SSOIdentitiesProviders, setSSOIdentitiesProviders] = useRecoilState(
|
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
|
||||||
SSOIdentitiesProvidersState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { loading } = useListSsoIdentityProvidersByWorkspaceIdQuery({
|
|
||||||
onCompleted: (data) => {
|
|
||||||
setSSOIdentitiesProviders(
|
|
||||||
data?.listSSOIdentityProvidersByWorkspaceId ?? [],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
enqueueSnackBar(error.message, {
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsListCard
|
<SettingsListCard
|
||||||
@ -39,7 +20,6 @@ export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {
|
|||||||
getItemLabel={(SSOIdentityProvider) =>
|
getItemLabel={(SSOIdentityProvider) =>
|
||||||
`${SSOIdentityProvider.name} - ${SSOIdentityProvider.type}`
|
`${SSOIdentityProvider.name} - ${SSOIdentityProvider.type}`
|
||||||
}
|
}
|
||||||
isLoading={loading}
|
|
||||||
RowIconFn={(SSOIdentityProvider) =>
|
RowIconFn={(SSOIdentityProvider) =>
|
||||||
guessSSOIdentityProviderIconByUrl(SSOIdentityProvider.issuer)
|
guessSSOIdentityProviderIconByUrl(SSOIdentityProvider.issuer)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,24 +2,98 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|||||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useRecoilState } from 'recoil';
|
import styled from '@emotion/styled';
|
||||||
import { Card, IconLink, isDefined } from 'twenty-ui';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import {
|
||||||
|
IconLink,
|
||||||
|
Card,
|
||||||
|
IconGoogle,
|
||||||
|
IconMicrosoft,
|
||||||
|
IconPassword,
|
||||||
|
} from 'twenty-ui';
|
||||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
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 = () => {
|
export const SettingsSecurityOptionsList = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
|
||||||
|
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
);
|
);
|
||||||
if (!isDefined(currentWorkspace)) {
|
|
||||||
throw new Error(
|
|
||||||
'The current workspace must be defined to edit its security options.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
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) => {
|
const handleChange = async (value: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.id) {
|
if (!currentWorkspace?.id) {
|
||||||
@ -44,17 +118,49 @@ export const SettingsSecurityOptionsList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card rounded>
|
<StyledSettingsSecurityOptionsList>
|
||||||
<SettingsOptionCardContentToggle
|
{currentWorkspace && (
|
||||||
Icon={IconLink}
|
<>
|
||||||
title="Invite by Link"
|
<Card>
|
||||||
description="Allow the invitation of new users by sharing an invite link."
|
<SettingsOptionCardContentToggle
|
||||||
checked={currentWorkspace.isPublicInviteLinkEnabled}
|
Icon={IconGoogle}
|
||||||
advancedMode
|
title="Google"
|
||||||
onChange={() =>
|
description="Allow logins through Google's single sign-on functionality."
|
||||||
handleChange(!currentWorkspace.isPublicInviteLinkEnabled)
|
checked={currentWorkspace.isGoogleAuthEnabled}
|
||||||
}
|
advancedMode
|
||||||
/>
|
onChange={() => toggleAuthMethod('google')}
|
||||||
</Card>
|
/>
|
||||||
|
<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 { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { IdpType, SsoIdentityProviderStatus } from '~/generated/graphql';
|
import {
|
||||||
|
IdentityProviderType,
|
||||||
|
SsoIdentityProviderStatus,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
export type SSOIdentityProvider = {
|
export type SSOIdentityProvider = {
|
||||||
__typename: 'SSOIdentityProvider';
|
__typename: 'SSOIdentityProvider';
|
||||||
id: string;
|
id: string;
|
||||||
type: IdpType;
|
type: IdentityProviderType;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
status: SsoIdentityProviderStatus;
|
status: SsoIdentityProviderStatus;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export const parseSAMLMetadataFromXMLFile = (
|
|||||||
try {
|
try {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
|
const xmlDoc = parser.parseFromString(xmlString, 'application/xml');
|
||||||
|
|
||||||
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
||||||
throw new Error('Error parsing XML');
|
throw new Error('Error parsing XML');
|
||||||
}
|
}
|
||||||
@ -28,10 +27,10 @@ export const parseSAMLMetadataFromXMLFile = (
|
|||||||
'md:IDPSSODescriptor',
|
'md:IDPSSODescriptor',
|
||||||
)?.[0];
|
)?.[0];
|
||||||
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
|
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
|
||||||
const keyInfo = keyDescriptor.getElementsByTagName('ds:KeyInfo')[0];
|
const keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0];
|
||||||
const x509Data = keyInfo.getElementsByTagName('ds:X509Data')[0];
|
const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0];
|
||||||
const x509Certificate = x509Data
|
const x509Certificate = x509Data
|
||||||
.getElementsByTagName('ds:X509Certificate')?.[0]
|
?.getElementsByTagName('ds:X509Certificate')?.[0]
|
||||||
.textContent?.trim();
|
.textContent?.trim();
|
||||||
|
|
||||||
const singleSignOnServices = Array.from(
|
const singleSignOnServices = Array.from(
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
/* @license Enterprise */
|
/* @license Enterprise */
|
||||||
|
|
||||||
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||||
import { IdpType } from '~/generated/graphql';
|
import { IdentityProviderType } from '~/generated/graphql';
|
||||||
|
|
||||||
export const sSOIdentityProviderDefaultValues: Record<
|
export const sSOIdentityProviderDefaultValues: Record<
|
||||||
IdpType,
|
IdentityProviderType,
|
||||||
() => SettingSecurityNewSSOIdentityFormValues
|
() => SettingSecurityNewSSOIdentityFormValues
|
||||||
> = {
|
> = {
|
||||||
SAML: () => ({
|
SAML: () => ({
|
||||||
type: 'SAML',
|
type: 'SAML',
|
||||||
ssoURL: '',
|
ssoURL: '',
|
||||||
name: '',
|
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: '',
|
certificate: '',
|
||||||
issuer: '',
|
issuer: '',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|||||||
|
|
||||||
export const WorkspaceLogoUploader = () => {
|
export const WorkspaceLogoUploader = () => {
|
||||||
const [uploadLogo] = useUploadWorkspaceLogoMutation();
|
const [uploadLogo] = useUploadWorkspaceLogoMutation();
|
||||||
const [updateWorkspce] = useUpdateWorkspaceMutation();
|
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
);
|
);
|
||||||
@ -39,7 +39,7 @@ export const WorkspaceLogoUploader = () => {
|
|||||||
if (!currentWorkspace?.id) {
|
if (!currentWorkspace?.id) {
|
||||||
throw new Error('Workspace id not found');
|
throw new Error('Workspace id not found');
|
||||||
}
|
}
|
||||||
await updateWorkspce({
|
await updateWorkspace({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
logo: null,
|
logo: null,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export enum SettingsPath {
|
|||||||
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
||||||
WorkspaceMembersPage = 'workspace-members',
|
WorkspaceMembersPage = 'workspace-members',
|
||||||
Workspace = 'workspace',
|
Workspace = 'workspace',
|
||||||
|
Domain = 'domain',
|
||||||
CRMMigration = 'crm-migration',
|
CRMMigration = 'crm-migration',
|
||||||
Developers = 'developers',
|
Developers = 'developers',
|
||||||
ServerlessFunctions = 'functions',
|
ServerlessFunctions = 'functions',
|
||||||
|
|||||||
@ -15,6 +15,13 @@ import { useState } from 'react';
|
|||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
|
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
|
||||||
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
|
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 }>`
|
const StyledLogo = styled.div<{ logo: string }>`
|
||||||
background: url(${({ logo }) => logo});
|
background: url(${({ logo }) => logo});
|
||||||
@ -72,6 +79,7 @@ export const MultiWorkspaceDropdownButton = ({
|
|||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const { switchWorkspace } = useWorkspaceSwitching();
|
const { switchWorkspace } = useWorkspaceSwitching();
|
||||||
|
const { buildWorkspaceUrl } = useUrlManager();
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
||||||
|
|
||||||
@ -96,13 +104,9 @@ export const MultiWorkspaceDropdownButton = ({
|
|||||||
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
||||||
>
|
>
|
||||||
<StyledLogo
|
<StyledLogo
|
||||||
logo={
|
logo={getImageAbsoluteURI(
|
||||||
getImageAbsoluteURI(
|
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||||
currentWorkspace?.logo === null
|
)}
|
||||||
? DEFAULT_WORKSPACE_LOGO
|
|
||||||
: currentWorkspace?.logo,
|
|
||||||
) ?? ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<NavigationDrawerAnimatedCollapseWrapper>
|
<NavigationDrawerAnimatedCollapseWrapper>
|
||||||
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
|
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
|
||||||
@ -118,23 +122,26 @@ export const MultiWorkspaceDropdownButton = ({
|
|||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<MenuItemSelectAvatar
|
<StyledLink
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
text={workspace.displayName ?? ''}
|
to={buildWorkspaceUrl(workspace.subdomain)}
|
||||||
avatar={
|
>
|
||||||
<StyledLogo
|
<MenuItemSelectAvatar
|
||||||
logo={
|
text={workspace.displayName ?? ''}
|
||||||
getImageAbsoluteURI(
|
avatar={
|
||||||
workspace.logo === null
|
<StyledLogo
|
||||||
? DEFAULT_WORKSPACE_LOGO
|
logo={getImageAbsoluteURI(
|
||||||
: workspace.logo,
|
workspace.logo ?? DEFAULT_WORKSPACE_LOGO,
|
||||||
) ?? ''
|
)}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
selected={currentWorkspace?.id === workspace.id}
|
||||||
selected={currentWorkspace?.id === workspace.id}
|
onClick={(event) => {
|
||||||
onClick={() => handleChange(workspace.id)}
|
event?.preventDefault();
|
||||||
/>
|
handleChange(workspace.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledLink>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
|
|||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
||||||
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -60,14 +61,17 @@ export const NavigationDrawerHeader = ({
|
|||||||
}: NavigationDrawerHeaderProps) => {
|
}: NavigationDrawerHeaderProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const workspaces = useRecoilValue(workspacesState);
|
const workspaces = useRecoilValue(workspacesState);
|
||||||
const isMultiWorkspace = workspaces !== null && workspaces.length > 1;
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
|
|
||||||
const isNavigationDrawerExpanded = useRecoilValue(
|
const isNavigationDrawerExpanded = useRecoilValue(
|
||||||
isNavigationDrawerExpandedState,
|
isNavigationDrawerExpandedState,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{isMultiWorkspace ? (
|
{isMultiWorkspaceEnabled &&
|
||||||
|
workspaces !== null &&
|
||||||
|
workspaces.length > 1 ? (
|
||||||
<MultiWorkspaceDropdownButton workspaces={workspaces} />
|
<MultiWorkspaceDropdownButton workspaces={workspaces} />
|
||||||
) : (
|
) : (
|
||||||
<StyledSingleWorkspaceContainer>
|
<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 { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import {
|
|
||||||
SignInUpStep,
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
signInUpStepState,
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
} from '@/auth/states/signInUpStepState';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
import { useSwitchWorkspaceMutation } from '~/generated/graphql';
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { useGenerateJwtMutation } from '~/generated/graphql';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { sleep } from '~/utils/sleep';
|
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||||
|
|
||||||
export const useWorkspaceSwitching = () => {
|
export const useWorkspaceSwitching = () => {
|
||||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
const [switchWorkspaceMutation] = useSwitchWorkspaceMutation();
|
||||||
const [generateJWT] = useGenerateJwtMutation();
|
|
||||||
const { redirectToSSOLoginPage } = useSSO();
|
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
const setAvailableWorkspacesForSSOState = useSetRecoilState(
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
availableSSOIdentityProvidersState,
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
);
|
const { redirectToHome, redirectToWorkspace } = useUrlManager();
|
||||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
|
||||||
const { clearSession } = useAuth();
|
|
||||||
|
|
||||||
const switchWorkspace = async (workspaceId: string) => {
|
const switchWorkspace = async (workspaceId: string) => {
|
||||||
if (currentWorkspace?.id === workspaceId) return;
|
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: {
|
variables: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDefined(jwt.errors)) {
|
if (isDefined(errors) || !isDefined(data?.switchWorkspace.subdomain)) {
|
||||||
throw jwt.errors;
|
return redirectToHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDefined(jwt.data?.generateJWT)) {
|
redirectToWorkspace(data.switchWorkspace.subdomain);
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { switchWorkspace };
|
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
|
allowImpersonation
|
||||||
activationStatus
|
activationStatus
|
||||||
isPublicInviteLinkEnabled
|
isPublicInviteLinkEnabled
|
||||||
|
isGoogleAuthEnabled
|
||||||
|
isMicrosoftAuthEnabled
|
||||||
|
isPasswordAuthEnabled
|
||||||
|
subdomain
|
||||||
hasValidEntrepriseKey
|
hasValidEntrepriseKey
|
||||||
featureFlags {
|
featureFlags {
|
||||||
id
|
id
|
||||||
@ -53,6 +57,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
logo
|
logo
|
||||||
displayName
|
displayName
|
||||||
domainName
|
domainName
|
||||||
|
subdomain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userVars
|
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`
|
export const ACTIVATE_WORKSPACE = gql`
|
||||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||||
activateWorkspace(data: $input) {
|
activateWorkspace(data: $input) {
|
||||||
id
|
workspace {
|
||||||
|
id
|
||||||
|
subdomain
|
||||||
|
}
|
||||||
|
loginToken {
|
||||||
|
...AuthTokenFragment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -5,9 +5,14 @@ export const UPDATE_WORKSPACE = gql`
|
|||||||
updateWorkspace(data: $input) {
|
updateWorkspace(data: $input) {
|
||||||
id
|
id
|
||||||
domainName
|
domainName
|
||||||
|
subdomain
|
||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
allowImpersonation
|
allowImpersonation
|
||||||
|
isPublicInviteLinkEnabled
|
||||||
|
isGoogleAuthEnabled
|
||||||
|
isMicrosoftAuthEnabled
|
||||||
|
isPasswordAuthEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Logo } from '@/auth/components/Logo';
|
import { Logo } from '@/auth/components/Logo';
|
||||||
import { Title } from '@/auth/components/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
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 { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
@ -16,6 +16,7 @@ import {
|
|||||||
useAddUserToWorkspaceMutation,
|
useAddUserToWorkspaceMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||||
@ -28,6 +29,7 @@ export const Invite = () => {
|
|||||||
|
|
||||||
const { form } = useSignInUpForm();
|
const { form } = useSignInUpForm();
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
||||||
const [addUserToWorkspaceByInviteToken] =
|
const [addUserToWorkspaceByInviteToken] =
|
||||||
useAddUserToWorkspaceByInviteTokenMutation();
|
useAddUserToWorkspaceByInviteTokenMutation();
|
||||||
@ -77,7 +79,7 @@ export const Invite = () => {
|
|||||||
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
|
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
|
||||||
</AnimatedEaseIn>
|
</AnimatedEaseIn>
|
||||||
<Title animate>{title}</Title>
|
<Title animate>{title}</Title>
|
||||||
{isDefined(currentWorkspace) ? (
|
{isDefined(currentUser) ? (
|
||||||
<>
|
<>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<MainButton
|
<MainButton
|
||||||
@ -91,7 +93,7 @@ export const Invite = () => {
|
|||||||
<FooterNote />
|
<FooterNote />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<SignInUpForm />
|
<SignInUpWorkspaceScopeForm />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { useState } from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { AnimatedEaseIn, MainButton } from 'twenty-ui';
|
import { AnimatedEaseIn, MainButton } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
useValidatePasswordResetTokenQuery,
|
useValidatePasswordResetTokenQuery,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
|
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||||
|
|
||||||
const validationSchema = z
|
const validationSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -71,6 +72,8 @@ const StyledInputContainer = styled.div`
|
|||||||
export const PasswordReset = () => {
|
export const PasswordReset = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@ -163,7 +166,7 @@ export const PasswordReset = () => {
|
|||||||
isTokenValid && (
|
isTokenValid && (
|
||||||
<StyledMainContainer>
|
<StyledMainContainer>
|
||||||
<AnimatedEaseIn>
|
<AnimatedEaseIn>
|
||||||
<Logo />
|
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||||
</AnimatedEaseIn>
|
</AnimatedEaseIn>
|
||||||
<Title animate>Reset Password</Title>
|
<Title animate>Reset Password</Title>
|
||||||
<StyledContentContainer>
|
<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 { 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 { Logo } from '@/auth/components/Logo';
|
||||||
import { Title } from '@/auth/components/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||||
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
import { SignInUpStep } from '@/auth/states/signInUpStepState';
|
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||||
import { IconLockCustom } from '@ui/display/icon/components/IconLock';
|
import { useMemo } from 'react';
|
||||||
import { AnimatedEaseIn } from 'twenty-ui';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { SSOWorkspaceSelection } from './SSOWorkspaceSelection';
|
|
||||||
|
|
||||||
export const SignInUp = () => {
|
export const SignInUp = () => {
|
||||||
const { form } = useSignInUpForm();
|
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 (
|
if (
|
||||||
signInUpStep === SignInUpStep.Init ||
|
(!isMultiWorkspaceEnabled ||
|
||||||
signInUpStep === SignInUpStep.Email
|
(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)) {
|
if (
|
||||||
return <></>;
|
isDefined(workspacePublicData) &&
|
||||||
}
|
(!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain)
|
||||||
|
) {
|
||||||
|
return <SignInUpWorkspaceScopeForm />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SignInUpGlobalScopeForm />;
|
||||||
|
}, [
|
||||||
|
isTwentyHomePage,
|
||||||
|
isMultiWorkspaceEnabled,
|
||||||
|
isTwentyWorkspaceSubdomain,
|
||||||
|
signInUpStep,
|
||||||
|
workspacePublicData,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatedEaseIn>
|
<AnimatedEaseIn>
|
||||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
<Logo secondaryLogo={workspacePublicData?.logo} />
|
||||||
<IconLockCustom size={40} />
|
|
||||||
) : (
|
|
||||||
<Logo />
|
|
||||||
)}
|
|
||||||
</AnimatedEaseIn>
|
</AnimatedEaseIn>
|
||||||
<Title animate>{title}</Title>
|
<Title animate>
|
||||||
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
|
{`Welcome to ${workspacePublicData?.displayName ?? DEFAULT_WORKSPACE_NAME}`}
|
||||||
<SSOWorkspaceSelection />
|
</Title>
|
||||||
) : (
|
{signInUpForm}
|
||||||
<SignInUpForm />
|
{signInUpStep !== SignInUpStep.Password && <FooterNote />}
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -22,6 +22,9 @@ import {
|
|||||||
useActivateWorkspaceMutation,
|
useActivateWorkspaceMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
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`
|
const StyledContentContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -47,6 +50,8 @@ type Form = z.infer<typeof validationSchema>;
|
|||||||
export const CreateWorkspace = () => {
|
export const CreateWorkspace = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
|
const { redirectToWorkspace } = useUrlManager();
|
||||||
|
|
||||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
@ -75,8 +80,19 @@ export const CreateWorkspace = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsCurrentUserLoaded(false);
|
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({
|
await apolloMetadataClient?.refetchQueries({
|
||||||
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
||||||
});
|
});
|
||||||
@ -93,7 +109,9 @@ export const CreateWorkspace = () => {
|
|||||||
[
|
[
|
||||||
activateWorkspace,
|
activateWorkspace,
|
||||||
setIsCurrentUserLoaded,
|
setIsCurrentUserLoaded,
|
||||||
|
isMultiWorkspaceEnabled,
|
||||||
apolloMetadataClient,
|
apolloMetadataClient,
|
||||||
|
redirectToWorkspace,
|
||||||
enqueueSnackBar,
|
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 { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
||||||
@ -9,39 +13,61 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace
|
|||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import packageJson from '../../../package.json';
|
import packageJson from '../../../package.json';
|
||||||
export const SettingsWorkspace = () => (
|
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||||
<SubMenuTopBarContainer
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
title="General"
|
|
||||||
links={[
|
const StyledLink = styled(Link)`
|
||||||
{
|
text-decoration: none;
|
||||||
children: 'Workspace',
|
`;
|
||||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
|
||||||
},
|
export const SettingsWorkspace = () => {
|
||||||
{ children: 'General' },
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
]}
|
return (
|
||||||
>
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
title="General"
|
||||||
<Section>
|
links={[
|
||||||
<H2Title title="Picture" />
|
{
|
||||||
<WorkspaceLogoUploader />
|
children: 'Workspace',
|
||||||
</Section>
|
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||||
<Section>
|
},
|
||||||
<H2Title title="Name" description="Name of your workspace" />
|
{ children: 'General' },
|
||||||
<NameField />
|
]}
|
||||||
</Section>
|
>
|
||||||
<Section>
|
<SettingsPageContainer>
|
||||||
<H2Title
|
<Section>
|
||||||
title="Support"
|
<H2Title title="Picture" />
|
||||||
adornment={<ToggleImpersonate />}
|
<WorkspaceLogoUploader />
|
||||||
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>
|
||||||
</Section>
|
<H2Title title="Name" description="Name of your workspace" />
|
||||||
<Section>
|
<NameField />
|
||||||
<DeleteWorkspace />
|
</Section>
|
||||||
</Section>
|
{isMultiWorkspaceEnabled && (
|
||||||
<Section>
|
<Section>
|
||||||
<GithubVersionLink version={packageJson.version} />
|
<H2Title
|
||||||
</Section>
|
title="Domain"
|
||||||
</SettingsPageContainer>
|
description="Edit your subdomain name or set a custom domain."
|
||||||
</SubMenuTopBarContainer>
|
/>
|
||||||
);
|
<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 { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
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`
|
const StyledContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -26,6 +29,10 @@ const StyledSSOSection = styled(Section)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsSecurity = () => {
|
export const SettingsSecurity = () => {
|
||||||
|
const isSSOEnabled = useRecoilValue(isSSOEnabledState);
|
||||||
|
const isSSOSectionDisplay =
|
||||||
|
useIsFeatureEnabled('IS_SSO_ENABLED') && isSSOEnabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title="Security"
|
title="Security"
|
||||||
@ -40,26 +47,28 @@ export const SettingsSecurity = () => {
|
|||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledMainContent>
|
<StyledMainContent>
|
||||||
<StyledSSOSection>
|
{isSSOSectionDisplay && (
|
||||||
<H2Title
|
<StyledSSOSection>
|
||||||
title="SSO"
|
<H2Title
|
||||||
description="Configure an SSO connection"
|
title="SSO"
|
||||||
adornment={
|
description="Configure an SSO connection"
|
||||||
<Tag
|
adornment={
|
||||||
text={'Enterprise'}
|
<Tag
|
||||||
color={'transparent'}
|
text={'Enterprise'}
|
||||||
Icon={IconLock}
|
color={'transparent'}
|
||||||
variant={'border'}
|
Icon={IconLock}
|
||||||
/>
|
variant={'border'}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
<SettingsSSOIdentitiesProvidersListCard />
|
/>
|
||||||
</StyledSSOSection>
|
<SettingsSSOIdentitiesProvidersListCard />
|
||||||
|
</StyledSSOSection>
|
||||||
|
)}
|
||||||
<Section>
|
<Section>
|
||||||
<AdvancedSettingsWrapper>
|
<AdvancedSettingsWrapper>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Other"
|
title="Authentication"
|
||||||
description="Customize your workspace security"
|
description="Customize your workspace security"
|
||||||
/>
|
/>
|
||||||
<SettingsSecurityOptionsList />
|
<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 { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||||
import { IconsProvider } from 'twenty-ui';
|
import { IconsProvider } from 'twenty-ui';
|
||||||
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
|
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
|
||||||
|
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||||
|
|
||||||
export type PageDecoratorArgs = {
|
export type PageDecoratorArgs = {
|
||||||
routePath: string;
|
routePath: string;
|
||||||
@ -72,6 +73,7 @@ const Providers = () => {
|
|||||||
<ClientConfigProviderEffect />
|
<ClientConfigProviderEffect />
|
||||||
<ClientConfigProvider>
|
<ClientConfigProvider>
|
||||||
<UserProviderEffect />
|
<UserProviderEffect />
|
||||||
|
<WorkspaceProviderEffect />
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ApolloMetadataClientMockedProvider>
|
<ApolloMetadataClientMockedProvider>
|
||||||
<ObjectMetadataItemsProvider>
|
<ObjectMetadataItemsProvider>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/gen
|
|||||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||||
import { mockedRemoteServers } from './mock-data/remote-servers';
|
import { mockedRemoteServers } from './mock-data/remote-servers';
|
||||||
import { mockedViewFieldsData } from './mock-data/view-fields';
|
import { mockedViewFieldsData } from './mock-data/view-fields';
|
||||||
|
import { GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataBySubdomain';
|
||||||
|
|
||||||
const peopleMock = getPeopleMock();
|
const peopleMock = getPeopleMock();
|
||||||
const companiesMock = getCompaniesMock();
|
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) ?? '', () => {
|
graphql.mutation(getOperationName(TRACK) ?? '', () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -3,18 +3,13 @@ import { CaptchaDriverType } from '~/generated/graphql';
|
|||||||
|
|
||||||
export const mockedClientConfig: ClientConfig = {
|
export const mockedClientConfig: ClientConfig = {
|
||||||
signInPrefilled: true,
|
signInPrefilled: true,
|
||||||
signUpDisabled: false,
|
isMultiWorkspaceEnabled: false,
|
||||||
|
isSSOEnabled: false,
|
||||||
|
frontDomain: 'localhost',
|
||||||
|
defaultSubdomain: 'app',
|
||||||
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
chromeExtensionId: 'MOCKED_EXTENSION_ID',
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
analyticsEnabled: true,
|
analyticsEnabled: true,
|
||||||
authProviders: {
|
|
||||||
sso: false,
|
|
||||||
google: true,
|
|
||||||
password: true,
|
|
||||||
magicLink: false,
|
|
||||||
microsoft: false,
|
|
||||||
__typename: 'AuthProviders',
|
|
||||||
},
|
|
||||||
support: {
|
support: {
|
||||||
supportDriver: 'front',
|
supportDriver: 'front',
|
||||||
supportFrontChatId: null,
|
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=';
|
'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 = {
|
export const mockDefaultWorkspace: Workspace = {
|
||||||
|
subdomain: 'acme.twenty.com',
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w',
|
||||||
displayName: 'Twenty',
|
displayName: 'Twenty',
|
||||||
domainName: 'twenty.com',
|
domainName: 'twenty.com',
|
||||||
@ -45,6 +46,9 @@ export const mockDefaultWorkspace: Workspace = {
|
|||||||
allowImpersonation: true,
|
allowImpersonation: true,
|
||||||
activationStatus: WorkspaceActivationStatus.Active,
|
activationStatus: WorkspaceActivationStatus.Active,
|
||||||
hasValidEntrepriseKey: false,
|
hasValidEntrepriseKey: false,
|
||||||
|
isGoogleAuthEnabled: true,
|
||||||
|
isPasswordAuthEnabled: true,
|
||||||
|
isMicrosoftAuthEnabled: false,
|
||||||
featureFlags: [
|
featureFlags: [
|
||||||
{
|
{
|
||||||
id: '1492de61-5018-4368-8923-4f1eeaf988c4',
|
id: '1492de61-5018-4368-8923-4f1eeaf988c4',
|
||||||
|
|||||||
@ -16,9 +16,9 @@ class CookieStorage {
|
|||||||
Cookies.set(key, value, attributes);
|
Cookies.set(key, value, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeItem(key: string): void {
|
removeItem(key: string, attributes?: Cookies.CookieAttributes): void {
|
||||||
this.keys.delete(key);
|
this.keys.delete(key);
|
||||||
Cookies.remove(key);
|
Cookies.remove(key, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { AtomEffect } from 'recoil';
|
import { AtomEffect } from 'recoil';
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
import { cookieStorage } from '~/utils/cookie-storage';
|
import { cookieStorage } from '~/utils/cookie-storage';
|
||||||
|
|
||||||
@ -20,25 +21,50 @@ export const localStorageEffect =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const cookieStorageEffect =
|
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 }) => {
|
({ setSelf, onSet }) => {
|
||||||
const savedValue = cookieStorage.getItem(key);
|
const savedValue = cookieStorage.getItem(key);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(savedValue) &&
|
isDefined(savedValue) &&
|
||||||
isDefined(JSON.parse(savedValue)['accessToken'])
|
(!isDefined(hooks?.validateInitFn) ||
|
||||||
|
hooks.validateInitFn(JSON.parse(savedValue)))
|
||||||
) {
|
) {
|
||||||
setSelf(JSON.parse(savedValue));
|
setSelf(JSON.parse(savedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultAttributes = {
|
||||||
|
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||||
|
...(attributes ?? {}),
|
||||||
|
};
|
||||||
|
|
||||||
onSet((newValue, _, isReset) => {
|
onSet((newValue, _, isReset) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
cookieStorage.removeItem(key);
|
cookieStorage.removeItem(key, defaultAttributes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cookieAttributes = {
|
||||||
|
...defaultAttributes,
|
||||||
|
...(typeof newValue === 'object' &&
|
||||||
|
'cookieAttributes' in newValue &&
|
||||||
|
typeof newValue.cookieAttributes === 'object'
|
||||||
|
? newValue.cookieAttributes
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
isReset
|
isReset
|
||||||
? cookieStorage.removeItem(key)
|
? cookieStorage.removeItem(key, defaultAttributes)
|
||||||
: cookieStorage.setItem(key, JSON.stringify(newValue), {
|
: cookieStorage.setItem(
|
||||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
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
|
# IS_BILLING_ENABLED=false
|
||||||
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
||||||
# AUTH_PASSWORD_ENABLED=false
|
# AUTH_PASSWORD_ENABLED=false
|
||||||
# IS_SIGN_UP_DISABLED=false
|
# IS_MULTIWORKSPACE_ENABLED=false
|
||||||
# AUTH_MICROSOFT_ENABLED=false
|
# AUTH_MICROSOFT_ENABLED=false
|
||||||
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
|
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
|
||||||
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_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'
|
| 'domainName'
|
||||||
| 'inviteHash'
|
| 'inviteHash'
|
||||||
| 'logo'
|
| 'logo'
|
||||||
|
| 'subdomain'
|
||||||
| 'activationStatus'
|
| 'activationStatus'
|
||||||
>;
|
>;
|
||||||
} = {
|
} = {
|
||||||
@ -30,6 +31,7 @@ export const seedWorkspaces = async (
|
|||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
displayName: 'Apple',
|
displayName: 'Apple',
|
||||||
domainName: 'apple.dev',
|
domainName: 'apple.dev',
|
||||||
|
subdomain: 'apple',
|
||||||
inviteHash: 'apple.dev-invite-hash',
|
inviteHash: 'apple.dev-invite-hash',
|
||||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
@ -38,6 +40,7 @@ export const seedWorkspaces = async (
|
|||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
displayName: 'Acme',
|
displayName: 'Acme',
|
||||||
domainName: 'acme.dev',
|
domainName: 'acme.dev',
|
||||||
|
subdomain: 'acme',
|
||||||
inviteHash: 'acme.dev-invite-hash',
|
inviteHash: 'acme.dev-invite-hash',
|
||||||
logo: 'https://logos-world.net/wp-content/uploads/2022/05/Acme-Logo-700x394.png',
|
logo: 'https://logos-world.net/wp-content/uploads/2022/05/Acme-Logo-700x394.png',
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
@ -51,6 +54,7 @@ export const seedWorkspaces = async (
|
|||||||
'id',
|
'id',
|
||||||
'displayName',
|
'displayName',
|
||||||
'domainName',
|
'domainName',
|
||||||
|
'subdomain',
|
||||||
'inviteHash',
|
'inviteHash',
|
||||||
'logo',
|
'logo',
|
||||||
'activationStatus',
|
'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 { 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 { Query } from 'src/engine/api/rest/core/types/query.type';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
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()
|
@Injectable()
|
||||||
export class CoreQueryBuilderFactory {
|
export class CoreQueryBuilderFactory {
|
||||||
@ -40,7 +40,7 @@ export class CoreQueryBuilderFactory {
|
|||||||
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
|
||||||
private readonly objectMetadataService: ObjectMetadataService,
|
private readonly objectMetadataService: ObjectMetadataService,
|
||||||
private readonly accessTokenService: AccessTokenService,
|
private readonly accessTokenService: AccessTokenService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getObjectMetadata(
|
async getObjectMetadata(
|
||||||
@ -50,16 +50,20 @@ export class CoreQueryBuilderFactory {
|
|||||||
objectMetadataItems: ObjectMetadataEntity[];
|
objectMetadataItems: ObjectMetadataEntity[];
|
||||||
objectMetadataItem: ObjectMetadataEntity;
|
objectMetadataItem: ObjectMetadataEntity;
|
||||||
}> {
|
}> {
|
||||||
const { workspace } = await this.accessTokenService.validateToken(request);
|
const { workspace } =
|
||||||
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
|
|
||||||
const objectMetadataItems =
|
const objectMetadataItems =
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
||||||
|
|
||||||
if (!objectMetadataItems.length) {
|
if (!objectMetadataItems.length) {
|
||||||
throw new BadRequestException(
|
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(
|
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.domainManagerService
|
||||||
'FRONT_BASE_URL',
|
.buildWorkspaceURL({
|
||||||
)}/settings/developers`,
|
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 { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ObjectMetadataModule, AuthModule],
|
imports: [ObjectMetadataModule, AuthModule, DomainManagerModule],
|
||||||
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory],
|
||||||
exports: [CoreQueryBuilderFactory],
|
exports: [CoreQueryBuilderFactory],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export class RestApiMetadataService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async get(request: Request) {
|
async get(request: Request) {
|
||||||
await this.accessTokenService.validateToken(request);
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
const data = await this.metadataQueryBuilderFactory.get(request);
|
const data = await this.metadataQueryBuilderFactory.get(request);
|
||||||
|
|
||||||
return await this.restApiService.call(
|
return await this.restApiService.call(
|
||||||
@ -29,7 +29,7 @@ export class RestApiMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(request: Request) {
|
async create(request: Request) {
|
||||||
await this.accessTokenService.validateToken(request);
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
const data = await this.metadataQueryBuilderFactory.create(request);
|
const data = await this.metadataQueryBuilderFactory.create(request);
|
||||||
|
|
||||||
return await this.restApiService.call(
|
return await this.restApiService.call(
|
||||||
@ -40,7 +40,7 @@ export class RestApiMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(request: Request) {
|
async update(request: Request) {
|
||||||
await this.accessTokenService.validateToken(request);
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
const data = await this.metadataQueryBuilderFactory.update(request);
|
const data = await this.metadataQueryBuilderFactory.update(request);
|
||||||
|
|
||||||
return await this.restApiService.call(
|
return await this.restApiService.call(
|
||||||
@ -51,7 +51,7 @@ export class RestApiMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(request: Request) {
|
async delete(request: Request) {
|
||||||
await this.accessTokenService.validateToken(request);
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
const data = await this.metadataQueryBuilderFactory.delete(request);
|
const data = await this.metadataQueryBuilderFactory.delete(request);
|
||||||
|
|
||||||
return await this.restApiService.call(
|
return await this.restApiService.call(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable no-restricted-imports */
|
/* eslint-disable no-restricted-imports */
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
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 { 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 { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
|
||||||
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-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 { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.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';
|
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 { 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 { 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 { 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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.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 { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
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 { 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 { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
|
import { 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';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@ -54,7 +54,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
JwtModule,
|
JwtModule,
|
||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
forwardRef(() => UserModule),
|
DomainManagerModule,
|
||||||
|
TokenModule,
|
||||||
|
UserModule,
|
||||||
WorkspaceManagerModule,
|
WorkspaceManagerModule,
|
||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
@ -69,22 +71,20 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
'core',
|
'core',
|
||||||
),
|
),
|
||||||
HttpModule,
|
HttpModule,
|
||||||
TokenModule,
|
|
||||||
UserWorkspaceModule,
|
UserWorkspaceModule,
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
OnboardingModule,
|
OnboardingModule,
|
||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
WorkspaceInvitationModule,
|
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
WorkspaceSSOModule,
|
WorkspaceSSOModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
|
WorkspaceInvitationModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
GoogleAuthController,
|
GoogleAuthController,
|
||||||
MicrosoftAuthController,
|
MicrosoftAuthController,
|
||||||
GoogleAPIsAuthController,
|
GoogleAPIsAuthController,
|
||||||
MicrosoftAPIsAuthController,
|
MicrosoftAPIsAuthController,
|
||||||
VerifyAuthController,
|
|
||||||
SSOAuthController,
|
SSOAuthController,
|
||||||
],
|
],
|
||||||
providers: [
|
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 { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
|
|
||||||
import { AuthResolver } from './auth.resolver';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@ -43,6 +44,14 @@ describe('AuthResolver', () => {
|
|||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: DomainManagerService,
|
||||||
|
useValue: {
|
||||||
|
buildWorkspaceURL: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(new URL('http://localhost:3001')),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: UserWorkspaceService,
|
provide: UserWorkspaceService,
|
||||||
useValue: {},
|
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 { 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 { 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 { 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 { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||||
import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token.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';
|
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import { 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 { ChallengeInput } from './dto/challenge.input';
|
||||||
import { LoginToken } from './dto/login-token.entity';
|
import { LoginToken } from './dto/login-token.entity';
|
||||||
import { SignUpInput } from './dto/sign-up.input';
|
import { SignUpInput } from './dto/sign-up.input';
|
||||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
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 { CheckUserExistsInput } from './dto/user-exists.input';
|
||||||
import { Verify } from './dto/verify.entity';
|
import { Verify } from './dto/verify.entity';
|
||||||
import { VerifyInput } from './dto/verify.input';
|
import { VerifyInput } from './dto/verify.input';
|
||||||
@ -62,18 +66,15 @@ export class AuthResolver {
|
|||||||
private switchWorkspaceService: SwitchWorkspaceService,
|
private switchWorkspaceService: SwitchWorkspaceService,
|
||||||
private transientTokenService: TransientTokenService,
|
private transientTokenService: TransientTokenService,
|
||||||
private oauthService: OAuthService,
|
private oauthService: OAuthService,
|
||||||
|
private domainManagerService: DomainManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@Query(() => UserExists)
|
@Query(() => UserExistsOutput)
|
||||||
async checkUserExists(
|
async checkUserExists(
|
||||||
@Args() checkUserExistsInput: CheckUserExistsInput,
|
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||||
): Promise<UserExists> {
|
): Promise<typeof UserExistsOutput> {
|
||||||
const { exists } = await this.authService.checkUserExists(
|
return await this.authService.checkUserExists(checkUserExistsInput.email);
|
||||||
checkUserExistsInput.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { exists };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => WorkspaceInviteHashValid)
|
@Query(() => WorkspaceInviteHashValid)
|
||||||
@ -96,8 +97,20 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@Mutation(() => LoginToken)
|
@Mutation(() => LoginToken)
|
||||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
async challenge(
|
||||||
const user = await this.authService.challenge(challengeInput);
|
@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(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
);
|
);
|
||||||
@ -107,10 +120,22 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@Mutation(() => LoginToken)
|
@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({
|
const user = await this.authService.signInUp({
|
||||||
...signUpInput,
|
...signUpInput,
|
||||||
|
targetWorkspaceSubdomain:
|
||||||
|
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||||
fromSSO: false,
|
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(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
@ -124,11 +149,9 @@ export class AuthResolver {
|
|||||||
async exchangeAuthorizationCode(
|
async exchangeAuthorizationCode(
|
||||||
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||||
) {
|
) {
|
||||||
const tokens = await this.oauthService.verifyAuthorizationCode(
|
return await this.oauthService.verifyAuthorizationCode(
|
||||||
exchangeAuthCodeInput,
|
exchangeAuthCodeInput,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TransientToken)
|
@Mutation(() => TransientToken)
|
||||||
@ -156,14 +179,18 @@ export class AuthResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Verify)
|
@Mutation(() => Verify)
|
||||||
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
|
async verify(
|
||||||
const email = await this.loginTokenService.verifyLoginToken(
|
@Args() verifyInput: VerifyInput,
|
||||||
|
@OriginHeader() origin: string,
|
||||||
|
): Promise<Verify> {
|
||||||
|
const workspace =
|
||||||
|
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||||
|
|
||||||
|
const { sub: email } = await this.loginTokenService.verifyLoginToken(
|
||||||
verifyInput.loginToken,
|
verifyInput.loginToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.authService.verify(email);
|
return await this.authService.verify(email, workspace?.id);
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AuthorizeApp)
|
@Mutation(() => AuthorizeApp)
|
||||||
@ -172,50 +199,22 @@ export class AuthResolver {
|
|||||||
@Args() authorizeAppInput: AuthorizeAppInput,
|
@Args() authorizeAppInput: AuthorizeAppInput,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
): Promise<AuthorizeApp> {
|
): Promise<AuthorizeApp> {
|
||||||
const authorizedApp = await this.authService.generateAuthorizationCode(
|
return await this.authService.generateAuthorizationCode(
|
||||||
authorizeAppInput,
|
authorizeAppInput,
|
||||||
user,
|
user,
|
||||||
);
|
);
|
||||||
|
|
||||||
return authorizedApp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => GenerateJWTOutput)
|
@Mutation(() => PublicWorkspaceDataOutput)
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||||
async generateJWT(
|
async switchWorkspace(
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@Args() args: GenerateJwtInput,
|
@Args() args: SwitchWorkspaceInput,
|
||||||
): Promise<GenerateJWTOutputWithAuthTokens | GenerateJWTOutputWithSSOAUTH> {
|
): Promise<PublicWorkspaceDataOutput> {
|
||||||
const result = await this.switchWorkspaceService.switchWorkspace(
|
return await this.switchWorkspaceService.switchWorkspace(
|
||||||
user,
|
user,
|
||||||
args.workspaceId,
|
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)
|
@Mutation(() => AuthTokens)
|
||||||
@ -278,4 +277,11 @@ export class AuthResolver {
|
|||||||
args.passwordResetToken,
|
args.passwordResetToken,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query(() => [AvailableWorkspaceOutput])
|
||||||
|
async findAvailableWorkspacesByEmail(
|
||||||
|
@Args('email') email: string,
|
||||||
|
): Promise<AvailableWorkspaceOutput[]> {
|
||||||
|
return this.authService.findAvailableWorkspacesByEmail(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import {
|
|||||||
UseFilters,
|
UseFilters,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthException,
|
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 { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.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')
|
@Controller('auth/google-apis')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -30,6 +34,9 @@ export class GoogleAPIsAuthController {
|
|||||||
private readonly transientTokenService: TransientTokenService,
|
private readonly transientTokenService: TransientTokenService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly onboardingService: OnboardingService,
|
private readonly onboardingService: OnboardingService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@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(
|
return res.redirect(
|
||||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
this.domainManagerService
|
||||||
redirectLocation || '/settings/accounts'
|
.buildWorkspaceURL({
|
||||||
}`,
|
subdomain: workspace.subdomain,
|
||||||
|
pathname: redirectLocation || '/settings/accounts',
|
||||||
|
})
|
||||||
|
.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import {
|
|||||||
UseFilters,
|
UseFilters,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
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 { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
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 { 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')
|
@Controller('auth/google')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -23,6 +33,10 @@ export class GoogleAuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -36,29 +50,81 @@ export class GoogleAuthController {
|
|||||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||||
@UseFilters(AuthOAuthExceptionFilter)
|
@UseFilters(AuthOAuthExceptionFilter)
|
||||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||||
const {
|
try {
|
||||||
firstName,
|
const {
|
||||||
lastName,
|
firstName,
|
||||||
email,
|
lastName,
|
||||||
picture,
|
email,
|
||||||
workspaceInviteHash,
|
picture,
|
||||||
workspacePersonalInviteToken,
|
workspaceInviteHash,
|
||||||
} = req.user;
|
workspacePersonalInviteToken,
|
||||||
|
targetWorkspaceSubdomain,
|
||||||
|
} = req.user;
|
||||||
|
|
||||||
const user = await this.authService.signInUp({
|
const signInUpParams = {
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
picture,
|
picture,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
workspacePersonalInviteToken,
|
workspacePersonalInviteToken,
|
||||||
fromSSO: true,
|
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(
|
if (
|
||||||
user.email,
|
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,
|
UseFilters,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthException,
|
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 { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.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')
|
@Controller('auth/microsoft-apis')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -29,7 +34,11 @@ export class MicrosoftAPIsAuthController {
|
|||||||
private readonly microsoftAPIsService: MicrosoftAPIsService,
|
private readonly microsoftAPIsService: MicrosoftAPIsService,
|
||||||
private readonly transientTokenService: TransientTokenService,
|
private readonly transientTokenService: TransientTokenService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly onboardingService: OnboardingService,
|
private readonly onboardingService: OnboardingService,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@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(
|
return res.redirect(
|
||||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
this.domainManagerService
|
||||||
redirectLocation || '/settings/accounts'
|
.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 { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
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 { 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')
|
@Controller('auth/microsoft')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -22,6 +29,8 @@ export class MicrosoftAuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -37,29 +46,55 @@ export class MicrosoftAuthController {
|
|||||||
@Req() req: MicrosoftRequest,
|
@Req() req: MicrosoftRequest,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
const {
|
try {
|
||||||
firstName,
|
const {
|
||||||
lastName,
|
firstName,
|
||||||
email,
|
lastName,
|
||||||
picture,
|
email,
|
||||||
workspaceInviteHash,
|
picture,
|
||||||
workspacePersonalInviteToken,
|
workspaceInviteHash,
|
||||||
} = req.user;
|
workspacePersonalInviteToken,
|
||||||
|
targetWorkspaceSubdomain,
|
||||||
|
} = req.user;
|
||||||
|
|
||||||
const user = await this.authService.signInUp({
|
const user = await this.authService.signInUp({
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
picture,
|
picture,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
workspacePersonalInviteToken,
|
workspacePersonalInviteToken,
|
||||||
fromSSO: true,
|
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(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
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 { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
|
||||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
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 { 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 { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
import {
|
import {
|
||||||
IdentityProviderType,
|
IdentityProviderType,
|
||||||
WorkspaceSSOIdentityProvider,
|
WorkspaceSSOIdentityProvider,
|
||||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
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')
|
@Controller('auth')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -40,9 +40,9 @@ export class SSOAuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly ssoService: SSOService,
|
private readonly ssoService: SSOService,
|
||||||
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||||
@ -50,7 +50,7 @@ export class SSOAuthController {
|
|||||||
|
|
||||||
@Get('saml/metadata/:identityProviderId')
|
@Get('saml/metadata/:identityProviderId')
|
||||||
@UseGuards(SSOProviderEnabledGuard)
|
@UseGuards(SSOProviderEnabledGuard)
|
||||||
async generateMetadata(@Req() req: any): Promise<string> {
|
async generateMetadata(@Req() req: any): Promise<string | void> {
|
||||||
return generateServiceProviderMetadata({
|
return generateServiceProviderMetadata({
|
||||||
wantAssertionsSigned: false,
|
wantAssertionsSigned: false,
|
||||||
issuer: this.ssoService.buildIssuerURL({
|
issuer: this.ssoService.buildIssuerURL({
|
||||||
@ -81,14 +81,26 @@ export class SSOAuthController {
|
|||||||
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||||
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
try {
|
try {
|
||||||
const loginToken = await this.generateLoginToken(req.user);
|
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||||
|
req.user,
|
||||||
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.authService.computeRedirectURI(loginToken.token),
|
await this.authService.computeRedirectURI(
|
||||||
|
loginToken.token,
|
||||||
|
identityProvider.workspace.subdomain,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO: improve error management
|
if (err instanceof AuthException) {
|
||||||
res.status(403).send(err.message);
|
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)
|
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||||
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
try {
|
try {
|
||||||
const loginToken = await this.generateLoginToken(req.user);
|
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||||
|
req.user,
|
||||||
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.authService.computeRedirectURI(loginToken.token),
|
await this.authService.computeRedirectURI(
|
||||||
|
loginToken.token,
|
||||||
|
identityProvider.workspace.subdomain,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO: improve error management
|
if (err instanceof AuthException) {
|
||||||
res
|
return res.redirect(
|
||||||
.status(403)
|
this.domainManagerService.computeRedirectErrorUrl({
|
||||||
.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`);
|
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||||
|
errorMessage: err.message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +138,13 @@ export class SSOAuthController {
|
|||||||
identityProviderId?: string;
|
identityProviderId?: string;
|
||||||
user: { email: string } & Record<string, string>;
|
user: { email: string } & Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
|
if (!identityProviderId) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Identity provider ID is required',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const identityProvider =
|
const identityProvider =
|
||||||
await this.workspaceSSOIdentityProviderRepository.findOne({
|
await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||||
where: { id: identityProviderId },
|
where: { id: identityProviderId },
|
||||||
@ -129,20 +158,15 @@ export class SSOAuthController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitation =
|
await this.authService.signInUp({
|
||||||
await this.workspaceInvitationService.getOneWorkspaceInvitation(
|
...user,
|
||||||
identityProvider.workspaceId,
|
...(this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||||
user.email,
|
? {
|
||||||
);
|
targetWorkspaceSubdomain: identityProvider.workspace.subdomain,
|
||||||
|
}
|
||||||
if (invitation) {
|
: {}),
|
||||||
await this.authService.signInUp({
|
fromSSO: true,
|
||||||
...user,
|
});
|
||||||
workspacePersonalInviteToken: invitation.value,
|
|
||||||
workspaceInviteHash: identityProvider.workspace.inviteHash,
|
|
||||||
fromSSO: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserExistInWorkspace =
|
const isUserExistInWorkspace =
|
||||||
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
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';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class GenerateJwtInput {
|
export class SwitchWorkspaceInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@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()
|
@ObjectType()
|
||||||
export class UserExists {
|
export class UserExists {
|
||||||
@Field(() => Boolean)
|
@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
Reference in New Issue
Block a user