refactor(auth): add workspaces selection (#12098)
This commit is contained in:
@ -143,6 +143,14 @@ export type AvailableWorkspaceOutput = {
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
};
|
||||
|
||||
export type AvailableWorkspacesToJoin = {
|
||||
__typename?: 'AvailableWorkspacesToJoin';
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
workspaceUrl: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingUrl?: Maybe<Scalars['String']['output']>;
|
||||
@ -298,6 +306,13 @@ export enum CaptchaDriverType {
|
||||
TURNSTILE = 'TURNSTILE'
|
||||
}
|
||||
|
||||
export type CheckUserExistOutput = {
|
||||
__typename?: 'CheckUserExistOutput';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
exists: Scalars['Boolean']['output'];
|
||||
isEmailVerified: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
analyticsEnabled: Scalars['Boolean']['output'];
|
||||
@ -942,6 +957,7 @@ export type Mutation = {
|
||||
createOneRole: Role;
|
||||
createOneServerlessFunction: ServerlessFunction;
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createUserAndWorkspace: SignUpOutput;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
deactivateWorkflowVersion: Scalars['Boolean']['output'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
|
||||
@ -1103,6 +1119,16 @@ export type MutationCreateSamlIdentityProviderArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateUserAndWorkspaceArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']['input']>;
|
||||
email: Scalars['String']['input'];
|
||||
firstName?: InputMaybe<Scalars['String']['input']>;
|
||||
lastName?: InputMaybe<Scalars['String']['input']>;
|
||||
locale?: InputMaybe<Scalars['String']['input']>;
|
||||
picture?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionStepArgs = {
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
};
|
||||
@ -1607,7 +1633,7 @@ export type PublishServerlessFunctionInput = {
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
billingPortalSession: BillingSessionOutput;
|
||||
checkUserExists: UserExistsOutput;
|
||||
checkUserExists: CheckUserExistOutput;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
@ -1640,6 +1666,7 @@ export type Query = {
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
listAvailableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
object: Object;
|
||||
objects: ObjectConnection;
|
||||
plans: Array<BillingPlanOutput>;
|
||||
@ -1771,6 +1798,12 @@ export type QueryIndexMetadatasArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryListAvailableWorkspacesArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']['input']>;
|
||||
email: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryObjectArgs = {
|
||||
id: Scalars['UUID']['input'];
|
||||
};
|
||||
@ -2353,6 +2386,7 @@ export type UpsertSettingPermissionsInput = {
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
availableWorkspaces: Array<AvailableWorkspacesToJoin>;
|
||||
canAccessFullAdminPanel: Scalars['Boolean']['output'];
|
||||
canImpersonate: Scalars['Boolean']['output'];
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@ -2386,15 +2420,6 @@ export type UserEdge = {
|
||||
node: User;
|
||||
};
|
||||
|
||||
export type UserExists = {
|
||||
__typename?: 'UserExists';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
exists: Scalars['Boolean']['output'];
|
||||
isEmailVerified: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type UserExistsOutput = UserExists | UserNotExists;
|
||||
|
||||
export type UserInfo = {
|
||||
__typename?: 'UserInfo';
|
||||
email: Scalars['String']['output'];
|
||||
@ -2424,11 +2449,6 @@ export type UserMappingOptionsUser = {
|
||||
user?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type UserNotExists = {
|
||||
__typename?: 'UserNotExists';
|
||||
exists: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type UserWorkspace = {
|
||||
__typename?: 'UserWorkspace';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
||||
@ -126,15 +126,30 @@ export type AuthorizeApp = {
|
||||
redirectUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AvailableWorkspaceOutput = {
|
||||
__typename?: 'AvailableWorkspaceOutput';
|
||||
export type AvailableWorkspace = {
|
||||
__typename?: 'AvailableWorkspace';
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
inviteHash?: Maybe<Scalars['String']>;
|
||||
loginToken?: Maybe<Scalars['String']>;
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
personalInviteToken?: Maybe<Scalars['String']>;
|
||||
sso: Array<SsoConnection>;
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
};
|
||||
|
||||
export type AvailableWorkspaces = {
|
||||
__typename?: 'AvailableWorkspaces';
|
||||
availableWorkspacesForSignIn: Array<AvailableWorkspace>;
|
||||
availableWorkspacesForSignUp: Array<AvailableWorkspace>;
|
||||
};
|
||||
|
||||
export type AvailableWorkspacesAndAccessTokensOutput = {
|
||||
__typename?: 'AvailableWorkspacesAndAccessTokensOutput';
|
||||
availableWorkspaces: AvailableWorkspaces;
|
||||
tokens: AuthTokenPair;
|
||||
};
|
||||
|
||||
export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingUrl?: Maybe<Scalars['String']>;
|
||||
@ -290,6 +305,13 @@ export enum CaptchaDriverType {
|
||||
TURNSTILE = 'TURNSTILE'
|
||||
}
|
||||
|
||||
export type CheckUserExistOutput = {
|
||||
__typename?: 'CheckUserExistOutput';
|
||||
availableWorkspacesCount: Scalars['Float'];
|
||||
exists: Scalars['Boolean'];
|
||||
isEmailVerified: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
analyticsEnabled: Scalars['Boolean'];
|
||||
@ -922,8 +944,10 @@ export type Mutation = {
|
||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||
runWorkflowVersion: WorkflowRun;
|
||||
sendInvitations: SendInvitationsOutput;
|
||||
signUp: SignUpOutput;
|
||||
signIn: AvailableWorkspacesAndAccessTokensOutput;
|
||||
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
||||
signUpInNewWorkspace: SignUpOutput;
|
||||
signUpInWorkspace: SignUpOutput;
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||
submitFormStep: Scalars['Boolean'];
|
||||
switchToEnterprisePlan: BillingUpdateOutput;
|
||||
@ -1172,7 +1196,21 @@ export type MutationSendInvitationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignInArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignUpArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignUpInWorkspaceArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
locale?: InputMaybe<Scalars['String']>;
|
||||
@ -1510,7 +1548,7 @@ export type PublishServerlessFunctionInput = {
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
billingPortalSession: BillingSessionOutput;
|
||||
checkUserExists: UserExistsOutput;
|
||||
checkUserExists: CheckUserExistOutput;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
@ -2183,6 +2221,7 @@ export type UpsertSettingPermissionsInput = {
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
availableWorkspaces: AvailableWorkspaces;
|
||||
canAccessFullAdminPanel: Scalars['Boolean'];
|
||||
canImpersonate: Scalars['Boolean'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -2202,7 +2241,7 @@ export type User = {
|
||||
passwordHash?: Maybe<Scalars['String']>;
|
||||
supportUserHash?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userVars: Scalars['JSONObject'];
|
||||
userVars?: Maybe<Scalars['JSONObject']>;
|
||||
workspaceMember?: Maybe<WorkspaceMember>;
|
||||
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
||||
workspaces: Array<UserWorkspace>;
|
||||
@ -2216,15 +2255,6 @@ export type UserEdge = {
|
||||
node: User;
|
||||
};
|
||||
|
||||
export type UserExists = {
|
||||
__typename?: 'UserExists';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
exists: Scalars['Boolean'];
|
||||
isEmailVerified: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UserExistsOutput = UserExists | UserNotExists;
|
||||
|
||||
export type UserInfo = {
|
||||
__typename?: 'UserInfo';
|
||||
email: Scalars['String'];
|
||||
@ -2244,11 +2274,6 @@ export type UserMappingOptionsUser = {
|
||||
user?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UserNotExists = {
|
||||
__typename?: 'UserNotExists';
|
||||
exists: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UserWorkspace = {
|
||||
__typename?: 'UserWorkspace';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -2510,6 +2535,10 @@ export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: strin
|
||||
|
||||
export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } };
|
||||
|
||||
export type AvailableWorkspaceFragmentFragment = { __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> };
|
||||
|
||||
export type AvailableWorkspacesFragmentFragment = { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> };
|
||||
|
||||
export type AvailableSsoIdentityProvidersFragmentFragment = { __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } };
|
||||
|
||||
export type AuthorizeAppMutationVariables = Exact<{
|
||||
@ -2600,7 +2629,30 @@ export type ResendEmailVerificationTokenMutationVariables = Exact<{
|
||||
|
||||
export type ResendEmailVerificationTokenMutation = { __typename?: 'Mutation', resendEmailVerificationToken: { __typename?: 'ResendEmailVerificationTokenOutput', success: boolean } };
|
||||
|
||||
export type SignInMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type SignInMutation = { __typename?: 'Mutation', signIn: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
|
||||
export type SignUpMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
|
||||
export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type SignUpInNewWorkspaceMutation = { __typename?: 'Mutation', signUpInNewWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||
|
||||
export type SignUpInWorkspaceMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
@ -2612,12 +2664,7 @@ export type SignUpMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||
|
||||
export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type SignUpInNewWorkspaceMutation = { __typename?: 'Mutation', signUpInNewWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||
export type SignUpInWorkspaceMutation = { __typename?: 'Mutation', signUpInWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||
|
||||
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
|
||||
token: Scalars['String'];
|
||||
@ -2633,7 +2680,7 @@ export type CheckUserExistsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, isEmailVerified: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
|
||||
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename?: 'CheckUserExistOutput', exists: boolean, availableWorkspacesCount: number, isEmailVerified: boolean } };
|
||||
|
||||
export type GetPublicWorkspaceDataByDomainQueryVariables = Exact<{
|
||||
origin: Scalars['String'];
|
||||
@ -2909,7 +2956,9 @@ export type OnDbEventSubscriptionVariables = Exact<{
|
||||
|
||||
export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } };
|
||||
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
|
||||
|
||||
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
|
||||
|
||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -2926,7 +2975,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, 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, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
|
||||
|
||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@ -3208,6 +3257,12 @@ export const ObjectPermissionFragmentFragmentDoc = gql`
|
||||
canDestroyObjectRecords
|
||||
}
|
||||
`;
|
||||
export const WorkspaceUrlsFragmentFragmentDoc = gql`
|
||||
fragment WorkspaceUrlsFragment on WorkspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
`;
|
||||
export const RoleFragmentFragmentDoc = gql`
|
||||
fragment RoleFragment on Role {
|
||||
id
|
||||
@ -3222,6 +3277,37 @@ export const RoleFragmentFragmentDoc = gql`
|
||||
canDestroyAllObjectRecords
|
||||
}
|
||||
`;
|
||||
export const AvailableWorkspaceFragmentFragmentDoc = gql`
|
||||
fragment AvailableWorkspaceFragment on AvailableWorkspace {
|
||||
id
|
||||
displayName
|
||||
loginToken
|
||||
inviteHash
|
||||
personalInviteToken
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const AvailableWorkspacesFragmentFragmentDoc = gql`
|
||||
fragment AvailableWorkspacesFragment on AvailableWorkspaces {
|
||||
availableWorkspacesForSignIn {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
availableWorkspacesForSignUp {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
}
|
||||
${AvailableWorkspaceFragmentFragmentDoc}`;
|
||||
export const UserQueryFragmentFragmentDoc = gql`
|
||||
fragment UserQueryFragment on User {
|
||||
id
|
||||
@ -3264,8 +3350,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
customDomain
|
||||
isCustomDomainEnabled
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
@ -3302,25 +3387,17 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
...RoleFragment
|
||||
}
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
subdomain
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
userVars
|
||||
}
|
||||
${WorkspaceMemberQueryFragmentFragmentDoc}
|
||||
${DeletedWorkspaceMemberQueryFragmentFragmentDoc}
|
||||
${ObjectPermissionFragmentFragmentDoc}
|
||||
${RoleFragmentFragmentDoc}`;
|
||||
${WorkspaceUrlsFragmentFragmentDoc}
|
||||
${RoleFragmentFragmentDoc}
|
||||
${AvailableWorkspacesFragmentFragmentDoc}`;
|
||||
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
|
||||
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {
|
||||
getTimelineCalendarEventsFromCompanyId(
|
||||
@ -3858,12 +3935,12 @@ export const GetLoginTokenFromEmailVerificationTokenDocument = gql`
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
${AuthTokenFragmentFragmentDoc}
|
||||
${WorkspaceUrlsFragmentFragmentDoc}`;
|
||||
export type GetLoginTokenFromEmailVerificationTokenMutationFn = Apollo.MutationFunction<GetLoginTokenFromEmailVerificationTokenMutation, GetLoginTokenFromEmailVerificationTokenMutationVariables>;
|
||||
|
||||
/**
|
||||
@ -3898,8 +3975,7 @@ export const ImpersonateDocument = gql`
|
||||
impersonate(userId: $userId, workspaceId: $workspaceId) {
|
||||
workspace {
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
id
|
||||
}
|
||||
@ -3908,7 +3984,8 @@ export const ImpersonateDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
${WorkspaceUrlsFragmentFragmentDoc}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
export type ImpersonateMutationFn = Apollo.MutationFunction<ImpersonateMutation, ImpersonateMutationVariables>;
|
||||
|
||||
/**
|
||||
@ -4005,31 +4082,60 @@ export function useResendEmailVerificationTokenMutation(baseOptions?: Apollo.Mut
|
||||
export type ResendEmailVerificationTokenMutationHookResult = ReturnType<typeof useResendEmailVerificationTokenMutation>;
|
||||
export type ResendEmailVerificationTokenMutationResult = Apollo.MutationResult<ResendEmailVerificationTokenMutation>;
|
||||
export type ResendEmailVerificationTokenMutationOptions = Apollo.BaseMutationOptions<ResendEmailVerificationTokenMutation, ResendEmailVerificationTokenMutationVariables>;
|
||||
export const SignUpDocument = gql`
|
||||
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
export const SignInDocument = gql`
|
||||
mutation SignIn($email: String!, $password: String!, $captchaToken: String) {
|
||||
signIn(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
${AvailableWorkspacesFragmentFragmentDoc}
|
||||
${AuthTokensFragmentFragmentDoc}`;
|
||||
export type SignInMutationFn = Apollo.MutationFunction<SignInMutation, SignInMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignInMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignInMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignInMutation` 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 [signInMutation, { data, loading, error }] = useSignInMutation({
|
||||
* variables: {
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignInMutation(baseOptions?: Apollo.MutationHookOptions<SignInMutation, SignInMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignInMutation, SignInMutationVariables>(SignInDocument, options);
|
||||
}
|
||||
export type SignInMutationHookResult = ReturnType<typeof useSignInMutation>;
|
||||
export type SignInMutationResult = Apollo.MutationResult<SignInMutation>;
|
||||
export type SignInMutationOptions = Apollo.BaseMutationOptions<SignInMutation, SignInMutationVariables>;
|
||||
export const SignUpDocument = gql`
|
||||
mutation SignUp($email: String!, $password: String!, $captchaToken: String) {
|
||||
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${AvailableWorkspacesFragmentFragmentDoc}
|
||||
${AuthTokensFragmentFragmentDoc}`;
|
||||
export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMutationVariables>;
|
||||
|
||||
/**
|
||||
@ -4047,12 +4153,7 @@ export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMut
|
||||
* variables: {
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* workspaceId: // value for 'workspaceId'
|
||||
* locale: // value for 'locale'
|
||||
* verifyEmailNextPath: // value for 'verifyEmailNextPath'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@ -4072,13 +4173,13 @@ export const SignUpInNewWorkspaceDocument = gql`
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
${AuthTokenFragmentFragmentDoc}
|
||||
${WorkspaceUrlsFragmentFragmentDoc}`;
|
||||
export type SignUpInNewWorkspaceMutationFn = Apollo.MutationFunction<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
|
||||
|
||||
/**
|
||||
@ -4104,6 +4205,64 @@ export function useSignUpInNewWorkspaceMutation(baseOptions?: Apollo.MutationHoo
|
||||
export type SignUpInNewWorkspaceMutationHookResult = ReturnType<typeof useSignUpInNewWorkspaceMutation>;
|
||||
export type SignUpInNewWorkspaceMutationResult = Apollo.MutationResult<SignUpInNewWorkspaceMutation>;
|
||||
export type SignUpInNewWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
|
||||
export const SignUpInWorkspaceDocument = gql`
|
||||
mutation SignUpInWorkspace($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) {
|
||||
signUpInWorkspace(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AuthTokenFragmentFragmentDoc}`;
|
||||
export type SignUpInWorkspaceMutationFn = Apollo.MutationFunction<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignUpInWorkspaceMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignUpInWorkspaceMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignUpInWorkspaceMutation` 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 [signUpInWorkspaceMutation, { data, loading, error }] = useSignUpInWorkspaceMutation({
|
||||
* variables: {
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* workspaceId: // value for 'workspaceId'
|
||||
* locale: // value for 'locale'
|
||||
* verifyEmailNextPath: // value for 'verifyEmailNextPath'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignUpInWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>(SignUpInWorkspaceDocument, options);
|
||||
}
|
||||
export type SignUpInWorkspaceMutationHookResult = ReturnType<typeof useSignUpInWorkspaceMutation>;
|
||||
export type SignUpInWorkspaceMutationResult = Apollo.MutationResult<SignUpInWorkspaceMutation>;
|
||||
export type SignUpInWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>;
|
||||
export const UpdatePasswordViaResetTokenDocument = gql`
|
||||
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
||||
updatePasswordViaResetToken(
|
||||
@ -4144,30 +4303,9 @@ export type UpdatePasswordViaResetTokenMutationOptions = Apollo.BaseMutationOpti
|
||||
export const CheckUserExistsDocument = gql`
|
||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
isEmailVerified
|
||||
}
|
||||
... on UserNotExists {
|
||||
exists
|
||||
}
|
||||
exists
|
||||
availableWorkspacesCount
|
||||
isEmailVerified
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -4207,8 +4345,7 @@ export const GetPublicWorkspaceDataByDomainDocument = gql`
|
||||
logo
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
authProviders {
|
||||
sso {
|
||||
@ -4225,7 +4362,7 @@ export const GetPublicWorkspaceDataByDomainDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${WorkspaceUrlsFragmentFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useGetPublicWorkspaceDataByDomainQuery__
|
||||
|
||||
@ -12,6 +12,7 @@ import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
|
||||
jest.mock('@/onboarding/hooks/useOnboardingStatus');
|
||||
const setupMockOnboardingStatus = (
|
||||
@ -50,6 +51,11 @@ jest.mocked(useDefaultHomePagePath).mockReturnValue({
|
||||
defaultHomePagePath,
|
||||
});
|
||||
|
||||
jest.mock('@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace');
|
||||
jest.mocked(useIsCurrentLocationOnAWorkspace).mockReturnValue({
|
||||
isOnAWorkspace: true,
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom');
|
||||
const setupMockUseParams = (objectNamePlural?: string) => {
|
||||
jest
|
||||
|
||||
@ -12,9 +12,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
|
||||
export const usePageChangeEffectNavigateLocation = () => {
|
||||
const isLoggedIn = useIsLogged();
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
@ -41,13 +43,13 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
|
||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
const objectMetadataItem = objectMetadataItems?.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
|
||||
);
|
||||
const verifyEmailNextPath = useRecoilValue(verifyEmailNextPathState);
|
||||
|
||||
if (
|
||||
!isLoggedIn &&
|
||||
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
|
||||
!someMatchingLocationOf([
|
||||
...onGoingUserCreationPaths,
|
||||
AppPath.ResetPassword,
|
||||
|
||||
@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { previousUrlState } from '@/auth/states/previousUrlState';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
@ -33,8 +32,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const [, setPreviousUrl] = useRecoilState(previousUrlState);
|
||||
const setPreviousUrl = useSetRecoilState(previousUrlState);
|
||||
const location = useLocation();
|
||||
|
||||
const apolloClient = useMemo(() => {
|
||||
@ -65,7 +63,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentWorkspaceMember(null);
|
||||
setCurrentWorkspace(null);
|
||||
setCurrentUserWorkspace(null);
|
||||
setWorkspaces([]);
|
||||
if (
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.SignInUp) &&
|
||||
@ -89,7 +86,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentUser,
|
||||
setCurrentWorkspaceMember,
|
||||
setCurrentWorkspace,
|
||||
setWorkspaces,
|
||||
setPreviousUrl,
|
||||
]);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ type LogoProps = {
|
||||
primaryLogo?: string | null;
|
||||
secondaryLogo?: string | null;
|
||||
placeholder?: string | null;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -53,6 +54,7 @@ export const Logo = ({
|
||||
primaryLogo,
|
||||
secondaryLogo,
|
||||
placeholder,
|
||||
onClick,
|
||||
}: LogoProps) => {
|
||||
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
||||
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/android-launchericon-192-192.png`;
|
||||
@ -72,7 +74,7 @@ export const Logo = ({
|
||||
const isUsingDefaultLogo = !isDefined(primaryLogo);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer onClick={() => onClick?.()}>
|
||||
{isUsingDefaultLogo ? (
|
||||
<UndecoratedLink
|
||||
to={AppPath.SignInUp}
|
||||
|
||||
@ -17,3 +17,36 @@ export const AUTH_TOKENS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT = gql`
|
||||
fragment AvailableWorkspaceFragment on AvailableWorkspace {
|
||||
id
|
||||
displayName
|
||||
loginToken
|
||||
inviteHash
|
||||
personalInviteToken
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT = gql`
|
||||
fragment AvailableWorkspacesFragment on AvailableWorkspaces {
|
||||
availableWorkspacesForSignIn {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
availableWorkspacesForSignUp {
|
||||
...AvailableWorkspaceFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -17,8 +17,7 @@ export const GET_LOGIN_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql`
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ export const IMPERSONATE = gql`
|
||||
impersonate(userId: $userId, workspaceId: $workspaceId) {
|
||||
workspace {
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_IN = gql`
|
||||
mutation SignIn($email: String!, $password: String!, $captchaToken: String) {
|
||||
signIn(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -1,35 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_UP = gql`
|
||||
mutation SignUp(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
$workspaceId: String
|
||||
$locale: String
|
||||
$verifyEmailNextPath: String
|
||||
) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
mutation SignUp($email: String!, $password: String!, $captchaToken: String) {
|
||||
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
tokens {
|
||||
...AuthTokensFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,7 @@ export const SIGN_UP_IN_NEW_WORKSPACE = gql`
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SIGN_UP_IN_WORKSPACE = gql`
|
||||
mutation SignUpInWorkspace(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
$workspaceId: String
|
||||
$locale: String
|
||||
$verifyEmailNextPath: String
|
||||
) {
|
||||
signUpInWorkspace(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
locale: $locale
|
||||
verifyEmailNextPath: $verifyEmailNextPath
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -3,30 +3,9 @@ import { gql } from '@apollo/client';
|
||||
export const CHECK_USER_EXISTS = gql`
|
||||
query CheckUserExists($email: String!, $captchaToken: String) {
|
||||
checkUserExists(email: $email, captchaToken: $captchaToken) {
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
logo
|
||||
sso {
|
||||
type
|
||||
id
|
||||
issuer
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
isEmailVerified
|
||||
}
|
||||
... on UserNotExists {
|
||||
exists
|
||||
}
|
||||
exists
|
||||
availableWorkspacesCount
|
||||
isEmailVerified
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
|
||||
|
||||
export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
query GetPublicWorkspaceDataByDomain($origin: String!) {
|
||||
@ -7,8 +8,7 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
logo
|
||||
displayName
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
authProviders {
|
||||
sso {
|
||||
@ -25,4 +25,5 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
${WORKSPACE_URLS_FRAGMENT}
|
||||
`;
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
GetCurrentUserDocument,
|
||||
GetLoginTokenFromCredentialsDocument,
|
||||
SignUpDocument,
|
||||
SignUpInWorkspaceDocument,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const queries = {
|
||||
@ -10,6 +11,7 @@ export const queries = {
|
||||
getAuthTokensFromLoginToken: GetAuthTokensFromLoginTokenDocument,
|
||||
signup: SignUpDocument,
|
||||
getCurrentUser: GetCurrentUserDocument,
|
||||
signUpInWorkspace: SignUpInWorkspaceDocument,
|
||||
};
|
||||
|
||||
export const email = 'test@test.com';
|
||||
@ -29,7 +31,13 @@ export const variables = {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: "",
|
||||
locale: '',
|
||||
},
|
||||
signUpInWorkspace: {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: '',
|
||||
},
|
||||
getCurrentUser: {},
|
||||
};
|
||||
@ -48,6 +56,16 @@ export const results = {
|
||||
},
|
||||
},
|
||||
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
|
||||
signUpInWorkspace: {
|
||||
loginToken: { token, expiresAt: 'expiresAt' },
|
||||
workspace: {
|
||||
id: 'workspace-id',
|
||||
workspaceUrls: {
|
||||
subdomainUrl: 'https://subdomain.twenty.com',
|
||||
customUrl: 'https://custom.twenty.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
getCurrentUser: {
|
||||
currentUser: {
|
||||
id: 'id',
|
||||
@ -67,6 +85,7 @@ export const results = {
|
||||
avatarUrl: 'avatarUrl',
|
||||
locale: 'locale',
|
||||
},
|
||||
availableWorkspaces: [],
|
||||
currentWorkspace: {
|
||||
id: 'id',
|
||||
displayName: 'displayName',
|
||||
@ -74,6 +93,11 @@ export const results = {
|
||||
inviteHash: 'inviteHash',
|
||||
allowImpersonation: true,
|
||||
subscriptionStatus: 'subscriptionStatus',
|
||||
customDomain: null,
|
||||
workspaceUrls: {
|
||||
customUrl: undefined,
|
||||
subdomainUrl: 'https://twenty.com',
|
||||
},
|
||||
featureFlags: {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
@ -85,8 +109,8 @@ export const results = {
|
||||
},
|
||||
};
|
||||
|
||||
export const mocks = [
|
||||
{
|
||||
export const mocks = {
|
||||
getLoginTokenFromCredentials: {
|
||||
request: {
|
||||
query: queries.getLoginTokenFromCredentials,
|
||||
variables: variables.getLoginTokenFromCredentials,
|
||||
@ -97,7 +121,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getAuthTokensFromLoginToken: {
|
||||
request: {
|
||||
query: queries.getAuthTokensFromLoginToken,
|
||||
variables: variables.getAuthTokensFromLoginToken,
|
||||
@ -108,7 +132,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
signup: {
|
||||
request: {
|
||||
query: queries.signup,
|
||||
variables: variables.signup,
|
||||
@ -119,7 +143,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getCurrentUser: {
|
||||
request: {
|
||||
query: queries.getCurrentUser,
|
||||
variables: variables.getCurrentUser,
|
||||
@ -128,4 +152,15 @@ export const mocks = [
|
||||
data: results.getCurrentUser,
|
||||
})),
|
||||
},
|
||||
];
|
||||
signUpInWorkspace: {
|
||||
request: {
|
||||
query: queries.signUpInWorkspace,
|
||||
variables: variables.signUpInWorkspace,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
signUpInWorkspace: results.signUpInWorkspace,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -30,9 +31,13 @@ jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
@ -63,7 +68,7 @@ describe('useAuth', () => {
|
||||
).toStrictEqual(results.getLoginTokenFromCredentials);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should verify user', async () => {
|
||||
@ -73,19 +78,19 @@ describe('useAuth', () => {
|
||||
await result.current.getAuthTokensFromLoginToken(token);
|
||||
});
|
||||
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks[3].result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
expect(mocks.getCurrentUser.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle credential sign-in', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signInWithCredentials(email, password);
|
||||
await result.current.signInWithCredentialsInWorkspace(email, password);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle google sign-in', async () => {
|
||||
@ -94,6 +99,7 @@ describe('useAuth', () => {
|
||||
await act(async () => {
|
||||
await result.current.signInWithGoogle({
|
||||
workspaceInviteHash: 'workspaceInviteHash',
|
||||
action: 'join-workspace',
|
||||
});
|
||||
});
|
||||
|
||||
@ -163,9 +169,12 @@ describe('useAuth', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signUpWithCredentials({ email, password });
|
||||
await result.current.signUpWithCredentialsInWorkspace({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
expect(mocks.signUpInWorkspace.result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,19 +11,21 @@ import {
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import {
|
||||
AuthTokenPair,
|
||||
useCheckUserExistsLazyQuery,
|
||||
useGetAuthTokensFromLoginTokenMutation,
|
||||
useGetCurrentUserLazyQuery,
|
||||
useGetLoginTokenFromCredentialsMutation,
|
||||
useGetLoginTokenFromEmailVerificationTokenMutation,
|
||||
useSignUpMutation,
|
||||
useSignInMutation,
|
||||
useSignUpInWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
@ -68,10 +70,17 @@ import { iconsState } from 'twenty-ui/display';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import {
|
||||
countAvailableWorkspaces,
|
||||
getFirstAvailableWorkspaces,
|
||||
} from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -87,16 +96,18 @@ export const useAuth = () => {
|
||||
);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const { redirect } = useRedirect();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
|
||||
const [getLoginTokenFromCredentials] =
|
||||
useGetLoginTokenFromCredentialsMutation();
|
||||
const [signIn] = useSignInMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
|
||||
const [getAuthTokensFromLoginToken] =
|
||||
useGetAuthTokensFromLoginTokenMutation();
|
||||
const [getLoginTokenFromEmailVerificationToken] =
|
||||
@ -280,6 +291,10 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers(workspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(user.availableWorkspaces)) {
|
||||
setAvailableWorkspaces(user.availableWorkspaces);
|
||||
}
|
||||
|
||||
if (isDefined(user.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace(user.currentUserWorkspace);
|
||||
}
|
||||
@ -326,17 +341,6 @@ export const useAuth = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(user.workspaces)) {
|
||||
const validWorkspaces = user.workspaces
|
||||
.filter(
|
||||
({ workspace }) => workspace !== null && workspace !== undefined,
|
||||
)
|
||||
.map((validWorkspace) => validWorkspace.workspace)
|
||||
.filter(isDefined);
|
||||
|
||||
setWorkspaces(validWorkspaces);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
workspaceMember,
|
||||
@ -352,9 +356,17 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers,
|
||||
setDateTimeFormat,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
setWorkspaces,
|
||||
setAvailableWorkspaces,
|
||||
]);
|
||||
|
||||
const handleSetAuthTokens = useCallback(
|
||||
(tokens: AuthTokenPair) => {
|
||||
setTokenPair(tokens);
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
|
||||
},
|
||||
[setTokenPair],
|
||||
);
|
||||
|
||||
const handleGetAuthTokensFromLoginToken = useCallback(
|
||||
async (loginToken: string) => {
|
||||
const getAuthTokensResult = await getAuthTokensFromLoginToken({
|
||||
@ -372,14 +384,8 @@ export const useAuth = () => {
|
||||
throw new Error('No getAuthTokensFromLoginToken result');
|
||||
}
|
||||
|
||||
setTokenPair(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
cookieStorage.setItem(
|
||||
'tokenPair',
|
||||
JSON.stringify(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
),
|
||||
handleSetAuthTokens(
|
||||
getAuthTokensResult.data.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
|
||||
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
|
||||
@ -390,14 +396,97 @@ export const useAuth = () => {
|
||||
},
|
||||
[
|
||||
getAuthTokensFromLoginToken,
|
||||
setTokenPair,
|
||||
loadCurrentUser,
|
||||
origin,
|
||||
handleSetAuthTokens,
|
||||
refreshObjectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignIn = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signIn({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signIn.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
const availableWorkspacesCount = countAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
|
||||
if (availableWorkspacesCount === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
if (availableWorkspacesCount === 1) {
|
||||
const targetWorkspace = getFirstAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(targetWorkspace.workspaceUrls),
|
||||
targetWorkspace.loginToken ? AppPath.Verify : AppPath.SignInUp,
|
||||
{
|
||||
...(targetWorkspace.loginToken && {
|
||||
loginToken: targetWorkspace.loginToken,
|
||||
}),
|
||||
email: user.email,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (
|
||||
error instanceof ApolloError &&
|
||||
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
|
||||
) {
|
||||
setSearchParams({ email });
|
||||
setSignInUpStep(SignInUpStep.EmailVerification);
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
redirectToWorkspaceDomain,
|
||||
signIn,
|
||||
loadCurrentUser,
|
||||
setSearchParams,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signUp({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signUp.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
if (countAvailableWorkspaces(user.availableWorkspaces) === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
signUp,
|
||||
loadCurrentUser,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignInInWorkspace = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
const { loginToken } = await handleGetLoginTokenFromCredentials(
|
||||
email,
|
||||
@ -413,7 +502,7 @@ export const useAuth = () => {
|
||||
await clearSession();
|
||||
}, [clearSession]);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
const handleCredentialsSignUpInWorkspace = useCallback(
|
||||
async ({
|
||||
email,
|
||||
password,
|
||||
@ -429,7 +518,7 @@ export const useAuth = () => {
|
||||
captchaToken?: string;
|
||||
verifyEmailNextPath?: string;
|
||||
}) => {
|
||||
const signUpResult = await signUp({
|
||||
const signUpInWorkspaceResult = await signUpInWorkspace({
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
@ -444,11 +533,11 @@ export const useAuth = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(signUpResult.errors)) {
|
||||
throw signUpResult.errors;
|
||||
if (isDefined(signUpInWorkspaceResult.errors)) {
|
||||
throw signUpInWorkspaceResult.errors;
|
||||
}
|
||||
|
||||
if (!signUpResult.data?.signUp) {
|
||||
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
@ -460,11 +549,15 @@ export const useAuth = () => {
|
||||
|
||||
if (isMultiWorkspaceEnabled) {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls),
|
||||
getWorkspaceUrl(
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
|
||||
.workspaceUrls,
|
||||
),
|
||||
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
|
||||
{
|
||||
...(!isEmailVerificationRequired && {
|
||||
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||
loginToken:
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
|
||||
}),
|
||||
email,
|
||||
},
|
||||
@ -472,11 +565,11 @@ export const useAuth = () => {
|
||||
}
|
||||
|
||||
await handleGetAuthTokensFromLoginToken(
|
||||
signUpResult.data?.signUp.loginToken.token,
|
||||
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
|
||||
);
|
||||
},
|
||||
[
|
||||
signUp,
|
||||
signUpInWorkspace,
|
||||
workspacePublicData,
|
||||
isMultiWorkspaceEnabled,
|
||||
handleGetAuthTokensFromLoginToken,
|
||||
@ -494,6 +587,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action?: string;
|
||||
},
|
||||
) => {
|
||||
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
|
||||
@ -513,6 +607,10 @@ export const useAuth = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(params.action)) {
|
||||
url.searchParams.set('action', params.action);
|
||||
}
|
||||
|
||||
if (isDefined(workspacePublicData)) {
|
||||
url.searchParams.set('workspaceId', workspacePublicData.id);
|
||||
}
|
||||
@ -527,6 +625,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/google', params));
|
||||
},
|
||||
@ -538,6 +637,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/microsoft', params));
|
||||
},
|
||||
@ -556,8 +656,11 @@ export const useAuth = () => {
|
||||
clearSession,
|
||||
signOut: handleSignOut,
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signUpWithCredentialsInWorkspace: handleCredentialsSignUpInWorkspace,
|
||||
signInWithCredentialsInWorkspace: handleCredentialsSignInInWorkspace,
|
||||
signInWithCredentials: handleCredentialsSignIn,
|
||||
signInWithGoogle: handleGoogleLogin,
|
||||
signInWithMicrosoft: handleMicrosoftLogin,
|
||||
setAuthTokens: handleSetAuthTokens,
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,14 +2,17 @@ import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
|
||||
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
@ -17,19 +20,23 @@ import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { HorizontalSeparator } from 'twenty-ui/display';
|
||||
import {
|
||||
HorizontalSeparator,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
Avatar,
|
||||
} from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
|
||||
const StyledContentContainer = styled(motion.div)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -44,29 +51,101 @@ const StyledForm = styled.form`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceContainer = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: ${({ theme }) => theme.spacing(15)};
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWorkspaceContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
width: 100%;
|
||||
padding: 0 ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceTextContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceLogo = styled.div`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceName = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceUrl = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
`;
|
||||
|
||||
const StyledChevronIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SignInUpGlobalScopeForm = () => {
|
||||
const authProviders = useRecoilValue(authProvidersState);
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
|
||||
const { checkUserExists } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
const [signInUpMode] = useRecoilState(signInUpModeState);
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
|
||||
const isRequestingCaptchaToken = useRecoilValue(
|
||||
isRequestingCaptchaTokenState,
|
||||
);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
||||
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { submitCredentials } = useSignInUp(form);
|
||||
const { submitCredentials, continueWithCredentials } = useSignInUp(form);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isDefined(form?.formState?.errors?.email)) {
|
||||
@ -79,38 +158,7 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await readCaptchaToken();
|
||||
await checkUserExists.checkUserExistsQuery({
|
||||
variables: {
|
||||
email: form.getValues('email').toLowerCase().trim(),
|
||||
captchaToken: token,
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
onCompleted: async (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
const response = data.checkUserExists;
|
||||
if (response.__typename === 'UserExists') {
|
||||
if (response.availableWorkspaces.length >= 1) {
|
||||
const workspace = response.availableWorkspaces[0];
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(workspace.workspaceUrls),
|
||||
pathname,
|
||||
{
|
||||
email: form.getValues('email'),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (response.__typename === 'UserNotExists') {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
}
|
||||
},
|
||||
});
|
||||
continueWithCredentials();
|
||||
};
|
||||
|
||||
const onEmailChange = (email: string) => {
|
||||
@ -119,42 +167,118 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableWorkspaceUrl = (availableWorkspace: AvailableWorkspace) => {
|
||||
const { pathname, searchParams } = getAvailableWorkspacePathAndSearchParams(
|
||||
availableWorkspace,
|
||||
{ email: form.getValues('email') },
|
||||
);
|
||||
|
||||
return buildWorkspaceUrl(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && <SignInUpWithGoogle />}
|
||||
{authProviders.microsoft && <SignInUpWithMicrosoft />}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<SignInUpEmailField
|
||||
showErrors={showErrors}
|
||||
onInputChange={onEmailChange}
|
||||
/>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
{signInUpStep === SignInUpStep.WorkspaceSelection && (
|
||||
<StyledWorkspaceContainer>
|
||||
{[
|
||||
...availableWorkspaces.availableWorkspacesForSignIn,
|
||||
...availableWorkspaces.availableWorkspacesForSignUp,
|
||||
].map((availableWorkspace) => (
|
||||
<UndecoratedLink
|
||||
key={availableWorkspace.id}
|
||||
to={getAvailableWorkspaceUrl(availableWorkspace)}
|
||||
>
|
||||
<StyledWorkspaceItem>
|
||||
<StyledWorkspaceContent>
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
<StyledWorkspaceTextContainer>
|
||||
<StyledWorkspaceName>
|
||||
{availableWorkspace.displayName || availableWorkspace.id}
|
||||
</StyledWorkspaceName>
|
||||
<StyledWorkspaceUrl>
|
||||
{
|
||||
new URL(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
).hostname
|
||||
}
|
||||
</StyledWorkspaceUrl>
|
||||
</StyledWorkspaceTextContainer>
|
||||
<StyledChevronIcon>
|
||||
<IconChevronRight size={theme.icon.size.md} />
|
||||
</StyledChevronIcon>
|
||||
</StyledWorkspaceContent>
|
||||
</StyledWorkspaceItem>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
<StyledWorkspaceItem onClick={() => createWorkspace()}>
|
||||
<StyledWorkspaceContent>
|
||||
<StyledWorkspaceLogo>
|
||||
<IconPlus size={theme.icon.size.lg} />
|
||||
</StyledWorkspaceLogo>
|
||||
<StyledWorkspaceTextContainer>
|
||||
<StyledWorkspaceName>{t`Create a workspace`}</StyledWorkspaceName>
|
||||
</StyledWorkspaceTextContainer>
|
||||
<StyledChevronIcon>
|
||||
<IconChevronRight size={theme.icon.size.md} />
|
||||
</StyledChevronIcon>
|
||||
</StyledWorkspaceContent>
|
||||
</StyledWorkspaceItem>
|
||||
</StyledWorkspaceContainer>
|
||||
)}
|
||||
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<SignInUpWithGoogle action="list-available-workspaces" />
|
||||
)}
|
||||
{authProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft action="list-available-workspaces" />
|
||||
)}
|
||||
{(authProviders.google || authProviders.microsoft) && (
|
||||
<HorizontalSeparator />
|
||||
)}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FormProvider {...form}>
|
||||
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
|
||||
<SignInUpEmailField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
onInputChange={onEmailChange}
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
disabled={isRequestingCaptchaToken}
|
||||
title={
|
||||
signInUpStep === SignInUpStep.Password ? 'Sign Up' : 'Continue'
|
||||
}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</StyledContentContainer>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<SignInUpPasswordField
|
||||
showErrors={showErrors}
|
||||
signInUpMode={signInUpMode}
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
disabled={isRequestingCaptchaToken}
|
||||
title={
|
||||
signInUpStep === SignInUpStep.Password
|
||||
? signInUpMode === SignInUpMode.SignIn
|
||||
? t`Sign In`
|
||||
: t`Sign Up`
|
||||
: t`Continue`
|
||||
}
|
||||
type="submit"
|
||||
variant={
|
||||
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
|
||||
}
|
||||
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
</FormProvider>
|
||||
</StyledContentContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
|
||||
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/internal/SignInUpWithCredentials';
|
||||
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
|
||||
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
|
||||
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/internal/SignInUpWithSSO';
|
||||
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';
|
||||
@ -36,9 +36,13 @@ export const SignInUpWorkspaceScopeForm = () => {
|
||||
return (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
{workspaceAuthProviders.google && <SignInUpWithGoogle />}
|
||||
{workspaceAuthProviders.google && (
|
||||
<SignInUpWithGoogle action="join-workspace" />
|
||||
)}
|
||||
|
||||
{workspaceAuthProviders.microsoft && <SignInUpWithMicrosoft />}
|
||||
{workspaceAuthProviders.microsoft && (
|
||||
<SignInUpWithMicrosoft action="join-workspace" />
|
||||
)}
|
||||
|
||||
{workspaceAuthProviders.sso.length > 0 && <SignInUpWithSSO />}
|
||||
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export const SignInUpGlobalScopeFormEffect = () => {
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { setAuthTokens, loadCurrentUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const tokenPair = searchParams.get('tokenPair');
|
||||
if (isDefined(tokenPair)) {
|
||||
setAuthTokens(JSON.parse(tokenPair));
|
||||
searchParams.delete('tokenPair');
|
||||
setSearchParams(searchParams);
|
||||
loadCurrentUser();
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
}
|
||||
}, [
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
setSignInUpStep,
|
||||
loadCurrentUser,
|
||||
setAuthTokens,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -5,8 +5,8 @@ import {
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
|
||||
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
|
||||
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
@ -9,23 +9,27 @@ import { memo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
const GoogleIcon = memo(() => {
|
||||
const theme = useTheme();
|
||||
return <IconGoogle size={theme.icon.size.md} />;
|
||||
});
|
||||
|
||||
export const SignInUpWithGoogle = () => {
|
||||
export const SignInUpWithGoogle = ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={GoogleIcon}
|
||||
title={t`Continue with Google`}
|
||||
onClick={signInWithGoogle}
|
||||
onClick={() => signInWithGoogle({ action })}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
@ -8,8 +8,13 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const SignInUpWithMicrosoft = () => {
|
||||
export const SignInUpWithMicrosoft = ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
|
||||
@ -21,7 +26,7 @@ export const SignInUpWithMicrosoft = () => {
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
|
||||
title={t`Continue with Microsoft`}
|
||||
onClick={signInWithMicrosoft}
|
||||
onClick={() => signInWithMicrosoft({ action })}
|
||||
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
|
||||
fullWidth
|
||||
/>
|
||||
@ -43,9 +43,12 @@ describe('useSignInWithGoogle', () => {
|
||||
const { result } = renderHook(() => useSignInWithGoogle(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithGoogle();
|
||||
result.current.signInWithGoogle({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: 'testToken',
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
@ -66,9 +69,12 @@ describe('useSignInWithGoogle', () => {
|
||||
const { result } = renderHook(() => useSignInWithGoogle(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithGoogle();
|
||||
result.current.signInWithGoogle({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: undefined,
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
|
||||
@ -43,9 +43,12 @@ describe('useSignInWithMicrosoft', () => {
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithMicrosoft();
|
||||
result.current.signInWithMicrosoft({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: inviteTokenMock,
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
@ -67,9 +70,12 @@ describe('useSignInWithMicrosoft', () => {
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
result.current.signInWithMicrosoft();
|
||||
result.current.signInWithMicrosoft({
|
||||
action: 'join-workspace',
|
||||
});
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
action: 'join-workspace',
|
||||
billingCheckoutSession: mockBillingCheckoutSession,
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: undefined,
|
||||
|
||||
@ -19,12 +19,14 @@ import { useRecoilState } from 'recoil';
|
||||
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
@ -38,7 +40,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
);
|
||||
|
||||
const {
|
||||
signInWithCredentialsInWorkspace,
|
||||
signInWithCredentials,
|
||||
signUpWithCredentialsInWorkspace,
|
||||
signUpWithCredentials,
|
||||
checkUserExists: { checkUserExistsQuery },
|
||||
} = useAuth();
|
||||
@ -71,11 +75,11 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
if (data?.checkUserExists.exists) {
|
||||
setSignInUpMode(SignInUpMode.SignIn);
|
||||
} else {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
}
|
||||
setSignInUpMode(
|
||||
data?.checkUserExists.exists
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
},
|
||||
});
|
||||
@ -97,31 +101,60 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
|
||||
await signInWithCredentials(
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
isOnAWorkspace
|
||||
) {
|
||||
return await signInWithCredentialsInWorkspace(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
} else {
|
||||
const verifyEmailNextPath = buildAppPathWithQueryParams(
|
||||
AppPath.PlanRequired,
|
||||
await buildSearchParamsFromUrlSyncedStates(),
|
||||
);
|
||||
|
||||
await signUpWithCredentials({
|
||||
email: data.email.toLowerCase().trim(),
|
||||
password: data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken: token,
|
||||
verifyEmailNextPath,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
!isOnAWorkspace
|
||||
) {
|
||||
return await signInWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignUp &&
|
||||
!isOnAWorkspace
|
||||
) {
|
||||
return await signUpWithCredentials(
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
const verifyEmailNextPath = buildAppPathWithQueryParams(
|
||||
AppPath.PlanRequired,
|
||||
await buildSearchParamsFromUrlSyncedStates(),
|
||||
);
|
||||
|
||||
await signUpWithCredentialsInWorkspace({
|
||||
email: data.email.toLowerCase().trim(),
|
||||
password: data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken: token,
|
||||
verifyEmailNextPath,
|
||||
});
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
} finally {
|
||||
requestFreshCaptchaToken();
|
||||
}
|
||||
},
|
||||
@ -129,13 +162,16 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
readCaptchaToken,
|
||||
signInUpMode,
|
||||
isInviteMode,
|
||||
signInWithCredentialsInWorkspace,
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
signUpWithCredentialsInWorkspace,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
enqueueSnackBar,
|
||||
requestFreshCaptchaToken,
|
||||
buildSearchParamsFromUrlSyncedStates,
|
||||
isOnAWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -30,7 +30,6 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
|
||||
|
||||
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
|
||||
export const useSignInUpForm = () => {
|
||||
const location = useLocation();
|
||||
const signInUpStep = useRecoilValue(signInUpStepState);
|
||||
|
||||
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
|
||||
@ -61,11 +60,6 @@ export const useSignInUpForm = () => {
|
||||
form.setValue('email', prefilledEmail ?? 'tim@apple.dev');
|
||||
form.setValue('password', 'tim@apple.dev');
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
isDeveloperDefaultSignInPrefilled,
|
||||
prefilledEmail,
|
||||
location.search,
|
||||
]);
|
||||
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail]);
|
||||
return { form: form };
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const useSignInWithGoogle = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
@ -15,12 +16,14 @@ export const useSignInWithGoogle = () => {
|
||||
} as BillingCheckoutSession;
|
||||
|
||||
const { signInWithGoogle } = useAuth();
|
||||
|
||||
return {
|
||||
signInWithGoogle: () =>
|
||||
signInWithGoogle: ({ action }: { action: SocialSSOSignInUpActionType }) =>
|
||||
signInWithGoogle({
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
billingCheckoutSession,
|
||||
action,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
|
||||
|
||||
export const useSignInWithMicrosoft = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
@ -13,11 +14,16 @@ export const useSignInWithMicrosoft = () => {
|
||||
|
||||
const { signInWithMicrosoft } = useAuth();
|
||||
return {
|
||||
signInWithMicrosoft: () =>
|
||||
signInWithMicrosoft: ({
|
||||
action,
|
||||
}: {
|
||||
action: SocialSSOSignInUpActionType;
|
||||
}) =>
|
||||
signInWithMicrosoft({
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
billingCheckoutSession,
|
||||
action,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
export const useSignUpInNewWorkspace = () => {
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
|
||||
|
||||
const createWorkspace = () => {
|
||||
signUpInNewWorkspaceMutation({
|
||||
onCompleted: async (data) => {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
|
||||
AppPath.Verify,
|
||||
{
|
||||
loginToken: data.signUpInNewWorkspace.loginToken.token,
|
||||
},
|
||||
'_blank',
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createWorkspace,
|
||||
};
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import { UserExists } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const availableSSOIdentityProvidersForAuthState = createState<
|
||||
NonNullable<UserExists['availableWorkspaces']>[0]['sso']
|
||||
>({
|
||||
key: 'availableSSOIdentityProvidersForAuth',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { AvailableWorkspaces } from '~/generated/graphql';
|
||||
|
||||
export const availableWorkspacesState = createState<AvailableWorkspaces>({
|
||||
key: 'availableWorkspacesState',
|
||||
defaultValue: {
|
||||
availableWorkspacesForSignIn: [],
|
||||
availableWorkspacesForSignUp: [],
|
||||
},
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { Workspace } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export type Workspaces = Pick<
|
||||
Workspace,
|
||||
'id' | 'logo' | 'displayName' | 'workspaceUrls'
|
||||
>[];
|
||||
|
||||
export const workspacesState = createState<Workspaces>({
|
||||
key: 'workspacesState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
export type SocialSSOSignInUpActionType =
|
||||
| 'create-new-workspace'
|
||||
| 'list-available-workspaces'
|
||||
| 'join-workspace';
|
||||
@ -0,0 +1,70 @@
|
||||
import { AvailableWorkspaces, AvailableWorkspace } from '~/generated/graphql';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
export const countAvailableWorkspaces = ({
|
||||
availableWorkspacesForSignIn,
|
||||
availableWorkspacesForSignUp,
|
||||
}: AvailableWorkspaces): number => {
|
||||
return (
|
||||
availableWorkspacesForSignIn.length + availableWorkspacesForSignUp.length
|
||||
);
|
||||
};
|
||||
|
||||
export const getFirstAvailableWorkspaces = ({
|
||||
availableWorkspacesForSignIn,
|
||||
availableWorkspacesForSignUp,
|
||||
}: AvailableWorkspaces): AvailableWorkspace => {
|
||||
return availableWorkspacesForSignIn[0] ?? availableWorkspacesForSignUp[0];
|
||||
};
|
||||
|
||||
const getAvailableWorkspacePathname = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
) => {
|
||||
if (isDefined(availableWorkspace.loginToken)) {
|
||||
return AppPath.Verify;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(availableWorkspace.personalInviteToken) &&
|
||||
isDefined(availableWorkspace.inviteHash)
|
||||
) {
|
||||
return generatePath(AppPath.Invite, {
|
||||
workspaceInviteHash: availableWorkspace.inviteHash,
|
||||
});
|
||||
}
|
||||
|
||||
return AppPath.SignInUp;
|
||||
};
|
||||
|
||||
const getAvailableWorkspaceSearchParams = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
defaultSearchParams: Record<string, string> = {},
|
||||
) => {
|
||||
const searchParams: Record<string, string> = defaultSearchParams;
|
||||
|
||||
if (isDefined(availableWorkspace.loginToken)) {
|
||||
searchParams.loginToken = availableWorkspace.loginToken;
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
if (isDefined(availableWorkspace.personalInviteToken)) {
|
||||
searchParams.inviteToken = availableWorkspace.personalInviteToken;
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
};
|
||||
|
||||
export const getAvailableWorkspacePathAndSearchParams = (
|
||||
availableWorkspace: AvailableWorkspace,
|
||||
defaultSearchParams: Record<string, string> = {},
|
||||
): { pathname: string; searchParams: Record<string, string> } => {
|
||||
return {
|
||||
pathname: getAvailableWorkspacePathname(availableWorkspace),
|
||||
searchParams: getAvailableWorkspaceSearchParams(
|
||||
availableWorkspace,
|
||||
defaultSearchParams,
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
|
||||
describe('useReadCaptchaToken', () => {
|
||||
it('should return undefined when no token exists', async () => {
|
||||
const { result } = renderHook(() => useReadCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const token = await result.current.readCaptchaToken();
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the token when it exists', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const hook = useReadCaptchaToken();
|
||||
return hook;
|
||||
},
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(captchaTokenState, 'test-token');
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const token = await result.current.readCaptchaToken();
|
||||
expect(token).toBe('test-token');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,66 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import * as ReactRouterDom from 'react-router-dom';
|
||||
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useRequestFreshCaptchaToken', () => {
|
||||
beforeEach(() => {
|
||||
// Mock useLocation to return a path that requires captcha
|
||||
(ReactRouterDom.useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/sign-in',
|
||||
});
|
||||
|
||||
// Mock window.grecaptcha
|
||||
window.grecaptcha = {
|
||||
execute: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve('google-recaptcha-token');
|
||||
}),
|
||||
};
|
||||
|
||||
// Mock window.turnstile
|
||||
window.turnstile = {
|
||||
render: jest.fn().mockReturnValue('turnstile-widget-id'),
|
||||
execute: jest.fn().mockImplementation((widgetId, options) => {
|
||||
return options?.callback('turnstile-token');
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete window.grecaptcha;
|
||||
delete window.turnstile;
|
||||
});
|
||||
|
||||
it('should not request a token if captcha is not required for the path', async () => {
|
||||
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.requestFreshCaptchaToken();
|
||||
});
|
||||
|
||||
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
|
||||
expect(window.turnstile.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not request a token if captcha provider is not defined', async () => {
|
||||
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
|
||||
wrapper: RecoilRoot,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.requestFreshCaptchaToken();
|
||||
});
|
||||
|
||||
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
|
||||
expect(window.turnstile.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -18,8 +18,9 @@ export const useIsCurrentLocationOnAWorkspace = () => {
|
||||
throw new Error('frontDomain and defaultSubdomain are required');
|
||||
}
|
||||
|
||||
const isOnAWorkspace =
|
||||
isMultiWorkspaceEnabled && window.location.hostname !== defaultDomain;
|
||||
const isOnAWorkspace = !isMultiWorkspaceEnabled
|
||||
? true
|
||||
: window.location.hostname !== defaultDomain;
|
||||
|
||||
return {
|
||||
isOnAWorkspace,
|
||||
|
||||
@ -150,8 +150,7 @@ export const queries = {
|
||||
hasValidEnterpriseKey
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
featureFlags {
|
||||
id
|
||||
@ -179,8 +178,7 @@ export const queries = {
|
||||
subdomain
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
|
||||
const mockCurrentUser = {
|
||||
id: 'fake-user-id',
|
||||
@ -41,7 +42,11 @@ const initializeState = ({ set }: MutableSnapshot) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -37,9 +36,14 @@ import {
|
||||
MenuItemSelectAvatar,
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui/navigation';
|
||||
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
|
||||
import {
|
||||
useSignUpInNewWorkspaceMutation,
|
||||
AvailableWorkspace,
|
||||
} from '~/generated/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
@ -50,7 +54,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const { t } = useLingui();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const workspaces = useRecoilValue(workspacesState);
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const availableWorkspacesCount =
|
||||
countAvailableWorkspaces(availableWorkspaces);
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
|
||||
const { signOut } = useAuth();
|
||||
@ -63,8 +69,10 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
multiWorkspaceDropdownState,
|
||||
);
|
||||
|
||||
const handleChange = async (workspace: Workspaces[0]) => {
|
||||
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
|
||||
const handleChange = async (availableWorkspace: AvailableWorkspace) => {
|
||||
redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
);
|
||||
};
|
||||
|
||||
const createWorkspace = () => {
|
||||
@ -127,36 +135,41 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
>
|
||||
{currentWorkspace?.displayName}
|
||||
</DropdownMenuHeader>
|
||||
{workspaces.length > 1 && (
|
||||
{availableWorkspacesCount > 1 && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{workspaces
|
||||
{[
|
||||
...availableWorkspaces.availableWorkspacesForSignIn,
|
||||
...availableWorkspaces.availableWorkspacesForSignUp,
|
||||
]
|
||||
.filter(({ id }) => id !== currentWorkspace?.id)
|
||||
.slice(0, 3)
|
||||
.map((workspace) => (
|
||||
.map((availableWorkspace) => (
|
||||
<UndecoratedLink
|
||||
key={workspace.id}
|
||||
key={availableWorkspace.id}
|
||||
to={buildWorkspaceUrl(
|
||||
getWorkspaceUrl(workspace.workspaceUrls),
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event?.preventDefault();
|
||||
handleChange(workspace);
|
||||
handleChange(availableWorkspace);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={workspace.displayName ?? '(No name)'}
|
||||
text={availableWorkspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={workspace.displayName || ''}
|
||||
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={
|
||||
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
|
||||
}
|
||||
/>
|
||||
}
|
||||
selected={false}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
{workspaces.length > 4 && (
|
||||
{availableWorkspacesCount > 4 && (
|
||||
<MenuItem
|
||||
LeftIcon={IconSwitchHorizontal}
|
||||
text={t`Other workspaces`}
|
||||
|
||||
@ -1,32 +1,23 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Avatar, IconChevronLeft } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
|
||||
import { WorkspacesForSignIn } from './components/WorkspacesForSignIn';
|
||||
import { WorkspacesForSignUp } from './components/WorkspacesForSignUp';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const workspaces = useRecoilValue(workspacesState);
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleChange = async (workspace: Workspaces[0]) => {
|
||||
await redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
|
||||
};
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
|
||||
const setMultiWorkspaceDropdownState = useSetRecoilState(
|
||||
multiWorkspaceDropdownState,
|
||||
);
|
||||
@ -52,37 +43,10 @@ export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
{workspaces
|
||||
.filter(
|
||||
(workspace) =>
|
||||
workspace.id !== currentWorkspace?.id &&
|
||||
workspace.displayName
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
)
|
||||
.map((workspace) => (
|
||||
<UndecoratedLink
|
||||
key={workspace.id}
|
||||
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
|
||||
onClick={(event) => {
|
||||
event?.preventDefault();
|
||||
handleChange(workspace);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={workspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={workspace.displayName || ''}
|
||||
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
/>
|
||||
}
|
||||
selected={currentWorkspace?.id === workspace.id}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<WorkspacesForSignIn searchValue={searchValue} />
|
||||
{availableWorkspaces.availableWorkspacesForSignUp.length > 0 && (
|
||||
<WorkspacesForSignUp searchValue={searchValue} />
|
||||
)}
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import React from 'react';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
|
||||
export const AvailableWorkspaceItem = ({
|
||||
availableWorkspace,
|
||||
isSelected,
|
||||
}: {
|
||||
availableWorkspace: AvailableWorkspace;
|
||||
isSelected: boolean;
|
||||
}) => {
|
||||
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
|
||||
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
|
||||
const { pathname, searchParams } =
|
||||
getAvailableWorkspacePathAndSearchParams(availableWorkspace);
|
||||
|
||||
const handleChange = async () => {
|
||||
await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<UndecoratedLink
|
||||
key={availableWorkspace.id}
|
||||
to={buildWorkspaceUrl(
|
||||
getWorkspaceUrl(availableWorkspace.workspaceUrls),
|
||||
pathname,
|
||||
searchParams,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleChange();
|
||||
}}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
text={availableWorkspace.displayName ?? '(No name)'}
|
||||
avatar={
|
||||
<Avatar
|
||||
placeholder={availableWorkspace.displayName || ''}
|
||||
avatarUrl={availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO}
|
||||
/>
|
||||
}
|
||||
selected={isSelected}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
|
||||
|
||||
export const WorkspacesForSignIn = ({
|
||||
searchValue,
|
||||
}: {
|
||||
searchValue: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{t`Member of`}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{searchAvailableWorkspaces(
|
||||
searchValue,
|
||||
availableWorkspaces.availableWorkspacesForSignIn,
|
||||
).map((availableWorkspace) => (
|
||||
<AvailableWorkspaceItem
|
||||
key={availableWorkspace.id}
|
||||
availableWorkspace={availableWorkspace}
|
||||
isSelected={currentWorkspace?.id === availableWorkspace.id}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
|
||||
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const WorkspacesForSignUp = ({
|
||||
searchValue,
|
||||
}: {
|
||||
searchValue: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSubheader>{t`Invitations`}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{searchAvailableWorkspaces(
|
||||
searchValue,
|
||||
availableWorkspaces.availableWorkspacesForSignUp,
|
||||
).map((availableWorkspace) => (
|
||||
<AvailableWorkspaceItem
|
||||
key={availableWorkspace.id}
|
||||
availableWorkspace={availableWorkspace}
|
||||
isSelected={currentWorkspace?.id === availableWorkspace.id}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AvailableWorkspace } from '~/generated/graphql';
|
||||
|
||||
export const useFilteredAvailableWorkspaces = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const searchAvailableWorkspaces = (
|
||||
searchValue: string,
|
||||
availableWorkspaces: Array<AvailableWorkspace>,
|
||||
) => {
|
||||
return availableWorkspaces.filter(
|
||||
(availableWorkspace) =>
|
||||
currentWorkspace?.id &&
|
||||
availableWorkspace.id !== currentWorkspace.id &&
|
||||
availableWorkspace.displayName
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
searchAvailableWorkspaces,
|
||||
};
|
||||
};
|
||||
@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
@ -30,6 +29,7 @@ import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
|
||||
export const UserProviderEffect = () => {
|
||||
const location = useLocation();
|
||||
@ -40,7 +40,7 @@ export const UserProviderEffect = () => {
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
const isLoggedIn = useIsLogged();
|
||||
|
||||
@ -102,7 +102,7 @@ export const UserProviderEffect = () => {
|
||||
workspaceMember,
|
||||
workspaceMembers,
|
||||
deletedWorkspaceMembers,
|
||||
workspaces: userWorkspaces,
|
||||
availableWorkspaces,
|
||||
} = queryData.currentUser;
|
||||
|
||||
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
|
||||
@ -153,12 +153,8 @@ export const UserProviderEffect = () => {
|
||||
setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaces)) {
|
||||
const workspaces = userWorkspaces
|
||||
.map(({ workspace }) => workspace)
|
||||
.filter(isDefined);
|
||||
|
||||
setWorkspaces(workspaces);
|
||||
if (isDefined(availableWorkspaces)) {
|
||||
setAvailableWorkspaces(availableWorkspaces);
|
||||
}
|
||||
}, [
|
||||
queryLoading,
|
||||
@ -166,9 +162,9 @@ export const UserProviderEffect = () => {
|
||||
setCurrentUser,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspaceMembers,
|
||||
setAvailableWorkspaces,
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setWorkspaces,
|
||||
setIsCurrentUserLoaded,
|
||||
setDateTimeFormat,
|
||||
setCurrentWorkspaceMembersWithDeleted,
|
||||
|
||||
@ -3,6 +3,7 @@ import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
import { AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT } from '@/auth/graphql/fragments/authFragments';
|
||||
|
||||
export const USER_QUERY_FRAGMENT = gql`
|
||||
${ROLE_FRAGMENT}
|
||||
@ -48,8 +49,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
customDomain
|
||||
isCustomDomainEnabled
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
...WorkspaceUrlsFragment
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
@ -86,22 +86,13 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
...RoleFragment
|
||||
}
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
logo
|
||||
displayName
|
||||
subdomain
|
||||
customDomain
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
}
|
||||
userVars
|
||||
}
|
||||
|
||||
${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT}
|
||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const WORKSPACE_URLS_FRAGMENT = gql`
|
||||
fragment WorkspaceUrlsFragment on WorkspaceUrls {
|
||||
subdomainUrl
|
||||
customUrl
|
||||
}
|
||||
`;
|
||||
@ -124,7 +124,7 @@ export const PasswordReset = () => {
|
||||
const [updatePasswordViaToken, { loading: isUpdatingPassword }] =
|
||||
useUpdatePasswordViaResetTokenMutation();
|
||||
|
||||
const { signInWithCredentials } = useAuth();
|
||||
const { signInWithCredentialsInWorkspace } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
|
||||
const onSubmit = async (formData: Form) => {
|
||||
@ -153,7 +153,11 @@ export const PasswordReset = () => {
|
||||
|
||||
const token = await readCaptchaToken();
|
||||
|
||||
await signInWithCredentials(email || '', formData.newPassword, token);
|
||||
await signInWithCredentialsInWorkspace(
|
||||
email || '',
|
||||
formData.newPassword,
|
||||
token,
|
||||
);
|
||||
navigate(AppPath.Index);
|
||||
} catch (err) {
|
||||
logError(err);
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
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 {
|
||||
SignInUpStep,
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
|
||||
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
|
||||
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection';
|
||||
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
|
||||
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
|
||||
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
@ -26,17 +29,20 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnimatedEaseIn } from 'twenty-ui/utilities';
|
||||
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
|
||||
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
|
||||
|
||||
const StandardContent = ({
|
||||
workspacePublicData,
|
||||
signInUpForm,
|
||||
signInUpStep,
|
||||
title,
|
||||
onClickOnLogo,
|
||||
}: {
|
||||
workspacePublicData: PublicWorkspaceDataOutput | null;
|
||||
signInUpForm: JSX.Element | null;
|
||||
signInUpStep: SignInUpStep;
|
||||
title: string;
|
||||
onClickOnLogo: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Modal.Content isVerticalCentered isHorizontalCentered>
|
||||
@ -44,6 +50,7 @@ const StandardContent = ({
|
||||
<Logo
|
||||
secondaryLogo={workspacePublicData?.logo}
|
||||
placeholder={workspacePublicData?.displayName}
|
||||
onClick={onClickOnLogo}
|
||||
/>
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
@ -55,6 +62,7 @@ const StandardContent = ({
|
||||
|
||||
export const SignInUp = () => {
|
||||
const { t } = useLingui();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { signInUpStep } = useSignInUp(form);
|
||||
@ -67,10 +75,20 @@ export const SignInUp = () => {
|
||||
useWorkspaceFromInviteHash();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const onClickOnLogo = () => {
|
||||
setSignInUpStep(SignInUpStep.Init);
|
||||
};
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (isDefined(workspaceInviteHash)) {
|
||||
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.WorkspaceSelection) {
|
||||
return t`Choose a Workspace`;
|
||||
}
|
||||
|
||||
const workspaceName = !isDefined(workspacePublicData?.displayName)
|
||||
? DEFAULT_WORKSPACE_NAME
|
||||
: workspacePublicData?.displayName === ''
|
||||
@ -79,31 +97,32 @@ export const SignInUp = () => {
|
||||
|
||||
return t`Welcome to ${workspaceName}`;
|
||||
}, [
|
||||
workspaceFromInviteHash?.displayName,
|
||||
workspaceInviteHash,
|
||||
signInUpStep,
|
||||
workspacePublicData?.displayName,
|
||||
t,
|
||||
workspaceFromInviteHash?.displayName,
|
||||
]);
|
||||
|
||||
const signInUpForm = useMemo(() => {
|
||||
if (loading) return null;
|
||||
|
||||
if (isDefaultDomain && isMultiWorkspaceEnabled) {
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
return (
|
||||
<>
|
||||
<SignInUpGlobalScopeFormEffect />
|
||||
<SignInUpGlobalScopeForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(!isMultiWorkspaceEnabled ||
|
||||
(isMultiWorkspaceEnabled && isOnAWorkspace)) &&
|
||||
isOnAWorkspace &&
|
||||
signInUpStep === SignInUpStep.SSOIdentityProviderSelection
|
||||
) {
|
||||
return <SignInUpSSOIdentityProviderSelection />;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(workspacePublicData) &&
|
||||
(!isMultiWorkspaceEnabled || isOnAWorkspace)
|
||||
) {
|
||||
if (isDefined(workspacePublicData) && isOnAWorkspace) {
|
||||
return (
|
||||
<>
|
||||
<SignInUpWorkspaceScopeFormEffect />
|
||||
@ -112,7 +131,12 @@ export const SignInUp = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return <SignInUpGlobalScopeForm />;
|
||||
return (
|
||||
<>
|
||||
<SignInUpGlobalScopeFormEffect />
|
||||
<SignInUpGlobalScopeForm />
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isDefaultDomain,
|
||||
isMultiWorkspaceEnabled,
|
||||
@ -136,6 +160,7 @@ export const SignInUp = () => {
|
||||
signInUpForm={signInUpForm}
|
||||
signInUpStep={signInUpStep}
|
||||
title={title}
|
||||
onClickOnLogo={onClickOnLogo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { z } from 'zod';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { z } from 'zod';
|
||||
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
export const SettingsSecurityApprovedAccessDomain = () => {
|
||||
const navigate = useNavigateSettings();
|
||||
@ -62,7 +62,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
enqueueSnackBar(t`Domain added successfully.`, {
|
||||
enqueueSnackBar(t`Please check your email for a verification link.`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
navigate(SettingsPath.Security);
|
||||
|
||||
@ -19,6 +19,7 @@ import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
import { mockedViewsData } from '~/testing/mock-data/views';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain';
|
||||
|
||||
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
|
||||
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
|
||||
@ -90,22 +91,8 @@ export const graphqlMocks = {
|
||||
() => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getPublicWorkspaceDataByDomain: {
|
||||
id: 'id',
|
||||
logo: 'logo',
|
||||
displayName: 'displayName',
|
||||
workspaceUrls: {
|
||||
customUrl: undefined,
|
||||
subdomainUrl: 'https://twenty.com',
|
||||
},
|
||||
authProviders: {
|
||||
google: true,
|
||||
microsoft: false,
|
||||
password: true,
|
||||
magicLink: false,
|
||||
sso: [],
|
||||
},
|
||||
},
|
||||
getPublicWorkspaceDataByDomain:
|
||||
mockedPublicWorkspaceDataBySubdomain,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -7,6 +7,7 @@ export const mockedPublicWorkspaceDataBySubdomain: GetPublicWorkspaceDataByDomai
|
||||
logo: 'workspace-logo/original/c88deb49-7636-4560-918d-08c3265ffb20.49?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3Jrc3BhY2VJZCI6Ijk4NzAzMjNlLTIyYzMtNGQxNC05YjdmLTViZGM4NGY3ZDZlZSIsImlhdCI6MTczNjU0MDU0MywiZXhwIjoxNzM2NjI2OTQzfQ.C8cnHu09VGseRbQAMM4nhiO6z4TLG03ntFTvxm53-xg',
|
||||
displayName: 'Twenty Eng',
|
||||
workspaceUrls: {
|
||||
__typename: 'WorkspaceUrls',
|
||||
customUrl: 'https://twenty-eng.com',
|
||||
subdomainUrl: 'https://custom.twenty.com',
|
||||
},
|
||||
|
||||
@ -25,6 +25,7 @@ type MockedUser = Pick<
|
||||
| 'supportUserHash'
|
||||
| 'onboardingStatus'
|
||||
| 'userVars'
|
||||
| 'availableWorkspaces'
|
||||
> & {
|
||||
workspaceMember: WorkspaceMember | null;
|
||||
locale: string;
|
||||
@ -132,6 +133,10 @@ export const mockedUserData: MockedUser = {
|
||||
workspaces: [{ workspace: mockCurrentWorkspace }],
|
||||
workspaceMembers: [mockedWorkspaceMemberData],
|
||||
onboardingStatus: OnboardingStatus.COMPLETED,
|
||||
availableWorkspaces: {
|
||||
availableWorkspacesForSignIn: [],
|
||||
availableWorkspacesForSignUp: [],
|
||||
},
|
||||
userVars: {},
|
||||
};
|
||||
|
||||
|
||||
@ -22,4 +22,4 @@ MESSAGING_PROVIDER_GMAIL_CALLBACK_URL=http://localhost:3000/auth/google-gmail/ge
|
||||
AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
|
||||
AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token
|
||||
|
||||
CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
|
||||
CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
|
||||
6
packages/twenty-server/@types/express.d.ts
vendored
6
packages/twenty-server/@types/express.d.ts
vendored
@ -1,15 +1,17 @@
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: User | null;
|
||||
apiKey?: ApiKeyWorkspaceEntity | null;
|
||||
workspace: Workspace;
|
||||
workspaceId: string;
|
||||
workspace?: Workspace;
|
||||
workspaceId?: string;
|
||||
workspaceMetadataVersion?: number;
|
||||
workspaceMemberId?: string;
|
||||
userWorkspaceId?: string;
|
||||
authProvider?: AuthProviderEnum | null;
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/twenty-server/@types/jest.d.ts
vendored
1
packages/twenty-server/@types/jest.d.ts
vendored
@ -22,6 +22,7 @@ declare global {
|
||||
const MEMBER_ACCESS_TOKEN: string;
|
||||
const GUEST_ACCESS_TOKEN: string;
|
||||
const API_KEY_ACCESS_TOKEN: string;
|
||||
const WORKSPACE_AGNOSTIC_TOKEN: string;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@ -37,6 +37,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
args: Input;
|
||||
@ -83,11 +84,15 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
try {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
await this.validate(args, options);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
});
|
||||
|
||||
@ -124,7 +129,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
|
||||
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const executedByApiKey = isDefined(authContext.apiKey);
|
||||
@ -169,7 +174,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
const resultWithGetters = await this.queryResultGettersFactory.create(
|
||||
results,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
authContext.workspace.id,
|
||||
workspace.id,
|
||||
options.objectMetadataMaps,
|
||||
);
|
||||
|
||||
@ -191,6 +196,10 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
) {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
if (
|
||||
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
@ -206,7 +215,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
setting: permissionRequired,
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
isExecutedByApiKey: isDefined(authContext.apiKey),
|
||||
});
|
||||
|
||||
@ -231,11 +240,15 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
const requiredPermission =
|
||||
this.getRequiredPermissionForMethod(operationName);
|
||||
|
||||
const workspace = options.authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasObjectRecordsPermission({
|
||||
userWorkspaceId: options.authContext.userWorkspaceId,
|
||||
requiredPermission,
|
||||
workspaceId: options.authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
isExecutedByApiKey: isDefined(options.authContext.apiKey),
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class ApiEventEmitterService {
|
||||
@ -33,7 +34,7 @@ export class ApiEventEmitterService {
|
||||
after: record,
|
||||
},
|
||||
})),
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: authContext.workspace?.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,6 +51,10 @@ export class ApiEventEmitterService {
|
||||
authContext: AuthContext;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
}): void {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const mappedExistingRecords = existingRecords.reduce(
|
||||
(acc, { id, ...record }) => ({
|
||||
...acc,
|
||||
@ -84,7 +89,7 @@ export class ApiEventEmitterService {
|
||||
},
|
||||
};
|
||||
}),
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -97,6 +102,10 @@ export class ApiEventEmitterService {
|
||||
authContext: AuthContext;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
}): void {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular,
|
||||
action: DatabaseEventAction.DELETED,
|
||||
@ -111,7 +120,7 @@ export class ApiEventEmitterService {
|
||||
},
|
||||
};
|
||||
}),
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -124,6 +133,10 @@ export class ApiEventEmitterService {
|
||||
authContext: AuthContext;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
}): void {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular,
|
||||
action: DatabaseEventAction.RESTORED,
|
||||
@ -138,7 +151,7 @@ export class ApiEventEmitterService {
|
||||
},
|
||||
};
|
||||
}),
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -151,6 +164,10 @@ export class ApiEventEmitterService {
|
||||
authContext: AuthContext;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
}): void {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular,
|
||||
action: DatabaseEventAction.DESTROYED,
|
||||
@ -165,7 +182,7 @@ export class ApiEventEmitterService {
|
||||
},
|
||||
};
|
||||
}),
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
type ArgPositionBackfillInput = {
|
||||
argIndex?: number;
|
||||
@ -171,7 +172,10 @@ export class QueryRunnerArgsFactory {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
const workspaceId = options.authContext.workspace.id;
|
||||
const workspace = options.authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
let isFieldPositionPresent = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -192,7 +196,7 @@ export class QueryRunnerArgsFactory {
|
||||
const newValue = await this.recordPositionService.buildRecordPosition(
|
||||
{
|
||||
value,
|
||||
workspaceId,
|
||||
workspaceId: workspace.id,
|
||||
objectMetadata: {
|
||||
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
|
||||
nameSingular:
|
||||
@ -234,7 +238,7 @@ export class QueryRunnerArgsFactory {
|
||||
'position',
|
||||
await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
workspaceId,
|
||||
workspaceId: workspace.id,
|
||||
objectMetadata: {
|
||||
isCustom: options.objectMetadataItemWithFieldMaps.isCustom,
|
||||
nameSingular:
|
||||
|
||||
@ -22,6 +22,7 @@ import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-ru
|
||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
@ -89,6 +90,10 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
): Promise<ReturnType<WorkspacePreQueryHookInstance['execute']>> {
|
||||
const methodName = 'execute';
|
||||
|
||||
const workspace = executeParams?.[0].workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
if (isRequestScoped) {
|
||||
const contextId = createContextId();
|
||||
|
||||
@ -96,7 +101,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
this.moduleRef.registerRequestByContextId(
|
||||
{
|
||||
req: {
|
||||
workspaceId: executeParams?.[0].workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
},
|
||||
contextId,
|
||||
@ -152,6 +157,10 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
): Promise<ReturnType<WorkspacePostQueryHookInstance['execute']>> {
|
||||
const methodName = 'execute';
|
||||
|
||||
const workspace = executeParams?.[0].workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const transformedPayload = this.transformPayload(executeParams[2]);
|
||||
|
||||
if (isRequestScoped) {
|
||||
@ -161,7 +170,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
this.moduleRef.registerRequestByContextId(
|
||||
{
|
||||
req: {
|
||||
workspaceId: executeParams?.[0].workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: executeParams?.[0].userWorkspaceId,
|
||||
apiKey: executeParams?.[0].apiKey,
|
||||
workspaceMemberId: executeParams?.[0].workspaceMemberId,
|
||||
|
||||
@ -18,6 +18,7 @@ import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-module
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class CoreQueryBuilderFactory {
|
||||
@ -42,6 +43,8 @@ export class CoreQueryBuilderFactory {
|
||||
const { workspace } =
|
||||
await this.accessTokenService.validateTokenByRequest(request);
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/compos
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CreateInput = Record<string, any>;
|
||||
@ -30,6 +31,10 @@ export class CreatedByFromAuthContextService {
|
||||
objectMetadataNameSingular: string,
|
||||
authContext: AuthContext,
|
||||
): Promise<CreateInput[]> {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
// TODO: Once all objects have it, we can remove this check
|
||||
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
@ -37,7 +42,7 @@ export class CreatedByFromAuthContextService {
|
||||
nameSingular: objectMetadataNameSingular,
|
||||
},
|
||||
name: 'createdBy',
|
||||
workspaceId: authContext.workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -78,6 +83,8 @@ export class CreatedByFromAuthContextService {
|
||||
): Promise<ActorMetadata> {
|
||||
const { workspace, workspaceMemberId, user, apiKey } = authContext;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
// TODO: remove that code once we have the workspace member id in all tokens
|
||||
if (isDefined(workspaceMemberId) && isDefined(user)) {
|
||||
return buildCreatedByFromFullNameMetadata({
|
||||
|
||||
@ -26,7 +26,6 @@ export enum AppTokenType {
|
||||
AuthorizationCode = 'AUTHORIZATION_CODE',
|
||||
PasswordResetToken = 'PASSWORD_RESET_TOKEN',
|
||||
InvitationToken = 'INVITATION_TOKEN',
|
||||
OIDCCodeVerifier = 'OIDC_CODE_VERIFIER',
|
||||
EmailVerificationToken = 'EMAIL_VERIFICATION_TOKEN',
|
||||
}
|
||||
|
||||
|
||||
@ -203,4 +203,20 @@ export class ApprovedAccessDomainService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findValidatedApprovedAccessDomainWithWorkspacesAndSSOIdentityProvidersDomain(
|
||||
domain: string,
|
||||
) {
|
||||
return await this.approvedAccessDomainRepository.find({
|
||||
relations: [
|
||||
'workspace',
|
||||
'workspace.workspaceSSOIdentityProviders',
|
||||
'workspace.approvedAccessDomains',
|
||||
],
|
||||
where: {
|
||||
domain,
|
||||
isValidated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,4 +26,5 @@ export enum AuthExceptionCode {
|
||||
GOOGLE_API_AUTH_DISABLED = 'GOOGLE_API_AUTH_DISABLED',
|
||||
MICROSOFT_API_AUTH_DISABLED = 'MICROSOFT_API_AUTH_DISABLED',
|
||||
MISSING_ENVIRONMENT_VARIABLE = 'MISSING_ENVIRONMENT_VARIABLE',
|
||||
INVALID_JWT_TOKEN_TYPE = 'INVALID_JWT_TOKEN_TYPE',
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -45,6 +47,10 @@ describe('AuthResolver', () => {
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: RefreshTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
@ -81,6 +87,10 @@ describe('AuthResolver', () => {
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceAgnosticTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TransientTokenService,
|
||||
useValue: {},
|
||||
|
||||
@ -28,7 +28,6 @@ import {
|
||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
||||
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
||||
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
|
||||
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
|
||||
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
@ -55,14 +54,21 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { AvailableWorkspacesAndAccessTokensOutput } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.output';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||
import { UserCredentialsInput } from './dto/user-credentials.input';
|
||||
import { LoginToken } from './dto/login-token.entity';
|
||||
import { SignUpInput } from './dto/sign-up.input';
|
||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||
import { UserExistsOutput } from './dto/user-exists.entity';
|
||||
import { CheckUserExistsInput } from './dto/user-exists.input';
|
||||
import { CheckUserExistOutput } from './dto/user-exists.entity';
|
||||
import { EmailAndCaptchaInput } from './dto/user-exists.input';
|
||||
import { WorkspaceInviteHashValid } from './dto/workspace-invite-hash-valid.entity';
|
||||
import { WorkspaceInviteHashValidInput } from './dto/workspace-invite-hash.input';
|
||||
import { AuthService } from './services/auth.service';
|
||||
@ -85,6 +91,8 @@ export class AuthResolver {
|
||||
private apiKeyService: ApiKeyService,
|
||||
private resetPasswordService: ResetPasswordService,
|
||||
private loginTokenService: LoginTokenService,
|
||||
private workspaceAgnosticTokenService: WorkspaceAgnosticTokenService,
|
||||
private refreshTokenService: RefreshTokenService,
|
||||
private signInUpService: SignInUpService,
|
||||
private transientTokenService: TransientTokenService,
|
||||
private emailVerificationService: EmailVerificationService,
|
||||
@ -96,10 +104,10 @@ export class AuthResolver {
|
||||
) {}
|
||||
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
@Query(() => UserExistsOutput)
|
||||
@Query(() => CheckUserExistOutput)
|
||||
async checkUserExists(
|
||||
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||
): Promise<typeof UserExistsOutput> {
|
||||
@Args() checkUserExistsInput: EmailAndCaptchaInput,
|
||||
): Promise<CheckUserExistOutput> {
|
||||
return await this.authService.checkUserExists(
|
||||
checkUserExistsInput.email.toLowerCase(),
|
||||
);
|
||||
@ -136,11 +144,11 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
async getLoginTokenFromCredentials(
|
||||
@Args()
|
||||
getLoginTokenFromCredentialsInput: GetLoginTokenFromCredentialsInput,
|
||||
getLoginTokenFromCredentialsInput: UserCredentialsInput,
|
||||
@Args('origin') origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const workspace =
|
||||
@ -156,7 +164,7 @@ export class AuthResolver {
|
||||
),
|
||||
);
|
||||
|
||||
const user = await this.authService.getLoginTokenFromCredentials(
|
||||
const user = await this.authService.validateLoginWithPassword(
|
||||
getLoginTokenFromCredentialsInput,
|
||||
workspace,
|
||||
);
|
||||
@ -164,17 +172,58 @@ export class AuthResolver {
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
// email validation is active only for password flow
|
||||
AuthProviderEnum.Password,
|
||||
);
|
||||
|
||||
return { loginToken };
|
||||
}
|
||||
|
||||
@Mutation(() => AvailableWorkspacesAndAccessTokensOutput)
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
async signIn(
|
||||
@Args()
|
||||
userCredentials: UserCredentialsInput,
|
||||
): Promise<AvailableWorkspacesAndAccessTokensOutput> {
|
||||
const user =
|
||||
await this.authService.validateLoginWithPassword(userCredentials);
|
||||
|
||||
const availableWorkspaces =
|
||||
await this.userWorkspaceService.findAvailableWorkspacesByEmail(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return {
|
||||
availableWorkspaces:
|
||||
await this.userWorkspaceService.setLoginTokenToAvailableWorkspacesWhenAuthProviderMatch(
|
||||
availableWorkspaces,
|
||||
user,
|
||||
AuthProviderEnum.Password,
|
||||
),
|
||||
tokens: {
|
||||
accessToken:
|
||||
await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken(
|
||||
{
|
||||
userId: user.id,
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
},
|
||||
),
|
||||
refreshToken: await this.refreshTokenService.generateRefreshToken({
|
||||
userId: user.id,
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
targetedTokenType: JwtTokenTypeEnum.WORKSPACE_AGNOSTIC,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => GetLoginTokenFromEmailVerificationTokenOutput)
|
||||
@UseGuards(PublicEndpointGuard)
|
||||
async getLoginTokenFromEmailVerificationToken(
|
||||
@Args()
|
||||
getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput,
|
||||
@Args('origin') origin: string,
|
||||
@AuthProvider() authProvider: AuthProviderEnum,
|
||||
) {
|
||||
const appToken =
|
||||
await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow(
|
||||
@ -195,6 +244,7 @@ export class AuthResolver {
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
appToken.user.email,
|
||||
workspace.id,
|
||||
authProvider,
|
||||
);
|
||||
|
||||
const workspaceUrls = this.domainManagerService.getWorkspaceUrls(workspace);
|
||||
@ -202,12 +252,59 @@ export class AuthResolver {
|
||||
return { loginToken, workspaceUrls };
|
||||
}
|
||||
|
||||
@Mutation(() => AvailableWorkspacesAndAccessTokensOutput)
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
async signUp(
|
||||
@Args() signUpInput: UserCredentialsInput,
|
||||
): Promise<AvailableWorkspacesAndAccessTokensOutput> {
|
||||
const user = await this.signInUpService.signUpWithoutWorkspace(
|
||||
{
|
||||
email: signUpInput.email,
|
||||
},
|
||||
{
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: signUpInput.password,
|
||||
},
|
||||
);
|
||||
|
||||
const availableWorkspaces =
|
||||
await this.userWorkspaceService.findAvailableWorkspacesByEmail(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return {
|
||||
availableWorkspaces:
|
||||
await this.userWorkspaceService.setLoginTokenToAvailableWorkspacesWhenAuthProviderMatch(
|
||||
availableWorkspaces,
|
||||
user,
|
||||
AuthProviderEnum.Password,
|
||||
),
|
||||
tokens: {
|
||||
accessToken:
|
||||
await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken(
|
||||
{
|
||||
userId: user.id,
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
},
|
||||
),
|
||||
refreshToken: await this.refreshTokenService.generateRefreshToken({
|
||||
userId: user.id,
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
targetedTokenType: JwtTokenTypeEnum.WORKSPACE_AGNOSTIC,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => SignUpOutput)
|
||||
async signUp(@Args() signUpInput: SignUpInput): Promise<SignUpOutput> {
|
||||
@UseGuards(CaptchaGuard, PublicEndpointGuard)
|
||||
async signUpInWorkspace(
|
||||
@Args() signUpInput: SignUpInput,
|
||||
@AuthProvider() authProvider: AuthProviderEnum,
|
||||
): Promise<SignUpOutput> {
|
||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||
workspaceInviteHash: signUpInput.workspaceInviteHash,
|
||||
authProvider: 'password',
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
workspaceId: signUpInput.workspaceId,
|
||||
});
|
||||
|
||||
@ -246,7 +343,7 @@ export class AuthResolver {
|
||||
workspace: currentWorkspace,
|
||||
invitation,
|
||||
authParams: {
|
||||
provider: 'password',
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: signUpInput.password,
|
||||
},
|
||||
});
|
||||
@ -262,6 +359,7 @@ export class AuthResolver {
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
authProvider,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -277,6 +375,7 @@ export class AuthResolver {
|
||||
@UseGuards(UserAuthGuard)
|
||||
async signUpInNewWorkspace(
|
||||
@AuthUser() currentUser: User,
|
||||
@AuthProvider() authProvider: AuthProviderEnum,
|
||||
): Promise<SignUpOutput> {
|
||||
const { user, workspace } = await this.signInUpService.signUpOnNewWorkspace(
|
||||
{ type: 'existingUser', existingUser: currentUser },
|
||||
@ -285,6 +384,7 @@ export class AuthResolver {
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
authProvider,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -320,11 +420,11 @@ export class AuthResolver {
|
||||
return;
|
||||
}
|
||||
const transientToken =
|
||||
await this.transientTokenService.generateTransientToken(
|
||||
workspaceMember.id,
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
await this.transientTokenService.generateTransientToken({
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
});
|
||||
|
||||
return { transientToken };
|
||||
}
|
||||
@ -335,6 +435,14 @@ export class AuthResolver {
|
||||
@Args() getAuthTokensFromLoginTokenInput: GetAuthTokensFromLoginTokenInput,
|
||||
@Args('origin') origin: string,
|
||||
): Promise<AuthTokens> {
|
||||
const {
|
||||
sub: email,
|
||||
workspaceId,
|
||||
authProvider,
|
||||
} = await this.loginTokenService.verifyLoginToken(
|
||||
getAuthTokensFromLoginTokenInput.loginToken,
|
||||
);
|
||||
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
@ -342,11 +450,6 @@ export class AuthResolver {
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const { sub: email, workspaceId } =
|
||||
await this.loginTokenService.verifyLoginToken(
|
||||
getAuthTokensFromLoginTokenInput.loginToken,
|
||||
);
|
||||
|
||||
if (workspaceId !== workspace.id) {
|
||||
throw new AuthException(
|
||||
'Token is not valid for this workspace',
|
||||
@ -354,7 +457,7 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
return await this.authService.verify(email, workspace.id);
|
||||
return await this.authService.verify(email, workspace.id, authProvider);
|
||||
}
|
||||
|
||||
@Mutation(() => AuthorizeApp)
|
||||
|
||||
@ -6,10 +6,8 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
@ -17,23 +15,13 @@ import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oau
|
||||
import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
|
||||
@Controller('auth/google')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
export class GoogleAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard, PublicEndpointGuard)
|
||||
@ -46,90 +34,11 @@ export class GoogleAuthController {
|
||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard, PublicEndpointGuard)
|
||||
@UseFilters(AuthOAuthExceptionFilter)
|
||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email: rawEmail,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspaceId,
|
||||
billingCheckoutSessionState,
|
||||
locale,
|
||||
} = req.user;
|
||||
|
||||
const email = rawEmail.toLowerCase();
|
||||
|
||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||
workspaceId,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
authProvider: 'google',
|
||||
});
|
||||
|
||||
try {
|
||||
const invitation =
|
||||
currentWorkspace && email
|
||||
? await this.authService.findInvitationForSignInUp({
|
||||
currentWorkspace,
|
||||
email,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
const { userData } = this.authService.formatUserDataPayload(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
locale,
|
||||
},
|
||||
existingUser,
|
||||
);
|
||||
|
||||
await this.authService.checkAccessForSignIn({
|
||||
userData,
|
||||
invitation,
|
||||
workspaceInviteHash,
|
||||
workspace: currentWorkspace,
|
||||
});
|
||||
|
||||
const { user, workspace } = await this.authService.signInUp({
|
||||
invitation,
|
||||
workspace: currentWorkspace,
|
||||
userData,
|
||||
authParams: {
|
||||
provider: 'google',
|
||||
},
|
||||
billingCheckoutSessionState,
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
workspace,
|
||||
billingCheckoutSessionState,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.redirect(
|
||||
await this.authService.signInUpWithSocialSSO(
|
||||
req.user,
|
||||
AuthProviderEnum.Google,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,33 +6,21 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
|
||||
import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
export class MicrosoftAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(
|
||||
@ -55,90 +43,11 @@ export class MicrosoftAuthController {
|
||||
@Req() req: MicrosoftRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email: rawEmail,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspaceId,
|
||||
billingCheckoutSessionState,
|
||||
locale,
|
||||
} = req.user;
|
||||
|
||||
const email = rawEmail.toLowerCase();
|
||||
|
||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||
workspaceId,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
authProvider: 'microsoft',
|
||||
});
|
||||
|
||||
try {
|
||||
const invitation =
|
||||
currentWorkspace && email
|
||||
? await this.authService.findInvitationForSignInUp({
|
||||
currentWorkspace,
|
||||
email,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
const { userData } = this.authService.formatUserDataPayload(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
locale,
|
||||
},
|
||||
existingUser,
|
||||
);
|
||||
|
||||
await this.authService.checkAccessForSignIn({
|
||||
userData,
|
||||
invitation,
|
||||
workspaceInviteHash,
|
||||
workspace: currentWorkspace,
|
||||
});
|
||||
|
||||
const { user, workspace } = await this.authService.signInUp({
|
||||
invitation,
|
||||
workspace: currentWorkspace,
|
||||
userData,
|
||||
authParams: {
|
||||
provider: 'microsoft',
|
||||
},
|
||||
billingCheckoutSessionState,
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
workspace,
|
||||
billingCheckoutSessionState,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res.redirect(
|
||||
await this.authService.signInUpWithSocialSSO(
|
||||
req.user,
|
||||
AuthProviderEnum.Microsoft,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
|
||||
@Controller('auth')
|
||||
export class SSOAuthController {
|
||||
@ -136,7 +137,7 @@ export class SSOAuthController {
|
||||
workspaceId: workspaceIdentityProvider.workspaceId,
|
||||
workspaceInviteHash: req.user.workspaceInviteHash,
|
||||
email: req.user.email,
|
||||
authProvider: 'sso',
|
||||
authProvider: AuthProviderEnum.SSO,
|
||||
});
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
@ -206,7 +207,7 @@ export class SSOAuthController {
|
||||
workspace: currentWorkspace,
|
||||
invitation,
|
||||
authParams: {
|
||||
provider: 'sso',
|
||||
provider: AuthProviderEnum.SSO,
|
||||
},
|
||||
});
|
||||
|
||||
@ -215,6 +216,7 @@ export class SSOAuthController {
|
||||
loginToken: await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
AuthProviderEnum.SSO,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { AvailableWorkspaces } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
|
||||
import { AuthTokenPair } from './token.entity';
|
||||
|
||||
@ObjectType()
|
||||
export class AvailableWorkspacesAndAccessTokensOutput {
|
||||
@Field(() => AuthTokenPair)
|
||||
tokens: AuthTokenPair;
|
||||
|
||||
@Field(() => AvailableWorkspaces)
|
||||
availableWorkspaces: AvailableWorkspaces;
|
||||
}
|
||||
@ -28,13 +28,22 @@ class SSOConnection {
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AvailableWorkspaceOutput {
|
||||
export class AvailableWorkspace {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
displayName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
loginToken?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
personalInviteToken?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
inviteHash?: string;
|
||||
|
||||
@Field(() => WorkspaceUrls)
|
||||
workspaceUrls: WorkspaceUrls;
|
||||
|
||||
@ -44,3 +53,12 @@ export class AvailableWorkspaceOutput {
|
||||
@Field(() => [SSOConnection])
|
||||
sso: SSOConnection[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AvailableWorkspaces {
|
||||
@Field(() => [AvailableWorkspace])
|
||||
availableWorkspacesForSignIn: Array<AvailableWorkspace>;
|
||||
|
||||
@Field(() => [AvailableWorkspace])
|
||||
availableWorkspacesForSignUp: Array<AvailableWorkspace>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateUserAndWorkspaceInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
picture?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
locale?: keyof typeof APP_LOCALES;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
captchaToken?: string;
|
||||
}
|
||||
@ -41,3 +41,9 @@ export class PasswordResetToken {
|
||||
@Field(() => String)
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class WorkspaceAgnosticToken {
|
||||
@Field(() => AuthToken)
|
||||
token: AuthToken;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class GetLoginTokenFromCredentialsInput {
|
||||
export class UserCredentialsInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
@ -1,33 +1,13 @@
|
||||
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class UserExists {
|
||||
export class CheckUserExistOutput {
|
||||
@Field(() => Boolean)
|
||||
exists: true;
|
||||
exists: boolean;
|
||||
|
||||
@Field(() => [AvailableWorkspaceOutput])
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
@Field(() => Number)
|
||||
availableWorkspacesCount: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isEmailVerified: boolean;
|
||||
}
|
||||
|
||||
@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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class CheckUserExistsInput {
|
||||
export class EmailAndCaptchaInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@ -28,6 +28,7 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
||||
case AuthExceptionCode.INVALID_JWT_TOKEN_TYPE:
|
||||
throw new ForbiddenError(exception.message);
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
|
||||
@ -2,14 +2,11 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ApiKeyToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService {
|
||||
constructor(
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
constructor(private readonly jwtWrapperService: JwtWrapperService) {}
|
||||
|
||||
async generateApiKeyToken(
|
||||
workspaceId: string,
|
||||
@ -19,13 +16,8 @@ export class ApiKeyService {
|
||||
if (!apiKeyId) {
|
||||
return;
|
||||
}
|
||||
const jwtPayload = {
|
||||
sub: workspaceId,
|
||||
type: 'API_KEY',
|
||||
workspaceId,
|
||||
};
|
||||
const secret = this.jwtWrapperService.generateAppSecret(
|
||||
'ACCESS',
|
||||
JwtTokenTypeEnum.ACCESS,
|
||||
workspaceId,
|
||||
);
|
||||
let expiresIn: string | number;
|
||||
@ -37,11 +29,18 @@ export class ApiKeyService {
|
||||
} else {
|
||||
expiresIn = '100y';
|
||||
}
|
||||
const token = this.jwtWrapperService.sign(jwtPayload, {
|
||||
secret,
|
||||
expiresIn,
|
||||
jwtid: apiKeyId,
|
||||
});
|
||||
const token = this.jwtWrapperService.sign(
|
||||
{
|
||||
sub: workspaceId,
|
||||
type: JwtTokenTypeEnum.API_KEY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
secret,
|
||||
expiresIn,
|
||||
jwtid: apiKeyId,
|
||||
},
|
||||
);
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
@ -4,10 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class AuthSsoService {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
@ -15,18 +16,16 @@ export class AuthSsoService {
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
private getAuthProviderColumnNameByProvider(
|
||||
authProvider: WorkspaceAuthProvider,
|
||||
) {
|
||||
if (authProvider === 'google') {
|
||||
private getAuthProviderColumnNameByProvider(authProvider: AuthProviderEnum) {
|
||||
if (authProvider === AuthProviderEnum.Google) {
|
||||
return 'isGoogleAuthEnabled';
|
||||
}
|
||||
|
||||
if (authProvider === 'microsoft') {
|
||||
if (authProvider === AuthProviderEnum.Microsoft) {
|
||||
return 'isMicrosoftAuthEnabled';
|
||||
}
|
||||
|
||||
if (authProvider === 'password') {
|
||||
if (authProvider === AuthProviderEnum.Password) {
|
||||
return 'isPasswordAuthEnabled';
|
||||
}
|
||||
|
||||
@ -34,10 +33,7 @@ export class AuthSsoService {
|
||||
}
|
||||
|
||||
async findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||
{
|
||||
authProvider,
|
||||
email,
|
||||
}: { authProvider: WorkspaceAuthProvider; email: string },
|
||||
{ authProvider, email }: { authProvider: AuthProviderEnum; email: string },
|
||||
workspaceId?: string,
|
||||
) {
|
||||
if (
|
||||
|
||||
@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
|
||||
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
|
||||
describe('AuthSsoService', () => {
|
||||
let authSsoService: AuthSsoService;
|
||||
@ -49,7 +50,7 @@ describe('AuthSsoService', () => {
|
||||
|
||||
const result =
|
||||
await authSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||
{ authProvider: 'google', email: 'test@example.com' },
|
||||
{ authProvider: AuthProviderEnum.Google, email: 'test@example.com' },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
@ -63,7 +64,7 @@ describe('AuthSsoService', () => {
|
||||
});
|
||||
|
||||
it('should return a workspace from authProvider and email when multi-workspace mode is enabled', async () => {
|
||||
const authProvider = 'google';
|
||||
const authProvider = AuthProviderEnum.Google;
|
||||
const email = 'test@example.com';
|
||||
const mockWorkspace = { id: 'workspace-id-456' } as Workspace;
|
||||
|
||||
@ -102,7 +103,7 @@ describe('AuthSsoService', () => {
|
||||
|
||||
const result =
|
||||
await authSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({
|
||||
authProvider: 'google',
|
||||
authProvider: AuthProviderEnum.Google,
|
||||
email: 'notfound@example.com',
|
||||
});
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { ExistingUserOrNewUser } from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@ -22,26 +21,26 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
jest.mock('bcrypt');
|
||||
|
||||
const UserFindOneMock = jest.fn();
|
||||
const UserWorkspacefindOneMock = jest.fn();
|
||||
|
||||
const userWorkspaceServiceCheckUserWorkspaceExistsMock = jest.fn();
|
||||
const workspaceInvitationGetOneWorkspaceInvitationMock = jest.fn();
|
||||
const workspaceInvitationValidatePersonalInvitationMock = jest.fn();
|
||||
const userWorkspaceAddUserToWorkspaceMock = jest.fn();
|
||||
|
||||
const twentyConfigServiceGetMock = jest.fn();
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
let userService: UserService;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let userRepository: Repository<User>;
|
||||
let authSsoService: AuthSsoService;
|
||||
let userWorkspaceService: UserWorkspaceService;
|
||||
let workspaceInvitationService: WorkspaceInvitationService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -56,7 +55,7 @@ describe('AuthService', () => {
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {
|
||||
findOne: UserFindOneMock,
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -70,6 +69,22 @@ describe('AuthService', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceAgnosticTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: GuardRedirectService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: SignInUpService,
|
||||
useValue: {},
|
||||
@ -80,10 +95,6 @@ describe('AuthService', () => {
|
||||
get: twentyConfigServiceGetMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: EmailService,
|
||||
useValue: {},
|
||||
@ -99,10 +110,9 @@ describe('AuthService', () => {
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
checkUserWorkspaceExists:
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock,
|
||||
addUserToWorkspaceIfUserNotInWorkspace:
|
||||
userWorkspaceAddUserToWorkspaceMock,
|
||||
checkUserWorkspaceExists: jest.fn(),
|
||||
addUserToWorkspaceIfUserNotInWorkspace: jest.fn(),
|
||||
findAvailableWorkspacesByEmail: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -114,10 +124,8 @@ describe('AuthService', () => {
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
getOneWorkspaceInvitation:
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock,
|
||||
validatePersonalInvitation:
|
||||
workspaceInvitationValidatePersonalInvitationMock,
|
||||
getOneWorkspaceInvitation: jest.fn(),
|
||||
validatePersonalInvitation: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -131,10 +139,18 @@ describe('AuthService', () => {
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||
WorkspaceInvitationService,
|
||||
);
|
||||
authSsoService = module.get<AuthSsoService>(AuthSsoService);
|
||||
userWorkspaceService =
|
||||
module.get<UserWorkspaceService>(UserWorkspaceService);
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
userRepository = module.get<Repository<User>>(
|
||||
getRepositoryToken(User, 'core'),
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -155,17 +171,17 @@ describe('AuthService', () => {
|
||||
|
||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce({
|
||||
jest.spyOn(userRepository, 'findOne').mockReturnValueOnce({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
} as unknown as Promise<User>);
|
||||
|
||||
UserWorkspacefindOneMock.mockReturnValueOnce({});
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockReturnValueOnce({} as any);
|
||||
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock.mockReturnValueOnce({});
|
||||
|
||||
const response = await service.getLoginTokenFromCredentials(
|
||||
const response = await service.validateLoginWithPassword(
|
||||
{
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
@ -188,20 +204,32 @@ describe('AuthService', () => {
|
||||
captchaToken: 'captchaToken',
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
const UserFindOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockReturnValueOnce({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
} as unknown as Promise<User>);
|
||||
|
||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock.mockReturnValueOnce(false);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockReturnValueOnce(null as any);
|
||||
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock.mockReturnValueOnce({});
|
||||
workspaceInvitationValidatePersonalInvitationMock.mockReturnValueOnce({});
|
||||
userWorkspaceAddUserToWorkspaceMock.mockReturnValueOnce({});
|
||||
const getOneWorkspaceInvitationSpy = jest
|
||||
.spyOn(workspaceInvitationService, 'getOneWorkspaceInvitation')
|
||||
.mockReturnValueOnce({} as any);
|
||||
|
||||
const response = await service.getLoginTokenFromCredentials(
|
||||
const workspaceInvitationValidatePersonalInvitationSpy = jest
|
||||
.spyOn(workspaceInvitationService, 'validatePersonalInvitation')
|
||||
.mockReturnValueOnce({} as any);
|
||||
|
||||
const addUserToWorkspaceIfUserNotInWorkspaceSpy = jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockReturnValueOnce({} as any);
|
||||
|
||||
const response = await service.validateLoginWithPassword(
|
||||
{
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
@ -218,14 +246,12 @@ describe('AuthService', () => {
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
|
||||
expect(getOneWorkspaceInvitationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock,
|
||||
workspaceInvitationValidatePersonalInvitationSpy,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
workspaceInvitationValidatePersonalInvitationMock,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(userWorkspaceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
||||
expect(UserFindOneMock).toHaveBeenCalledTimes(1);
|
||||
expect(addUserToWorkspaceIfUserNotInWorkspaceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(UserFindOneSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('checkAccessForSignIn', () => {
|
||||
@ -418,7 +444,7 @@ describe('AuthService', () => {
|
||||
);
|
||||
|
||||
const result = await service.findWorkspaceForSignInUp({
|
||||
authProvider: 'password',
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
workspaceId: 'workspaceId',
|
||||
});
|
||||
|
||||
@ -438,7 +464,7 @@ describe('AuthService', () => {
|
||||
);
|
||||
|
||||
const result = await service.findWorkspaceForSignInUp({
|
||||
authProvider: 'password',
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
workspaceId: 'workspaceId',
|
||||
workspaceInviteHash: 'workspaceInviteHash',
|
||||
});
|
||||
@ -459,7 +485,7 @@ describe('AuthService', () => {
|
||||
);
|
||||
|
||||
const result = await service.findWorkspaceForSignInUp({
|
||||
authProvider: 'password',
|
||||
authProvider: AuthProviderEnum.Password,
|
||||
workspaceId: 'workspaceId',
|
||||
workspaceInviteHash: 'workspaceInviteHash',
|
||||
});
|
||||
@ -476,7 +502,7 @@ describe('AuthService', () => {
|
||||
.mockResolvedValue({} as Workspace);
|
||||
|
||||
const result = await service.findWorkspaceForSignInUp({
|
||||
authProvider: 'google',
|
||||
authProvider: AuthProviderEnum.Google,
|
||||
workspaceId: 'workspaceId',
|
||||
email: 'email',
|
||||
});
|
||||
@ -493,7 +519,7 @@ describe('AuthService', () => {
|
||||
.mockResolvedValue({} as Workspace);
|
||||
|
||||
const result = await service.findWorkspaceForSignInUp({
|
||||
authProvider: 'sso',
|
||||
authProvider: AuthProviderEnum.SSO,
|
||||
workspaceId: 'workspaceId',
|
||||
email: 'email',
|
||||
});
|
||||
|
||||
@ -30,13 +30,9 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
|
||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||
import { GetLoginTokenFromCredentialsInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-credentials.input';
|
||||
import { UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input';
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
|
||||
import {
|
||||
UserExists,
|
||||
UserNotExists,
|
||||
} from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
|
||||
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
@ -57,17 +53,27 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { CheckUserExistOutput } from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.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 { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly workspaceAgnosticTokenService: WorkspaceAgnosticTokenService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly authSsoService: AuthSsoService,
|
||||
@ -121,11 +127,11 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
async getLoginTokenFromCredentials(
|
||||
input: GetLoginTokenFromCredentialsInput,
|
||||
targetWorkspace: Workspace,
|
||||
async validateLoginWithPassword(
|
||||
input: UserCredentialsInput,
|
||||
targetWorkspace?: Workspace,
|
||||
) {
|
||||
if (!targetWorkspace.isPasswordAuthEnabled) {
|
||||
if (targetWorkspace && !targetWorkspace.isPasswordAuthEnabled) {
|
||||
throw new AuthException(
|
||||
'Email/Password auth is not enabled for this workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
@ -146,7 +152,9 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.checkAccessAndUseInvitationOrThrow(targetWorkspace, user);
|
||||
if (targetWorkspace) {
|
||||
await this.checkAccessAndUseInvitationOrThrow(targetWorkspace, user);
|
||||
}
|
||||
|
||||
if (!user.passwordHash) {
|
||||
throw new AuthException(
|
||||
@ -182,7 +190,7 @@ export class AuthService {
|
||||
userData: ExistingUserOrNewUser['userData'],
|
||||
authParams: Extract<
|
||||
AuthProviderWithPasswordType['authParams'],
|
||||
{ provider: 'password' }
|
||||
{ provider: AuthProviderEnum.Password }
|
||||
>,
|
||||
) {
|
||||
if (userData.type === 'newUser') {
|
||||
@ -203,7 +211,7 @@ export class AuthService {
|
||||
authParams: AuthProviderWithPasswordType['authParams'],
|
||||
workspace: Workspace | undefined | null,
|
||||
) {
|
||||
if (authParams.provider === 'password') {
|
||||
if (authParams.provider === AuthProviderEnum.Password) {
|
||||
await this.validatePassword(userData, authParams);
|
||||
}
|
||||
|
||||
@ -248,7 +256,11 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async verify(email: string, workspaceId: string): Promise<AuthTokens> {
|
||||
async verify(
|
||||
email: string,
|
||||
workspaceId: string,
|
||||
authProvider: AuthProviderEnum,
|
||||
): Promise<AuthTokens> {
|
||||
if (!email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
@ -268,14 +280,17 @@ export class AuthService {
|
||||
// passwordHash is hidden for security reasons
|
||||
user.passwordHash = '';
|
||||
|
||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
const accessToken = await this.accessTokenService.generateAccessToken({
|
||||
userId: user.id,
|
||||
workspaceId,
|
||||
);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
user.id,
|
||||
authProvider,
|
||||
});
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken({
|
||||
userId: user.id,
|
||||
workspaceId,
|
||||
);
|
||||
authProvider,
|
||||
targetedTokenType: JwtTokenTypeEnum.ACCESS,
|
||||
});
|
||||
|
||||
return {
|
||||
tokens: {
|
||||
@ -285,21 +300,25 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async checkUserExists(email: string): Promise<UserExists | UserNotExists> {
|
||||
async countAvailableWorkspacesByEmail(email: string): Promise<number> {
|
||||
return Object.values(
|
||||
await this.userWorkspaceService.findAvailableWorkspacesByEmail(email),
|
||||
).flat(2).length;
|
||||
}
|
||||
|
||||
async checkUserExists(email: string): Promise<CheckUserExistOutput> {
|
||||
const user = await this.userRepository.findOneBy({
|
||||
email,
|
||||
});
|
||||
|
||||
if (userValidator.isDefined(user)) {
|
||||
return {
|
||||
exists: true,
|
||||
availableWorkspaces:
|
||||
await this.userWorkspaceService.findAvailableWorkspacesByEmail(email),
|
||||
isEmailVerified: user.isEmailVerified,
|
||||
};
|
||||
}
|
||||
const isUserExist = userValidator.isDefined(user);
|
||||
|
||||
return { exists: false };
|
||||
return {
|
||||
exists: isUserExist,
|
||||
availableWorkspacesCount:
|
||||
await this.countAvailableWorkspacesByEmail(email),
|
||||
isEmailVerified: isUserExist ? user.isEmailVerified : false,
|
||||
};
|
||||
}
|
||||
|
||||
async checkWorkspaceInviteHashIsValid(
|
||||
@ -533,10 +552,10 @@ export class AuthService {
|
||||
workspaceInviteHash?: string;
|
||||
} & (
|
||||
| {
|
||||
authProvider: Exclude<WorkspaceAuthProvider, 'password'>;
|
||||
authProvider: Exclude<AuthProviderEnum, AuthProviderEnum.Password>;
|
||||
email: string;
|
||||
}
|
||||
| { authProvider: Extract<WorkspaceAuthProvider, 'password'> }
|
||||
| { authProvider: Extract<AuthProviderEnum, AuthProviderEnum.Password> }
|
||||
),
|
||||
) {
|
||||
if (params.workspaceInviteHash) {
|
||||
@ -550,7 +569,7 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.authProvider !== 'password') {
|
||||
if (params.authProvider !== AuthProviderEnum.Password) {
|
||||
return (
|
||||
(await this.authSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||
{
|
||||
@ -649,4 +668,142 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async signInUpWithSocialSSO(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email: rawEmail,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspaceId,
|
||||
billingCheckoutSessionState,
|
||||
action,
|
||||
locale,
|
||||
}: MicrosoftRequest['user'] | GoogleRequest['user'],
|
||||
authProvider: AuthProviderEnum.Google | AuthProviderEnum.Microsoft,
|
||||
): Promise<string> {
|
||||
const email = rawEmail.toLowerCase();
|
||||
|
||||
const availableWorkspacesCount =
|
||||
action === 'list-available-workspaces'
|
||||
? await this.countAvailableWorkspacesByEmail(email)
|
||||
: 0;
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (
|
||||
!workspaceId &&
|
||||
!workspaceInviteHash &&
|
||||
action === 'list-available-workspaces' &&
|
||||
availableWorkspacesCount !== 0
|
||||
) {
|
||||
const user =
|
||||
existingUser ??
|
||||
(await this.signInUpService.signUpWithoutWorkspace(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
},
|
||||
{
|
||||
provider: authProvider,
|
||||
},
|
||||
));
|
||||
|
||||
const url = this.domainManagerService.buildBaseUrl({
|
||||
pathname: '/welcome',
|
||||
searchParams: {
|
||||
tokenPair: JSON.stringify({
|
||||
accessToken:
|
||||
await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken(
|
||||
{
|
||||
userId: user.id,
|
||||
authProvider,
|
||||
},
|
||||
),
|
||||
refreshToken: await this.refreshTokenService.generateRefreshToken({
|
||||
userId: user.id,
|
||||
authProvider,
|
||||
targetedTokenType: JwtTokenTypeEnum.WORKSPACE_AGNOSTIC,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const currentWorkspace =
|
||||
action === 'create-new-workspace'
|
||||
? undefined
|
||||
: await this.findWorkspaceForSignInUp({
|
||||
workspaceId,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
authProvider,
|
||||
});
|
||||
|
||||
try {
|
||||
const invitation =
|
||||
currentWorkspace && email
|
||||
? await this.findInvitationForSignInUp({
|
||||
currentWorkspace,
|
||||
email,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const { userData } = this.formatUserDataPayload(
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
locale,
|
||||
},
|
||||
existingUser,
|
||||
);
|
||||
|
||||
await this.checkAccessForSignIn({
|
||||
userData,
|
||||
invitation,
|
||||
workspaceInviteHash,
|
||||
workspace: currentWorkspace,
|
||||
});
|
||||
|
||||
const { user, workspace } = await this.signInUp({
|
||||
invitation,
|
||||
workspace: currentWorkspace,
|
||||
userData,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Google,
|
||||
},
|
||||
billingCheckoutSessionState,
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
authProvider,
|
||||
);
|
||||
|
||||
return this.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
workspace,
|
||||
billingCheckoutSessionState,
|
||||
});
|
||||
} catch (error) {
|
||||
return this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
@ -28,6 +29,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
jest.mock('src/utils/image', () => {
|
||||
return {
|
||||
@ -96,6 +98,10 @@ describe('SignInUpService', () => {
|
||||
provide: HttpService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
@ -155,7 +161,10 @@ describe('SignInUpService', () => {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as Workspace,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as User,
|
||||
@ -206,7 +215,10 @@ describe('SignInUpService', () => {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as Workspace,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as User,
|
||||
@ -230,7 +242,10 @@ describe('SignInUpService', () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'newUserWithPicture',
|
||||
newUserWithPicture: {
|
||||
@ -283,7 +298,10 @@ describe('SignInUpService', () => {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as Workspace,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as User,
|
||||
@ -315,7 +333,10 @@ describe('SignInUpService', () => {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as Workspace,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as User,
|
||||
@ -340,7 +361,10 @@ describe('SignInUpService', () => {
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: null,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'existinguser@example.com' } as User,
|
||||
|
||||
@ -25,8 +25,6 @@ import {
|
||||
SignInUpNewUserPayload,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@ -34,9 +32,10 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -46,16 +45,14 @@ export class SignInUpService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly httpService: HttpService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly userService: UserService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async computeParamsForNewUser(
|
||||
@ -72,7 +69,7 @@ export class SignInUpService {
|
||||
);
|
||||
}
|
||||
|
||||
if (authParams.provider === 'password') {
|
||||
if (authParams.provider === AuthProviderEnum.Password) {
|
||||
newUserParams.passwordHash = await this.generateHash(authParams.password);
|
||||
}
|
||||
|
||||
@ -293,11 +290,27 @@ export class SignInUpService {
|
||||
return await this.userRepository.save(userCreated);
|
||||
}
|
||||
|
||||
private async setDefaultImpersonateAndAccessFullAdminPanel() {
|
||||
if (!this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
const workspacesCount = await this.workspaceRepository.count();
|
||||
|
||||
// let the creation of the first workspace
|
||||
if (workspacesCount > 0) {
|
||||
throw new AuthException(
|
||||
'New workspace setup is disabled',
|
||||
AuthExceptionCode.SIGNUP_DISABLED,
|
||||
);
|
||||
}
|
||||
|
||||
return { canImpersonate: true, canAccessFullAdminPanel: true };
|
||||
}
|
||||
|
||||
return { canImpersonate: false, canAccessFullAdminPanel: false };
|
||||
}
|
||||
|
||||
async signUpOnNewWorkspace(
|
||||
userData: ExistingUserOrPartialUserWithPicture['userData'],
|
||||
) {
|
||||
let canImpersonate = false;
|
||||
let canAccessFullAdminPanel = false;
|
||||
const email =
|
||||
userData.type === 'newUserWithPicture'
|
||||
? userData.newUserWithPicture.email
|
||||
@ -310,21 +323,8 @@ export class SignInUpService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
const workspacesCount = await this.workspaceRepository.count();
|
||||
|
||||
// if the workspace doesn't exist it means it's the first user of the workspace
|
||||
canImpersonate = true;
|
||||
canAccessFullAdminPanel = true;
|
||||
|
||||
// let the creation of the first workspace
|
||||
if (workspacesCount > 0) {
|
||||
throw new AuthException(
|
||||
'New workspace setup is disabled',
|
||||
AuthExceptionCode.SIGNUP_DISABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
const { canImpersonate, canAccessFullAdminPanel } =
|
||||
await this.setDefaultImpersonateAndAccessFullAdminPanel();
|
||||
|
||||
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
|
||||
const isLogoUrlValid = async () => {
|
||||
@ -380,4 +380,14 @@ export class SignInUpService {
|
||||
|
||||
return { user, workspace };
|
||||
}
|
||||
|
||||
async signUpWithoutWorkspace(
|
||||
newUserParams: SignInUpNewUserPayload,
|
||||
authParams: AuthProviderWithPasswordType['authParams'],
|
||||
) {
|
||||
return this.saveNewUser(
|
||||
await this.computeParamsForNewUser(newUserParams, authParams),
|
||||
await this.setDefaultImpersonateAndAccessFullAdminPanel(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { SocialSSOSignInUpActionType } from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
|
||||
export type GoogleRequest = Omit<
|
||||
Request,
|
||||
@ -19,6 +20,7 @@ export type GoogleRequest = Omit<
|
||||
locale?: keyof typeof APP_LOCALES | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
action: SocialSSOSignInUpActionType;
|
||||
workspaceId?: string;
|
||||
billingCheckoutSessionState?: string;
|
||||
};
|
||||
@ -45,6 +47,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
workspaceId: req.params.workspaceId,
|
||||
billingCheckoutSessionState: req.query.billingCheckoutSessionState,
|
||||
workspacePersonalInviteToken: req.query.workspacePersonalInviteToken,
|
||||
action: req.query.action,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -53,8 +56,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
|
||||
async validate(
|
||||
request: GoogleRequest,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
_accessToken: string,
|
||||
_refreshToken: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
profile: any,
|
||||
done: VerifyCallback,
|
||||
@ -74,6 +77,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||
workspaceId: state.workspaceId,
|
||||
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
||||
action: state.action,
|
||||
locale: state.locale,
|
||||
};
|
||||
|
||||
|
||||
@ -155,12 +155,12 @@ describe('JwtAuthStrategy', () => {
|
||||
);
|
||||
|
||||
await expect(strategy.validate(payload as JwtPayload)).rejects.toThrow(
|
||||
new AuthException('User not found', expect.any(String)),
|
||||
new AuthException('UserWorkspace not found', expect.any(String)),
|
||||
);
|
||||
try {
|
||||
await strategy.validate(payload as JwtPayload);
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(AuthExceptionCode.USER_NOT_FOUND);
|
||||
expect(e.code).toBe(AuthExceptionCode.USER_WORKSPACE_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -10,8 +10,12 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
AccessTokenJwtPayload,
|
||||
ApiKeyTokenJwtPayload,
|
||||
AuthContext,
|
||||
FileTokenJwtPayload,
|
||||
JwtPayload,
|
||||
WorkspaceAgnosticTokenJwtPayload,
|
||||
} from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@ -19,6 +23,9 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@ -36,13 +43,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const secretOrKeyProviderFunction = async (_request, rawJwtToken, done) => {
|
||||
try {
|
||||
const decodedToken = jwtWrapperService.decode(
|
||||
rawJwtToken,
|
||||
) as JwtPayload;
|
||||
const workspaceId = decodedToken.workspaceId;
|
||||
const decodedToken = jwtWrapperService.decode<
|
||||
| FileTokenJwtPayload
|
||||
| AccessTokenJwtPayload
|
||||
| WorkspaceAgnosticTokenJwtPayload
|
||||
>(rawJwtToken);
|
||||
|
||||
const appSecretBody =
|
||||
decodedToken.type === 'WORKSPACE_AGNOSTIC'
|
||||
? decodedToken.userId
|
||||
: decodedToken.workspaceId;
|
||||
|
||||
const secret = jwtWrapperService.generateAppSecret(
|
||||
'ACCESS',
|
||||
workspaceId,
|
||||
decodedToken.type,
|
||||
appSecretBody,
|
||||
);
|
||||
|
||||
done(null, secret);
|
||||
@ -58,19 +72,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
});
|
||||
}
|
||||
|
||||
private async validateAPIKey(payload: JwtPayload): Promise<AuthContext> {
|
||||
let apiKey: ApiKeyWorkspaceEntity | null = null;
|
||||
|
||||
private async validateAPIKey(
|
||||
payload: ApiKeyTokenJwtPayload,
|
||||
): Promise<AuthContext> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload['sub'],
|
||||
id: payload.sub,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
const apiKeyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ApiKeyWorkspaceEntity>(
|
||||
@ -78,7 +93,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
'apiKey',
|
||||
);
|
||||
|
||||
apiKey = await apiKeyRepository.findOne({
|
||||
const apiKey = await apiKeyRepository.findOne({
|
||||
where: {
|
||||
id: payload.jti,
|
||||
},
|
||||
@ -91,13 +106,15 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
);
|
||||
}
|
||||
|
||||
return { apiKey, workspace };
|
||||
return { apiKey, workspace, workspaceMemberId: payload.workspaceMemberId };
|
||||
}
|
||||
|
||||
private async validateAccessToken(payload: JwtPayload): Promise<AuthContext> {
|
||||
private async validateAccessToken(
|
||||
payload: AccessTokenJwtPayload,
|
||||
): Promise<AuthContext> {
|
||||
let user: User | null = null;
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload['workspaceId'],
|
||||
id: payload.workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
@ -107,16 +124,19 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
);
|
||||
}
|
||||
|
||||
user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
if (!user) {
|
||||
const userId = payload.sub ?? payload.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!payload.userWorkspaceId) {
|
||||
throw new AuthException(
|
||||
'UserWorkspace not found',
|
||||
@ -130,27 +150,62 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new AuthException(
|
||||
userWorkspaceValidator.assertIsDefinedOrThrow(
|
||||
userWorkspace,
|
||||
new AuthException(
|
||||
'UserWorkspace not found',
|
||||
AuthExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
return { user, workspace, userWorkspaceId: userWorkspace.id };
|
||||
return {
|
||||
user,
|
||||
workspace,
|
||||
authProvider: payload.authProvider,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
workspaceMemberId: payload.workspaceMemberId,
|
||||
};
|
||||
}
|
||||
|
||||
private async validateWorkspaceAgnosticToken(
|
||||
payload: WorkspaceAgnosticTokenJwtPayload,
|
||||
) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
user,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
return { user, authProvider: payload.authProvider };
|
||||
}
|
||||
|
||||
private isLegacyApiKeyPayload(
|
||||
payload: JwtPayload,
|
||||
): payload is ApiKeyTokenJwtPayload {
|
||||
return !payload.type && !('workspaceId' in payload);
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<AuthContext> {
|
||||
const workspaceMemberId = payload.workspaceMemberId;
|
||||
|
||||
if (!payload.type && !payload.workspaceId) {
|
||||
return { ...(await this.validateAPIKey(payload)), workspaceMemberId };
|
||||
// Support legacy api keys
|
||||
if (payload.type === 'API_KEY' || this.isLegacyApiKeyPayload(payload)) {
|
||||
return await this.validateAPIKey(payload);
|
||||
}
|
||||
|
||||
if (payload.type === 'API_KEY') {
|
||||
return { ...(await this.validateAPIKey(payload)), workspaceMemberId };
|
||||
if (payload.type === 'WORKSPACE_AGNOSTIC') {
|
||||
return await this.validateWorkspaceAgnosticToken(payload);
|
||||
}
|
||||
|
||||
return { ...(await this.validateAccessToken(payload)), workspaceMemberId };
|
||||
// `!payload.type` is here to support legacy token
|
||||
if (payload.type === 'ACCESS' || !payload.type) {
|
||||
return await this.validateAccessToken(payload);
|
||||
}
|
||||
|
||||
throw new AuthException(
|
||||
'Invalid token',
|
||||
AuthExceptionCode.INVALID_JWT_TOKEN_TYPE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { SocialSSOSignInUpActionType } from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
|
||||
export type MicrosoftRequest = Omit<
|
||||
Request,
|
||||
@ -25,6 +26,7 @@ export type MicrosoftRequest = Omit<
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceId?: string;
|
||||
billingCheckoutSessionState?: string;
|
||||
action: SocialSSOSignInUpActionType;
|
||||
};
|
||||
};
|
||||
|
||||
@ -50,6 +52,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
locale: req.query.locale,
|
||||
billingCheckoutSessionState: req.query.billingCheckoutSessionState,
|
||||
workspacePersonalInviteToken: req.query.workspacePersonalInviteToken,
|
||||
action: req.query.action,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -58,8 +61,8 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
|
||||
async validate(
|
||||
request: MicrosoftRequest,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
_accessToken: string,
|
||||
_refreshToken: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
profile: any,
|
||||
done: VerifyCallback,
|
||||
@ -90,6 +93,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
workspaceId: state.workspaceId,
|
||||
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
||||
locale: state.locale,
|
||||
action: state.action,
|
||||
};
|
||||
|
||||
done(null, user);
|
||||
|
||||
@ -36,7 +36,7 @@ describe('AccessTokenService', () => {
|
||||
provide: JwtWrapperService,
|
||||
useValue: {
|
||||
sign: jest.fn(),
|
||||
verifyWorkspaceToken: jest.fn(),
|
||||
verifyJwtToken: jest.fn(),
|
||||
decode: jest.fn(),
|
||||
generateAppSecret: jest.fn(),
|
||||
extractJwtFromRequest: jest.fn(),
|
||||
@ -138,7 +138,7 @@ describe('AccessTokenService', () => {
|
||||
} as any);
|
||||
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
|
||||
|
||||
const result = await service.generateAccessToken(userId, workspaceId);
|
||||
const result = await service.generateAccessToken({ userId, workspaceId });
|
||||
|
||||
expect(result).toEqual({
|
||||
token: mockToken,
|
||||
@ -159,7 +159,10 @@ describe('AccessTokenService', () => {
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generateAccessToken('non-existent-user', 'workspace-id'),
|
||||
service.generateAccessToken({
|
||||
userId: 'non-existent-user',
|
||||
workspaceId: 'workspace-id',
|
||||
}),
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
});
|
||||
@ -184,7 +187,7 @@ describe('AccessTokenService', () => {
|
||||
.spyOn(jwtWrapperService, 'extractJwtFromRequest')
|
||||
.mockReturnValue(() => mockToken);
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'verifyWorkspaceToken')
|
||||
.spyOn(jwtWrapperService, 'verifyJwtToken')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'decode')
|
||||
@ -196,7 +199,7 @@ describe('AccessTokenService', () => {
|
||||
const result = await service.validateTokenByRequest(mockRequest);
|
||||
|
||||
expect(result).toEqual(mockAuthContext);
|
||||
expect(jwtWrapperService.verifyWorkspaceToken).toHaveBeenCalledWith(
|
||||
expect(jwtWrapperService.verifyJwtToken).toHaveBeenCalledWith(
|
||||
mockToken,
|
||||
'ACCESS',
|
||||
);
|
||||
|
||||
@ -14,8 +14,9 @@ import {
|
||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
|
||||
import {
|
||||
AccessTokenJwtPayload,
|
||||
AuthContext,
|
||||
JwtPayload,
|
||||
JwtTokenTypeEnum,
|
||||
} from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@ -26,6 +27,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { userWorkspaceValidator } from 'src/engine/core-modules/user-workspace/user-workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenService {
|
||||
@ -42,10 +44,14 @@ export class AccessTokenService {
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
) {}
|
||||
|
||||
async generateAccessToken(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<AuthToken> {
|
||||
async generateAccessToken({
|
||||
userId,
|
||||
workspaceId,
|
||||
authProvider,
|
||||
}: Omit<
|
||||
AccessTokenJwtPayload,
|
||||
'type' | 'workspaceMemberId' | 'userWorkspaceId' | 'sub'
|
||||
>): Promise<AuthToken> {
|
||||
const expiresIn = this.twentyConfigService.get('ACCESS_TOKEN_EXPIRES_IN');
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
@ -99,16 +105,24 @@ export class AccessTokenService {
|
||||
},
|
||||
});
|
||||
|
||||
const jwtPayload: JwtPayload = {
|
||||
userWorkspaceValidator.assertIsDefinedOrThrow(userWorkspace);
|
||||
|
||||
const jwtPayload: AccessTokenJwtPayload = {
|
||||
sub: user.id,
|
||||
userId: user.id,
|
||||
workspaceId,
|
||||
workspaceMemberId: tokenWorkspaceMemberId,
|
||||
userWorkspaceId: userWorkspace?.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
type: JwtTokenTypeEnum.ACCESS,
|
||||
authProvider,
|
||||
};
|
||||
|
||||
return {
|
||||
token: this.jwtWrapperService.sign(jwtPayload, {
|
||||
secret: this.jwtWrapperService.generateAppSecret('ACCESS', workspaceId),
|
||||
secret: this.jwtWrapperService.generateAppSecret(
|
||||
JwtTokenTypeEnum.ACCESS,
|
||||
workspaceId,
|
||||
),
|
||||
expiresIn,
|
||||
}),
|
||||
expiresAt,
|
||||
@ -116,14 +130,27 @@ export class AccessTokenService {
|
||||
}
|
||||
|
||||
async validateToken(token: string): Promise<AuthContext> {
|
||||
await this.jwtWrapperService.verifyWorkspaceToken(token, 'ACCESS');
|
||||
await this.jwtWrapperService.verifyJwtToken(token, JwtTokenTypeEnum.ACCESS);
|
||||
|
||||
const decoded = await this.jwtWrapperService.decode(token);
|
||||
const decoded = this.jwtWrapperService.decode<AccessTokenJwtPayload>(token);
|
||||
|
||||
const { user, apiKey, workspace, workspaceMemberId, userWorkspaceId } =
|
||||
await this.jwtStrategy.validate(decoded as JwtPayload);
|
||||
const {
|
||||
user,
|
||||
apiKey,
|
||||
workspace,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
authProvider,
|
||||
} = await this.jwtStrategy.validate(decoded);
|
||||
|
||||
return { user, apiKey, workspace, workspaceMemberId, userWorkspaceId };
|
||||
return {
|
||||
user,
|
||||
apiKey,
|
||||
workspace,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
authProvider,
|
||||
};
|
||||
}
|
||||
|
||||
async validateTokenByRequest(request: Request): Promise<AuthContext> {
|
||||
|
||||
@ -19,7 +19,7 @@ describe('LoginTokenService', () => {
|
||||
useValue: {
|
||||
generateAppSecret: jest.fn(),
|
||||
sign: jest.fn(),
|
||||
verifyWorkspaceToken: jest.fn(),
|
||||
verifyJwtToken: jest.fn(),
|
||||
decode: jest.fn(),
|
||||
},
|
||||
},
|
||||
@ -69,7 +69,7 @@ describe('LoginTokenService', () => {
|
||||
'LOGIN_TOKEN_EXPIRES_IN',
|
||||
);
|
||||
expect(jwtWrapperService.sign).toHaveBeenCalledWith(
|
||||
{ sub: email, workspaceId },
|
||||
{ sub: email, workspaceId, type: 'LOGIN' },
|
||||
{ secret: mockSecret, expiresIn: mockExpiresIn },
|
||||
);
|
||||
});
|
||||
@ -81,7 +81,7 @@ describe('LoginTokenService', () => {
|
||||
const mockEmail = 'test@example.com';
|
||||
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'verifyWorkspaceToken')
|
||||
.spyOn(jwtWrapperService, 'verifyJwtToken')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'decode')
|
||||
@ -90,7 +90,7 @@ describe('LoginTokenService', () => {
|
||||
const result = await service.verifyLoginToken(mockToken);
|
||||
|
||||
expect(result).toEqual({ sub: mockEmail });
|
||||
expect(jwtWrapperService.verifyWorkspaceToken).toHaveBeenCalledWith(
|
||||
expect(jwtWrapperService.verifyJwtToken).toHaveBeenCalledWith(
|
||||
mockToken,
|
||||
'LOGIN',
|
||||
);
|
||||
@ -103,7 +103,7 @@ describe('LoginTokenService', () => {
|
||||
const mockToken = 'invalid-token';
|
||||
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'verifyWorkspaceToken')
|
||||
.spyOn(jwtWrapperService, 'verifyJwtToken')
|
||||
.mockRejectedValue(new Error('Invalid token'));
|
||||
|
||||
await expect(service.verifyLoginToken(mockToken)).rejects.toThrow();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user