3628 timebox separate user creation from workspace creation (#3737)

* Remove workspace schema creation from signUp

* Set user workspaceMember nullable

* Remove workspace creation

* Handle null workspace in tokens

* Update onboarding status

* Generate types

* Move createWorkspace to workspace resolver

* Create workspace after signup

* Update createWorkspace return type

* Update createWorkspace return type

* Create core.workspace at signup

* WIP

* Fix create workspace

* Fix create workspace

* Clean code

* Remove useless recoil set

* Simplify create workspace request

* Set currentWorkspace at login

* Fix tests

* Create a recoil value for is workspaceSchema created

* Rename createWorkspace to createWorkspaceSchema

* Code review returns

* Use AppPath when possible

* Try without state

* Fix

* Fixes

* Rename createWorkspaceSchema to activateWorkspace

* Remove defaultAvatarUrl from user

* Add defaultAvatarUrl to core user

This reverts commit 1701c30eb18804558293cc42043aedf96ea888df.

* Add defaultAvatarUrl to core user

This reverts commit 1701c30eb18804558293cc42043aedf96ea888df.

* Fix ci

* Fix tests

* Fix storybook

* Fix test

* Remove useless query

* Fix test

* Fix test

* Fix mock data

* Fix test

* Clean Mock Requests

* Fix tentative

* Revert "Clean Mock Requests"

This reverts commit 8aa20a34363ffddfdee24f18fc80b27ea0ad5e1d.

* Fix

* Revert "Fix"

This reverts commit 2df7e9b6569b8bfb53f6a45391db725e28d16a18.

* Revert "Revert "Clean Mock Requests""

This reverts commit 3aefef8e9600d161434a047e845563d1b8e0692e.

* Revert "Fix tentative"

This reverts commit 13e7748d6f3b3858d30fb08adbc8ad347c5556ee.

* Update filename

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2024-02-09 12:06:11 +01:00
committed by GitHub
parent 3fc18aeec1
commit 7425223f83
49 changed files with 769 additions and 355 deletions

View File

@ -101,6 +101,10 @@ export class AuthResolver {
@AuthUser() user: User,
): Promise<TransientToken | void> {
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (!workspaceMember) {
return;
}
const transientToken = await this.tokenService.generateTransientToken(
workspaceMember.id,
user.defaultWorkspace.id,

View File

@ -7,11 +7,11 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import FileType from 'file-type';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { render } from '@react-email/components';
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
import FileType from 'file-type';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
@ -29,11 +29,11 @@ import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
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 { TokenService } from './token.service';
@ -135,18 +135,8 @@ export class AuthService {
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(workspace.id);
}
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
let imagePath: string | undefined = undefined;
if (picture) {
@ -166,9 +156,18 @@ export class AuthService {
imagePath = paths[0];
}
await this.userService.createWorkspaceMember(user, imagePath);
return user;
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
return await this.userRepository.save(userToCreate);
}
async verify(email: string): Promise<Verify> {
@ -189,7 +188,11 @@ export class AuthService {
// passwordHash is hidden for security reasons
user.passwordHash = '';
user.workspaceMember = await this.userService.loadWorkspaceMember(user);
const workspaceMember = await this.userService.loadWorkspaceMember(user);
if (workspaceMember) {
user.workspaceMember = workspaceMember;
}
const accessToken = await this.tokenService.generateAccessToken(user.id);
const refreshToken = await this.tokenService.generateRefreshToken(user.id);

View File

@ -21,11 +21,23 @@ export class UserService extends TypeOrmQueryService<User> {
}
async loadWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
const dataSourcesMetadata =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
user.defaultWorkspace.id,
);
if (!dataSourcesMetadata.length) {
return;
}
if (dataSourcesMetadata.length > 1) {
throw new Error(
`user '${user.id}' default workspace '${user.defaultWorkspace.id}' has multiple data source metadata`,
);
}
const dataSourceMetadata = dataSourcesMetadata[0];
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
@ -33,6 +45,10 @@ export class UserService extends TypeOrmQueryService<User> {
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
);
if (!workspaceMembers.length) {
return;
}
assert(workspaceMembers.length === 1, 'WorkspaceMember not found');
const userWorkspaceMember = new WorkspaceMember();
@ -63,7 +79,7 @@ export class UserService extends TypeOrmQueryService<User> {
);
}
async createWorkspaceMember(user: User, avatarUrl?: string) {
async createWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
@ -77,7 +93,7 @@ export class UserService extends TypeOrmQueryService<User> {
("nameFirstName", "nameLastName", "colorScheme", "userId", "userEmail", "avatarUrl")
VALUES ('${user.firstName}', '${user.lastName}', 'Light', '${
user.id
}', '${user.email}', '${avatarUrl ?? ''}')`,
}', '${user.email}', '${user.defaultAvatarUrl ?? ''}')`,
);
}

View File

@ -34,6 +34,10 @@ export class User {
@Column()
email: string;
@Field({ nullable: true })
@Column({ nullable: true })
defaultAvatarUrl: string;
@Field()
@Column({ default: false })
emailVerified: boolean;
@ -81,6 +85,6 @@ export class User {
})
refreshTokens: RefreshToken[];
@Field(() => WorkspaceMember, { nullable: false })
@Field(() => WorkspaceMember, { nullable: true })
workspaceMember: WorkspaceMember;
}

View File

@ -55,9 +55,11 @@ export class UserResolver {
}
@ResolveField(() => WorkspaceMember, {
nullable: false,
nullable: true,
})
async workspaceMember(@Parent() user: User): Promise<WorkspaceMember> {
async workspaceMember(
@Parent() user: User,
): Promise<WorkspaceMember | undefined> {
return this.userService.loadWorkspaceMember(user);
}

View File

@ -0,0 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional, IsString } from 'class-validator';
@InputType()
export class ActivateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
displayName?: string;
}

View File

@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceService } from './workspace.service';
@ -21,6 +22,10 @@ describe('WorkspaceService', () => {
provide: WorkspaceManagerService,
useValue: {},
},
{
provide: UserService,
useValue: {},
},
],
}).compile();

View File

@ -1,4 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import assert from 'assert';
@ -7,16 +8,33 @@ import { Repository } from 'typeorm';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity';
import { UserService } from 'src/core/user/services/user.service';
import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userService: UserService,
) {
super(workspaceRepository);
}
async activateWorkspace(user: User, data: ActivateWorkspaceInput) {
if (!data.displayName || !data.displayName.length) {
throw new BadRequestException("'displayName' not provided");
}
await this.workspaceRepository.update(user.defaultWorkspace.id, {
displayName: data.displayName,
});
await this.workspaceManagerService.init(user.defaultWorkspace.id);
await this.userService.createWorkspaceMember(user);
return user.defaultWorkspace;
}
async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) {
const workspace = await this.workspaceRepository.findOneBy({ id });

View File

@ -8,6 +8,7 @@ import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspac
import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
import { UserModule } from 'src/core/user/user.module';
import { Workspace } from './workspace.entity';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
@ -24,6 +25,7 @@ import { WorkspaceService } from './services/workspace.service';
'core',
),
WorkspaceManagerModule,
UserModule,
FileModule,
],
services: [WorkspaceService],

View File

@ -12,6 +12,9 @@ import { assert } from 'src/utils/assert';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UpdateWorkspaceInput } from 'src/core/workspace/dtos/update-workspace-input';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity';
import { AuthUser } from 'src/decorators/auth/auth-user.decorator';
import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input';
import { Workspace } from './workspace.entity';
@ -35,6 +38,15 @@ export class WorkspaceResolver {
return workspace;
}
@Mutation(() => Workspace)
@UseGuards(JwtAuthGuard)
async activateWorkspace(
@Args('data') data: ActivateWorkspaceInput,
@AuthUser() user: User,
) {
return await this.workspaceService.activateWorkspace(user, data);
}
@Mutation(() => Workspace)
async updateWorkspace(
@Args('data') data: UpdateWorkspaceInput,