From 07bde4883e23379e630eb82351fee1e8ceee7e0c Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Tue, 17 Dec 2024 19:56:19 +0100 Subject: [PATCH] feat(auth): add default workspace support for user handling (#9099) Introduce `defaultWorkspaceId` to improve workspace redirection logic. Updated GraphQL schema, server logic, and frontend components accordingly to prioritize default workspaces when available. ## Summary This PR adds a mechanism to handle and prioritize default workspace selection for users during authentication. It updates the logic in multiple components and services to ensure users are redirected to their default workspaces if no specific selection is provided. ### Main changes: - **GraphQL Schema Updates**: - Enhanced `UserExists` GraphQL entity with a new `defaultWorkspaceId` field to specify the user's default workspace. - Updated queries and mutations to handle the `defaultWorkspaceId`. - **Client-Side Updates**: - Enhanced `useAuth` hook to include logic for managing default workspace redirection. - Adjusted UI logic in `SignInUpGlobalScopeForm` to utilize the `defaultWorkspaceId`. - **Server-Side Adjustments**: - Modified `AuthService` to include `defaultWorkspaceId` in `checkUserExists`. - Default workspace logic added to the backend flow for consistent handling. - **Tests/Helpers**: - Added utility and type changes to integrate the new backend response changes (e.g., `UserExists` GraphQL). - **Subsequent function lifecycle** was adjusted to include recheck for workspace token states when performing sign-in flows. --- .../twenty-front/src/generated/graphql.tsx | 4 +++- .../auth/graphql/queries/checkUserExists.ts | 1 + .../src/modules/auth/hooks/useAuth.ts | 4 +++- .../components/SignInUpGlobalScopeForm.tsx | 24 +++++++++---------- .../auth/dto/user-exists.entity.ts | 3 +++ .../auth/services/auth.service.ts | 1 + packages/twenty-server/src/main.ts | 2 +- 7 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 73ff4e9954a..57a4082fb4d 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1333,6 +1333,7 @@ export type UserEdge = { export type UserExists = { __typename?: 'UserExists'; availableWorkspaces: Array; + defaultWorkspaceId: Scalars['String']; exists: Scalars['Boolean']; }; @@ -1927,7 +1928,7 @@ export type CheckUserExistsQueryVariables = Exact<{ }>; -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 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 GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>; @@ -3069,6 +3070,7 @@ export const CheckUserExistsDocument = gql` __typename ... on UserExists { exists + defaultWorkspaceId availableWorkspaces { id displayName 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 0a3c9b0aca4..9f886cbda76 100644 --- a/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts +++ b/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts @@ -6,6 +6,7 @@ export const CHECK_USER_EXISTS = gql` __typename ... on UserExists { exists + defaultWorkspaceId availableWorkspaces { id displayName diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 510f3f66c72..c9dd1968bab 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -136,8 +136,10 @@ export const useAuth = () => { await client.clearStore(); sessionStorage.clear(); localStorage.clear(); + // We need to explicitly clear the state to trigger the cookie deletion which include the parent domain + setLastAuthenticateWorkspaceDomain(null); }, - [client, goToRecoilSnapshot], + [client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain], ); const handleChallenge = useCallback( 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 870754bab9c..c38e45ebedb 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 @@ -83,21 +83,19 @@ export const SignInUpGlobalScopeForm = () => { }, onCompleted: (data) => { requestFreshCaptchaToken(); - if (data.checkUserExists.__typename === 'UserExists') { - if ( - isDefined(data?.checkUserExists.availableWorkspaces) && - data.checkUserExists.availableWorkspaces.length >= 1 - ) { - return redirectToWorkspaceDomain( - data?.checkUserExists.availableWorkspaces[0].subdomain, - pathname, - { - email: form.getValues('email'), - }, - ); + 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]; + return redirectToWorkspaceDomain(workspace.subdomain, pathname, { + email: form.getValues('email'), + }); } } - if (data.checkUserExists.__typename === 'UserNotExists') { + if (response.__typename === 'UserNotExists') { setSignInUpMode(SignInUpMode.SignUp); setSignInUpStep(SignInUpStep.Password); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/user-exists.entity.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/user-exists.entity.ts index e8f7f9a3e2e..db49594fe06 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/user-exists.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/user-exists.entity.ts @@ -7,6 +7,9 @@ export class UserExists { @Field(() => Boolean) exists: true; + @Field(() => String) + defaultWorkspaceId: string; + @Field(() => [AvailableWorkspaceOutput]) availableWorkspaces: Array; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index f96ae829be3..0adc408f35a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -257,6 +257,7 @@ export class AuthService { if (userValidator.isDefined(user)) { return { exists: true, + defaultWorkspaceId: user.defaultWorkspaceId, availableWorkspaces: await this.findAvailableWorkspacesByEmail(email), }; } diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index 28cce925ded..c15387c59a5 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -38,7 +38,7 @@ const bootstrap = async () => { const logger = app.get(LoggerService); const environmentService = app.get(EnvironmentService); - // TODO: Double check this as it's not working for now, it's going to be heplful for durable trees in twenty "orm" + // TODO: Double check this as it's not working for now, it's going to be helpful for durable trees in twenty "orm" // // Apply context id strategy for durable trees // ContextIdFactory.apply(new AggregateByWorkspaceContextIdStrategy());