diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
index 72e7f4105..ccff7d414 100644
--- a/packages/twenty-front/src/App.tsx
+++ b/packages/twenty-front/src/App.tsx
@@ -38,6 +38,7 @@ import { Authorize } from '~/pages/auth/Authorize';
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan';
import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
+import { Invite } from '~/pages/auth/Invite';
import { PasswordReset } from '~/pages/auth/PasswordReset';
import { PaymentSuccess } from '~/pages/auth/PaymentSuccess';
import { SignInUp } from '~/pages/auth/SignInUp';
@@ -128,7 +129,7 @@ const createRouter = (isBillingEnabled?: boolean) =>
}>
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
index dba2d07ab..9f8186343 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { matchPath, useLocation, useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconCheckbox } from 'twenty-ui';
@@ -20,10 +20,8 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useGetWorkspaceFromInviteHashLazyQuery } from '~/generated/graphql';
+import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined';
-import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
-
-import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation';
// TODO: break down into smaller functions and / or hooks
export const PageChangeEffect = () => {
@@ -70,13 +68,6 @@ export const PageChangeEffect = () => {
isMatchingLocation(AppPath.PlanRequired) ||
isMatchingLocation(AppPath.PlanRequiredSuccess);
- const navigateToSignUp = () => {
- enqueueSnackBar('workspace does not exist', {
- variant: 'error',
- });
- navigate(AppPath.SignInUp);
- };
-
if (
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
!isMatchingOngoingUserCreationRoute &&
@@ -115,7 +106,8 @@ export const PageChangeEffect = () => {
navigate(AppPath.CreateProfile);
} else if (
onboardingStatus === OnboardingStatus.Completed &&
- isMatchingOnboardingRoute
+ isMatchingOnboardingRoute &&
+ !isMatchingLocation(AppPath.Invite)
) {
navigate(AppPath.Index);
} else if (
@@ -124,24 +116,6 @@ export const PageChangeEffect = () => {
!isMatchingLocation(AppPath.PlanRequired)
) {
navigate(AppPath.Index);
- } else if (isMatchingLocation(AppPath.Invite)) {
- const inviteHash =
- matchPath({ path: '/invite/:workspaceInviteHash' }, location.pathname)
- ?.params.workspaceInviteHash || '';
-
- workspaceFromInviteHashQuery({
- variables: {
- inviteHash,
- },
- onCompleted: (data) => {
- if (isUndefinedOrNull(data.findWorkspaceFromInviteHash)) {
- navigateToSignUp();
- }
- },
- onError: (_) => {
- navigateToSignUp();
- },
- });
}
}, [
enqueueSnackBar,
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 00405467f..c096e728d 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -260,6 +260,7 @@ export type LoginToken = {
export type Mutation = {
__typename?: 'Mutation';
activateWorkspace: Workspace;
+ addUserToWorkspace: User;
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
@@ -294,6 +295,11 @@ export type MutationActivateWorkspaceArgs = {
};
+export type MutationAddUserToWorkspaceArgs = {
+ inviteHash: Scalars['String'];
+};
+
+
export type MutationAuthorizeAppArgs = {
clientId: Scalars['String'];
codeChallenge?: InputMaybe;
@@ -539,6 +545,7 @@ export type RelationConnection = {
export type RelationDefinition = {
__typename?: 'RelationDefinition';
direction: RelationDefinitionType;
+ relationId: Scalars['UUID'];
sourceFieldMetadata: Field;
sourceObjectMetadata: Object;
targetFieldMetadata: Field;
@@ -578,6 +585,7 @@ export type RemoteTable = {
id?: Maybe;
name: Scalars['String'];
schema?: Maybe;
+ schemaPendingUpdates?: Maybe>;
status: RemoteTableStatus;
};
@@ -617,6 +625,14 @@ export type Support = {
supportFrontChatId?: Maybe;
};
+/** Schema update on a table */
+export enum TableUpdate {
+ ColumnsAdded = 'COLUMNS_ADDED',
+ ColumnsDeleted = 'COLUMNS_DELETED',
+ ColumnsTypeChanged = 'COLUMNS_TYPE_CHANGED',
+ TableDeleted = 'TABLE_DELETED'
+}
+
export type Telemetry = {
__typename?: 'Telemetry';
anonymizationEnabled: Scalars['Boolean'];
@@ -1191,6 +1207,13 @@ 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, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, 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, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
+export type AddUserToWorkspaceMutationVariables = Exact<{
+ inviteHash: Scalars['String'];
+}>;
+
+
+export type AddUserToWorkspaceMutation = { __typename?: 'Mutation', addUserToWorkspace: { __typename?: 'User', id: any } };
+
export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
@@ -2456,6 +2479,39 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType;
export type GetCurrentUserLazyQueryHookResult = ReturnType;
export type GetCurrentUserQueryResult = Apollo.QueryResult;
+export const AddUserToWorkspaceDocument = gql`
+ mutation AddUserToWorkspace($inviteHash: String!) {
+ addUserToWorkspace(inviteHash: $inviteHash) {
+ id
+ }
+}
+ `;
+export type AddUserToWorkspaceMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useAddUserToWorkspaceMutation__
+ *
+ * To run a mutation, you first call `useAddUserToWorkspaceMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useAddUserToWorkspaceMutation` 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 [addUserToWorkspaceMutation, { data, loading, error }] = useAddUserToWorkspaceMutation({
+ * variables: {
+ * inviteHash: // value for 'inviteHash'
+ * },
+ * });
+ */
+export function useAddUserToWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(AddUserToWorkspaceDocument, options);
+ }
+export type AddUserToWorkspaceMutationHookResult = ReturnType;
+export type AddUserToWorkspaceMutationResult = Apollo.MutationResult;
+export type AddUserToWorkspaceMutationOptions = Apollo.BaseMutationOptions;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
index 80e5d1310..06a38993b 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
@@ -6,11 +6,17 @@ import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconGoogle, IconMicrosoft } from 'twenty-ui';
+import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
+import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
+import {
+ SignInUpMode,
+ SignInUpStep,
+ useSignInUp,
+} from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
-import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
@@ -18,16 +24,8 @@ import { Loader } from '@/ui/feedback/loader/components/Loader';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
-import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { isDefined } from '~/utils/isDefined';
-import { Logo } from '../../components/Logo';
-import { Title } from '../../components/Title';
-import { SignInUpMode, SignInUpStep, useSignInUp } from '../hooks/useSignInUp';
-
-import { FooterNote } from './FooterNote';
-import { HorizontalSeparator } from './HorizontalSeparator';
-
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
@@ -55,14 +53,12 @@ export const SignInUpForm = () => {
);
const [authProviders] = useRecoilState(authProvidersState);
const [showErrors, setShowErrors] = useState(false);
- const { handleResetPassword } = useHandleResetPassword();
- const workspace = useWorkspaceFromInviteHash();
const { signInWithGoogle } = useSignInWithGoogle();
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const { form } = useSignInUpForm();
+ const { handleResetPassword } = useHandleResetPassword();
const {
- isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,
@@ -101,23 +97,6 @@ export const SignInUpForm = () => {
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
}, [signInUpMode, signInUpStep]);
- const title = useMemo(() => {
- if (isInviteMode) {
- return `Join ${workspace?.displayName ?? ''} team`;
- }
-
- if (
- signInUpStep === SignInUpStep.Init ||
- signInUpStep === SignInUpStep.Email
- ) {
- return 'Welcome to Twenty';
- }
-
- return signInUpMode === SignInUpMode.SignIn
- ? 'Sign in to Twenty'
- : 'Sign up to Twenty';
- }, [signInUpMode, workspace?.displayName, isInviteMode, signInUpStep]);
-
const theme = useTheme();
const shouldWaitForCaptchaToken =
@@ -143,10 +122,6 @@ export const SignInUpForm = () => {
return (
<>
-
-
-
- {title}
{authProviders.google && (
<>
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
index f5403e9b4..391730dde 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
@@ -4,8 +4,13 @@ import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
- const { data: workspaceFromInviteHash } = useGetWorkspaceFromInviteHashQuery({
- variables: { inviteHash: workspaceInviteHash || '' },
- });
- return workspaceFromInviteHash?.findWorkspaceFromInviteHash;
+ const { data: workspaceFromInviteHash, loading } =
+ useGetWorkspaceFromInviteHashQuery({
+ variables: { inviteHash: workspaceInviteHash || '' },
+ });
+ return {
+ workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
+ workspaceInviteHash,
+ loading,
+ };
};
diff --git a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx
index 8cbe647b5..0b3ad1bcd 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx
@@ -80,6 +80,7 @@ export const DefaultLayout = () => {
OnboardingStatus.OngoingWorkspaceActivation,
].includes(onboardingStatus)) ||
isMatchingLocation(AppPath.ResetPassword) ||
+ isMatchingLocation(AppPath.Invite) ||
(isMatchingLocation(AppPath.PlanRequired) &&
(OnboardingStatus.CompletedWithoutSubscription ||
OnboardingStatus.Canceled))
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts
index 4ba363686..a5c3de011 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts
@@ -1,13 +1,12 @@
-import { useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { tokenPairState } from '@/auth/states/tokenPairState';
+import { AppPath } from '@/types/AppPath';
import { useGenerateJwtMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const useWorkspaceSwitching = () => {
- const navigate = useNavigate();
const setTokenPair = useSetRecoilState(tokenPairState);
const [generateJWT] = useGenerateJwtMutation();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
@@ -30,8 +29,7 @@ export const useWorkspaceSwitching = () => {
const { tokens } = jwt.data.generateJWT;
setTokenPair(tokens);
- navigate(`/objects/companies`);
- window.location.reload();
+ window.location.href = AppPath.Index;
};
return { switchWorkspace };
diff --git a/packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspace.ts b/packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspace.ts
new file mode 100644
index 000000000..a57f07931
--- /dev/null
+++ b/packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspace.ts
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const ADD_USER_TO_WORKSPACE = gql`
+ mutation AddUserToWorkspace($inviteHash: String!) {
+ addUserToWorkspace(inviteHash: $inviteHash) {
+ id
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx
new file mode 100644
index 000000000..c6ea7afc7
--- /dev/null
+++ b/packages/twenty-front/src/pages/auth/Invite.tsx
@@ -0,0 +1,122 @@
+import { useEffect, useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
+
+import { Logo } from '@/auth/components/Logo';
+import { Title } from '@/auth/components/Title';
+import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
+import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
+import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
+import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { AppPath } from '@/types/AppPath';
+import { Loader } from '@/ui/feedback/loader/components/Loader';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { MainButton } from '@/ui/input/button/components/MainButton';
+import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
+import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
+import { useAddUserToWorkspaceMutation } from '~/generated/graphql';
+import { isDefined } from '~/utils/isDefined';
+
+const StyledContentContainer = styled.div`
+ margin-bottom: ${({ theme }) => theme.spacing(8)};
+ margin-top: ${({ theme }) => theme.spacing(4)};
+`;
+
+export const Invite = () => {
+ const { enqueueSnackBar } = useSnackBar();
+ const navigate = useNavigate();
+ const {
+ workspace: workspaceFromInviteHash,
+ loading: workspaceFromInviteHashLoading,
+ workspaceInviteHash,
+ } = useWorkspaceFromInviteHash();
+ const { form } = useSignInUpForm();
+ const currentWorkspace = useRecoilValue(currentWorkspaceState);
+ const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
+ const { switchWorkspace } = useWorkspaceSwitching();
+
+ const title = useMemo(() => {
+ return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
+ }, [workspaceFromInviteHash?.displayName]);
+
+ const handleUserJoinWorkspace = async () => {
+ if (
+ !(isDefined(workspaceInviteHash) && isDefined(workspaceFromInviteHash))
+ ) {
+ return;
+ }
+ await addUserToWorkspace({
+ variables: {
+ inviteHash: workspaceInviteHash,
+ },
+ });
+ await switchWorkspace(workspaceFromInviteHash.id);
+ };
+
+ useEffect(() => {
+ if (
+ !isDefined(workspaceFromInviteHash) &&
+ !workspaceFromInviteHashLoading
+ ) {
+ enqueueSnackBar('workspace does not exist', {
+ variant: 'error',
+ });
+ if (isDefined(currentWorkspace)) {
+ navigate(AppPath.Index);
+ } else {
+ navigate(AppPath.SignInUp);
+ }
+ }
+ if (
+ isDefined(currentWorkspace) &&
+ currentWorkspace.id === workspaceFromInviteHash?.id
+ ) {
+ enqueueSnackBar(
+ `You already belong to ${workspaceFromInviteHash?.displayName} workspace`,
+ {
+ variant: 'info',
+ },
+ );
+ navigate(AppPath.Index);
+ }
+ }, [
+ navigate,
+ enqueueSnackBar,
+ currentWorkspace,
+ workspaceFromInviteHash,
+ workspaceFromInviteHashLoading,
+ ]);
+
+ return (
+ !workspaceFromInviteHashLoading && (
+ <>
+
+
+
+ {title}
+ {isDefined(currentWorkspace) && workspaceFromInviteHash ? (
+ <>
+
+ form.formState.isSubmitting && }
+ fullWidth
+ />
+
+
+ By using Twenty, you agree to the Terms of Service and Privacy
+ Policy.
+
+ >
+ ) : (
+
+ )}
+ >
+ )
+ );
+};
diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx
index 8d4ffd3c2..f786fb543 100644
--- a/packages/twenty-front/src/pages/auth/SignInUp.tsx
+++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx
@@ -1,3 +1,43 @@
-import { SignInUpForm } from '../../modules/auth/sign-in-up/components/SignInUpForm';
+import { useMemo } from 'react';
+import { useRecoilValue } from 'recoil';
-export const SignInUp = () => ;
+import { Title } from '@/auth/components/Title';
+import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
+import {
+ SignInUpMode,
+ SignInUpStep,
+ useSignInUp,
+} from '@/auth/sign-in-up/hooks/useSignInUp';
+import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { isDefined } from '~/utils/isDefined';
+
+export const SignInUp = () => {
+ const { form } = useSignInUpForm();
+ const currentWorkspace = useRecoilValue(currentWorkspaceState);
+
+ const { signInUpStep, signInUpMode } = useSignInUp(form);
+
+ const title = useMemo(() => {
+ if (
+ signInUpStep === SignInUpStep.Init ||
+ signInUpStep === SignInUpStep.Email
+ ) {
+ return 'Welcome to Twenty';
+ }
+ return signInUpMode === SignInUpMode.SignIn
+ ? 'Sign in to Twenty'
+ : 'Sign up to Twenty';
+ }, [signInUpMode, signInUpStep]);
+
+ if (isDefined(currentWorkspace)) {
+ return <>>;
+ }
+
+ return (
+ <>
+ {title}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx
index 9ff9f3035..3805d33ad 100644
--- a/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx
+++ b/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx
@@ -10,7 +10,6 @@ import {
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
-import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { SignInUp } from '../SignInUp';
@@ -24,14 +23,25 @@ const meta: Meta = {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
- data: {
- currentUser: mockedOnboardingUsersData[0],
- },
+ data: null,
+ errors: [
+ {
+ message: 'Unauthorized',
+ extensions: {
+ code: 'UNAUTHENTICATED',
+ response: {
+ statusCode: 401,
+ message: 'Unauthorized',
+ },
+ },
+ },
+ ],
});
}),
graphqlMocks.handlers,
],
},
+ cookie: '',
},
};
diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts
index f3fb18a3d..9ce29e324 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts
@@ -27,7 +27,6 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
import { GenerateJwtInput } from 'src/engine/core-modules/auth/dto/generate-jwt.input';
-import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
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 { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
@@ -56,7 +55,6 @@ export class AuthResolver {
private authService: AuthService,
private tokenService: TokenService,
private userService: UserService,
- private userWorkspaceService: UserWorkspaceService,
) {}
@UseGuards(CaptchaGuard)
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
index 954c649aa..3397f2603 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
@@ -150,26 +150,10 @@ export class SignInUpService {
);
if (existingUser) {
- const userWorkspaceExists =
- await this.userWorkspaceService.checkUserWorkspaceExists(
- existingUser.id,
- workspace.id,
- );
-
- if (!userWorkspaceExists) {
- await this.userWorkspaceService.create(existingUser.id, workspace.id);
-
- await this.userWorkspaceService.createWorkspaceMember(
- workspace.id,
- existingUser,
- );
- }
-
- const updatedUser = await this.userRepository.save({
- id: existingUser.id,
- defaultWorkspace: workspace,
- updatedAt: new Date().toISOString(),
- });
+ const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
+ existingUser,
+ workspace,
+ );
return Object.assign(existingUser, updatedUser);
}
diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts
index bc827d9d3..09d3c621f 100644
--- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts
@@ -8,12 +8,13 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
+import { User } from 'src/engine/core-modules/user/user.entity';
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
- NestjsQueryTypeOrmModule.forFeature([UserWorkspace], 'core'),
+ NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'),
TypeORMModule,
DataSourceModule,
WorkspaceDataSourceModule,
diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts
new file mode 100644
index 000000000..065e0c4bd
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts
@@ -0,0 +1,41 @@
+import { UseGuards } from '@nestjs/common';
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { InjectRepository } from '@nestjs/typeorm';
+
+import { Repository } from 'typeorm';
+
+import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
+import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
+import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
+import { User } from 'src/engine/core-modules/user/user.entity';
+import { WorkspaceInviteHashValidInput } from 'src/engine/core-modules/auth/dto/workspace-invite-hash.input';
+import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
+import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
+
+@UseGuards(JwtAuthGuard)
+@Resolver(() => UserWorkspace)
+export class UserWorkspaceResolver {
+ constructor(
+ @InjectRepository(Workspace, 'core')
+ private readonly workspaceRepository: Repository,
+ @InjectRepository(User, 'core')
+ private readonly userRepository: Repository,
+ private readonly userWorkspaceService: UserWorkspaceService,
+ ) {}
+
+ @Mutation(() => User)
+ async addUserToWorkspace(
+ @AuthUser() user: User,
+ @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
+ ) {
+ const workspace = await this.workspaceRepository.findOneBy({
+ inviteHash: workspaceInviteHashValidInput.inviteHash,
+ });
+
+ if (!workspace) {
+ return;
+ }
+
+ return await this.userWorkspaceService.addUserToWorkspace(user, workspace);
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts
index 46653ee07..33aad64e7 100644
--- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts
@@ -12,11 +12,14 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
+import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export class UserWorkspaceService extends TypeOrmQueryService {
constructor(
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository,
+ @InjectRepository(User, 'core')
+ private readonly userRepository: Repository,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@@ -70,6 +73,25 @@ export class UserWorkspaceService extends TypeOrmQueryService {
this.eventEmitter.emit('workspaceMember.created', payload);
}
+ async addUserToWorkspace(user: User, workspace: Workspace) {
+ const userWorkspaceExists = await this.checkUserWorkspaceExists(
+ user.id,
+ workspace.id,
+ );
+
+ if (!userWorkspaceExists) {
+ await this.create(user.id, workspace.id);
+
+ await this.createWorkspaceMember(workspace.id, user);
+ }
+
+ return await this.userRepository.save({
+ id: user.id,
+ defaultWorkspace: workspace,
+ updatedAt: new Date().toISOString(),
+ });
+ }
+
public async getWorkspaceMemberCount(
workspaceId: string,
): Promise {
diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
index ad5ad20ed..d151781f4 100644
--- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
@@ -15,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { User } from 'src/engine/core-modules/user/user.entity';
+import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
@@ -46,6 +47,7 @@ import { WorkspaceService } from './services/workspace.service';
providers: [
WorkspaceResolver,
WorkspaceService,
+ UserWorkspaceResolver,
WorkspaceWorkspaceMemberListener,
],
})