From cd2946b670272a28eadef87cf0ec4de8341d2444 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Tue, 24 Dec 2024 12:47:41 +0100 Subject: [PATCH] refacto(*): remove everything about default workspace (#9157) ## Summary - [x] Remove defaultWorkspace in user - [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId - [x] Improve activate workspace flow - [x] Improve security on social login - [x] Add `ImpersonateGuard` - [x] Allow to use impersonation with couple `User/Workspace` - [x] Prevent unexpected reload on activate workspace - [x] Scope login token with workspaceId Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042 --- packages/twenty-front/codegen-metadata.cjs | 4 +- packages/twenty-front/codegen.cjs | 4 +- .../src/generated-metadata/graphql.ts | 30 +- .../twenty-front/src/generated/graphql.tsx | 118 +++---- .../modules/app/components/SettingsRoutes.tsx | 8 +- .../auth/graphql/mutations/impersonate.ts | 13 +- .../modules/auth/graphql/mutations/signUp.ts | 4 + .../modules/auth/graphql/mutations/verify.ts | 3 - .../auth/graphql/queries/checkUserExists.ts | 1 - .../modules/auth/hooks/__mocks__/useAuth.ts | 29 +- .../auth/hooks/__tests__/useAuth.test.tsx | 5 +- .../src/modules/auth/hooks/useAuth.ts | 204 +++++++----- .../components/SignInUpGlobalScopeForm.tsx | 5 +- .../hooks/__mocks__/useFieldMetadataItem.ts | 4 +- .../components/SettingsAdminContent.tsx} | 168 +++++----- .../SettingsAdminImpersonateUsers.tsx | 67 ---- .../graphql/mutations/userLookupAdminPanel.ts | 1 + .../admin-panel/hooks/useImpersonate.ts | 27 +- .../admin-panel/types/WorkspaceInfo.ts | 1 + .../users/components/UserProviderEffect.tsx | 5 +- .../graphql/fragments/userQueryFragment.ts | 2 +- .../graphql/mutations/activateWorkspace.ts | 9 +- .../src/pages/onboarding/CreateWorkspace.tsx | 39 +-- .../settings/admin-panel/SettingsAdmin.tsx | 19 +- .../src/testing/mock-data/users.ts | 6 +- .../database/typeorm-seeds/core/demo/users.ts | 4 - .../src/database/typeorm-seeds/core/users.ts | 4 - ...34544295083-remove-default-workspace-id.ts | 25 ++ .../admin-panel/admin-panel.resolver.ts | 28 +- .../admin-panel/admin-panel.service.ts | 109 +++--- .../admin-panel/dtos/impersonate.input.ts | 5 + .../admin-panel/dtos/impersonate.output.ts | 13 + .../admin-panel/dtos/user-lookup.entity.ts | 3 + .../engine/core-modules/auth/auth.module.ts | 5 +- .../core-modules/auth/auth.resolver.spec.ts | 10 +- .../engine/core-modules/auth/auth.resolver.ts | 75 +++-- .../controllers/google-auth.controller.ts | 15 +- .../controllers/microsoft-auth.controller.ts | 59 ++-- .../auth/controllers/sso-auth.controller.ts | 9 +- .../core-modules/auth/dto/sign-up.output.ts | 14 + .../auth/dto/user-exists.entity.ts | 3 - .../core-modules/auth/dto/verify.entity.ts | 11 - .../auth/services/auth.service.ts | 46 +-- .../auth/services/oauth.service.ts | 312 +++++++++--------- .../auth/services/sign-in-up.service.spec.ts | 39 ++- .../auth/services/sign-in-up.service.ts | 189 ++++++----- .../services/switch-workspace.service.spec.ts | 56 +--- .../auth/services/switch-workspace.service.ts | 62 +--- .../services/access-token.service.spec.ts | 18 +- .../token/services/access-token.service.ts | 43 ++- .../services/login-token.service.spec.ts | 10 +- .../token/services/login-token.service.ts | 15 +- .../core-modules/auth/token/token.module.ts | 2 + .../core-modules/billing/billing.resolver.ts | 3 +- .../billing-portal.workspace-service.ts | 3 +- .../billing/stripe/stripe.service.ts | 3 +- .../client-config/client-config.entity.ts | 2 +- .../domain-manager.exception.ts | 11 + .../service/domain-manager.service.ts | 17 +- .../onboarding/onboarding.service.ts | 22 +- .../user-workspace/user-workspace.service.ts | 8 +- .../user/services/user.service.ts | 46 ++- .../engine/core-modules/user/user.entity.ts | 14 +- .../core-modules/user/user.exception.ts | 11 + .../engine/core-modules/user/user.module.ts | 2 + .../engine/core-modules/user/user.resolver.ts | 65 ++-- .../engine/core-modules/user/user.validate.ts | 9 +- .../services/workspace-invitation.service.ts | 9 +- .../dtos/activate-workspace-output.ts | 13 - ...put.ts => public-workspace-data-output.ts} | 0 .../dtos/workspace-subdomain-id.dto.ts | 10 + .../workspace/services/workspace.service.ts | 72 +--- .../get-auth-providers-by-workspace.util.ts | 2 +- .../workspace/workspace.entity.ts | 4 - .../workspace/workspace.resolver.ts | 27 +- .../workspace/workspace.validate.ts | 25 +- .../src/engine/guards/impersonate-guard.ts | 15 + .../src/utils/workspace-url.util.ts | 33 -- 78 files changed, 1146 insertions(+), 1240 deletions(-) rename packages/twenty-front/src/{pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx => modules/settings/admin-panel/components/SettingsAdminContent.tsx} (62%) delete mode 100644 packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1734544295083-remove-default-workspace-id.ts create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.output.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.output.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/auth/dto/verify.entity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/user/user.exception.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/workspace/dtos/activate-workspace-output.ts rename packages/twenty-server/src/engine/core-modules/workspace/dtos/{public-workspace-data.output.ts => public-workspace-data-output.ts} (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto.ts create mode 100644 packages/twenty-server/src/engine/guards/impersonate-guard.ts delete mode 100644 packages/twenty-server/src/utils/workspace-url.util.ts diff --git a/packages/twenty-front/codegen-metadata.cjs b/packages/twenty-front/codegen-metadata.cjs index e0ed6e079cb5..10b82ffdb8ba 100644 --- a/packages/twenty-front/codegen-metadata.cjs +++ b/packages/twenty-front/codegen-metadata.cjs @@ -1,5 +1,7 @@ module.exports = { - schema: (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + '/metadata', + schema: + (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + + '/metadata', documents: [ './src/modules/databases/graphql/**/*.ts', './src/modules/object-metadata/graphql/*.ts', diff --git a/packages/twenty-front/codegen.cjs b/packages/twenty-front/codegen.cjs index ceb453fb9825..d0ea0e8aae4d 100644 --- a/packages/twenty-front/codegen.cjs +++ b/packages/twenty-front/codegen.cjs @@ -1,5 +1,7 @@ module.exports = { - schema: (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + '/graphql', + schema: + (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + + '/graphql', documents: [ '!./src/modules/databases/**', '!./src/modules/object-metadata/**', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 58c136673f13..52ed927fb8d1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -176,6 +176,7 @@ export type ClientConfig = { __typename?: 'ClientConfig'; analyticsEnabled: Scalars['Boolean']['output']; api: ApiConfig; + authProviders: AuthProviders; billing: Billing; captcha: Captcha; chromeExtensionId?: Maybe; @@ -358,13 +359,6 @@ export type EmailPasswordResetLink = { success: Scalars['Boolean']['output']; }; -export type ExchangeAuthCode = { - __typename?: 'ExchangeAuthCode'; - accessToken: AuthToken; - loginToken: AuthToken; - refreshToken: AuthToken; -}; - export type ExecuteServerlessFunctionInput = { /** Id of the serverless function to execute */ id: Scalars['UUID']['input']; @@ -581,12 +575,11 @@ export type Mutation = { editSSOIdentityProvider: EditSsoOutput; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; - exchangeAuthorizationCode: ExchangeAuthCode; executeOneServerlessFunction: ServerlessFunctionExecutionResult; generateApiKeyToken: ApiKeyToken; generateTransientToken: TransientToken; getAuthorizationUrl: GetAuthorizationUrlOutput; - impersonate: Verify; + impersonate: AuthTokens; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; resendWorkspaceInvitation: SendInvitationsOutput; @@ -613,7 +606,7 @@ export type Mutation = { uploadProfilePicture: Scalars['String']['output']; uploadWorkspaceLogo: Scalars['String']['output']; userLookupAdminPanel: UserLookup; - verify: Verify; + verify: AuthTokens; }; @@ -762,13 +755,6 @@ export type MutationEmailPasswordResetLinkArgs = { }; -export type MutationExchangeAuthorizationCodeArgs = { - authorizationCode: Scalars['String']['input']; - clientSecret?: InputMaybe; - codeVerifier?: InputMaybe; -}; - - export type MutationExecuteOneServerlessFunctionArgs = { input: ExecuteServerlessFunctionInput; }; @@ -787,6 +773,7 @@ export type MutationGetAuthorizationUrlArgs = { export type MutationImpersonateArgs = { userId: Scalars['String']['input']; + workspaceId: Scalars['String']['input']; }; @@ -1593,9 +1580,8 @@ export type User = { analyticsTinybirdJwts?: Maybe; canImpersonate: Scalars['Boolean']['output']; createdAt: Scalars['DateTime']['output']; + currentWorkspace?: Maybe; defaultAvatarUrl?: Maybe; - defaultWorkspace: Workspace; - defaultWorkspaceId: Scalars['String']['output']; deletedAt?: Maybe; disabled?: Maybe; email: Scalars['String']['output']; @@ -1681,12 +1667,6 @@ export type ValidatePasswordResetToken = { id: Scalars['String']['output']; }; -export type Verify = { - __typename?: 'Verify'; - tokens: AuthTokenPair; - user: User; -}; - export type WorkflowAction = { __typename?: 'WorkflowAction'; id: Scalars['UUID']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 57a4082fb4d3..6290fbfcd3bd 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -25,12 +25,6 @@ export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; -export type ActivateWorkspaceOutput = { - __typename?: 'ActivateWorkspaceOutput'; - loginToken: AuthToken; - workspace: Workspace; -}; - export type Analytics = { __typename?: 'Analytics'; /** Boolean that confirms query was dispatched */ @@ -260,13 +254,6 @@ export type EmailPasswordResetLink = { success: Scalars['Boolean']; }; -export type ExchangeAuthCode = { - __typename?: 'ExchangeAuthCode'; - accessToken: AuthToken; - loginToken: AuthToken; - refreshToken: AuthToken; -}; - export type ExecuteServerlessFunctionInput = { /** Id of the serverless function to execute */ id: Scalars['UUID']; @@ -382,6 +369,12 @@ export enum IdentityProviderType { Saml = 'SAML' } +export type ImpersonateOutput = { + __typename?: 'ImpersonateOutput'; + loginToken: AuthToken; + workspace: WorkspaceSubdomainAndId; +}; + export type IndexConnection = { __typename?: 'IndexConnection'; /** Array of edges. */ @@ -445,7 +438,7 @@ export enum MessageChannelVisibility { export type Mutation = { __typename?: 'Mutation'; activateWorkflowVersion: Scalars['Boolean']; - activateWorkspace: ActivateWorkspaceOutput; + activateWorkspace: Workspace; addUserToWorkspace: User; addUserToWorkspaceByInviteToken: User; authorizeApp: AuthorizeApp; @@ -470,18 +463,17 @@ export type Mutation = { editSSOIdentityProvider: EditSsoOutput; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; - exchangeAuthorizationCode: ExchangeAuthCode; executeOneServerlessFunction: ServerlessFunctionExecutionResult; generateApiKeyToken: ApiKeyToken; generateTransientToken: TransientToken; getAuthorizationUrl: GetAuthorizationUrlOutput; - impersonate: Verify; + impersonate: ImpersonateOutput; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; resendWorkspaceInvitation: SendInvitationsOutput; runWorkflowVersion: WorkflowRun; sendInvitations: SendInvitationsOutput; - signUp: LoginToken; + signUp: SignUpOutput; skipSyncEmailOnboardingStep: OnboardingStepSuccess; switchWorkspace: PublicWorkspaceDataOutput; track: Analytics; @@ -497,7 +489,7 @@ export type Mutation = { uploadProfilePicture: Scalars['String']; uploadWorkspaceLogo: Scalars['String']; userLookupAdminPanel: UserLookup; - verify: Verify; + verify: AuthTokens; }; @@ -606,13 +598,6 @@ export type MutationEmailPasswordResetLinkArgs = { }; -export type MutationExchangeAuthorizationCodeArgs = { - authorizationCode: Scalars['String']; - clientSecret?: InputMaybe; - codeVerifier?: InputMaybe; -}; - - export type MutationExecuteOneServerlessFunctionArgs = { input: ExecuteServerlessFunctionInput; }; @@ -631,6 +616,7 @@ export type MutationGetAuthorizationUrlArgs = { export type MutationImpersonateArgs = { userId: Scalars['String']; + workspaceId: Scalars['String']; }; @@ -1121,6 +1107,12 @@ export type SetupSsoOutput = { type: IdentityProviderType; }; +export type SignUpOutput = { + __typename?: 'SignUpOutput'; + loginToken: AuthToken; + workspace: WorkspaceSubdomainAndId; +}; + /** Sort Directions */ export enum SortDirection { Asc = 'ASC', @@ -1302,9 +1294,8 @@ export type User = { analyticsTinybirdJwts?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; + currentWorkspace?: Maybe; defaultAvatarUrl?: Maybe; - defaultWorkspace: Workspace; - defaultWorkspaceId: Scalars['String']; deletedAt?: Maybe; disabled?: Maybe; email: Scalars['String']; @@ -1333,7 +1324,6 @@ export type UserEdge = { export type UserExists = { __typename?: 'UserExists'; availableWorkspaces: Array; - defaultWorkspaceId: Scalars['String']; exists: Scalars['Boolean']; }; @@ -1381,12 +1371,6 @@ export type ValidatePasswordResetToken = { id: Scalars['String']; }; -export type Verify = { - __typename?: 'Verify'; - tokens: AuthTokenPair; - user: User; -}; - export type WorkflowAction = { __typename?: 'WorkflowAction'; id: Scalars['UUID']; @@ -1471,6 +1455,7 @@ export type WorkspaceEdge = { export type WorkspaceInfo = { __typename?: 'WorkspaceInfo'; + allowImpersonation: Scalars['Boolean']; featureFlags: Array; id: Scalars['String']; logo?: Maybe; @@ -1524,6 +1509,12 @@ export type WorkspaceNameAndId = { id: Scalars['String']; }; +export type WorkspaceSubdomainAndId = { + __typename?: 'WorkspaceSubdomainAndId'; + id: Scalars['String']; + subdomain: Scalars['String']; +}; + export type BillingCustomer = { __typename?: 'billingCustomer'; id: Scalars['UUID']; @@ -1877,10 +1868,11 @@ export type GetAuthorizationUrlMutation = { __typename?: 'Mutation', getAuthoriz export type ImpersonateMutationVariables = Exact<{ userId: Scalars['String']; + workspaceId: Scalars['String']; }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'ImpersonateOutput', workspace: { __typename?: 'WorkspaceSubdomainAndId', subdomain: string, id: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -1898,7 +1890,7 @@ export type SignUpMutationVariables = Exact<{ }>; -export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } }; +export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceSubdomainAndId', id: string, subdomain: string } } }; export type SwitchWorkspaceMutationVariables = Exact<{ workspaceId: Scalars['String']; @@ -1920,7 +1912,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1928,7 +1920,7 @@ export type CheckUserExistsQueryVariables = Exact<{ }>; -export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, defaultWorkspaceId: string, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } }; +export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } }; export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>; @@ -1993,7 +1985,7 @@ export type UserLookupAdminPanelMutationVariables = Exact<{ }>; -export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } }; +export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } }; export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; @@ -2028,7 +2020,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2045,7 +2037,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2143,7 +2135,7 @@ export type ActivateWorkspaceMutationVariables = Exact<{ }>; -export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'ActivateWorkspaceOutput', workspace: { __typename?: 'Workspace', id: any, subdomain: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } }; +export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: any, subdomain: string } }; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -2308,7 +2300,7 @@ export const UserQueryFragmentFragmentDoc = gql` workspaceMembers { ...WorkspaceMemberQueryFragment } - defaultWorkspace { + currentWorkspace { id displayName logo @@ -2823,18 +2815,18 @@ export type GetAuthorizationUrlMutationHookResult = ReturnType; export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions; export const ImpersonateDocument = gql` - mutation Impersonate($userId: String!) { - impersonate(userId: $userId) { - user { - ...UserQueryFragment + mutation Impersonate($userId: String!, $workspaceId: String!) { + impersonate(userId: $userId, workspaceId: $workspaceId) { + workspace { + subdomain + id } - tokens { - ...AuthTokensFragment + loginToken { + ...AuthTokenFragment } } } - ${UserQueryFragmentFragmentDoc} -${AuthTokensFragmentFragmentDoc}`; + ${AuthTokenFragmentFragmentDoc}`; export type ImpersonateMutationFn = Apollo.MutationFunction; /** @@ -2851,6 +2843,7 @@ export type ImpersonateMutationFn = Apollo.MutationFunction; /** @@ -3070,7 +3063,6 @@ export const CheckUserExistsDocument = gql` __typename ... on UserExists { exists - defaultWorkspaceId availableWorkspaces { id displayName @@ -3507,6 +3499,7 @@ export const UserLookupAdminPanelDocument = gql` name logo totalUsers + allowImpersonation users { id email @@ -4281,16 +4274,11 @@ export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutation export const ActivateWorkspaceDocument = gql` mutation ActivateWorkspace($input: ActivateWorkspaceInput!) { activateWorkspace(data: $input) { - workspace { - id - subdomain - } - loginToken { - ...AuthTokenFragment - } + id + subdomain } } - ${AuthTokenFragmentFragmentDoc}`; + `; export type ActivateWorkspaceMutationFn = Apollo.MutationFunction; /** diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index efa14807373d..6077e7b457b6 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -254,10 +254,10 @@ const SettingsAdmin = lazy(() => })), ); -const SettingsAdminFeatureFlags = lazy(() => - import('~/pages/settings/admin-panel/SettingsAdminFeatureFlags').then( +const SettingsAdminContent = lazy(() => + import('@/settings/admin-panel/components/SettingsAdminContent').then( (module) => ({ - default: module.SettingsAdminFeatureFlags, + default: module.SettingsAdminContent, }), ), ); @@ -402,7 +402,7 @@ export const SettingsRoutes = ({ } /> } + element={} /> )} diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts index e26740b42020..77e062cfa86b 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts @@ -2,13 +2,14 @@ import { gql } from '@apollo/client'; // TODO: Fragments should be used instead of duplicating the user fields ! export const IMPERSONATE = gql` - mutation Impersonate($userId: String!) { - impersonate(userId: $userId) { - user { - ...UserQueryFragment + mutation Impersonate($userId: String!, $workspaceId: String!) { + impersonate(userId: $userId, workspaceId: $workspaceId) { + workspace { + subdomain + id } - tokens { - ...AuthTokensFragment + loginToken { + ...AuthTokenFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts index 57499f773f7e..a1b45dc8f386 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts @@ -18,6 +18,10 @@ export const SIGN_UP = gql` loginToken { ...AuthTokenFragment } + workspace { + id + subdomain + } } } `; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/verify.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/verify.ts index 6842f4b7949e..4a09f463daba 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/verify.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/verify.ts @@ -3,9 +3,6 @@ import { gql } from '@apollo/client'; export const VERIFY = gql` mutation Verify($loginToken: String!) { verify(loginToken: $loginToken) { - user { - ...UserQueryFragment - } tokens { ...AuthTokensFragment } diff --git a/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts b/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts index 9f886cbda764..0a3c9b0aca4e 100644 --- a/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts +++ b/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts @@ -6,7 +6,6 @@ export const CHECK_USER_EXISTS = gql` __typename ... on UserExists { exists - defaultWorkspaceId availableWorkspaces { id displayName diff --git a/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts index 48e7526792fd..32016c3ecbdd 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts @@ -1,5 +1,6 @@ import { ChallengeDocument, + GetCurrentUserDocument, SignUpDocument, VerifyDocument, } from '~/generated/graphql'; @@ -8,6 +9,7 @@ export const queries = { challenge: ChallengeDocument, verify: VerifyDocument, signup: SignUpDocument, + getCurrentUser: GetCurrentUserDocument, }; export const email = 'test@test.com'; @@ -22,6 +24,7 @@ export const variables = { }, verify: { loginToken: token }, signup: {}, + getCurrentUser: {}, }; export const results = { @@ -32,7 +35,14 @@ export const results = { }, }, verify: { - user: { + tokens: { + accessToken: { token, expiresAt: 'expiresAt' }, + refreshToken: { token, expiresAt: 'expiresAt' }, + }, + }, + signUp: { loginToken: { token, expiresAt: 'expiresAt' } }, + getCurrentUser: { + currentUser: { id: 'id', firstName: 'firstName', lastName: 'lastName', @@ -49,7 +59,7 @@ export const results = { avatarUrl: 'avatarUrl', locale: 'locale', }, - defaultWorkspace: { + currentWorkspace: { id: 'id', displayName: 'displayName', logo: 'logo', @@ -65,13 +75,7 @@ export const results = { }, }, }, - tokens: { - accessToken: { token, expiresAt: 'expiresAt' }, - refreshToken: { token, expiresAt: 'expiresAt' }, - }, - signup: {}, }, - signUp: { loginToken: { token, expiresAt: 'expiresAt' } }, }; export const mocks = [ @@ -108,4 +112,13 @@ export const mocks = [ }, })), }, + { + request: { + query: queries.getCurrentUser, + variables: variables.getCurrentUser, + }, + result: jest.fn(() => ({ + data: results.getCurrentUser, + })), + }, ]; diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 7fea507688ed..6430246e359f 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -1,8 +1,7 @@ import { useApolloClient } from '@apollo/client'; import { MockedProvider } from '@apollo/client/testing'; import { expect } from '@storybook/test'; -import { act, renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; +import { ReactNode, act } from 'react'; import { RecoilRoot, useRecoilValue } from 'recoil'; import { iconsState } from 'twenty-ui'; @@ -15,6 +14,7 @@ import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthPro import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { email, mocks, password, results, token } from '../__mocks__/useAuth'; +import { renderHook } from '@testing-library/react'; const Wrapper = ({ children }: { children: ReactNode }) => ( @@ -59,6 +59,7 @@ describe('useAuth', () => { }); expect(mocks[1].result).toHaveBeenCalled(); + expect(mocks[3].result).toHaveBeenCalled(); }); it('should handle credential sign-in', async () => { diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index c9dd1968babb..659cab893b82 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -4,6 +4,7 @@ import { snapshot_UNSTABLE, useGotoRecoilSnapshot, useRecoilCallback, + useRecoilValue, useSetRecoilState, } from 'recoil'; import { iconsState } from 'twenty-ui'; @@ -23,6 +24,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { useChallengeMutation, useCheckUserExistsLazyQuery, + useGetCurrentUserLazyQuery, useSignUpMutation, useVerifyMutation, } from '~/generated/graphql'; @@ -48,6 +50,8 @@ import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/h import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; +import { AppPath } from '@/types/AppPath'; +import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; export const useAuth = () => { @@ -62,15 +66,19 @@ export const useAuth = () => { const setCurrentWorkspaceMembers = useSetRecoilState( currentWorkspaceMembersState, ); + const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState); const setWorkspaces = useSetRecoilState(workspacesState); const { redirect } = useRedirect(); + const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [challenge] = useChallengeMutation(); const [signUp] = useSignUpMutation(); const [verify] = useVerifyMutation(); + const [getCurrentUser] = useGetCurrentUserLazyQuery(); + const { isOnAWorkspaceSubdomain } = useIsCurrentLocationOnAWorkspaceSubdomain(); const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation(); @@ -165,6 +173,98 @@ export const useAuth = () => { [challenge], ); + const loadCurrentUser = useCallback(async () => { + const currentUserResult = await getCurrentUser(); + + const user = currentUserResult.data?.currentUser; + + if (!user) { + throw new Error('No current user result'); + } + + let workspaceMember = null; + + setCurrentUser(user); + + if (isDefined(user.workspaceMembers)) { + const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({ + ...workspaceMember, + colorScheme: workspaceMember.colorScheme as ColorScheme, + locale: workspaceMember.locale ?? 'en', + })); + + setCurrentWorkspaceMembers(workspaceMembers); + } + + if (isDefined(user.workspaceMember)) { + workspaceMember = { + ...user.workspaceMember, + colorScheme: user.workspaceMember?.colorScheme as ColorScheme, + locale: user.workspaceMember?.locale ?? 'en', + }; + + setCurrentWorkspaceMember(workspaceMember); + + // TODO: factorize with UserProviderEffect + setDateTimeFormat({ + timeZone: + workspaceMember.timeZone && workspaceMember.timeZone !== 'system' + ? workspaceMember.timeZone + : detectTimeZone(), + dateFormat: isDefined(user.workspaceMember.dateFormat) + ? getDateFormatFromWorkspaceDateFormat( + user.workspaceMember.dateFormat, + ) + : DateFormat[detectDateFormat()], + timeFormat: isDefined(user.workspaceMember.timeFormat) + ? getTimeFormatFromWorkspaceTimeFormat( + user.workspaceMember.timeFormat, + ) + : TimeFormat[detectTimeFormat()], + }); + } + + const workspace = user.currentWorkspace ?? null; + + setCurrentWorkspace(workspace); + + if (isDefined(workspace) && isOnAWorkspaceSubdomain) { + setLastAuthenticateWorkspaceDomain({ + workspaceId: workspace.id, + subdomain: workspace.subdomain, + }); + } + + if (isDefined(user.workspaces)) { + const validWorkspaces = user.workspaces + .filter( + ({ workspace }) => workspace !== null && workspace !== undefined, + ) + .map((validWorkspace) => validWorkspace.workspace) + .filter(isDefined); + + setWorkspaces(validWorkspaces); + } + setIsAppWaitingForFreshObjectMetadataState(true); + + return { + user, + workspaceMember, + workspace, + }; + }, [ + getCurrentUser, + isOnAWorkspaceSubdomain, + setCurrentUser, + setCurrentWorkspace, + setCurrentWorkspaceMember, + setCurrentWorkspaceMembers, + setDateTimeFormat, + setIsAppWaitingForFreshObjectMetadataState, + setLastAuthenticateWorkspaceDomain, + setWorkspaces, + ]); + const handleVerify = useCallback( async (loginToken: string) => { const verifyResult = await verify({ @@ -181,74 +281,7 @@ export const useAuth = () => { setTokenPair(verifyResult.data?.verify.tokens); - const user = verifyResult.data?.verify.user; - - let workspaceMember = null; - - setCurrentUser(user); - - if (isDefined(user.workspaceMembers)) { - const workspaceMembers = user.workspaceMembers.map( - (workspaceMember) => ({ - ...workspaceMember, - colorScheme: workspaceMember.colorScheme as ColorScheme, - locale: workspaceMember.locale ?? 'en', - }), - ); - - setCurrentWorkspaceMembers(workspaceMembers); - } - - if (isDefined(user.workspaceMember)) { - workspaceMember = { - ...user.workspaceMember, - colorScheme: user.workspaceMember?.colorScheme as ColorScheme, - locale: user.workspaceMember?.locale ?? 'en', - }; - - setCurrentWorkspaceMember(workspaceMember); - - // TODO: factorize with UserProviderEffect - setDateTimeFormat({ - timeZone: - workspaceMember.timeZone && workspaceMember.timeZone !== 'system' - ? workspaceMember.timeZone - : detectTimeZone(), - dateFormat: isDefined(user.workspaceMember.dateFormat) - ? getDateFormatFromWorkspaceDateFormat( - user.workspaceMember.dateFormat, - ) - : DateFormat[detectDateFormat()], - timeFormat: isDefined(user.workspaceMember.timeFormat) - ? getTimeFormatFromWorkspaceTimeFormat( - user.workspaceMember.timeFormat, - ) - : TimeFormat[detectTimeFormat()], - }); - } - - const workspace = user.defaultWorkspace ?? null; - - setCurrentWorkspace(workspace); - - if (isDefined(workspace) && isOnAWorkspaceSubdomain) { - setLastAuthenticateWorkspaceDomain({ - workspaceId: workspace.id, - subdomain: workspace.subdomain, - }); - } - - if (isDefined(verifyResult.data?.verify.user.workspaces)) { - const validWorkspaces = verifyResult.data?.verify.user.workspaces - .filter( - ({ workspace }) => workspace !== null && workspace !== undefined, - ) - .map((validWorkspace) => validWorkspace.workspace) - .filter(isDefined); - - setWorkspaces(validWorkspaces); - } - setIsAppWaitingForFreshObjectMetadataState(true); + const { user, workspaceMember, workspace } = await loadCurrentUser(); return { user, @@ -257,19 +290,7 @@ export const useAuth = () => { tokens: verifyResult.data?.verify.tokens, }; }, - [ - verify, - setTokenPair, - setCurrentUser, - setCurrentWorkspace, - isOnAWorkspaceSubdomain, - setIsAppWaitingForFreshObjectMetadataState, - setCurrentWorkspaceMembers, - setCurrentWorkspaceMember, - setDateTimeFormat, - setLastAuthenticateWorkspaceDomain, - setWorkspaces, - ], + [verify, setTokenPair, loadCurrentUser], ); const handleCrendentialsSignIn = useCallback( @@ -328,6 +349,16 @@ export const useAuth = () => { throw new Error('No login token'); } + if (isMultiWorkspaceEnabled) { + return redirectToWorkspaceDomain( + signUpResult.data.signUp.workspace.subdomain, + AppPath.Verify, + { + loginToken: signUpResult.data.signUp.loginToken.token, + }, + ); + } + const { user, workspace, workspaceMember } = await handleVerify( signUpResult.data?.signUp.loginToken.token, ); @@ -336,7 +367,13 @@ export const useAuth = () => { return { user, workspaceMember, workspace }; }, - [setIsVerifyPendingState, signUp, handleVerify], + [ + setIsVerifyPendingState, + signUp, + isMultiWorkspaceEnabled, + handleVerify, + redirectToWorkspaceDomain, + ], ); const buildRedirectUrl = useCallback( @@ -357,6 +394,7 @@ export const useAuth = () => { params.workspacePersonalInviteToken, ); } + if (isDefined(workspaceSubdomain)) { url.searchParams.set('workspaceSubdomain', workspaceSubdomain); } @@ -390,6 +428,8 @@ export const useAuth = () => { challenge: handleChallenge, verify: handleVerify, + loadCurrentUser, + checkUserExists: { checkUserExistsData, checkUserExistsQuery }, clearSession, signOut: handleSignOut, diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx index 737ac7aa7c69..60e361538ad3 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx @@ -86,10 +86,7 @@ export const SignInUpGlobalScopeForm = () => { const response = data.checkUserExists; if (response.__typename === 'UserExists') { if (response.availableWorkspaces.length >= 1) { - const workspace = - response.availableWorkspaces.find( - (workspace) => workspace.id === response.defaultWorkspaceId, - ) ?? response.availableWorkspaces[0]; + const workspace = response.availableWorkspaces[0]; return redirectToWorkspaceDomain(workspace.subdomain, pathname, { email: form.getValues('email'), }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index 90a6ac1cccdc..f7fbf1efa9bb 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -134,7 +134,7 @@ export const queries = { workspaceMembers { ...WorkspaceMemberQueryFragment } - defaultWorkspace { + currentWorkspace { id displayName logo @@ -281,7 +281,7 @@ export const responseData = { timeFormat: '24', }, workspaceMembers: [], - defaultWorkspace: { + currentWorkspace: { id: 'test-workspace-id', displayName: 'Test Workspace', logo: null, diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx similarity index 62% rename from packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx rename to packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index b8a7ab3d7675..7dcdb1cf18b8 100644 --- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,10 +1,6 @@ import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; -import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; -import { SettingsPath } from '@/types/SettingsPath'; import { TextInput } from '@/ui/input/components/TextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { Table } from '@/ui/layout/table/components/Table'; @@ -22,11 +18,13 @@ import { H1TitleFontColor, H2Title, IconSearch, + IconUser, isDefined, Section, Toggle, } from 'twenty-ui'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; const StyledLinkContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; @@ -66,8 +64,16 @@ const StyledContentContainer = styled.div` padding: ${({ theme }) => theme.spacing(4)} 0; `; -export const SettingsAdminFeatureFlags = () => { +export const SettingsAdminContent = () => { const [userIdentifier, setUserIdentifier] = useState(''); + const [userId, setUserId] = useState(''); + + const { + handleImpersonate, + isLoading: isImpersonateLoading, + error: impersonateError, + canImpersonate, + } = useImpersonate(); const { activeTabId, setActiveTabId } = useTabList( SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID, @@ -86,6 +92,10 @@ export const SettingsAdminFeatureFlags = () => { const result = await handleUserLookup(userIdentifier); + if (isDefined(result?.user?.id) && !error) { + setUserId(result.user.id.trim()); + } + if ( isDefined(result?.workspaces) && result.workspaces.length > 0 && @@ -126,6 +136,21 @@ export const SettingsAdminFeatureFlags = () => { }`} description={'Total Users'} /> + {canImpersonate && ( +