diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx index 142b78813..506001e38 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx @@ -72,14 +72,24 @@ export const useSignInUp = (form: UseFormReturn
) => { }, onCompleted: (data) => { if (data?.checkUserExists.exists) { - setSignInUpMode(SignInUpMode.SignIn); + isMatchingLocation(AppPath.Invite) + ? setSignInUpMode(SignInUpMode.Invite) + : setSignInUpMode(SignInUpMode.SignIn); } else { - setSignInUpMode(SignInUpMode.SignUp); + isMatchingLocation(AppPath.Invite) + ? setSignInUpMode(SignInUpMode.Invite) + : setSignInUpMode(SignInUpMode.SignUp); } setSignInUpStep(SignInUpStep.Password); }, }); - }, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]); + }, [ + isMatchingLocation, + setSignInUpStep, + checkUserExistsQuery, + form, + setSignInUpMode, + ]); const submitCredentials: SubmitHandler = useCallback( async (data) => { diff --git a/packages/twenty-server/src/core/auth/auth.module.ts b/packages/twenty-server/src/core/auth/auth.module.ts index adcc31f7f..fe40a5c39 100644 --- a/packages/twenty-server/src/core/auth/auth.module.ts +++ b/packages/twenty-server/src/core/auth/auth.module.ts @@ -18,6 +18,7 @@ import { GoogleGmailAuthController } from 'src/core/auth/controllers/google-gmai import { VerifyAuthController } from 'src/core/auth/controllers/verify-auth.controller'; import { TokenService } from 'src/core/auth/services/token.service'; import { GoogleGmailService } from 'src/core/auth/services/google-gmail.service'; +import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module'; import { AuthResolver } from './auth.resolver'; @@ -45,6 +46,7 @@ const jwtModule = JwtModule.registerAsync({ TypeORMModule, TypeOrmModule.forFeature([Workspace, User, RefreshToken], 'core'), HttpModule, + UserWorkspaceModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index c2b2b2cea..686fcee98 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -91,6 +91,7 @@ export class AuthResolver { @Mutation(() => LoginToken) async signUp(@Args() signUpInput: SignUpInput): Promise { const user = await this.authService.signUp(signUpInput); + const loginToken = await this.tokenService.generateLoginToken(user.email); return { loginToken }; @@ -120,6 +121,8 @@ export class AuthResolver { verifyInput.loginToken, ); + assert(email, 'Invalid token', ForbiddenException); + const result = await this.authService.verify(email); return result; diff --git a/packages/twenty-server/src/core/auth/services/auth.service.spec.ts b/packages/twenty-server/src/core/auth/services/auth.service.spec.ts index 5776705a0..21e498657 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.spec.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.spec.ts @@ -9,6 +9,7 @@ import { Workspace } from 'src/core/workspace/workspace.entity'; import { User } from 'src/core/user/user.entity'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EmailService } from 'src/integrations/email/email.service'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { AuthService } from './auth.service'; import { TokenService } from './token.service'; @@ -28,6 +29,10 @@ describe('AuthService', () => { provide: UserService, useValue: {}, }, + { + provide: UserWorkspaceService, + useValue: {}, + }, { provide: WorkspaceManagerService, useValue: {}, diff --git a/packages/twenty-server/src/core/auth/services/auth.service.ts b/packages/twenty-server/src/core/auth/services/auth.service.ts index 2b481a836..990200711 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.ts @@ -34,6 +34,7 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser import { EmailService } from 'src/integrations/email/email.service'; import { UpdatePassword } from 'src/core/auth/dto/update-password.entity'; import { getImageBufferFromUrl } from 'src/utils/image'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { TokenService } from './token.service'; @@ -54,6 +55,7 @@ export class AuthService { private readonly workspaceRepository: Repository, @InjectRepository(User, 'core') private readonly userRepository: Repository, + private readonly userWorkspaceService: UserWorkspaceService, private readonly httpService: HttpService, private readonly environmentService: EnvironmentService, private readonly emailService: EmailService, @@ -95,11 +97,16 @@ export class AuthService { if (!firstName) firstName = ''; if (!lastName) lastName = ''; - const existingUser = await this.userRepository.findOneBy({ - email: email, + const existingUser = await this.userRepository.findOne({ + where: { + email: email, + }, + relations: ['defaultWorkspace'], }); - assert(!existingUser, 'This user already exists', ForbiddenException); + if (existingUser && !workspaceInviteHash) { + assert(!existingUser, 'This user already exists', ForbiddenException); + } if (password) { const isPasswordValid = PASSWORD_REGEX.test(password); @@ -157,6 +164,31 @@ export class AuthService { imagePath = paths[0]; } + if (existingUser && workspaceInviteHash) { + 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(), + }); + + return Object.assign(existingUser, updatedUser); + } + const userToCreate = this.userRepository.create({ email: email, firstName: firstName, @@ -169,9 +201,8 @@ export class AuthService { const user = await this.userRepository.save(userToCreate); - if (workspaceInviteHash) { - await this.userService.createWorkspaceMember(user); - } + await this.userWorkspaceService.create(user.id, workspace.id); + await this.userWorkspaceService.createWorkspaceMember(workspace.id, user); return user; } diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts new file mode 100644 index 000000000..adb1e0e06 --- /dev/null +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -0,0 +1,48 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +import { User } from 'src/core/user/user.entity'; +import { Workspace } from 'src/core/workspace/workspace.entity'; + +@Entity({ name: 'userWorkspace', schema: 'core' }) +@ObjectType('UserWorkspace') +@Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId']) +export class UserWorkspace { + @IDField(() => ID) + @PrimaryGeneratedColumn('uuid') + id: string; + + @JoinColumn({ name: 'userId' }) + user: User; + + @Column() + userId: string; + + @JoinColumn({ name: 'workspaceId' }) + workspace: Workspace; + + @Column() + workspaceId: string; + + @Field() + @CreateDateColumn({ type: 'timestamp with time zone' }) + createdAt: Date; + + @Field() + @UpdateDateColumn({ type: 'timestamp with time zone' }) + updatedAt: Date; + + @Field() + @Column('timestamp with time zone') + deletedAt: Date; +} diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.module.ts new file mode 100644 index 000000000..6ce5ba891 --- /dev/null +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; +import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; + +@Module({ + imports: [ + NestjsQueryGraphQLModule.forFeature({ + imports: [ + NestjsQueryTypeOrmModule.forFeature([UserWorkspace], 'core'), + TypeORMModule, + DataSourceModule, + ], + services: [UserWorkspaceService], + }), + ], + exports: [UserWorkspaceService], + providers: [UserWorkspaceService], +}) +export class UserWorkspaceModule {} diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts new file mode 100644 index 000000000..bb4d7a83c --- /dev/null +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts @@ -0,0 +1,65 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; +import { Repository } from 'typeorm'; + +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { User } from 'src/core/user/user.entity'; + +export class UserWorkspaceService extends TypeOrmQueryService { + constructor( + @InjectRepository(UserWorkspace, 'core') + private readonly userWorkspaceRepository: Repository, + private readonly dataSourceService: DataSourceService, + private readonly typeORMService: TypeORMService, + ) { + super(userWorkspaceRepository); + } + + async create(userId: string, workspaceId: string): Promise { + const userWorkspace = this.userWorkspaceRepository.create({ + userId, + workspaceId, + }); + + return this.userWorkspaceRepository.save(userWorkspace); + } + + async createWorkspaceMember(workspaceId: string, user: User) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + workspaceId, + ); + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + await workspaceDataSource?.query( + `INSERT INTO ${dataSourceMetadata.schema}."workspaceMember" + ("nameFirstName", "nameLastName", "colorScheme", "userId", "userEmail", "avatarUrl") + VALUES ('${user.firstName}', '${user.lastName}', 'Light', '${ + user.id + }', '${user.email}', '${user.defaultAvatarUrl ?? ''}')`, + ); + } + + async findUserWorkspaces(userId: string): Promise { + return this.userWorkspaceRepository.find({ + where: { + userId, + }, + }); + } + + async checkUserWorkspaceExists( + userId: string, + workspaceId: string, + ): Promise { + return this.userWorkspaceRepository.findOneBy({ + userId, + workspaceId, + }); + } +} diff --git a/packages/twenty-server/src/core/user/services/user.service.ts b/packages/twenty-server/src/core/user/services/user.service.ts index c3a322c9e..8cb85e93b 100644 --- a/packages/twenty-server/src/core/user/services/user.service.ts +++ b/packages/twenty-server/src/core/user/services/user.service.ts @@ -49,7 +49,10 @@ export class UserService extends TypeOrmQueryService { return; } - assert(workspaceMembers.length === 1, 'WorkspaceMember not found'); + assert( + workspaceMembers.length === 1, + 'WorkspaceMember not found or too many found', + ); const userWorkspaceMember = new WorkspaceMember(); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts new file mode 100644 index 000000000..e0e0b2013 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserWorkspaces1707778127558 implements MigrationInterface { + name = 'AddUserWorkspaces1707778127558'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."userWorkspace" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL REFERENCES core.user(id), + "workspaceId" uuid NOT NULL REFERENCES core.workspace(id), + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + "deletedAt" TIMESTAMP + )`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`, + ); + } + + public async down(): Promise {} +}