From b349380c93330a517323be3d95ccf1a5719c8fe0 Mon Sep 17 00:00:00 2001 From: aarbit Date: Thu, 14 May 2026 13:01:42 -0500 Subject: [PATCH] Implements get photo information endpoint --- .../controller/TeamController.kt | 5 +++-- .../error/ExceptionHandler.kt | 7 ++++++ .../error/exception/ForbiddenException.kt | 3 +++ .../model/converter/PhotoConverter.kt | 10 +++++++++ .../repository/PhotoRepository.kt | 12 +++------- .../scavengerhuntapi/service/HunterService.kt | 5 +++++ .../scavengerhuntapi/service/PhotoService.kt | 22 +++++++++++++++++++ 7 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ForbiddenException.kt diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt index c8a8182..3aa6109 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/TeamController.kt @@ -69,8 +69,9 @@ class TeamController(private val teamService: TeamService, private val photoServ fun getPhotoInfo(@PathVariable huntId: HuntId, @PathVariable teamId: TeamId, @PathVariable itemId: ItemId, - @PathVariable photoId: PhotoId): ResponseEntity { - TODO("Get photo information for the specified Team, Hunt, Item, and Photo. Join on the Hunter table to get the Hunter name. Also verify that the requesting user is either an admin or is on the same Hunt and Team as the Hunter who submitted the Photo") + @PathVariable photoId: PhotoId, + authentication: Authentication): ResponseEntity { + return ResponseEntity.ok(photoService.getPhotoInfo(huntId, teamId, itemId, photoId, authentication.name)) } @GetMapping("/{teamId}/item/{itemId}/photo/{photoId}/file") diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt index fcbe419..504ddfb 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.ForbiddenException import net.halfbinary.scavengerhuntapi.error.exception.InvalidEmailException import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException @@ -46,6 +47,12 @@ class ExceptionHandler { return e.message } + @ExceptionHandler(ForbiddenException::class) + @ResponseStatus(HttpStatus.FORBIDDEN) + fun forbiddenException(e: ForbiddenException): 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/ForbiddenException.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ForbiddenException.kt new file mode 100644 index 0000000..14fec32 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/ForbiddenException.kt @@ -0,0 +1,3 @@ +package net.halfbinary.scavengerhuntapi.error.exception + +class ForbiddenException(override val message: String): RuntimeException(message) diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/PhotoConverter.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/PhotoConverter.kt index 7d922a3..2a25db9 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/PhotoConverter.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/PhotoConverter.kt @@ -15,6 +15,16 @@ fun Photo.toRecord() = PhotoRecord( statusChangeDateTime = statusChangeDateTime ) +fun PhotoRecord.toDomain() = Photo( + id = id, + itemId = itemId, + huntId = huntId, + hunterId = hunterId, + foundDateTime = foundDateTime, + status = status, + statusChangeDateTime = statusChangeDateTime +) + fun Photo.toResponse(hunter: Hunter) = PhotoResponse( id = id, hunterName = hunter.name, diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/PhotoRepository.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/PhotoRepository.kt index b8df493..80148e4 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/PhotoRepository.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/PhotoRepository.kt @@ -1,19 +1,13 @@ package net.halfbinary.scavengerhuntapi.repository +import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.ItemId import net.halfbinary.scavengerhuntapi.model.PhotoId import net.halfbinary.scavengerhuntapi.model.record.PhotoRecord import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository - @Repository interface PhotoRepository : JpaRepository { - @Query(""" - SELECT * - FROM photo p - WHERE - p. - """, nativeQuery = true) - fun findPhotosByItemId(itemId: ItemId): List + fun findByItemId(itemId: ItemId): List + fun findByIdAndItemIdAndHuntId(id: PhotoId, itemId: ItemId, huntId: HuntId): PhotoRecord? } \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/HunterService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/HunterService.kt index 582b12b..92bd8bd 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/HunterService.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/HunterService.kt @@ -1,6 +1,7 @@ package net.halfbinary.scavengerhuntapi.service import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException +import net.halfbinary.scavengerhuntapi.model.HunterId import net.halfbinary.scavengerhuntapi.model.converter.toDomain import net.halfbinary.scavengerhuntapi.model.domain.Hunter import net.halfbinary.scavengerhuntapi.repository.HunterRepository @@ -12,4 +13,8 @@ class HunterService(private val hunterRepository: HunterRepository) { return hunterRepository.findByEmail(email)?.toDomain() ?: throw NotFoundException("No hunter with email $email found") } + + fun getHunterById(hunterId: HunterId): Hunter { + return hunterRepository.findById(hunterId).orElseThrow { NotFoundException("No hunter with id $hunterId found") }.toDomain() + } } \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt index d33232f..2f7ae67 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/PhotoService.kt @@ -3,11 +3,18 @@ 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.ForbiddenException +import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.ItemId +import net.halfbinary.scavengerhuntapi.model.PhotoId import net.halfbinary.scavengerhuntapi.model.PhotoStatus +import net.halfbinary.scavengerhuntapi.model.TeamId +import net.halfbinary.scavengerhuntapi.model.converter.toDomain import net.halfbinary.scavengerhuntapi.model.converter.toRecord +import net.halfbinary.scavengerhuntapi.model.converter.toResponse import net.halfbinary.scavengerhuntapi.model.domain.Photo +import net.halfbinary.scavengerhuntapi.model.response.PhotoResponse import net.halfbinary.scavengerhuntapi.repository.PhotoRepository import org.springframework.http.MediaType import org.springframework.stereotype.Service @@ -20,6 +27,7 @@ import java.time.LocalDateTime class PhotoService( private val photoRepository: PhotoRepository, private val hunterService: HunterService, + private val teamService: TeamService, private val s3StorageService: S3StorageService, private val fileProbeService: FileProbeService ) { @@ -54,6 +62,20 @@ class PhotoService( s3StorageService.upload("${baseName}_small.jpg", resize(originalBytes, 200), MediaType.IMAGE_JPEG_VALUE) } + fun getPhotoInfo(huntId: HuntId, teamId: TeamId, itemId: ItemId, photoId: PhotoId, email: String): PhotoResponse { + val requestingHunter = hunterService.getHunterByEmail(email) + val photoRecord = photoRepository.findByIdAndItemIdAndHuntId(photoId, itemId, huntId) + ?: throw NotFoundException("Photo not found") + + if (!requestingHunter.isAdmin) { + val team = teamService.getTeamForHunterInHunt(huntId, email) + if (team.id != teamId) throw ForbiddenException("Access denied") + } + + val submitter = hunterService.getHunterById(photoRecord.hunterId) + return photoRecord.toDomain().toResponse(submitter) + } + private fun toJpeg(bytes: ByteArray): ByteArray { val output = ByteArrayOutputStream() Thumbnails.of(ByteArrayInputStream(bytes))