Skip to content

Commit

Permalink
Merge pull request #1288 from Khaybee/fix/user-creation
Browse files Browse the repository at this point in the history
fix: Ensure user creation runs in a transaction
  • Loading branch information
Homoakin619 authored Feb 28, 2025
2 parents 30a1d12 + 3d149c8 commit 3b77e04
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 93 deletions.
140 changes: 81 additions & 59 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { TokenPayload } from 'google-auth-library';
import { UpdateProfileDto } from '@modules/profile/dto/update-profile.dto';
import { RequestSigninTokenDto } from './dto/request-signin-token.dto';
import { OtpDto } from '@modules/otp/dto/otp.dto';

import { DataSource, EntityManager } from 'typeorm';
import { CreateOrganisationRecordOptions } from '@modules/organisations/dto/create-organisation-options';
@Injectable()
export default class AuthenticationService {
constructor(
Expand All @@ -29,74 +30,89 @@ export default class AuthenticationService {
private otpService: OtpService,
private emailService: EmailService,
private organisationService: OrganisationsService,
private profileService: ProfileService
private profileService: ProfileService,
private dataSource: DataSource
) {}

async createNewUser(createUserDto: CreateUserDTO) {
const userExists = await this.userService.getUserRecord({
identifier: createUserDto.email,
identifierType: 'email',
});
const result = await this.dataSource.transaction(async (manager: EntityManager) => {
const userExists = await this.userService.getUserRecord({
identifier: createUserDto.email,
identifierType: 'email',
});

console.log('userExists', userExists);
if (userExists) {
throw new CustomHttpException(SYS_MSG.USER_ACCOUNT_EXIST, HttpStatus.BAD_REQUEST);
}

console.log('createUserDto', createUserDto);
const user = await this.userService.createUser(createUserDto, manager);

if (!user) {
throw new CustomHttpException(SYS_MSG.FAILED_TO_CREATE_USER, HttpStatus.BAD_REQUEST);
}
const newOrganisationPayload = {
name: `${user.first_name}'s Organisation`,
description: '',
email: user.email,
industry: '',
type: '',
country: '',
address: '',
state: '',
};

if (userExists) {
throw new CustomHttpException(SYS_MSG.USER_ACCOUNT_EXIST, HttpStatus.BAD_REQUEST);
}
const createOrganisationPayload: CreateOrganisationRecordOptions = {
createPayload: newOrganisationPayload,
dbTransaction: {
useTransaction: true,
transactionManager: manager,
},
};

await this.userService.createUser(createUserDto);
const newOrganisation = await this.organisationService.create(createOrganisationPayload);

const user = await this.userService.getUserRecord({ identifier: createUserDto.email, identifierType: 'email' });
const userOrganisations = await this.organisationService.getAllUserOrganisations(user.id, 1, 10);
const isSuperAdmin = userOrganisations.map(instance => instance.user_role).includes('super-admin');

if (!user) {
throw new CustomHttpException(SYS_MSG.FAILED_TO_CREATE_USER, HttpStatus.BAD_REQUEST);
}
const newOrganisationPayload = {
name: `${user.first_name}'s Organisation`,
description: '',
email: user.email,
industry: '',
type: '',
country: '',
address: '',
state: '',
};
const token = (await this.otpService.createOtp(user.id, manager)).token;

const newOrganisation = await this.organisationService.create(newOrganisationPayload, user.id);

const userOranisations = await this.organisationService.getAllUserOrganisations(user.id, 1, 10);
const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin');
const token = (await this.otpService.createOtp(user.id)).token;

const access_token = this.jwtService.sign({
id: user.id,
sub: user.id,
email: user.email,
});

const responsePayload = {
user: {
const access_token = this.jwtService.sign({
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
sub: user.id,
email: user.email,
avatar_url: user.profile.profile_pic_url,
is_superadmin: isSuperAdmin,
},
oranisations: userOranisations,
};

// send welcome mail
await this.emailService.sendUserConfirmationMail(
user.email,
user.first_name,
`${process.env.FRONTEND_URL}/confirm-email`,
token
);
});
const responsePayload = {
user: {
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
email: user.email,
avatar_url: user.profile.profile_pic_url,
is_superadmin: isSuperAdmin,
},
organisations: userOrganisations,
};
return {
message: SYS_MSG.USER_CREATED_SUCCESSFULLY,
access_token,
data: responsePayload,
};
});
try {
// send welcome mail
await this.emailService.sendUserConfirmationMail(
result.data.user.email,
result.data.user.first_name,
`${process.env.FRONTEND_URL}/confirm-email`,
result.access_token
);
} catch (emailError) {
console.error('Error sending confirmation email:', emailError);
}

return {
message: SYS_MSG.USER_CREATED_SUCCESSFULLY,
access_token,
data: responsePayload,
};
return result;
}

async forgotPassword(dto: ForgotPasswordDto): Promise<{ message: string } | null> {
Expand Down Expand Up @@ -374,7 +390,13 @@ export default class AuthenticationService {
state: '',
};

await this.organisationService.create(newOrganisationPaload, newUser.id);
const createOrganisationPayload: CreateOrganisationRecordOptions = {
createPayload: newOrganisationPaload,
dbTransaction: {
useTransaction: false,
},
};
await this.organisationService.create(createOrganisationPayload);

const userOranisations = await this.organisationService.getAllUserOrganisations(newUser.id, 1, 10);
const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin');
Expand Down
15 changes: 12 additions & 3 deletions src/modules/auth/tests/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ import { LoginDto } from '../dto/login.dto';
import UserResponseDTO from '@modules/user/dto/user-response.dto';
import { Otp } from '@modules/otp/entities/otp.entity';
import { Verify2FADto } from '../dto/verify-2fa.dto';

import { DataSource, EntityManager } from 'typeorm';
jest.mock('speakeasy');

describe('AuthenticationService', () => {
let service: AuthenticationService;
let userServiceMock: jest.Mocked<UserService>;
let profileServiceMock: jest.Mocked<ProfileService>;
let dataSourceMock: jest.Mocked<DataSource>;
let jwtServiceMock: jest.Mocked<JwtService>;
let otpServiceMock: jest.Mocked<OtpService>;
let emailServiceMock: jest.Mocked<EmailService>;
let organisationServiceMock: jest.Mocked<OrganisationsService>;

beforeEach(async () => {
dataSourceMock = {
transaction: jest.fn().mockImplementation(async cb => cb({} as EntityManager)),
manager: {} as EntityManager,
} as unknown as jest.Mocked<DataSource>;
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthenticationService,
Expand Down Expand Up @@ -77,6 +82,10 @@ describe('AuthenticationService', () => {
sendEmail: jest.fn(),
},
},
{
provide: DataSource,
useValue: dataSourceMock,
},
],
}).compile();

Expand Down Expand Up @@ -123,7 +132,7 @@ describe('AuthenticationService', () => {
it('should create a new user successfully', async () => {
userServiceMock.getUserRecord.mockResolvedValueOnce(null);

userServiceMock.createUser.mockResolvedValueOnce(undefined);
userServiceMock.createUser.mockResolvedValueOnce(mockUser as User);

userServiceMock.getUserRecord.mockResolvedValueOnce({
id: '1',
Expand Down Expand Up @@ -175,7 +184,7 @@ describe('AuthenticationService', () => {
is_superadmin: false,
avatar_url: 'some_url',
},
oranisations: [
organisations: [
{
organisation_id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa',
name: "John's Organisation",
Expand Down
4 changes: 2 additions & 2 deletions src/modules/organisations/dto/create-organisation-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OrganisationInterface } from '../interfaces/OrganisationInterface';

import { CreateRecordGeneric } from '@shared/helpers/createRecordGeneric';
type CreateOrganisationType = Partial<OrganisationInterface>;

export type CreateOrganisationRecordOptions = CreateRecordGeneric<CreateOrganisationType>;
export default CreateOrganisationType;
2 changes: 2 additions & 0 deletions src/modules/organisations/interfaces/OrganisationInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export interface OrganisationInterface {
address: string;

state: string;

userId: string;
}
6 changes: 5 additions & 1 deletion src/modules/organisations/organisations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export class OrganisationsController {
@Post('/')
async create(@Body() createOrganisationDto: OrganisationRequestDto, @Req() req) {
const user = req['user'];
return this.organisationsService.createOrganisation(createOrganisationDto, user.sub);
const payload = {
...createOrganisationDto,
userId: user.sub,
};
return this.organisationsService.createOrganisation(payload);
}

@UseGuards(OwnershipGuard)
Expand Down
42 changes: 29 additions & 13 deletions src/modules/organisations/organisations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, EntityManager } from 'typeorm';
import { UpdateOrganisationDto } from './dto/update-organisation.dto';
import { Organisation } from './entities/organisations.entity';
import { OrganisationMapper } from './mapper/organisation.mapper';
import CreateOrganisationType from './dto/create-organisation-options';
import CreateOrganisationType, { CreateOrganisationRecordOptions } from './dto/create-organisation-options';
import { OrganisationMemberMapper } from './mapper/org-members.mapper';
import { UpdateMemberRoleDto } from './dto/update-organisation-role.dto';
import { AddMemberDto } from './dto/add-member.dto';
Expand Down Expand Up @@ -65,35 +65,51 @@ export class OrganisationsService {
return { status_code: HttpStatus.OK, message: 'members retrieved successfully', data };
}

async createOrganisation(createOrganisationDto: CreateOrganisationType, userId: string) {
const query = await this.create(createOrganisationDto, userId);
async createOrganisation(createOrganisationDto: CreateOrganisationType) {
const createPayload: CreateOrganisationRecordOptions = {
createPayload: createOrganisationDto,
dbTransaction: {
useTransaction: false,
},
};
const query = await this.create(createPayload);
return { status_code: HttpStatus.CREATED, messge: 'Organisation created', data: query };
}

async create(createOrganisationDto: CreateOrganisationType, userId: string) {
if (createOrganisationDto.email) {
const emailFound = await this.emailExists(createOrganisationDto.email);
async create(createOrganisationDto: CreateOrganisationRecordOptions) {
const { createPayload, dbTransaction } = createOrganisationDto;

const repo = dbTransaction.useTransaction
? dbTransaction.transactionManager.getRepository(Organisation)
: this.organisationRepository;
const orgUserRoleRepo = dbTransaction.useTransaction
? dbTransaction.transactionManager.getRepository(OrganisationUserRole)
: this.organisationUserRole;
if (createPayload.email) {
const emailFound = await this.emailExists(createPayload.email);
if (emailFound) throw new ConflictException('Organisation with this email already exists');
}

const owner = await this.userRepository.findOne({
where: { id: userId },
where: { id: createPayload.userId },
});

const vendorRole = await this.roleRepository.findOne({ where: { name: 'admin' } });
const vendorRole = await this.roleRepository.findOne({
where: { name: 'admin' },
});

const organisationInstance = new Organisation();
Object.assign(organisationInstance, createOrganisationDto);
Object.assign(organisationInstance, createPayload);
organisationInstance.owner = owner;
organisationInstance.members = [owner];
const newOrganisation = await this.organisationRepository.save(organisationInstance);
const newOrganisation = await repo.save(organisationInstance);

const adminRole = new OrganisationUserRole();
adminRole.userId = owner.id;
adminRole.organisationId = newOrganisation.id;
adminRole.roleId = vendorRole.id;

await this.organisationUserRole.save(adminRole);
await orgUserRoleRepo.save(adminRole);

const mappedResponse = OrganisationMapper.mapToResponseFormat(newOrganisation);

Expand Down Expand Up @@ -159,7 +175,7 @@ export class OrganisationsService {
const user = await this.userRepository.findOne({ where: { id: userId } });

if (!user) {
throw new CustomHttpException('Invalid Request', HttpStatus.BAD_REQUEST);
return [];
}

const skip = (page - 1) * page_size;
Expand Down
17 changes: 10 additions & 7 deletions src/modules/organisations/tests/organisations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ import { User } from '../../user/entities/user.entity';
import { Organisation } from '../entities/organisations.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import UserService from '../../user/user.service';
import {
ForbiddenException,
NotFoundException,
} from '@nestjs/common';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { Profile } from '../../profile/entities/profile.entity';
import { OrganisationUserRole } from '../../../modules/role/entities/organisation-user-role.entity';
import { Role } from '../../../modules/role/entities/role.entity';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';

import { CreateOrganisationRecordOptions } from '../dto/create-organisation-options';
describe('OrganisationsService', () => {
let service: OrganisationsService;
let userRepository: Repository<User>;
Expand Down Expand Up @@ -90,8 +87,14 @@ describe('OrganisationsService', () => {
});
describe('create', () => {
it('should create a new organisation', async () => {
const createOrganisationDto = { name: 'Test Org', email: '[email protected]' };
const userId = 'user-id';
const createOrganisationDto = { name: 'Test Org', email: '[email protected]' };
const createOrganisationPayload: CreateOrganisationRecordOptions = {
createPayload: { ...createOrganisationDto, userId },
dbTransaction: {
useTransaction: false,
},
};
const user = { id: userId };
const superAdminRole = { id: 'role-id', name: 'super_admin', description: '', permissions: [] };
const newOrganisation = { ...createOrganisationDto, id: 'org-id', owner: user };
Expand All @@ -108,7 +111,7 @@ describe('OrganisationsService', () => {
jest.spyOn(organisationRepository, 'save').mockResolvedValue(newOrganisation as Organisation);
jest.spyOn(organisationUserRole, 'save').mockResolvedValue(adminReponse);

const result = await service.create(createOrganisationDto, userId);
const result = await service.create(createOrganisationPayload);

expect(result).toEqual(
expect.objectContaining({
Expand Down
Loading

0 comments on commit 3b77e04

Please sign in to comment.