diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f26598b..73e90983 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24' cache: 'yarn' diff --git a/package.json b/package.json index c6ea0183..2b4600ef 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "fast-xml-parser": "^5.2.3", "firebase": "^12.0.0", "firebase-admin": "^13.0.2", - "googleapis": "^161.0.0", + "googleapis": "^164.0.0", "handlebars": "^4.7.8", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", diff --git a/src/modules/photo/photo.controller.ts b/src/modules/photo/photo.controller.ts index fcdeb3da..fbbc2679 100644 --- a/src/modules/photo/photo.controller.ts +++ b/src/modules/photo/photo.controller.ts @@ -1,14 +1,16 @@ import { BadRequestException, + Body, Controller, + Delete, Get, InternalServerErrorException, - Post, - Patch, Param, - UseInterceptors, + Patch, + Post, + Query, Req, - Body, + UseInterceptors, UnauthorizedException, } from "@nestjs/common"; import { Request } from "express"; @@ -211,4 +213,37 @@ export class PhotoController { throw new InternalServerErrorException("Failed to reject photo"); } } + + @Delete(":photoId") + @Roles(Role.TEAM) + @ApiDoc({ + summary: "Delete a photo", + params: [{ name: "photoId" }], + query: [ + { + name: "originalName", + description: "Original filename including the extension", + }, + ], + response: { noContent: true }, + }) + async deletePhoto( + @Param("photoId") photoId: string, + @Query("originalName") originalName: string, + ): Promise { + if (!photoId) { + throw new BadRequestException("photoId is required"); + } + + if (!originalName) { + throw new BadRequestException("originalName is required"); + } + + try { + await this.photoService.deletePhoto(photoId, originalName); + } catch (error) { + console.error("Error deleting photo:", error); + throw new InternalServerErrorException("Failed to delete photo"); + } + } } diff --git a/src/modules/photo/photo.service.ts b/src/modules/photo/photo.service.ts index b4b3cf9f..de2d213f 100644 --- a/src/modules/photo/photo.service.ts +++ b/src/modules/photo/photo.service.ts @@ -129,4 +129,10 @@ export class PhotoService { }, }); } + + deletePhoto(photoId: string, originalName: string) { + return this.photoBucket + .file(this.getPhotoFileName(photoId, originalName)) + .delete({ ignoreNotFound: true }); + } } diff --git a/src/modules/team/team.controller.ts b/src/modules/team/team.controller.ts index f84d2c86..6fb8652a 100644 --- a/src/modules/team/team.controller.ts +++ b/src/modules/team/team.controller.ts @@ -9,6 +9,7 @@ import { Param, Patch, Post, + Delete, Query, UseFilters, ValidationPipe, @@ -16,6 +17,7 @@ import { import { InjectRepository, Repository } from "common/objection"; import { Team, TeamEntity } from "entities/team.entity"; import { User } from "entities/user.entity"; +import { Reservation } from "entities/reservation.entity"; import { Hackathon } from "entities/hackathon.entity"; import { ApiProperty, ApiTags, OmitType, PartialType } from "@nestjs/swagger"; import { Role, Roles } from "common/gcp"; @@ -92,6 +94,8 @@ export class TeamController { private readonly teamRepo: Repository, @InjectRepository(User) private readonly userRepo: Repository, + @InjectRepository(Reservation) + private readonly reservationRepo: Repository, ) {} @Get("/") @@ -356,6 +360,39 @@ export class TeamController { } const team = await this.teamRepo.patchOne(id, data).exec(); + + // Check if all members have been removed - if so, soft delete the team + const updatedMembers = [ + team.member1, + team.member2, + team.member3, + team.member4, + team.member5, + ].filter(Boolean); + + if (updatedMembers.length === 0) { + // No members left - soft delete team and all associated reservations + console.log( + `All members removed from team ${id}, auto soft-deleting team and reservations`, + ); + + const deletedReservationsCount = await this.reservationRepo + .findAll() + .raw() + .where("teamId", id) + .delete(); + + console.log( + `Deleted ${deletedReservationsCount} reservations for team ${id}`, + ); + + const deletedTeam = await this.teamRepo + .patchOne(id, { isActive: false }) + .exec(); + + return deletedTeam; + } + return team; } @@ -462,4 +499,43 @@ export class TeamController { const updatedTeam = await this.teamRepo.patchOne(id, updateData).exec(); return updatedTeam; } + + @Delete(":id") + @Roles(Role.NONE) + @ApiDoc({ + summary: "Soft delete a team and remove all associated reservations", + params: [ + { + name: "id", + description: "ID must be set to a team's ID", + }, + ], + response: { + ok: { type: TeamEntity }, + }, + auth: Role.TEAM, + }) + async deleteOne(@Param("id") id: string) { + const existingTeam = await this.teamRepo.findOne(id).exec(); + if (!existingTeam) { + throw new NotFoundException("Team not found"); + } + if (!existingTeam.isActive) { + throw new BadRequestException("Team is already inactive"); + } + + // Delete all reservations associated with team before soft-deleting + const deletedReservationsCount = await this.reservationRepo + .findAll() + .raw() + .where("teamId", id) + .delete(); + + console.log( + `Deleted ${deletedReservationsCount} reservations for team ${id}`, + ); + + const team = await this.teamRepo.patchOne(id, { isActive: false }).exec(); + return team; + } } diff --git a/src/modules/team/team.module.ts b/src/modules/team/team.module.ts index 60890356..f8a8cf2a 100644 --- a/src/modules/team/team.module.ts +++ b/src/modules/team/team.module.ts @@ -4,9 +4,10 @@ import { Team } from "entities/team.entity"; import { User } from "entities/user.entity"; import { TeamController } from "./team.controller"; import { Hackathon } from "entities/hackathon.entity"; +import { Reservation } from "entities/reservation.entity"; @Module({ - imports: [ObjectionModule.forFeature([Team, User, Hackathon])], + imports: [ObjectionModule.forFeature([Team, User, Hackathon, Reservation])], controllers: [TeamController], }) export class TeamModule {} diff --git a/yarn.lock b/yarn.lock index 9b9b8125..b05c4b07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4498,10 +4498,10 @@ googleapis-common@^8.0.0: qs "^6.7.0" url-template "^2.0.8" -googleapis@^161.0.0: - version "161.0.0" - resolved "https://registry.npmjs.org/googleapis/-/googleapis-161.0.0.tgz" - integrity sha512-JZy2cWMxgUF8E09KHzplI+z+FVG8NWDB/bsf4xevt9Um4bInb0X1qaG9qpDn49DHT5HsU0mOp3EOBGb8+AdE3Q== +googleapis@^164.0.0: + version "164.0.0" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-164.0.0.tgz#22945308a3b3cba938f762fd710fb761db2f6a82" + integrity sha512-aR2larBEvu6+HVC4Puu87T41/OA3WjVw2tQRYAMKit2kC95daFacIoDF70DeC4p9M/H5eZSalBJV1FOypR808A== dependencies: google-auth-library "^10.2.0" googleapis-common "^8.0.0"