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:
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 ?? ''}')`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user