diff --git a/.gitignore b/.gitignore index 29697a8be..214964b33 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,4 @@ dist *.dev *.prod +package-lock.json diff --git a/src/guards/admin.guard.ts b/src/guards/admin.guard.ts new file mode 100644 index 000000000..18e10f35a --- /dev/null +++ b/src/guards/admin.guard.ts @@ -0,0 +1,48 @@ +import { Injectable, CanActivate, ExecutionContext, HttpStatus } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User, UserType } from '../modules/user/entities/user.entity'; +import { Repository } from 'typeorm'; +import * as SYS_MSG from '../helpers/SystemMessages'; +import { CustomHttpException } from '../helpers/custom-http-filter'; +import { Organisation } from '../modules/organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../modules/role/entities/organisation-user-role.entity'; +import { Role } from '../modules/role/entities/role.entity'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class AdminGuard implements CanActivate { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(Organisation) + private readonly organisationRepository: Repository, + @InjectRepository(OrganisationUserRole) + private readonly organisationMembersRole: Repository, + @InjectRepository(Role) + private readonly userRoleManager: Repository + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const currentUserId = request.user.sub; + + // Retrieve admin roles with the permissions alloted to that role + const adminRole = await this.userRoleManager.findOne({ + where: { name: UserType.ADMIN }, + relations: ['permissions'], + }); + + if (!adminRole) { + throw new CustomHttpException('Admin Role does not exist', HttpStatus.BAD_REQUEST); + } + + const userRole = await this.organisationMembersRole.find({ + where: { userId: currentUserId, roleId: adminRole.id }, + }); + + if (userRole.length === 0) { + throw new CustomHttpException('Access denied', HttpStatus.FORBIDDEN); + } + return true; + } +} diff --git a/src/guards/super-admin.guard.ts b/src/guards/super-admin.guard.ts index abb75e46a..2615628fd 100644 --- a/src/guards/super-admin.guard.ts +++ b/src/guards/super-admin.guard.ts @@ -25,6 +25,7 @@ export class SuperAdminGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const currentUserId = request.user.sub; + // Retrieve admin roles with the permissions alloted to that role const adminRole = await this.userRoleManager.findOne({ where: { name: 'super-admin' }, relations: ['permissions'], @@ -38,7 +39,7 @@ export class SuperAdminGuard implements CanActivate { where: { userId: currentUserId, roleId: adminRole.id }, }); - if (!userRole.length) { + if (userRole.length === 0) { throw new CustomHttpException('Access denied', HttpStatus.FORBIDDEN); } return true; diff --git a/src/modules/comments/comments.controller.ts b/src/modules/comments/comments.controller.ts index a0d6f48dd..a1bbc2926 100644 --- a/src/modules/comments/comments.controller.ts +++ b/src/modules/comments/comments.controller.ts @@ -1,8 +1,11 @@ -import { Controller, Body, Post, Request, Get, Param } from '@nestjs/common'; +import { Controller, Body, Post, Request, Get, Param, UseGuards, Delete } from '@nestjs/common'; import { CommentsService } from './comments.service'; import { CreateCommentDto } from './dtos/create-comment.dto'; import { CommentResponseDto } from './dtos/comment-response.dto'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +// import { AuthGuard } from 'src/guards/auth.guard'; +import { AuthGuard } from '../../guards/auth.guard'; +import { AdminGuard } from '../../guards/admin.guard'; @ApiBearerAuth() @ApiTags('Comments') @@ -14,6 +17,7 @@ export class CommentsController { @ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto }) @ApiResponse({ status: 400, description: 'Bad Request.' }) @ApiResponse({ status: 500, description: 'Internal Server Error.' }) + @UseGuards(AuthGuard) async addComment(@Body() createCommentDto: CreateCommentDto, @Request() req): Promise { const { userId } = req.user; return await this.commentsService.addComment(createCommentDto, userId); @@ -25,4 +29,15 @@ export class CommentsController { async getAComment(@Param('id') id: string): Promise { return await this.commentsService.getAComment(id); } + + @ApiOperation({ summary: 'Delete a comment' }) + @ApiResponse({ status: 201, description: 'The comment has been deleted sucessfully', type: CommentResponseDto }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 500, description: 'Internal Server Error.' }) + @Delete(':id') + @UseGuards(AuthGuard, AdminGuard) + async delAComment(@Param('id') id: string, @Request() req): Promise { + const { userId, role } = req.user; + return await this.commentsService.delAComment(id, userId, role); + } } diff --git a/src/modules/comments/comments.module.ts b/src/modules/comments/comments.module.ts index 810a87ea2..1f154cef0 100644 --- a/src/modules/comments/comments.module.ts +++ b/src/modules/comments/comments.module.ts @@ -5,9 +5,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Comment } from './entities/comments.entity'; import { User } from '../user/entities/user.entity'; import { UserModule } from '../user/user.module'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; +// @Module({ - imports: [TypeOrmModule.forFeature([Comment, User]), UserModule], + imports: [TypeOrmModule.forFeature([Comment, User, Organisation, OrganisationUserRole, Role]), UserModule], controllers: [CommentsController], providers: [CommentsService], }) diff --git a/src/modules/comments/comments.service.ts b/src/modules/comments/comments.service.ts index cad87ebac..2b3939934 100644 --- a/src/modules/comments/comments.service.ts +++ b/src/modules/comments/comments.service.ts @@ -6,6 +6,8 @@ import { CreateCommentDto } from './dtos/create-comment.dto'; import { User } from '../user/entities/user.entity'; import { CommentResponseDto } from './dtos/comment-response.dto'; import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { UserType } from '../user/entities/user.entity'; + @Injectable() export class CommentsService { constructor( @@ -53,4 +55,21 @@ export class CommentsService { data: { comment }, }; } + + async delAComment(commentId: string, userId: string, role: string) { + const comment = await this.commentRepository.findOne({ + where: { id: commentId }, + relations: ['user'], + }); + + if (!comment) { + throw new CustomHttpException('Comment not found', HttpStatus.NOT_FOUND); + } + + await this.commentRepository.delete(commentId); + + return { + message: 'Comment deleted successfully (soft delete)', + }; + } } diff --git a/src/modules/comments/tests/comments.service.spec.ts b/src/modules/comments/tests/comments.service.spec.ts index 903b9ab00..fa917cdcf 100644 --- a/src/modules/comments/tests/comments.service.spec.ts +++ b/src/modules/comments/tests/comments.service.spec.ts @@ -6,10 +6,13 @@ import { User } from '../../user/entities/user.entity'; import { Repository } from 'typeorm'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import { HttpStatus } from '@nestjs/common'; +import { UserType } from '../../user/entities/user.entity'; const mockCommentRepository = () => ({ create: jest.fn(), save: jest.fn(), + findOne: jest.fn(), + delete: jest.fn(), }); const mockUserRepository = () => ({ @@ -75,4 +78,40 @@ describe('CommentsService', () => { }); }); }); + + describe('delAComment', () => { + it('should throw CustomHttpException if comment is not found', async () => { + commentRepository.findOne.mockResolvedValue(null); + + await expect(service.delAComment('comment-id', 'user-id', UserType.USER)).rejects.toThrow(CustomHttpException); + await expect(service.delAComment('comment-id', 'user-id', UserType.USER)).rejects.toMatchObject({ + message: 'Comment not found', + status: HttpStatus.NOT_FOUND, + }); + }); + + it('should throw CustomHttpException if user is not an admin', async () => { + const mockComment = { id: 'comment-id', user: { id: 'user-id' } }; + commentRepository.findOne.mockResolvedValue(mockComment); + + await expect(service.delAComment('comment-id', 'user-id', UserType.USER)).rejects.toThrow(CustomHttpException); + await expect(service.delAComment('comment-id', 'user-id', UserType.USER)).rejects.toMatchObject({ + message: 'Unauthorized action', + status: HttpStatus.FORBIDDEN, + }); + }); + + it('should delete a comment successfully if user is an admin', async () => { + const mockComment = { id: 'comment-id', user: { id: 'user-id' } }; + + commentRepository.findOne.mockResolvedValue(mockComment); + commentRepository.delete.mockResolvedValue({ affected: 1 }); + + const result = await service.delAComment('comment-id', 'admin-id', UserType.ADMIN); + + expect(result).toEqual({ + message: 'Comment deleted successfully (soft delete)', + }); + }); + }); });