Refactor sign-up into its own service (#4173)

* Refactor sign-up into its own service

* Fix tests
This commit is contained in:
Charles Bochet 2024-02-25 11:51:17 +01:00 committed by GitHub
parent b67957bf94
commit a108d36040
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 306 additions and 135 deletions

View File

@ -19,6 +19,7 @@ import { VerifyAuthController } from 'src/core/auth/controllers/verify-auth.cont
import { TokenService } from 'src/core/auth/services/token.service'; import { TokenService } from 'src/core/auth/services/token.service';
import { GoogleGmailService } from 'src/core/auth/services/google-gmail.service'; import { GoogleGmailService } from 'src/core/auth/services/google-gmail.service';
import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module';
import { SignUpService } from 'src/core/auth/services/sign-up.service';
import { AuthResolver } from './auth.resolver'; import { AuthResolver } from './auth.resolver';
@ -54,6 +55,7 @@ const jwtModule = JwtModule.registerAsync({
VerifyAuthController, VerifyAuthController,
], ],
providers: [ providers: [
SignUpService,
AuthService, AuthService,
TokenService, TokenService,
JwtAuthStrategy, JwtAuthStrategy,

View File

@ -1,15 +1,13 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import { UserService } from 'src/core/user/services/user.service'; import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { Workspace } from 'src/core/workspace/workspace.entity'; import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity'; import { User } from 'src/core/user/user.entity';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { EmailService } from 'src/integrations/email/email.service'; import { EmailService } from 'src/integrations/email/email.service';
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { SignUpService } from 'src/core/auth/services/sign-up.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { TokenService } from './token.service'; import { TokenService } from './token.service';
@ -30,21 +28,13 @@ describe('AuthService', () => {
useValue: {}, useValue: {},
}, },
{ {
provide: UserWorkspaceService, provide: SignUpService,
useValue: {}, useValue: {},
}, },
{ {
provide: WorkspaceManagerService, provide: WorkspaceManagerService,
useValue: {}, useValue: {},
}, },
{
provide: FileUploadService,
useValue: {},
},
{
provide: HttpService,
useValue: {},
},
{ {
provide: getRepositoryToken(Workspace, 'core'), provide: getRepositoryToken(Workspace, 'core'),
useValue: {}, useValue: {},

View File

@ -5,15 +5,10 @@ import {
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { render } from '@react-email/components'; import { render } from '@react-email/components';
import { PasswordUpdateNotifyEmail } from 'twenty-emails'; import { PasswordUpdateNotifyEmail } from 'twenty-emails';
import FileType from 'file-type';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { ChallengeInput } from 'src/core/auth/dto/challenge.input'; import { ChallengeInput } from 'src/core/auth/dto/challenge.input';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
@ -28,13 +23,10 @@ import { WorkspaceInviteHashValid } from 'src/core/auth/dto/workspace-invite-has
import { User } from 'src/core/user/user.entity'; import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity'; import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserService } from 'src/core/user/services/user.service'; import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { EmailService } from 'src/integrations/email/email.service'; import { EmailService } from 'src/integrations/email/email.service';
import { UpdatePassword } from 'src/core/auth/dto/update-password.entity'; import { UpdatePassword } from 'src/core/auth/dto/update-password.entity';
import { getImageBufferFromUrl } from 'src/utils/image'; import { SignUpService } from 'src/core/auth/services/sign-up.service';
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
import { TokenService } from './token.service'; import { TokenService } from './token.service';
@ -49,14 +41,11 @@ export class AuthService {
constructor( constructor(
private readonly tokenService: TokenService, private readonly tokenService: TokenService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly workspaceManagerService: WorkspaceManagerService, private readonly signUpService: SignUpService,
private readonly fileUploadService: FileUploadService,
@InjectRepository(Workspace, 'core') @InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>, private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(User, 'core') @InjectRepository(User, 'core')
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
private readonly emailService: EmailService, private readonly emailService: EmailService,
) {} ) {}
@ -94,117 +83,14 @@ export class AuthService {
workspaceInviteHash?: string | null; workspaceInviteHash?: string | null;
picture?: string | null; picture?: string | null;
}) { }) {
if (!firstName) firstName = ''; return await this.signUpService.signUp({
if (!lastName) lastName = ''; email,
password,
const existingUser = await this.userRepository.findOne({ firstName,
where: { lastName,
email: email, workspaceInviteHash,
},
relations: ['defaultWorkspace'],
});
if (existingUser && !workspaceInviteHash) {
assert(!existingUser, 'This user already exists', ForbiddenException);
}
if (password) {
const isPasswordValid = PASSWORD_REGEX.test(password);
assert(isPasswordValid, 'Password too weak', BadRequestException);
}
const passwordHash = password ? await hashPassword(password) : undefined;
let workspace: Workspace | null;
if (workspaceInviteHash) {
workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHash,
});
assert(
workspace,
'This workspace inviteHash is invalid',
ForbiddenException,
);
} else {
assert(
!this.environmentService.isSignUpDisabled(),
'Sign up is disabled',
ForbiddenException,
);
const workspaceToCreate = this.workspaceRepository.create({
displayName: '',
domainName: '',
inviteHash: v4(),
subscriptionStatus: 'incomplete',
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
}
let imagePath: string | undefined = undefined;
if (picture) {
const buffer = await getImageBufferFromUrl(
picture, picture,
this.httpService.axiosRef,
);
const type = await FileType.fromBuffer(buffer);
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename: `${v4()}.${type?.ext}`,
mimeType: type?.mime,
fileFolder: FileFolder.ProfilePicture,
}); });
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,
lastName: lastName,
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
await this.userWorkspaceService.create(user.id, workspace.id);
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
return user;
} }
async verify(email: string): Promise<Verify> { async verify(email: string): Promise<Verify> {

View File

@ -0,0 +1,52 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
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 { SignUpService } from 'src/core/auth/services/sign-up.service';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
describe('SignUpService', () => {
let service: SignUpService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SignUpService,
{
provide: FileUploadService,
useValue: {},
},
{
provide: UserWorkspaceService,
useValue: {},
},
{
provide: getRepositoryToken(Workspace, 'core'),
useValue: {},
},
{
provide: getRepositoryToken(User, 'core'),
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
{
provide: HttpService,
useValue: {},
},
],
}).compile();
service = module.get<SignUpService>(SignUpService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,241 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import FileType from 'file-type';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { assert } from 'src/utils/assert';
import { PASSWORD_REGEX, hashPassword } from 'src/core/auth/auth.util';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
export type SignUpServiceInput = {
email: string;
password?: string;
firstName?: string | null;
lastName?: string | null;
workspaceInviteHash?: string | null;
picture?: string | null;
};
@Injectable()
export class SignUpService {
constructor(
private readonly fileUploadService: FileUploadService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService,
) {}
async signUp({
email,
workspaceInviteHash,
password,
firstName,
lastName,
picture,
}: SignUpServiceInput) {
if (!firstName) firstName = '';
if (!lastName) lastName = '';
assert(email, 'Email is required', BadRequestException);
if (password) {
const isPasswordValid = PASSWORD_REGEX.test(password);
assert(isPasswordValid, 'Password too weak', BadRequestException);
}
const passwordHash = password ? await hashPassword(password) : undefined;
let imagePath: string | undefined;
if (picture) {
imagePath = await this.uploadPicture(picture);
}
if (workspaceInviteHash) {
return await this.signUpOnExistingWorkspace({
email,
passwordHash,
workspaceInviteHash,
firstName,
lastName,
imagePath,
});
} else {
return await this.signUpOnNewWorkspace({
email,
passwordHash,
firstName,
lastName,
imagePath,
});
}
}
private async signUpOnExistingWorkspace({
email,
passwordHash,
workspaceInviteHash,
firstName,
lastName,
imagePath,
}: {
email: string;
passwordHash: string | undefined;
workspaceInviteHash: string;
firstName: string;
lastName: string;
imagePath: string | undefined;
}) {
const existingUser = await this.userRepository.findOne({
where: {
email: email,
},
relations: ['defaultWorkspace'],
});
const workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHash,
});
assert(
workspace,
'This workspace inviteHash is invalid',
ForbiddenException,
);
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(),
});
return Object.assign(existingUser, updatedUser);
}
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
await this.userWorkspaceService.create(user.id, workspace.id);
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
return user;
}
private async signUpOnNewWorkspace({
email,
passwordHash,
firstName,
lastName,
imagePath,
}: {
email: string;
passwordHash: string | undefined;
firstName: string;
lastName: string;
imagePath: string | undefined;
}) {
const existingUser = await this.userRepository.findOne({
where: {
email: email,
},
relations: ['defaultWorkspace'],
});
if (existingUser) {
assert(!existingUser, 'This user already exists', ForbiddenException);
}
assert(
!this.environmentService.isSignUpDisabled(),
'Sign up is disabled',
ForbiddenException,
);
const workspaceToCreate = this.workspaceRepository.create({
displayName: '',
domainName: '',
inviteHash: v4(),
subscriptionStatus: 'incomplete',
});
const workspace = await this.workspaceRepository.save(workspaceToCreate);
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
defaultAvatarUrl: imagePath,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
await this.userWorkspaceService.create(user.id, workspace.id);
return user;
}
async uploadPicture(picture: string): Promise<string> {
const buffer = await getImageBufferFromUrl(
picture,
this.httpService.axiosRef,
);
const type = await FileType.fromBuffer(buffer);
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename: `${v4()}.${type?.ext}`,
mimeType: type?.mime,
fileFolder: FileFolder.ProfilePicture,
});
return paths[0];
}
}