From ac6f3a70143c3abc8c24b1e250850f91d3f51a74 Mon Sep 17 00:00:00 2001 From: aarbit Date: Thu, 14 May 2026 23:55:05 -0500 Subject: [PATCH] Implements remove photo endpoint --- README.md | 7 ++----- .../controller/TeamController.kt | 11 +++++++++++ .../scavengerhuntapi/error/ExceptionHandler.kt | 7 +++++++ .../error/exception/ConflictException.kt | 3 +++ .../scavengerhuntapi/service/PhotoService.kt | 17 +++++++++++++++++ 5 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ConflictException.kt diff --git a/README.md b/README.md index bbd2fe1..36c2f02 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ REST API to support a community scavenger hunt app. ## TODO: ### User Endpoints -* upload photo for hunt item POST `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo` - body: image binary * delete photo for hunt item DELETE `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo/{photoId}` -* list hunt teams with scores for hunt `GET /lead/hunt/{huntId}/team` -* list hunters with scores for hunt GET `/lead/hunt/{huntId}/hunter` -### Admin Endpoints -* approve photo for hunt item POST `/admin/hunt/{huntId}/team/{teamId}/item/{itemId}/photo/{photoId}` - body: approval status \ No newline at end of file +* list hunt teams with scores for hunt GET `/lead/hunt/{huntId}/team` +* list hunters with scores for hunt GET `/lead/hunt/{huntId}/hunter` \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt index 75eaef7..7f46d6d 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt @@ -16,6 +16,7 @@ import net.halfbinary.scavengerhuntapi.service.TeamService import org.springframework.http.ResponseEntity import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -64,6 +65,16 @@ class TeamController(private val teamService: TeamService, private val photoServ TODO("Get list of photo information for the specified Team, Hunt, and Item") } + @PatchMapping("/{teamId}/item/{itemId}/photo/{photoId}") + @Operation(summary = "Mark the specified Photo as removed") + fun removePhoto(@PathVariable huntId: HuntId, + @PathVariable teamId: TeamId, + @PathVariable itemId: ItemId, + @PathVariable photoId: PhotoId, + authentication: Authentication) { + photoService.removePhoto(huntId, teamId, itemId, photoId, authentication.name) + } + @GetMapping("/{teamId}/item/{itemId}/photo/{photoId}") @Operation(summary = "Get photo information for the specified Team, Hunt, Item, and Photo") fun getPhotoInfo(@PathVariable huntId: HuntId, diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt index 9c58fc8..0141a9c 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt @@ -1,6 +1,7 @@ package net.halfbinary.scavengerhuntapi.error import net.halfbinary.scavengerhuntapi.error.exception.BadFileException +import net.halfbinary.scavengerhuntapi.error.exception.ConflictException import net.halfbinary.scavengerhuntapi.error.exception.ForbiddenException import net.halfbinary.scavengerhuntapi.error.exception.InvalidEmailException import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException @@ -54,6 +55,12 @@ class ExceptionHandler { return e.message } + @ExceptionHandler(ConflictException::class) + @ResponseStatus(HttpStatus.CONFLICT) + fun conflictException(e: ConflictException): String? { + return e.message + } + @ExceptionHandler(HttpMessageNotReadableException::class) @ResponseStatus(HttpStatus.BAD_REQUEST) fun httpMessageNotReadableException(e: HttpMessageNotReadableException): Map { diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ConflictException.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ConflictException.kt new file mode 100644 index 0000000..b1409f2 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ConflictException.kt @@ -0,0 +1,3 @@ +package net.halfbinary.scavengerhuntapi.error.exception + +class ConflictException(override val message: String) : RuntimeException(message) diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt index 7195f27..93b530a 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt @@ -3,6 +3,7 @@ package net.halfbinary.scavengerhuntapi.service import net.coobird.thumbnailator.Thumbnails import net.coobird.thumbnailator.tasks.UnsupportedFormatException import net.halfbinary.scavengerhuntapi.error.exception.BadFileException +import net.halfbinary.scavengerhuntapi.error.exception.ConflictException import net.halfbinary.scavengerhuntapi.error.exception.ForbiddenException import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException import net.halfbinary.scavengerhuntapi.model.FoundStatus @@ -140,6 +141,22 @@ class PhotoService( } } + fun removePhoto(huntId: HuntId, teamId: TeamId, itemId: ItemId, photoId: PhotoId, email: String) { + val photoRecord = photoRepository.findByIdAndItemIdAndHuntId(photoId, itemId, huntId) + ?: throw NotFoundException(PHOTO_NOT_FOUND) + + val team = try { + teamService.getTeamForHunterInHunt(huntId, email) + } catch (_: NotFoundException) { + throw ForbiddenException() + } + if (team.id != teamId) throw ForbiddenException() + + if (photoRecord.status == PhotoStatus.APPROVED) throw ConflictException("Cannot remove an approved photo") + + photoRepository.save(photoRecord.copy(status = PhotoStatus.REMOVED, statusChangeDateTime = LocalDateTime.now())) + } + fun updatePhotoStatus(photoId: PhotoId, status: PhotoStatus) { val record = photoRepository.findByIdOrNull(photoId) ?: throw NotFoundException(PHOTO_NOT_FOUND)