Skip to content

Commit

Permalink
Merge pull request #1257 from Daggahh/dev
Browse files Browse the repository at this point in the history
feat(jobs): implement job post update endpoint and add comprehensive tests
  • Loading branch information
incredible-phoenix246 authored Feb 28, 2025
2 parents 5b93595 + 9b7e5db commit fd43262
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 8 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ Network Trash Folder
Temporary Items
.apdisk

=======
# Local
dist
/.env
Expand All @@ -401,14 +400,18 @@ dist
*.dev
*.prod


# User specific ignores
todo.txt
.vscode/
data/
docker-compose.yml
package-lock.json
.dev.env


package-lock.json
docker-compose.yml
data/
.dev.env


35 changes: 31 additions & 4 deletions src/database/seeding/seeding.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,43 @@ export class SeedingService {
async createSuperAdmin({ secret, ...adminDetails }: CreateAdminDto): Promise<CreateAdminResponseDto> {
try {
const userRepository = this.dataSource.getRepository(User);
const roleRepository = this.dataSource.getRepository(Role);
const orgUserRoleRepository = this.dataSource.getRepository(OrganisationUserRole);

// Check if user exists
const exists = await userRepository.findOne({ where: { email: adminDetails.email } });
if (exists) throw new ConflictException('A user already exist with the same email');

const user = userRepository.create(adminDetails);
// Verify admin secret
const { ADMIN_SECRET } = process.env;
if (secret !== ADMIN_SECRET) throw new UnauthorizedException(INVALID_ADMIN_SECRET);

// user.user_type = UserType.SUPER_ADMIN;
const admin = await userRepository.save(user);
return { status: 201, message: ADMIN_CREATED, data: admin };
// Find or create super-admin role
let adminRole = await roleRepository.findOne({ where: { name: 'super-admin' } });
if (!adminRole) {
adminRole = roleRepository.create({
name: 'super-admin',
description: 'Super Administrator',
});
adminRole = await roleRepository.save(adminRole);
}

// Create and save user
const user = userRepository.create(adminDetails);
const savedUser = await userRepository.save(user);

// Assign super-admin role to user
const userRole = orgUserRoleRepository.create({
userId: savedUser.id,
roleId: adminRole.id,
});
await orgUserRoleRepository.save(userRole);

return {
status: 201,
message: ADMIN_CREATED,
data: savedUser,
};
} catch (error) {
console.log('Error creating superAdmin:', error);
if (error instanceof UnauthorizedException || error instanceof ConflictException) throw error;
Expand Down
29 changes: 29 additions & 0 deletions src/guards/job-owner.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Job } from '../modules/jobs/entities/job.entity';

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

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

if (!jobId || !userId) return false;

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

if (!job) return false;

return job.user.id === userId;
}
}
101 changes: 101 additions & 0 deletions src/modules/jobs/dto/update-job.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsEnum, IsDateString, IsArray } from 'class-validator';
import { JobMode, JobType, SalaryRange } from './job.dto';

export class UpdateJobDto {
@ApiPropertyOptional({
description: 'The title of the job',
example: 'Senior Software Engineer',
})
@IsOptional()
@IsString()
title?: string;

@ApiPropertyOptional({
description: 'A detailed description of the job',
example: 'We are looking for an experienced developer...',
})
@IsOptional()
@IsString()
description?: string;

@ApiPropertyOptional({
description: 'The location of the job',
example: 'New York, NY',
})
@IsOptional()
@IsString()
location?: string;

@ApiPropertyOptional({
description: 'The deadline for applications',
example: '2024-12-31T23:59:59Z',
})
@IsOptional()
@IsDateString()
deadline?: string;

@ApiPropertyOptional({
description: 'The salary range',
enum: SalaryRange,
})
@IsOptional()
@IsEnum(SalaryRange)
salary_range?: string;

@ApiPropertyOptional({
description: 'The type of job',
enum: JobType,
})
@IsOptional()
@IsEnum(JobType)
job_type?: string;

@ApiPropertyOptional({
description: 'The mode of work',
enum: JobMode,
})
@IsOptional()
@IsEnum(JobMode)
job_mode?: string;

@ApiPropertyOptional({
description: 'Company name',
example: 'Tech Corp',
})
@IsOptional()
@IsString()
company_name?: string;

@ApiPropertyOptional({
description: 'Required qualifications',
type: [String],
})
@IsOptional()
@IsArray()
qualifications?: string[];

@ApiPropertyOptional({
description: 'Key responsibilities',
type: [String],
})
@IsOptional()
@IsArray()
key_responsibilities?: string[];

@ApiPropertyOptional({
description: 'Job benefits',
type: [String],
})
@IsOptional()
@IsArray()
benefits?: string[];

@ApiPropertyOptional({
description: 'Required experience level',
example: 'Senior',
})
@IsOptional()
@IsString()
experience_level?: string;
}
22 changes: 21 additions & 1 deletion src/modules/jobs/jobs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UseGuards,
ValidationPipe,
ParseUUIDPipe,
Patch,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
Expand All @@ -22,6 +23,7 @@ import {
ApiResponse,
ApiTags,
ApiUnprocessableEntityResponse,
ApiParam,
} from '@nestjs/swagger';
import { skipAuth } from '@shared/helpers/skipAuth';
import { JobApplicationErrorDto } from './dto/job-application-error.dto';
Expand All @@ -31,6 +33,9 @@ import { JobDto } from './dto/job.dto';
import { JobsService } from './jobs.service';
import { SuperAdminGuard } from '@guards/super-admin.guard';
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';

@ApiTags('Jobs')
@Controller('jobs')
Expand All @@ -57,7 +62,7 @@ export class JobsController {
return this.jobService.applyForJob(id, jobApplicationDto);
}

@UseGuards(SuperAdminGuard)
@UseGuards(AuthGuard)
@Post('/')
@ApiBearerAuth()
@ApiOperation({ summary: 'Create a new job' })
Expand Down Expand Up @@ -104,6 +109,21 @@ export class JobsController {
return this.jobService.getJob(id);
}

@Patch('/:id')
@UseGuards(AuthGuard)
@UseGuards(SuperAdminGuard, JobOwnerGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update a job posting' })
@ApiResponse({ status: 200, description: 'Job updated successfully' })
@ApiResponse({ status: 403, description: 'Forbidden' })
@ApiResponse({ status: 404, description: 'Job not found' })
@ApiBody({ type: UpdateJobDto })
@ApiParam({ name: 'id', type: 'string', description: 'Job ID' })
async updateJob(@Param('id', ParseUUIDPipe) id: string, @Body() updateJobDto: UpdateJobDto, @Request() req: any) {
const user = req.user;
return this.jobService.update(id, updateJobDto, user.sub);
}

@UseGuards(SuperAdminGuard)
@Delete('/:id')
@ApiBearerAuth()
Expand Down
5 changes: 4 additions & 1 deletion src/modules/jobs/jobs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +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';

@Module({
imports: [
TypeOrmModule.forFeature([Job, User, JobApplication, Organisation, OrganisationUserRole, Profile, Role]),
UserModule,
],
providers: [JobsService],
providers: [JobsService, JobOwnerGuard, AuthGuard, SuperAdminGuard],
controllers: [JobsController],
})
export class JobsModule {}
43 changes: 43 additions & 0 deletions src/modules/jobs/jobs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { JobSearchDto } from './dto/jobSearch.dto';
import { User } from '@modules/user/entities/user.entity';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import { pick } from '@shared/helpers/pick';
import { UpdateJobDto } from './dto/update-job.dto';

@Injectable()
export class JobsService {
Expand Down Expand Up @@ -151,4 +152,46 @@ export class JobsService {
data: jobs,
};
}

private validateUserId(userId: string) {
if (!userId) {
throw new CustomHttpException('User ID is required', HttpStatus.UNAUTHORIZED);
}
}

private validateUpdateData(updateDto: UpdateJobDto) {
if (Object.keys(updateDto).length === 0) {
throw new CustomHttpException('No updates provided', HttpStatus.BAD_REQUEST);
}
}

async update(id: string, updateJobDto: UpdateJobDto, userId: string) {
this.validateUserId(userId);
this.validateUpdateData(updateJobDto);

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

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

if (job.user.id !== userId) {
throw new CustomHttpException('Unauthorized to update this job', HttpStatus.FORBIDDEN);
}

const updatedJob = await this.jobRepository.save({
...job,
...updateJobDto,
});

return {
status: 'success',
status_code: 200,
message: 'Job updated successfully',
data: updatedJob,
};
}
}
Loading

0 comments on commit fd43262

Please sign in to comment.