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
22 changes: 22 additions & 0 deletions api/db/migrations/1775081823814-event-whitelist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class EventWhitelist1775081823814 implements MigrationInterface {
name = 'EventWhitelist1775081823814'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "event_whitelists" DROP CONSTRAINT "FK_event_whitelists_event"`);
await queryRunner.query(`DROP INDEX "public"."UQ_event_whitelists_event_username_platform"`);
await queryRunner.query(`ALTER TABLE "event_whitelists" ALTER COLUMN "eventId" DROP NOT NULL`);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why did you make eventId nullable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

shouldn't event_whitelists always have an event?

await queryRunner.query(`ALTER TABLE "event_whitelists" ADD CONSTRAINT "UQ_1e66e3e9750244ea3f6563a6d49" UNIQUE ("eventId", "username", "platform")`);
Comment on lines +9 to +10
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

Keep eventId non-nullable.

A whitelist row without an event is invalid, and in PostgreSQL this new unique key will still allow duplicate (NULL, username, platform) rows. Please keep eventId as NOT NULL here and mark the @ManyToOne in api/src/event/entities/event-whitelist.entity.ts as nullable: false so TypeORM does not regenerate this relaxation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/db/migrations/1775081823814-event-whitelist.ts` around lines 9 - 10, The
migration is making eventId nullable which allows duplicate (NULL, username,
platform) rows; revert that by removing or undoing the ALTER TABLE ... DROP NOT
NULL so "eventId" remains NOT NULL and keep the UNIQUE constraint addition, and
update the entity decorator in api/src/event/entities/event-whitelist.entity.ts
by setting the `@ManyToOne` relation on the EventWhitelist entity (the event
property) to nullable: false so TypeORM will not regenerate the column as
nullable in future migrations.

await queryRunner.query(`ALTER TABLE "event_whitelists" ADD CONSTRAINT "FK_17ccbcee3723a35d6d691532c22" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "event_whitelists" DROP CONSTRAINT "FK_17ccbcee3723a35d6d691532c22"`);
await queryRunner.query(`ALTER TABLE "event_whitelists" DROP CONSTRAINT "UQ_1e66e3e9750244ea3f6563a6d49"`);
await queryRunner.query(`ALTER TABLE "event_whitelists" ALTER COLUMN "eventId" SET NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "UQ_event_whitelists_event_username_platform" ON "event_whitelists" ("eventId", "platform", "username") `);
await queryRunner.query(`ALTER TABLE "event_whitelists" ADD CONSTRAINT "FK_event_whitelists_event" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

}
24 changes: 24 additions & 0 deletions api/src/event/dtos/whitelistDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IsArray, IsEnum, IsString, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
import { WhitelistPlatform } from "../entities/event-whitelist.entity";

export class WhitelistEntryDto {
@IsString()
username: string;

@IsEnum(WhitelistPlatform)
platform: WhitelistPlatform;
}

export class AddToWhitelistDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => WhitelistEntryDto)
entries: WhitelistEntryDto[];
}

export class BulkDeleteWhitelistDto {
@IsArray()
@IsString({ each: true })
ids: string[];
}
Comment on lines +13 to +24
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:

#!/bin/bash
set -euo pipefail

# Verify whitelist id type
fd -i 'event-whitelist.entity.ts' | xargs -r rg -n -C2 'PrimaryGeneratedColumn|id'

# Verify whether service/controller already short-circuit empty arrays
fd -i 'event.service.ts' | xargs -r rg -n -C2 'whitelist|entries|ids|bulk'
fd -i 'event.controller.ts' | xargs -r rg -n -C2 'whitelist|bulk-delete'

Repository: 42core-team/website

Length of output: 5360


🏁 Script executed:

cat -n api/src/event/dtos/whitelistDto.ts

Repository: 42core-team/website

Length of output: 799


Add @ArrayNotEmpty() and validate IDs as UUIDs in whitelist DTOs.

Both entries and ids currently accept empty arrays, which cause no-op requests. Additionally, ids are only validated as strings but are actually UUID-backed in the entity. Tighten validation at the DTO boundary.

Suggested DTO update
-import { IsArray, IsEnum, IsString, ValidateNested } from "class-validator";
+import {
+  ArrayNotEmpty,
+  IsArray,
+  IsEnum,
+  IsString,
+  IsUUID,
+  ValidateNested,
+} from "class-validator";

 export class AddToWhitelistDto {
   `@IsArray`()
+  `@ArrayNotEmpty`()
   `@ValidateNested`({ each: true })
   `@Type`(() => WhitelistEntryDto)
   entries: WhitelistEntryDto[];
 }

 export class BulkDeleteWhitelistDto {
   `@IsArray`()
-  `@IsString`({ each: true })
+  `@ArrayNotEmpty`()
+  `@IsUUID`("4", { each: true })
   ids: string[];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/event/dtos/whitelistDto.ts` around lines 13 - 24, Add
`@ArrayNotEmpty`() to AddToWhitelistDto.entries and to BulkDeleteWhitelistDto.ids
to reject empty arrays, and tighten BulkDeleteWhitelistDto.ids validation by
replacing `@IsString`({ each: true }) with `@IsUUID`('4', { each: true }) so each id
is validated as a UUID; update imports to include ArrayNotEmpty and IsUUID (and
keep Type/ValidateNested for AddToWhitelistDto.entries) so validation runs at
the DTO boundary.

35 changes: 35 additions & 0 deletions api/src/event/entities/event-whitelist.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
ManyToOne,
Unique,
} from "typeorm";
import { EventEntity } from "./event.entity";

export enum WhitelistPlatform {
GITHUB = "GITHUB",
FORTYTWO = "FORTYTWO",
}

@Entity("event_whitelists")
@Unique(["event", "username", "platform"])
export class EventWhitelistEntity {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column({ type: "enum", enum: WhitelistPlatform })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's better when we just store a text type in the db instead of a enum

platform: WhitelistPlatform;

@Column()
username: string;

@ManyToOne(() => EventEntity, {
onDelete: "CASCADE",
})
event: EventEntity;

@CreateDateColumn()
createdAt: Date;
}
6 changes: 6 additions & 0 deletions api/src/event/entities/event.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { TeamEntity } from "../../team/entities/team.entity";
import { Exclude } from "class-transformer";
import { EventStarterTemplateEntity } from "./event-starter-template.entity";
import { EventWhitelistEntity } from "./event-whitelist.entity";

@Entity("events")
export class EventEntity {
Expand Down Expand Up @@ -121,4 +122,9 @@ export class EventEntity {
cascade: true,
})
starterTemplates: EventStarterTemplateEntity[];

@OneToMany(() => EventWhitelistEntity, (whitelist) => whitelist.event, {
onDelete: "CASCADE",
})
whitelists: EventWhitelistEntity[];
}
98 changes: 98 additions & 0 deletions api/src/event/event.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SetLockTeamsDateDto } from "./dtos/setLockTeamsDateDto";
import { UpdateEventSettingsDto } from "./dtos/updateEventSettingsDto";
import { CreateEventStarterTemplateDto } from "./dtos/createEventStarterTemplateDto";
import { UpdateEventStarterTemplateDto } from "./dtos/updateEventStarterTemplateDto";
import { AddToWhitelistDto, BulkDeleteWhitelistDto } from "./dtos/whitelistDto";
import { JwtAuthGuard } from "../auth/jwt-auth.guard";
import { UserId } from "../guards/UserGuard";

Expand Down Expand Up @@ -169,6 +170,28 @@ export class EventController {
);
}

const event = await this.eventService.getEventById(eventId);

if (event.isPrivate) {
const user = await this.userService.getUserById(userId);
const socialAccounts = user.socialAccounts || [];
const fortyTwoAccount = socialAccounts.find(
(sa) => sa.platform === "42",
);

const isWhitelisted = await this.eventService.isUserWhitelistedForEvent(
eventId,
user.username,
fortyTwoAccount?.username || null,
);

if (!isWhitelisted) {
throw new UnauthorizedException(
"You are not whitelisted for this private event.",
);
}
}

this.logger.log({ action: "attempt_join_event", userId, eventId });

return this.userService.joinEvent(userId, eventId);
Expand Down Expand Up @@ -377,4 +400,79 @@ export class EventController {
});
return this.eventService.deleteStarterTemplate(eventId, templateId);
}

@UseGuards(JwtAuthGuard)
@Get(":id/whitelist")
async getWhitelist(
@Param("id", new ParseUUIDPipe()) eventId: string,
@UserId() userId: string,
) {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}
return this.eventService.getWhitelist(eventId);
}

@UseGuards(JwtAuthGuard)
@Post(":id/whitelist")
async addToWhitelist(
@Param("id", new ParseUUIDPipe()) eventId: string,
@UserId() userId: string,
@Body() body: AddToWhitelistDto,
) {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_add_to_whitelist",
userId,
eventId,
entriesCount: body.entries.length,
});

return this.eventService.addToWhitelist(eventId, body.entries);
}

@UseGuards(JwtAuthGuard)
@Delete(":id/whitelist/:whitelistId")
async removeFromWhitelist(
@Param("id", new ParseUUIDPipe()) eventId: string,
@Param("whitelistId", new ParseUUIDPipe()) whitelistId: string,
@UserId() userId: string,
) {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_remove_from_whitelist",
userId,
eventId,
whitelistId,
});

return this.eventService.removeFromWhitelist(eventId, whitelistId);
}

@UseGuards(JwtAuthGuard)
@Post(":id/whitelist/bulk-delete")
async bulkRemoveFromWhitelist(
@Param("id", new ParseUUIDPipe()) eventId: string,
@UserId() userId: string,
@Body() body: BulkDeleteWhitelistDto,
) {
if (!(await this.eventService.isEventAdmin(eventId, userId))) {
throw new UnauthorizedException("You are not an admin of this event");
}

this.logger.log({
action: "attempt_bulk_remove_from_whitelist",
userId,
eventId,
idsCount: body.ids.length,
});

return this.eventService.bulkRemoveFromWhitelist(eventId, body.ids);
}
}
2 changes: 2 additions & 0 deletions api/src/event/event.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { UserEventPermissionEntity } from "../user/entities/user.entity";
import { CheckController } from "./check.controller";

import { EventStarterTemplateEntity } from "./entities/event-starter-template.entity";
import { EventWhitelistEntity } from "./entities/event-whitelist.entity";

@Module({
imports: [
TypeOrmModule.forFeature([
EventEntity,
UserEventPermissionEntity,
EventStarterTemplateEntity,
EventWhitelistEntity,
]),
UserModule,
forwardRef(() => TeamModule),
Expand Down
Loading
Loading