diff --git a/package-lock.json b/package-lock.json index 1467deb4c..2814bc273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@nestjs/typeorm": "^10.0.2", "@types/nodemailer": "^6.4.15", "@types/speakeasy": "^2.0.10", + "@vitalets/google-translate-api": "^9.2.0", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", "bull": "^4.16.0", @@ -4493,6 +4494,26 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitalets/google-translate-api": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@vitalets/google-translate-api/-/google-translate-api-9.2.0.tgz", + "integrity": "sha512-w98IPWGuexlGmh8Y19AxF6cgWT0U5JLevVNDKEuFpTWtBC9z3YtDWKTDxF3nPP1k9bWicuB1V7I7YfHoZiDScw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "^1.8.2", + "http-errors": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vitalets/google-translate-api/node_modules/@types/http-errors": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", + "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "license": "MIT" + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -8619,7 +8640,8 @@ "node_modules/file-type-mime": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/file-type-mime/-/file-type-mime-0.4.3.tgz", - "integrity": "sha512-yumBt0l9E03Oyk3KZyq9KTM9LF0XClKWtVU+bDEOl+tbIlUr/Jnl0ZjkF/r6KmqmjJgGaWhUDSdG2HUvLJ3kNA==" + "integrity": "sha512-yumBt0l9E03Oyk3KZyq9KTM9LF0XClKWtVU+bDEOl+tbIlUr/Jnl0ZjkF/r6KmqmjJgGaWhUDSdG2HUvLJ3kNA==", + "license": "MIT" }, "node_modules/filelist": { "version": "1.0.4", diff --git a/package.json b/package.json index 8ccc3f611..089c75965 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@nestjs/typeorm": "^10.0.2", "@types/nodemailer": "^6.4.15", "@types/speakeasy": "^2.0.10", + "@vitalets/google-translate-api": "^9.2.0", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", "bull": "^4.16.0", diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e564f4276..06f60a2f1 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -8,7 +8,20 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; import * as SYS_MSG from '../../helpers/SystemMessages'; -import { Body, Controller, HttpCode, HttpStatus, Post, Req, Request, Res, UseGuards, Get, Patch } from '@nestjs/common'; +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Req, + Request, + Res, + UseGuards, + Get, + Patch, + Query, +} from '@nestjs/common'; import { CreateUserDTO } from './dto/create-user.dto'; import { skipAuth } from '../../helpers/skipAuth'; import AuthenticationService from './auth.service'; @@ -106,8 +119,8 @@ export default class RegistrationController { @ApiResponse({ status: 200, description: 'Verify Payload sent from google', type: AuthResponseDto }) @ApiBadRequestResponse({ description: 'Invalid Google token' }) @HttpCode(200) - async googleAuth(@Body() body: GoogleAuthPayload) { - return this.authService.googleAuth(body); + async googleAuth(@Body() body: GoogleAuthPayload, @Query('mobile') isMobile: string) { + return this.authService.googleAuth({ googleAuthPayload: body, isMobile }); } @skipAuth() diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 172b483c3..63e474ce3 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -283,9 +283,17 @@ export default class AuthenticationService { }; } - async googleAuth(googleAuthPayload: GoogleAuthPayload) { + async googleAuth({ googleAuthPayload, isMobile }: { googleAuthPayload: GoogleAuthPayload; isMobile: string }) { const idToken = googleAuthPayload.id_token; - const verifyTokenResponse: GoogleVerificationPayloadInterface = await this.googleAuthService.verifyToken(idToken); + let verifyTokenResponse: GoogleVerificationPayloadInterface; + + if (isMobile === 'true') { + const request = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${idToken}`); + verifyTokenResponse = await request.json(); + } else { + verifyTokenResponse = await this.googleAuthService.verifyToken(idToken); + } + const userEmail = verifyTokenResponse.email; const userExists = await this.userService.getUserRecord({ identifier: userEmail, identifierType: 'email' }); diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index e2e486af7..62c730b02 100644 --- a/src/modules/help-center/help-center.controller.ts +++ b/src/modules/help-center/help-center.controller.ts @@ -190,4 +190,4 @@ export class HelpCenterController { } } } -} +} \ No newline at end of file diff --git a/src/modules/help-center/help-center.service.ts b/src/modules/help-center/help-center.service.ts index 0e5a6a710..dc65405a2 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { HelpCenterEntity } from '../help-center/entities/help-center.entity'; @@ -88,16 +88,40 @@ export class HelpCenterService { } async updateTopic(id: string, updateHelpCenterDto: UpdateHelpCenterDto) { + const existingTopic = await this.helpCenterRepository.findOneBy({ id }); + if (!existingTopic) { + throw new HttpException( + { + status: 'error', + message: 'Topic not found, please check and try again', + status_code: HttpStatus.NOT_FOUND, + }, + HttpStatus.NOT_FOUND + ); + } + await this.helpCenterRepository.update(id, updateHelpCenterDto); - const query = await this.helpCenterRepository.findOneBy({ id }); + const updatedTopic = await this.helpCenterRepository.findOneBy({ id }); + return { status_code: HttpStatus.OK, message: REQUEST_SUCCESSFUL, - data: query, + data: updatedTopic, }; } async removeTopic(id: string): Promise { + const existingTopic = await this.helpCenterRepository.findOneBy({ id }); + if (!existingTopic) { + throw new HttpException( + { + status: 'error', + message: 'Topic not found, unable to delete', + status_code: HttpStatus.NOT_FOUND, + }, + HttpStatus.NOT_FOUND + ); + } await this.helpCenterRepository.delete(id); } } diff --git a/src/modules/testimonials/dto/update-testimonial.dto.ts b/src/modules/testimonials/dto/update-testimonial.dto.ts new file mode 100644 index 000000000..7e19ec19e --- /dev/null +++ b/src/modules/testimonials/dto/update-testimonial.dto.ts @@ -0,0 +1,14 @@ +import { IsString, IsOptional } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateTestimonialDto { + @ApiPropertyOptional({ description: 'Updated content of the testimonial' }) + @IsString() + @IsOptional() + content?: string; + + @ApiPropertyOptional({ description: 'Updated name associated with the testimonial' }) + @IsString() + @IsOptional() + name?: string; +} diff --git a/src/modules/testimonials/dto/update-testimonial.response.dto.ts b/src/modules/testimonials/dto/update-testimonial.response.dto.ts new file mode 100644 index 000000000..485584491 --- /dev/null +++ b/src/modules/testimonials/dto/update-testimonial.response.dto.ts @@ -0,0 +1,6 @@ +export class UpdateTestimonialResponseDto { + status: string; + message: string; + data: any; + } + \ No newline at end of file diff --git a/src/modules/testimonials/testimonials.controller.ts b/src/modules/testimonials/testimonials.controller.ts index 15fab5277..1d538835f 100644 --- a/src/modules/testimonials/testimonials.controller.ts +++ b/src/modules/testimonials/testimonials.controller.ts @@ -11,6 +11,7 @@ import { ParseUUIDPipe, HttpStatus, Delete, + Patch, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UserPayload } from '../user/interfaces/user-payload.interface'; @@ -23,6 +24,8 @@ import { GetTestimonials400ErrorResponseDto, GetTestimonials404ErrorResponseDto, } from './dto/get-testimonials.dto'; +import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; +import { UpdateTestimonialResponseDto } from './dto/update-testimonial.response.dto'; @ApiBearerAuth() @ApiTags('Testimonials') @@ -115,4 +118,26 @@ export class TestimonialsController { async deleteTestimonial(@Param('id') id: string) { return this.testimonialsService.deleteTestimonial(id); } + + @Patch(':id') + @ApiOperation({ summary: 'Update Testimonial' }) + @ApiResponse({ status: 200, description: 'Testimonial updated successfully' }) + @ApiResponse({ status: 404, description: 'Testimonial not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 500, description: 'Internal Server Error' }) + async update( + @Param('id') id: string, + @Body() updateTestimonialDto: UpdateTestimonialDto, + @Req() req: { user: UserPayload } + ): Promise { + const userId = req.user.id; + + const data = await this.testimonialsService.updateTestimonial(id, updateTestimonialDto, userId); + + return { + status: 'success', + message: 'Testimonial updated successfully', + data, + } + } } diff --git a/src/modules/testimonials/testimonials.service.ts b/src/modules/testimonials/testimonials.service.ts index 5b198b8a0..c6e5d97d2 100644 --- a/src/modules/testimonials/testimonials.service.ts +++ b/src/modules/testimonials/testimonials.service.ts @@ -15,6 +15,7 @@ import UserService from '../user/user.service'; import { TestimonialMapper } from './mappers/testimonial.mapper'; import { TestimonialResponseMapper } from './mappers/testimonial-response.mapper'; import { TestimonialResponse } from './interfaces/testimonial-response.interface'; +import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; @Injectable() export class TestimonialsService { @@ -106,6 +107,26 @@ export class TestimonialsService { return TestimonialResponseMapper.mapToEntity(testimonial); } + + async updateTestimonial(id: string, updateTestimonialDto: UpdateTestimonialDto, userId: string) { + const testimonial = await this.testimonialRepository.findOne({ where: { id, user: { id: userId } } }); + + if (!testimonial) { + throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); + } + + Object.assign(testimonial, updateTestimonialDto); + await this.testimonialRepository.save(testimonial); + + return { + id: testimonial.id, + user_id: userId, + content: testimonial.content, + name: testimonial.name, + updated_at: new Date(), + }; + } + async deleteTestimonial(id: string) { const testimonial = await this.testimonialRepository.findOne({ where: { id } }); if (!testimonial) { diff --git a/src/modules/testimonials/tests/update.service.spec.ts b/src/modules/testimonials/tests/update.service.spec.ts new file mode 100644 index 000000000..82186adcd --- /dev/null +++ b/src/modules/testimonials/tests/update.service.spec.ts @@ -0,0 +1,84 @@ +import { InternalServerErrorException, NotFoundException, HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Profile } from '../../profile/entities/profile.entity'; +import { User } from '../../user/entities/user.entity'; +import UserService from '../../user/user.service'; +import { UpdateTestimonialDto } from '../dto/update-testimonial.dto'; +import { Testimonial } from '../entities/testimonials.entity'; +import { TestimonialsService } from '../testimonials.service'; + +describe('TestimonialsService', () => { + let service: TestimonialsService; + let userService: UserService; + let testimonialRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TestimonialsService, + UserService, + { + provide: getRepositoryToken(Testimonial), + useClass: Repository, + }, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: getRepositoryToken(Profile), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(TestimonialsService); + userService = module.get(UserService); + testimonialRepository = module.get>(getRepositoryToken(Testimonial)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('updateTestimonial', () => { + it('should successfully update a testimonial', async () => { + const id = 'testimonial_id'; + const updateTestimonialDto: UpdateTestimonialDto = { + name: 'Updated Name', + content: 'Updated content!', + }; + const userId = 'user_id'; + const testimonial = { id, user: { id: userId }, ...updateTestimonialDto } as Testimonial; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(testimonial); + jest.spyOn(testimonialRepository, 'save').mockResolvedValue(testimonial); + + const result = await service.updateTestimonial(id, updateTestimonialDto, userId); + + expect(result).toEqual({ + id, + user_id: userId, + ...updateTestimonialDto, + updated_at: expect.any(Date), + }); + }); + + it('should throw a NotFoundException if testimonial is not found', async () => { + const id = 'testimonial_id'; + const updateTestimonialDto: UpdateTestimonialDto = { + name: 'Updated Name', + content: 'Updated content!', + }; + const userId = 'user_id'; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(null); + + await expect(service.updateTestimonial(id, updateTestimonialDto, userId)).rejects.toThrow( + new NotFoundException('Testimonial not found') + ); + }); + }); +});