diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 3226c220d..05f153123 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -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( @@ -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> { @@ -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'); diff --git a/src/modules/auth/tests/auth.service.spec.ts b/src/modules/auth/tests/auth.service.spec.ts index bc7697e59..723a5c06e 100644 --- a/src/modules/auth/tests/auth.service.spec.ts +++ b/src/modules/auth/tests/auth.service.spec.ts @@ -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; let profileServiceMock: jest.Mocked; + let dataSourceMock: jest.Mocked; let jwtServiceMock: jest.Mocked; let otpServiceMock: jest.Mocked; let emailServiceMock: jest.Mocked; let organisationServiceMock: jest.Mocked; beforeEach(async () => { + dataSourceMock = { + transaction: jest.fn().mockImplementation(async cb => cb({} as EntityManager)), + manager: {} as EntityManager, + } as unknown as jest.Mocked; const module: TestingModule = await Test.createTestingModule({ providers: [ AuthenticationService, @@ -77,6 +82,10 @@ describe('AuthenticationService', () => { sendEmail: jest.fn(), }, }, + { + provide: DataSource, + useValue: dataSourceMock, + }, ], }).compile(); @@ -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', @@ -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", diff --git a/src/modules/organisations/dto/create-organisation-options.ts b/src/modules/organisations/dto/create-organisation-options.ts index fa1609979..2cca017e1 100644 --- a/src/modules/organisations/dto/create-organisation-options.ts +++ b/src/modules/organisations/dto/create-organisation-options.ts @@ -1,5 +1,5 @@ import { OrganisationInterface } from '../interfaces/OrganisationInterface'; - +import { CreateRecordGeneric } from '@shared/helpers/createRecordGeneric'; type CreateOrganisationType = Partial; - +export type CreateOrganisationRecordOptions = CreateRecordGeneric; export default CreateOrganisationType; diff --git a/src/modules/organisations/interfaces/OrganisationInterface.ts b/src/modules/organisations/interfaces/OrganisationInterface.ts index 49a6a83f2..bcd6e8355 100644 --- a/src/modules/organisations/interfaces/OrganisationInterface.ts +++ b/src/modules/organisations/interfaces/OrganisationInterface.ts @@ -14,4 +14,6 @@ export interface OrganisationInterface { address: string; state: string; + + userId: string; } diff --git a/src/modules/organisations/organisations.controller.ts b/src/modules/organisations/organisations.controller.ts index c00414816..a30f4f58a 100644 --- a/src/modules/organisations/organisations.controller.ts +++ b/src/modules/organisations/organisations.controller.ts @@ -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) diff --git a/src/modules/organisations/organisations.service.ts b/src/modules/organisations/organisations.service.ts index d0d6e425e..f6c187a31 100644 --- a/src/modules/organisations/organisations.service.ts +++ b/src/modules/organisations/organisations.service.ts @@ -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'; @@ -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); @@ -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; diff --git a/src/modules/organisations/tests/organisations.service.spec.ts b/src/modules/organisations/tests/organisations.service.spec.ts index 809eb8135..deddecbd5 100644 --- a/src/modules/organisations/tests/organisations.service.spec.ts +++ b/src/modules/organisations/tests/organisations.service.spec.ts @@ -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; @@ -90,8 +87,14 @@ describe('OrganisationsService', () => { }); describe('create', () => { it('should create a new organisation', async () => { - const createOrganisationDto = { name: 'Test Org', email: 'test@example.com' }; const userId = 'user-id'; + const createOrganisationDto = { name: 'Test Org', email: 'test@example.com' }; + 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 }; @@ -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({ diff --git a/src/modules/otp/otp.service.ts b/src/modules/otp/otp.service.ts index 149dd4b30..299e59de7 100644 --- a/src/modules/otp/otp.service.ts +++ b/src/modules/otp/otp.service.ts @@ -1,6 +1,6 @@ import { HttpStatus, Injectable, NotAcceptableException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, EntityManager } from 'typeorm'; import { Otp } from './entities/otp.entity'; import { User } from '@modules/user/entities/user.entity'; import { generateSixDigitToken } from '@utils/generate-token'; @@ -15,9 +15,11 @@ export class OtpService { private userRepository: Repository ) {} - async createOtp(userId: string): Promise { + async createOtp(userId: string, manager?: EntityManager): Promise { try { - const user = await this.userRepository.findOne({ where: { id: userId } }); + const repo = manager ? manager.getRepository(User) : this.userRepository; + const otpRepo = manager ? manager.getRepository(Otp) : this.otpRepository; + const user = await repo.findOne({ where: { id: userId } }); if (!user) { throw new NotFoundException('User not found'); @@ -26,8 +28,8 @@ export class OtpService { const token = generateSixDigitToken(); const expiry = new Date(Date.now() + 5 * 60 * 1000); - const otp = this.otpRepository.create({ token, expiry, user, user_id: userId }); - await this.otpRepository.save(otp); + const otp = otpRepo.create({ token, expiry, user, user_id: userId }); + await otpRepo.save(otp); return otp; } catch (error) { diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 5d7918a52..0705b85e6 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -8,7 +8,7 @@ import { StreamableFile, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { EntityManager, Repository } from 'typeorm'; import { Profile } from '../profile/entities/profile.entity'; import { DeactivateAccountDto } from './dto/deactivate-account.dto'; import { UpdateUserDto } from './dto/update-user-dto'; @@ -38,13 +38,14 @@ export default class UserService { private profileRepository: Repository ) {} - async createUser(createUserPayload: CreateNewUserOptions): Promise { + async createUser(createUserPayload: CreateNewUserOptions, manager?: EntityManager): Promise { + const repo = manager ? manager.getRepository(User) : this.userRepository; const profile = await this.profileRepository.save({ email: createUserPayload.email, username: '' }); const newUser = new User(); Object.assign(newUser, createUserPayload); newUser.is_active = true; newUser.profile = profile; - return await this.userRepository.save(newUser); + return await repo.save(newUser); } async updateUserRecord(userUpdateOptions: UpdateUserRecordOption) { diff --git a/src/shared/helpers/createRecordGeneric.ts b/src/shared/helpers/createRecordGeneric.ts new file mode 100644 index 000000000..1e4e1dfb2 --- /dev/null +++ b/src/shared/helpers/createRecordGeneric.ts @@ -0,0 +1,13 @@ +import { EntityManager } from 'typeorm'; + +export interface CreateRecordGeneric { + createPayload: RecordType; + dbTransaction: + | { + useTransaction: false; + } + | { + useTransaction: true; + transactionManager: EntityManager; + }; +} diff --git a/src/shared/inteceptors/response.interceptor.ts b/src/shared/inteceptors/response.interceptor.ts index 52ee1f74b..537a45a48 100644 --- a/src/shared/inteceptors/response.interceptor.ts +++ b/src/shared/inteceptors/response.interceptor.ts @@ -41,6 +41,7 @@ export class ResponseInterceptor implements NestInterceptor { response.setHeader('Content-Type', 'application/json'); if (typeof res === 'object') { const { message, ...data } = res; + console.log('response', res); return { status_code,