Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: comment delete by admin #1314

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,4 @@ dist
*.dev
*.prod

package-lock.json
48 changes: 48 additions & 0 deletions src/guards/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -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<User>,
@InjectRepository(Organisation)
private readonly organisationRepository: Repository<Organisation>,
@InjectRepository(OrganisationUserRole)
private readonly organisationMembersRole: Repository<OrganisationUserRole>,
@InjectRepository(Role)
private readonly userRoleManager: Repository<Role>
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
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;
}
}
3 changes: 2 additions & 1 deletion src/guards/super-admin.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion src/modules/comments/comments.controller.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User the admin guard instead of manual check

async addComment(@Body() createCommentDto: CreateCommentDto, @Request() req): Promise<CommentResponseDto> {
const { userId } = req.user;
return await this.commentsService.addComment(createCommentDto, userId);
Expand All @@ -25,4 +29,15 @@ export class CommentsController {
async getAComment(@Param('id') id: string): Promise<any> {
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<any> {
const { userId, role } = req.user;
return await this.commentsService.delAComment(id, userId, role);
}
}
6 changes: 5 additions & 1 deletion src/modules/comments/comments.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
Expand Down
19 changes: 19 additions & 0 deletions src/modules/comments/comments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)',
};
}
}
39 changes: 39 additions & 0 deletions src/modules/comments/tests/comments.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => ({
Expand Down Expand Up @@ -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)',
});
});
});
});