Skip to content
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
29 changes: 20 additions & 9 deletions src/database/entities/jobs.entity.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Dá uma olhada na parte de enum no @column. os campos estão como string, o ideal é tipar direto com o enum e declarar no decorator.

  • Veja a questão de typeContract e contractType. Os dois estão representando a mesma coisa, então seria bom padronizar em um só campo

  • Revisar o campo openEndedContract, que está com default e nullable juntos.

  • Campos de texto (description, prerequisites, contractText) podem ter o tipo/tamanho mais definido

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { JobsAffirmativeTypeEnum } from '../../modules/jobs/enums/job-affirmativ
import { JobsTypeContractEnum } from '../../modules/jobs/enums/job-contract-type.enum';
import { JobsModalityEnum } from '../../modules/jobs/enums/job-modality.enum';
import { JobsTypeEnum } from '../../modules/jobs/enums/job-type.enum';
import { JobStatus } from '../../modules/jobs/enums/job-status.enum';
import { ApplicationEntity } from './applications.entity';
import { CommentsEntity } from './comments.entity';
import { CompaniesEntity } from './companies.entity';
Expand All @@ -27,10 +28,10 @@ export class JobsEntity {
@Column()
title: string;

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

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

@Column({ nullable: true })
Expand All @@ -44,7 +45,7 @@ export class JobsEntity {
JobsTypeEnum.TRAINEE,
JobsTypeEnum.INTERNSHIP,
],
default: JobsTypeEnum.JUNIOR,
nullable: true,
})
type: string;

Expand All @@ -55,7 +56,6 @@ export class JobsEntity {
JobsTypeContractEnum.PJ,
JobsTypeContractEnum.OTHER,
],
default: JobsTypeContractEnum.CLT,
nullable: true,
})
typeContract: string;
Expand Down Expand Up @@ -83,18 +83,16 @@ export class JobsEntity {
@Column({ nullable: true })
city: string;

@Column({
default: true,
})
@Column({ default: true, nullable: true })
openEndedContract: boolean;

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

@Column({ nullable: true })
contractText?: string;
contractText: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistência entre tipo TypeScript e schema do banco de dados.

O campo contractText foi alterado de opcional (contractText?: string) para obrigatório (contractText: string) no TypeScript, mas a coluna permanece nullable: true no banco. Isso cria uma discrepância onde o TypeScript exige o campo, mas o banco permite valores nulos.

Escolha uma das soluções:

Opção 1: Se o campo deve ser opcional (recomendado para rascunhos):

-contractText: string;
+contractText?: string;

Opção 2: Se o campo deve ser obrigatório:

-@Column({ nullable: true })
+@Column({ nullable: false })
 contractText: string;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
contractText: string;
contractText?: string;
🤖 Prompt for AI Agents
In src/database/entities/jobs.entity.ts around line 90, the TypeScript property
was changed from optional to required (contractText: string) while the DB column
is still nullable, causing a type/schema mismatch; either make the TS property
optional again (contractText?: string) to match nullable: true for drafts, or
make the DB column non-nullable (remove nullable: true / set nullable: false and
add a migration to set existing nulls to '' or a default) so the schema enforces
the required field—pick the correct option and update both the entity and
database migration accordingly.


@Column({ default: true })
@Column({ default: true, nullable: true })
affirmative: boolean;

@Column({
Expand All @@ -121,6 +119,19 @@ export class JobsEntity {
@Column({ nullable: false, default: StatusEnum.ACTIVE })
status: StatusEnum;

@Column({
type: 'enum',
enum: JobStatus,
default: JobStatus.DRAFT,
})
jobStatus: JobStatus;

@Column({ nullable: true })
publishedAt: Date;

@Column({ nullable: true })
canceledAt: Date;
Comment on lines +122 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" | grep -i "publish" | grep -i "service" | head -20

Repository: SouJunior/vagas-api

Length of output: 170


🏁 Script executed:

cat -n ./src/modules/jobs/services/publish-job.service.ts

Repository: SouJunior/vagas-api

Length of output: 1249


🏁 Script executed:

# Find and read the CompleteJobDto
find . -type f -name "*complete-job.dto*" | head -5

Repository: SouJunior/vagas-api

Length of output: 106


🏁 Script executed:

cat -n ./src/modules/jobs/dtos/complete-job.dto.ts

Repository: SouJunior/vagas-api

Length of output: 2471


🏁 Script executed:

# Search for controllers that use PublishJobService
rg -B 5 -A 10 "PublishJobService" --type=ts -g '!*.spec.ts' | grep -A 10 "@Controller\|@Post\|@Put\|execute"

Repository: SouJunior/vagas-api

Length of output: 1445


🏁 Script executed:

# Read the jobs controller to find the publish endpoint
cat -n ./src/modules/jobs/jobs.controller.ts | grep -A 20 -B 2 "publish"

Repository: SouJunior/vagas-api

Length of output: 3064


🏁 Script executed:

# Check for ValidationPipe setup
find . -name "main.ts" -o -name "app.module.ts" | head -5

Repository: SouJunior/vagas-api

Length of output: 96


🏁 Script executed:

cat -n ./src/main.ts

Repository: SouJunior/vagas-api

Length of output: 1588


🏁 Script executed:

# Find and read the job repository
find . -type f -name "*job.repository*" | grep -v spec

Repository: SouJunior/vagas-api

Length of output: 110


🏁 Script executed:

cat -n ./src/modules/jobs/repository/job.repository.ts

Repository: SouJunior/vagas-api

Length of output: 7671


🏁 Script executed:

cat -n ./src/database/entities/jobs.entity.ts

Repository: SouJunior/vagas-api

Length of output: 4346


🏁 Script executed:

# Check CreateJobDraftDto
cat -n ./src/modules/jobs/dtos/create-job-draft.dto.ts

Repository: SouJunior/vagas-api

Length of output: 2475


Validação de campos obrigatórios na publicação está parcialmente implementada, mas sem lógica explícita no serviço.

A validação ocorre no nível do DTO (CompleteJobDto) via decoradores @IsNotEmpty e o ValidationPipe global, garantindo que description, requirements, contractType, journey e selectionProcess sejam fornecidos. No entanto, PublishJobService.execute() não contém lógica explícita de validação — depende integralmente do pipeline de validação do NestJS. Para maior robustez e clareza, considere adicionar validações explícitas no PublishJobService, especialmente para cenários onde o repositório possa ser chamado diretamente, ou garantir que toda publicação passe obrigatoriamente pela validação do DTO.

🤖 Prompt for AI Agents
In src/database/entities/jobs.entity.ts around lines 118-129, the entity marks
publishedAt and canceledAt nullable and DTO-level validation exists but
PublishJobService.execute() lacks explicit checks; update
PublishJobService.execute to explicitly validate required publication fields
(description, requirements, contractType, journey, selectionProcess) before
persisting: fetch the job, verify each required field is present and non-empty,
throw a proper HTTP/Domain error (e.g., BadRequest) when any are missing, set
jobStatus to PUBLISHED and publishedAt to now, then save via the repository;
this ensures robustness if the service is called bypassing the global
ValidationPipe.


@OneToMany(() => CommentsEntity, (comment) => comment.job, {
cascade: true,
})
Expand Down
80 changes: 80 additions & 0 deletions src/database/migrations/1765483583757-AddJobStatusFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddJobStatusFields1765483583757 implements MigrationInterface {
name = 'AddJobStatusFields1765483583757'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tb_alerts" DROP CONSTRAINT "FK_a60dd357707a39803ce4cfbd90b"`);
await queryRunner.query(`CREATE TABLE "tb_saved_jobs" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "savedAt" TIMESTAMP NOT NULL DEFAULT now(), "expiresAt" TIMESTAMP NOT NULL, "userId" uuid NOT NULL, "jobId" uuid, CONSTRAINT "PK_b69821433d55c7266698911ead6" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_3845b06bfda63ccc1da359b378" ON "tb_saved_jobs" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_9bbd9a1f3bb4942f0471816b11" ON "tb_saved_jobs" ("jobId") `);
await queryRunner.query(`CREATE TYPE "public"."tb_jobs_jobstatus_enum" AS ENUM('DRAFT', 'PUBLISHED', 'ARCHIVED', 'CANCELED')`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ADD "jobStatus" "public"."tb_jobs_jobstatus_enum" NOT NULL DEFAULT 'DRAFT'`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ADD "publishedAt" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ADD "canceledAt" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "description" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "prerequisites" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "type" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "type" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "typeContract" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "openEndedContract" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "affirmative" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_candidacies" DROP COLUMN "date_candidacy"`);
await queryRunner.query(`ALTER TABLE "tb_candidacies" ADD "date_candidacy" date NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "tb_certifications" ADD CONSTRAINT "FK_75f15015611ef7f444ae3fade23" FOREIGN KEY ("personal_data_id") REFERENCES "tb_personal_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_courses" ADD CONSTRAINT "FK_beb3c749f9d5000af64c4442989" FOREIGN KEY ("personal_data_id") REFERENCES "tb_personal_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_languages" ADD CONSTRAINT "FK_ecd946bdf16c31509a966a5002f" FOREIGN KEY ("personal_data_id") REFERENCES "tb_personal_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_work_experiences" ADD CONSTRAINT "FK_e668e3b05cc240131e27a41e5b5" FOREIGN KEY ("personal_data_id") REFERENCES "tb_personal_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_curriculum" ADD CONSTRAINT "FK_2c53580e4d1f616f6ffee74ba51" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_comments" ADD CONSTRAINT "FK_b7d6e83dbfa5e98148529803894" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_comments" ADD CONSTRAINT "FK_d4e57c6e48ba93300dc02734659" FOREIGN KEY ("job_id") REFERENCES "tb_jobs"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ADD CONSTRAINT "FK_a64a855331c54d698baddb03b6f" FOREIGN KEY ("company_id") REFERENCES "tb_companies"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_applications" ADD CONSTRAINT "FK_1408b5e5220c7d0fe25573cb3b9" FOREIGN KEY ("job_id") REFERENCES "tb_jobs"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_applications" ADD CONSTRAINT "FK_08c82d5bde7b75b17f54460adda" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_applications" ADD CONSTRAINT "FK_f3502a850c1b2b75a5dbe2c04ee" FOREIGN KEY ("curriculum_id") REFERENCES "tb_curriculum"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_users" ADD CONSTRAINT "FK_0afe3b230cbd95a08c72f9df3f0" FOREIGN KEY ("personalDataId") REFERENCES "tb_personal_data"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_saved_jobs" ADD CONSTRAINT "FK_3845b06bfda63ccc1da359b378a" FOREIGN KEY ("userId") REFERENCES "tb_users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_saved_jobs" ADD CONSTRAINT "FK_9bbd9a1f3bb4942f0471816b111" FOREIGN KEY ("jobId") REFERENCES "tb_jobs"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Revisar comportamento ON DELETE para constraint de jobId em tb_saved_jobs (linha 37).

A FK para jobId usa ON DELETE NO ACTION, o que impedirá a exclusão de um job enquanto existirem referências em tb_saved_jobs. Isso é inconsistente com a FK para userId na mesma tabela (linha 36), que usa ON DELETE CASCADE. Considere usar ON DELETE CASCADE para permitir limpeza automática de saved_jobs ao deletar um job, ou ON DELETE SET NULL se preferir manter o histórico com jobId nulo.

🤖 Prompt for AI Agents
In src/database/migrations/1765483583757-AddJobStatusFields.ts around lines
36–37, the foreign key constraint for tb_saved_jobs.jobId is created with ON
DELETE NO ACTION which prevents deleting a job while saved_job rows exist;
change the constraint to ON DELETE CASCADE to match the userId behavior (or, if
the design requires preserving saved job records, change to ON DELETE SET NULL
and make the jobId column nullable) and update the migration SQL accordingly so
saved_job rows are cleaned up or preserved as intended.

await queryRunner.query(`ALTER TABLE "tb_reports" ADD CONSTRAINT "FK_c95aa2c975cb6369772d5f15c7d" FOREIGN KEY ("job_id") REFERENCES "tb_jobs"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_reports" ADD CONSTRAINT "FK_86d265112a095a1daae2e34d669" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "tb_alerts" ADD CONSTRAINT "FK_a60dd357707a39803ce4cfbd90b" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tb_alerts" DROP CONSTRAINT "FK_a60dd357707a39803ce4cfbd90b"`);
await queryRunner.query(`ALTER TABLE "tb_reports" DROP CONSTRAINT "FK_86d265112a095a1daae2e34d669"`);
await queryRunner.query(`ALTER TABLE "tb_reports" DROP CONSTRAINT "FK_c95aa2c975cb6369772d5f15c7d"`);
await queryRunner.query(`ALTER TABLE "tb_saved_jobs" DROP CONSTRAINT "FK_9bbd9a1f3bb4942f0471816b111"`);
await queryRunner.query(`ALTER TABLE "tb_saved_jobs" DROP CONSTRAINT "FK_3845b06bfda63ccc1da359b378a"`);
await queryRunner.query(`ALTER TABLE "tb_users" DROP CONSTRAINT "FK_0afe3b230cbd95a08c72f9df3f0"`);
await queryRunner.query(`ALTER TABLE "tb_applications" DROP CONSTRAINT "FK_f3502a850c1b2b75a5dbe2c04ee"`);
await queryRunner.query(`ALTER TABLE "tb_applications" DROP CONSTRAINT "FK_08c82d5bde7b75b17f54460adda"`);
await queryRunner.query(`ALTER TABLE "tb_applications" DROP CONSTRAINT "FK_1408b5e5220c7d0fe25573cb3b9"`);
await queryRunner.query(`ALTER TABLE "tb_jobs" DROP CONSTRAINT "FK_a64a855331c54d698baddb03b6f"`);
await queryRunner.query(`ALTER TABLE "tb_comments" DROP CONSTRAINT "FK_d4e57c6e48ba93300dc02734659"`);
await queryRunner.query(`ALTER TABLE "tb_comments" DROP CONSTRAINT "FK_b7d6e83dbfa5e98148529803894"`);
await queryRunner.query(`ALTER TABLE "tb_curriculum" DROP CONSTRAINT "FK_2c53580e4d1f616f6ffee74ba51"`);
await queryRunner.query(`ALTER TABLE "tb_work_experiences" DROP CONSTRAINT "FK_e668e3b05cc240131e27a41e5b5"`);
await queryRunner.query(`ALTER TABLE "tb_languages" DROP CONSTRAINT "FK_ecd946bdf16c31509a966a5002f"`);
await queryRunner.query(`ALTER TABLE "tb_courses" DROP CONSTRAINT "FK_beb3c749f9d5000af64c4442989"`);
await queryRunner.query(`ALTER TABLE "tb_certifications" DROP CONSTRAINT "FK_75f15015611ef7f444ae3fade23"`);
await queryRunner.query(`ALTER TABLE "tb_candidacies" DROP COLUMN "date_candidacy"`);
await queryRunner.query(`ALTER TABLE "tb_candidacies" ADD "date_candidacy" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "affirmative" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "openEndedContract" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "typeContract" SET DEFAULT 'CLT'`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "type" SET DEFAULT 'JUNIOR'`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "type" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "prerequisites" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" ALTER COLUMN "description" SET NOT NULL`);
await queryRunner.query(`ALTER TABLE "tb_jobs" DROP COLUMN "canceledAt"`);
await queryRunner.query(`ALTER TABLE "tb_jobs" DROP COLUMN "publishedAt"`);
await queryRunner.query(`ALTER TABLE "tb_jobs" DROP COLUMN "jobStatus"`);
await queryRunner.query(`DROP TYPE "public"."tb_jobs_jobstatus_enum"`);
await queryRunner.query(`DROP INDEX "public"."IDX_9bbd9a1f3bb4942f0471816b11"`);
await queryRunner.query(`DROP INDEX "public"."IDX_3845b06bfda63ccc1da359b378"`);
await queryRunner.query(`DROP TABLE "tb_saved_jobs"`);
await queryRunner.query(`ALTER TABLE "tb_alerts" ADD CONSTRAINT "FK_a60dd357707a39803ce4cfbd90b" FOREIGN KEY ("user_id") REFERENCES "tb_users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

}
85 changes: 85 additions & 0 deletions src/modules/jobs/dtos/complete-job.dto.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

O Enum no contractType: dá uma olhada no alinhamento do DTO (usa enum) e a entity, pra garantir que os valores salvos sejam compatíveis.

typeContract e contractType: bom seria padronizar um

Campos array no DTO (requirements, benefits, selectionProcess): o banco ta preparado pra receber o array?

Tamanho dos textos (description, additionalInfo): o limite do DTO bate com o banco?

Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
import { JobsTypeContractEnum } from '../enums/job-contract-type.enum';

export class CompleteJobDto {
@IsNotEmpty()
@IsString()
@MaxLength(5000)
@ApiProperty({
required: true,
description: 'Descrição da vaga',
example: 'Buscamos um desenvolvedor backend...',
})
description: string;

@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({
required: true,
description: 'Requisitos da vaga',
example: ['Node.js', 'TypeScript', 'NestJS'],
})
requirements: string[];

@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiProperty({
required: false,
description: 'Benefícios',
example: ['Vale alimentação', 'Plano de saúde'],
})
benefits?: string[];

@IsNotEmpty()
@IsEnum(JobsTypeContractEnum)
@ApiProperty({
required: true,
description: 'Tipo de contrato',
example: JobsTypeContractEnum.CLT,
enum: [
JobsTypeContractEnum.CLT,
JobsTypeContractEnum.PJ,
JobsTypeContractEnum.OTHER,
],
})
contractType: JobsTypeContractEnum;

@IsNotEmpty()
@IsString()
@ApiProperty({
required: true,
description: 'Jornada de trabalho',
example: '40 horas semanais',
})
journey: string;

@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({
required: true,
description: 'Processo seletivo',
example: ['Entrevista com RH', 'Teste técnico', 'Entrevista técnica'],
})
selectionProcess: string[];

@IsOptional()
@IsString()
@MaxLength(2000)
@ApiProperty({
required: false,
description: 'Informações adicionais',
example: 'Início imediato',
})
additionalInfo?: string;
}
92 changes: 92 additions & 0 deletions src/modules/jobs/dtos/create-job-draft.dto.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

city + modality: a validação está ok, bom é confirmar se no fluxo da aplicação o campo city é realmente tratado como obrigatório quando não for remoto.

salaryMin e salaryMax: seria bom garantir no serviço que o salário mínimo não seja maior que o máximo, pra evitar inconsistência de dados.

@IsNumber: só conferir se o ValidationPipe está com transform: true, senão esses campos podem chegar como string.

company_id: o campo não tem validação nem ApiProperty. avaliar se isso é intencional ou se precisa de um mínimo de padronização.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Max,
MaxLength,
Min,
ValidateIf,
} from 'class-validator';
import { JobsModalityEnum } from '../enums/job-modality.enum';

export class CreateJobDraftDto {
@IsNotEmpty()
@IsString()
@MaxLength(100)
@ApiProperty({
required: true,
description: 'Título da vaga',
example: 'Desenvolvedor Backend Junior',
})
title: string;

@IsNotEmpty()
@IsString()
@MaxLength(100)
@ApiProperty({
required: true,
description: 'Área de interesse',
example: 'Tecnologia',
})
interestArea: string;

@IsNotEmpty()
@IsEnum(JobsModalityEnum)
@ApiProperty({
required: true,
description: 'Modalidade do trabalho',
example: JobsModalityEnum.REMOTE,
enum: [
JobsModalityEnum.HYBRID,
JobsModalityEnum.ON_SITE,
JobsModalityEnum.REMOTE,
],
})
modality: JobsModalityEnum;

@ValidateIf((o) => o.modality !== JobsModalityEnum.REMOTE)
@IsString()
@ApiProperty({
required: false,
description: 'Cidade',
example: 'São Paulo',
})
city?: string;

@IsNotEmpty()
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0)
@Max(999999)
@ApiProperty({
required: true,
description: 'Salário mínimo',
example: 3000,
})
salaryMin: number;

@IsNotEmpty()
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0)
@Max(999999)
@ApiProperty({
required: true,
description: 'Salário máximo',
example: 5000,
})
salaryMax: number;

@IsOptional()
@IsString()
@ApiProperty({
required: false,
description: 'Opção de trabalho remoto',
example: 'Híbrido 2x por semana',
})
remoteWorkOption?: string;

@IsOptional()
company_id?: string;
}
6 changes: 6 additions & 0 deletions src/modules/jobs/enums/job-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum JobStatus {
DRAFT = 'DRAFT',
PUBLISHED = 'PUBLISHED',
ARCHIVED = 'ARCHIVED',
CANCELED = 'CANCELED',
}
37 changes: 37 additions & 0 deletions src/modules/jobs/guards/job-owner.guard.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

jobId vindo dos params: vale confirmar se o id sempre existe na rota. Se não, vai resultar no repo undefined.

Comparação job.company_id !== user.id: conferir se os dois valores têm o mesmo tipo (ex: ambos string ou number) assim vamos evitar falha na comparação

Mensagens de erro: estão claras , só avaliar se esse nível de detalhe é o esperado para um ForbiddenException (nada de urgente)

Responsabilidade do Guard: o guard está buscando no banco, o que funciona, mas vale só validar se isso está alinhado com o restante do projeto

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
} from '@nestjs/common';
import { JobRepository } from '../repository/job.repository';

@Injectable()
export class JobOwnerGuard implements CanActivate {
constructor(private jobRepository: JobRepository) {}

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

if (!user || !user.id) {
throw new ForbiddenException('User not authenticated');
}

const job = await this.jobRepository.findOneById(jobId);

if (!job) {
throw new NotFoundException('Job not found');
}

if (job.company_id !== user.id) {
throw new ForbiddenException(
'You do not have permission to modify this job',
);
}
Comment on lines +29 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, examine the job-owner.guard.ts file to understand the context
cat -n src/modules/jobs/guards/job-owner.guard.ts

Repository: SouJunior/vagas-api

Length of output: 1230


🏁 Script executed:

# Search for request.user patterns to understand user structure
rg -n "request\.user" --type ts -A 2 -B 2

Repository: SouJunior/vagas-api

Length of output: 1854


🏁 Script executed:

# Search for authentication strategy and user setup
rg -n "strategy|AuthGuard|auth" --type ts src/modules -A 2 | head -100

Repository: SouJunior/vagas-api

Length of output: 6362


🏁 Script executed:

# Check for company_id property usage on user object
rg -n "user\.company_id|user\.id" --type ts -B 1 -A 1

Repository: SouJunior/vagas-api

Length of output: 2577


🏁 Script executed:

# Search for user entity/interface definitions
rg -n "interface User|class User|type User" --type ts -A 5

Repository: SouJunior/vagas-api

Length of output: 6548


🏁 Script executed:

# Check LoggedCompany decorator to understand its structure
cat -n src/modules/auth/decorator/logged-company.decorator.ts

Repository: SouJunior/vagas-api

Length of output: 656


🏁 Script executed:

# Check LoggedUser decorator for comparison
cat -n src/modules/auth/decorator/logged-user.decorator.ts

Repository: SouJunior/vagas-api

Length of output: 613


🏁 Script executed:

# Check JWT strategy to see what gets attached to request.user
cat -n src/modules/auth/jtw/jwt.strategy.ts

Repository: SouJunior/vagas-api

Length of output: 1625


🏁 Script executed:

# Check Job entity structure to understand company_id
fd job.entity.ts -type f -exec cat -n {} \;

Repository: SouJunior/vagas-api

Length of output: 233


🏁 Script executed:

# Check how jobs controller uses these decorators
rg -n "@LoggedCompany|@LoggedUser" src/modules/jobs -B 2 -A 5

Repository: SouJunior/vagas-api

Length of output: 1645


🏁 Script executed:

# Find and check CompaniesEntity structure
find . -name "*compan*entity*" -type f | head -20

Repository: SouJunior/vagas-api

Length of output: 106


🏁 Script executed:

# Search for CompaniesEntity definition
rg -n "class CompaniesEntity|export class CompaniesEntity" --type ts -A 10

Repository: SouJunior/vagas-api

Length of output: 715


🏁 Script executed:

# Check if there are any middleware or guards that ensure only companies access job endpoints
rg -n "publishJob|deleteJobDraft|cancelJob" src/modules/jobs/jobs.controller.ts -B 5 -A 2

Repository: SouJunior/vagas-api

Length of output: 1312


🏁 Script executed:

# Final verification: Check if job endpoints should only be accessible by companies
# by looking at how jobs are created to understand the expected user type
cat -n src/modules/jobs/jobs.controller.ts | grep -A 10 "createJobDraft\|createNewJob"

Repository: SouJunior/vagas-api

Length of output: 1279


🏁 Script executed:

# Check if there's any role validation or company check elsewhere
rg -n "isCompany|role|company" src/modules/auth/decorator --type ts -A 3

Repository: SouJunior/vagas-api

Length of output: 45


A comparação job.company_id !== user.id possui um flaw semântico crítico.

O guarda assume que request.user.id representa uma empresa, mas a estratégia JWT (jwt.strategy.ts) pode retornar tanto UserEntity quanto CompaniesEntity. Como os endpoints de modificação de job (publishJob, cancelJob, deleteJobDraft) usam apenas @UseGuards(AuthGuard()) sem a validação explícita @LoggedCompany() presente nos endpoints de criação, um usuário comum autenticado poderia chegar até o guarda, causando falha na comparação ou comportamento de segurança incorreto.

Solução necessária: Adicione @LoggedCompany() aos endpoints de modificação de job (linhas 83, 94, 102) para garantir que apenas empresas acessem estes endpoints, alinhando com o padrão usado em createJobDraft e createNewJob.

🤖 Prompt for AI Agents
In src/modules/jobs/guards/job-owner.guard.ts around lines 29 to 33 the guard
compares job.company_id !== user.id but this is unsafe because request.user may
be a UserEntity or CompaniesEntity; update the endpoints that modify jobs to
ensure request.user is always a CompaniesEntity by adding the @LoggedCompany()
decorator to the publishJob, cancelJob, and deleteJobDraft controller methods
(around lines 83, 94, 102) so only companies can reach this guard and the id
comparison is semantically correct.


return true;
}
}
Loading
Loading