Skip to content

Commit

Permalink
Merge branch 'hngprojects:dev' into feat-implement-pagination-on-find…
Browse files Browse the repository at this point in the history
…AllInvitations-Endpoint
  • Loading branch information
G4EVA-dev authored Mar 2, 2025
2 parents cfa376b + 5c1fd59 commit a256d46
Show file tree
Hide file tree
Showing 26 changed files with 3,572 additions and 3,343 deletions.
6,492 changes: 3,210 additions & 3,282 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"postinstall": "npm install --platform=linux --arch=x64 sharp"
},
"dependencies": {

"@google/generative-ai": "^0.22.0",
"@nestjs/axios": "^4.0.0",
"@nestjs/bull": "^11.0.2",
Expand All @@ -47,7 +46,6 @@
"@nestjs/swagger": "^11.0.5",
"@nestjs/typeorm": "^11.0.0",
"@types/nodemailer": "^6.4.17",

"@types/speakeasy": "^2.0.10",
"@vitalets/google-translate-api": "^9.2.1",
"aws-sdk": "^2.1692.0",
Expand Down
10 changes: 3 additions & 7 deletions src/database/migrations/1740788110830-UpdatePhoneNumberType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ import { MigrationInterface, QueryRunner } from 'typeorm';

export class UpdatePhoneNumberType1740788110830 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE contact ALTER COLUMN phone TYPE VARCHAR(20) USING phone::text`
);
await queryRunner.query(`ALTER TABLE contact ALTER COLUMN phone TYPE VARCHAR(20) USING phone::text`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE contact ALTER COLUMN phone TYPE INTEGER USING phone::integer`
);
await queryRunner.query(`ALTER TABLE contact ALTER COLUMN phone TYPE INTEGER USING phone::integer`);
}
}
}
37 changes: 37 additions & 0 deletions src/guards/job-access.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Job } from '../modules/jobs/entities/job.entity';
import { User } from '@modules/user/entities/user.entity';

@Injectable()
export class JobAccessGuard implements CanActivate {
constructor(
@InjectRepository(Job)
private readonly jobRepository: Repository<Job>,
@InjectRepository(User)
private readonly userRepository: Repository<User>
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const jobId = request.params.id;
const userId = request.user.sub;

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

if (user?.is_superadmin) {
return true;
}

const job = await this.jobRepository.findOne({
where: { id: jobId },
relations: ['user'],
});

if (!job) return false;
return job.user.id === userId;
}
}
16 changes: 14 additions & 2 deletions src/modules/comments/comments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserPayload } from './../user/interfaces/user-payload.interface';
import { User } from './../user/entities/user.entity';
import { Controller, Body, Post, Request, Get, Param, Delete } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CreateCommentDto } from './dtos/create-comment.dto';
Expand All @@ -15,7 +17,7 @@ export class CommentsController {
@ApiResponse({ status: 400, description: 'Bad Request.' })
@ApiResponse({ status: 500, description: 'Internal Server Error.' })
async addComment(@Body() createCommentDto: CreateCommentDto, @Request() req): Promise<CommentResponseDto> {
const { userId } = req.user;
const userId = req.user.id;
return await this.commentsService.addComment(createCommentDto, userId);
}

Expand All @@ -26,10 +28,20 @@ export class CommentsController {
return await this.commentsService.getAComment(id);
}

@ApiOperation({ summary: 'Dislike a comment' })
@ApiResponse({ status: 200, description: 'Dislike updated successfully' })
@ApiResponse({ status: 404, description: 'Comment not found' })
@Post(':id/dislike')
async dislikeComment(@Param('id') id: string, @Request() req) {
const userId = req.user.id;
// console.log('User ID:', userId); debug
return await this.commentsService.dislikeComment(id, userId);
}

@ApiOperation({ summary: 'Delete a comment' })
@ApiResponse({ status: 200, description: 'The comment has been deleted successfully.' })
@Delete(':id/delete')
async deleteAComment(@Param('id') id: string, @Request() req): Promise<any> {
return await this.commentsService.deleteAComment(id, req.user.userId);
return await this.commentsService.deleteAComment(id, req.user.id);
}
}
33 changes: 32 additions & 1 deletion src/modules/comments/comments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,43 @@ export class CommentsService {
throw new CustomHttpException('You are not authorized to delete this comment', HttpStatus.FORBIDDEN);
}

await this.commentRepository.delete(comment);
await this.commentRepository.delete(comment.id);

return {
message: 'Comment deleted successfully!',
status: HttpStatus.OK,
data: { comment },
};
}

async dislikeComment(commentId: string, userId: string): Promise<{ message: string; dislikeCount: number }> {
const comment = await this.commentRepository
.createQueryBuilder('comment')
.where('comment.id = :id', { id: commentId })
.getOne();

if (!comment) {
throw new CustomHttpException('Comment not found', HttpStatus.NOT_FOUND);
}

if (!comment.dislikedBy) {
comment.dislikedBy = [];
}

// Check if the user has already disliked the comment
if (comment.dislikedBy.includes(userId)) {
throw new CustomHttpException('You have already disliked this comment', HttpStatus.BAD_REQUEST);
}

// Add the user to the dislikedBy array and increment dislikes
comment.dislikedBy.push(userId);
comment.dislikes = comment.dislikedBy.length;

await this.commentRepository.save(comment);

return {
message: 'Dislike updated successfully',
dislikeCount: comment.dislikes,
};
}
}
9 changes: 9 additions & 0 deletions src/modules/comments/dtos/dislike-comment.dto
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsUUID } from 'class-validator';

export class DislikeCommentDto {
@ApiProperty({ description: 'Comment ID to dislike' })
@IsNotEmpty()
@IsUUID()
commentId: string;
}
6 changes: 6 additions & 0 deletions src/modules/comments/entities/comments.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ export class Comment extends AbstractBaseEntity {

@Column({ nullable: true })
model_type: string;

@Column({ type: 'int', default: 0 }) // Add dislikes column
dislikes: number;

@Column('simple-array', { nullable: true }) // Store user IDs as an array
dislikedBy: string[];
}
49 changes: 45 additions & 4 deletions src/modules/comments/tests/comments.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ import { User } from '../../user/entities/user.entity';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import { HttpStatus } from '@nestjs/common';

const mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
getOne: jest.fn(),
};

const mockCommentRepository = () => ({
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(() => mockQueryBuilder),
});

const mockUserRepository = () => ({
Expand Down Expand Up @@ -105,24 +111,24 @@ describe('CommentsService', () => {
it('should delete a comment successfully', async () => {
const commentId = 'comment-id';
const userId = 'user-id';
const mockUser = { id: 'user-id' };
const mockUser = { id: userId };
const mockComment = {
id: 'comment-id',
id: commentId,
model_id: '1',
model_type: 'post',
comment: 'A valid comment',
user: mockUser,
};

commentRepository.findOne.mockResolvedValue(mockComment);
commentRepository.delete.mockResolvedValue(mockComment);
commentRepository.delete.mockResolvedValue({ affected: 1 });

console.log(await commentRepository.findOne({ where: { id: commentId }, relations: ['user'] })); // Debugging

const result = await service.deleteAComment(commentId, userId);

expect(commentRepository.findOne).toHaveBeenCalledWith({ where: { id: commentId }, relations: ['user'] });
expect(commentRepository.delete).toHaveBeenCalledWith(mockComment);
expect(commentRepository.delete).toHaveBeenCalledWith(commentId);
expect(result).toEqual({
message: 'Comment deleted successfully!',
status: HttpStatus.OK,
Expand All @@ -131,4 +137,39 @@ describe('CommentsService', () => {
});
});
});

describe('CommentsService - dislikeComment', () => {
it('should throw CustomHttpException if comment is not found', async () => {
commentRepository.findOne.mockResolvedValue(null);

await expect(service.dislikeComment('comment-id', 'user-id')).rejects.toThrow(CustomHttpException);
await expect(service.dislikeComment('comment-id', 'user-id')).rejects.toMatchObject({
message: 'Comment not found',
status: HttpStatus.NOT_FOUND,
});
});

it('should increase the dislike count successfully', async () => {
const mockComment = {
id: 'comment-id',
dislikes: 2,
dislikedBy: ['user1', 'user2'], // Ensure this is initialized
};

// Mock `getOne()` from `createQueryBuilder`
mockQueryBuilder.getOne.mockResolvedValue(mockComment);
commentRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);

commentRepository.save.mockResolvedValue({ ...mockComment, dislikes: 3 });

const result = await service.dislikeComment('comment-id', 'user-id');

expect(result).toEqual({
message: 'Dislike updated successfully',
dislikeCount: 3,
});

expect(commentRepository.save).toHaveBeenCalledWith({ ...mockComment, dislikes: 3 });
});
});
});
2 changes: 1 addition & 1 deletion src/modules/contact-us/contact-us.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ export class ContactUsService {
},
});
}
}
}
32 changes: 16 additions & 16 deletions src/modules/contact-us/dto/create-contact-us.dto.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { IsEmail, IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';

export class CreateContactDto {
@IsNotEmpty({ message: 'Name should not be empty' })
@IsString({ message: 'Name must be a string' })
name: string;
@IsNotEmpty({ message: 'Name should not be empty' })
@IsString({ message: 'Name must be a string' })
name: string;

@IsNotEmpty({ message: 'Email should not be empty' })
@IsEmail({}, { message: 'Email must be valid' })
email: string;
@IsNotEmpty({ message: 'Email should not be empty' })
@IsEmail({}, { message: 'Email must be valid' })
email: string;

@IsOptional()
@IsString({ message: 'Phone must be a string' })
@Matches(/^\+?[0-9\-\s()]{8,20}$/, {
message: 'Invalid phone number format'
})
phone: string;
@IsOptional()
@IsString({ message: 'Phone must be a string' })
@Matches(/^\+?[0-9\-\s()]{8,20}$/, {
message: 'Invalid phone number format',
})
phone: string;

@IsNotEmpty({ message: 'Message should not be empty' })
@IsString({ message: 'Message must be a string' })
message: string;
}
@IsNotEmpty({ message: 'Message should not be empty' })
@IsString({ message: 'Message must be a string' })
message: string;
}
22 changes: 11 additions & 11 deletions src/modules/contact-us/entities/contact-us.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { Entity, Column } from 'typeorm';

@Entity()
export class ContactUs extends AbstractBaseEntity {
@Column('varchar', { length: 20, nullable: true })
phone: string;
@Column('varchar', { length: 20, nullable: true })
phone: string;

@Column('varchar', { nullable: false })
name: string;
@Column('varchar', { nullable: false })
name: string;

@Column('varchar', { nullable: false })
email: string;
@Column('varchar', { nullable: false })
email: string;

@Column('text', { nullable: false })
message: string;
@Column('text', { nullable: false })
message: string;

@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}
1 change: 1 addition & 0 deletions src/modules/invite/mocks/mockUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const mockUser: User = {
first_name: 'John',
last_name: 'Doe',
is_active: true,
is_superadmin: false,
phone: '+1234567890',
status: 'Hello from the children of planet Earth',
id: 'some-uuid-value-here',
Expand Down
4 changes: 2 additions & 2 deletions src/modules/jobs/jobs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { JobSearchDto } from './dto/jobSearch.dto';
import { UpdateJobDto } from './dto/update-job.dto';
import { JobOwnerGuard } from '../../guards/job-owner.guard';
import { AuthGuard } from '../../guards/auth.guard';
import { JobAccessGuard } from '@guards/job-access.guard';

@ApiTags('Jobs')
@Controller('jobs')
Expand Down Expand Up @@ -124,8 +125,7 @@ export class JobsController {
}

@Patch('/:id')
@UseGuards(AuthGuard)
@UseGuards(SuperAdminGuard, JobOwnerGuard)
@UseGuards(AuthGuard, JobAccessGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update a job posting' })
@ApiResponse({ status: 200, description: 'Job updated successfully' })
Expand Down
6 changes: 3 additions & 3 deletions src/modules/jobs/jobs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import { OrganisationUserRole } from '@modules/role/entities/organisation-user-r
import { Profile } from '@modules/profile/entities/profile.entity';
import { Role } from '@modules/role/entities/role.entity';
import { UserModule } from '@modules/user/user.module';
import { JobOwnerGuard } from '@guards/job-owner.guard';
import { AuthGuard } from '@guards/auth.guard';
import { SuperAdminGuard } from '@guards/super-admin.guard';
import { JobAccessGuard } from '@guards/job-access.guard';
import { S3Service } from '@modules/s3/s3.service';

@Module({
imports: [
TypeOrmModule.forFeature([Job, User, JobApplication, Organisation, OrganisationUserRole, Profile, Role]),
UserModule,
],
providers: [JobsService, JobOwnerGuard, AuthGuard, SuperAdminGuard, S3Service],
providers: [JobsService, JobAccessGuard, AuthGuard, S3Service],
controllers: [JobsController],
})
export class JobsModule {}
Loading

0 comments on commit a256d46

Please sign in to comment.