Add global image tags and tag assignment
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.controller.dto.RoomImageTagResponse
|
||||
import com.android.trisolarisserver.controller.dto.RoomImageTagUpsertRequest
|
||||
import com.android.trisolarisserver.models.room.RoomImageTag
|
||||
import com.android.trisolarisserver.repo.AppUserRepo
|
||||
import com.android.trisolarisserver.repo.RoomImageRepo
|
||||
import com.android.trisolarisserver.repo.RoomImageTagRepo
|
||||
import com.android.trisolarisserver.security.MyPrincipal
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/image-tags")
|
||||
class RoomImageTags(
|
||||
private val roomImageTagRepo: RoomImageTagRepo,
|
||||
private val roomImageRepo: RoomImageRepo,
|
||||
private val appUserRepo: AppUserRepo
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun listTags(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): List<RoomImageTagResponse> {
|
||||
requirePrincipal(principal)
|
||||
return roomImageTagRepo.findAllByOrderByName().map { it.toResponse() }
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun createTag(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomImageTagUpsertRequest
|
||||
): RoomImageTagResponse {
|
||||
requireSuperAdmin(principal)
|
||||
if (roomImageTagRepo.existsByName(request.name)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Tag already exists")
|
||||
}
|
||||
val tag = RoomImageTag(name = request.name)
|
||||
return roomImageTagRepo.save(tag).toResponse()
|
||||
}
|
||||
|
||||
@PutMapping("/{tagId}")
|
||||
fun updateTag(
|
||||
@PathVariable tagId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomImageTagUpsertRequest
|
||||
): RoomImageTagResponse {
|
||||
requireSuperAdmin(principal)
|
||||
val tag = roomImageTagRepo.findById(tagId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Tag not found")
|
||||
if (roomImageTagRepo.existsByNameAndIdNot(request.name, tagId)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Tag already exists")
|
||||
}
|
||||
tag.name = request.name
|
||||
return roomImageTagRepo.save(tag).toResponse()
|
||||
}
|
||||
|
||||
@DeleteMapping("/{tagId}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun deleteTag(
|
||||
@PathVariable tagId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
) {
|
||||
requireSuperAdmin(principal)
|
||||
if (roomImageRepo.existsByTagsId(tagId)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Tag is used by images")
|
||||
}
|
||||
val tag = roomImageTagRepo.findById(tagId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Tag not found")
|
||||
roomImageTagRepo.delete(tag)
|
||||
}
|
||||
|
||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireSuperAdmin(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||
}
|
||||
if (!user.superAdmin) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Super admin only")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomImageTag.toResponse(): RoomImageTagResponse {
|
||||
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Tag id missing")
|
||||
return RoomImageTagResponse(
|
||||
id = id,
|
||||
name = name
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.component.RoomImageStorage
|
||||
import com.android.trisolarisserver.controller.dto.RoomImageResponse
|
||||
import com.android.trisolarisserver.controller.dto.RoomImageReorderRequest
|
||||
import com.android.trisolarisserver.controller.dto.RoomImageTagUpdateRequest
|
||||
import com.android.trisolarisserver.models.room.RoomImage
|
||||
import com.android.trisolarisserver.models.room.RoomImageTag
|
||||
import com.android.trisolarisserver.models.property.Role
|
||||
import com.android.trisolarisserver.repo.RoomImageRepo
|
||||
import com.android.trisolarisserver.repo.RoomImageTagRepo
|
||||
import com.android.trisolarisserver.repo.RoomRepo
|
||||
import com.android.trisolarisserver.security.MyPrincipal
|
||||
import org.springframework.core.io.FileSystemResource
|
||||
@@ -39,6 +42,7 @@ class RoomImages(
|
||||
private val propertyAccess: PropertyAccess,
|
||||
private val roomRepo: RoomRepo,
|
||||
private val roomImageRepo: RoomImageRepo,
|
||||
private val roomImageTagRepo: RoomImageTagRepo,
|
||||
private val storage: RoomImageStorage,
|
||||
@org.springframework.beans.factory.annotation.Value("\${storage.rooms.publicBaseUrl}")
|
||||
private val publicBaseUrl: String
|
||||
@@ -64,7 +68,7 @@ class RoomImages(
|
||||
@PathVariable roomId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestParam("file") file: MultipartFile,
|
||||
@RequestParam(required = false) tags: List<String>?
|
||||
@RequestParam(required = false) tagIds: List<UUID>?
|
||||
): RoomImageResponse {
|
||||
requirePrincipal(principal)
|
||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||
@@ -86,6 +90,7 @@ class RoomImages(
|
||||
|
||||
val nextRoomSortOrder = roomImageRepo.findMaxRoomSortOrder(roomId) + 1
|
||||
val nextRoomTypeSortOrder = roomImageRepo.findMaxRoomTypeSortOrder(room.roomType.code) + 1
|
||||
val tags = resolveTags(tagIds)
|
||||
val image = RoomImage(
|
||||
property = room.property,
|
||||
room = room,
|
||||
@@ -95,7 +100,7 @@ class RoomImages(
|
||||
sizeBytes = stored.sizeBytes,
|
||||
contentHash = contentHash,
|
||||
roomTypeCode = room.roomType.code,
|
||||
tags = tags?.toMutableSet() ?: mutableSetOf(),
|
||||
tags = tags.toMutableSet(),
|
||||
roomSortOrder = nextRoomSortOrder,
|
||||
roomTypeSortOrder = nextRoomTypeSortOrder
|
||||
)
|
||||
@@ -156,6 +161,28 @@ class RoomImages(
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{imageId}/tags")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
fun updateTags(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable roomId: UUID,
|
||||
@PathVariable imageId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomImageTagUpdateRequest
|
||||
) {
|
||||
requirePrincipal(principal)
|
||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
||||
ensureRoom(propertyId, roomId)
|
||||
|
||||
val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
||||
val tags = resolveTags(request.tagIds.toList())
|
||||
image.tags = tags.toMutableSet()
|
||||
roomImageRepo.save(image)
|
||||
}
|
||||
|
||||
@PutMapping("/reorder-room")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
@@ -262,6 +289,17 @@ class RoomImages(
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveTags(tagIds: List<UUID>?): Set<RoomImageTag> {
|
||||
if (tagIds.isNullOrEmpty()) {
|
||||
return emptySet()
|
||||
}
|
||||
val tags = roomImageTagRepo.findByIdIn(tagIds.toSet())
|
||||
if (tags.size != tagIds.size) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Tag not found")
|
||||
}
|
||||
return tags.toSet()
|
||||
}
|
||||
|
||||
private fun sha256Hex(bytes: ByteArray): String {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val hash = md.digest(bytes)
|
||||
@@ -284,9 +322,17 @@ private fun RoomImage.toResponse(baseUrl: String): RoomImageResponse {
|
||||
thumbnailUrl = "$baseUrl/properties/${property.id}/rooms/${room.id}/images/$id/file?size=thumb",
|
||||
contentType = contentType,
|
||||
sizeBytes = sizeBytes,
|
||||
tags = tags.toSet(),
|
||||
tags = tags.map { it.toResponse() }.toSet(),
|
||||
roomSortOrder = roomSortOrder,
|
||||
roomTypeSortOrder = roomTypeSortOrder,
|
||||
createdAt = createdAt.toString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun RoomImageTag.toResponse(): com.android.trisolarisserver.controller.dto.RoomImageTagResponse {
|
||||
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Tag id missing")
|
||||
return com.android.trisolarisserver.controller.dto.RoomImageTagResponse(
|
||||
id = id,
|
||||
name = name
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ data class RoomImageResponse(
|
||||
val thumbnailUrl: String,
|
||||
val contentType: String,
|
||||
val sizeBytes: Long,
|
||||
val tags: Set<String>,
|
||||
val tags: Set<RoomImageTagResponse>,
|
||||
val roomSortOrder: Int,
|
||||
val roomTypeSortOrder: Int,
|
||||
val createdAt: String
|
||||
@@ -65,3 +65,16 @@ data class RoomUpsertRequest(
|
||||
data class RoomImageReorderRequest(
|
||||
val imageIds: List<UUID>
|
||||
)
|
||||
|
||||
data class RoomImageTagUpsertRequest(
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class RoomImageTagResponse(
|
||||
val id: UUID,
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class RoomImageTagUpdateRequest(
|
||||
val tagIds: Set<UUID>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user