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