Add global image tags and tag assignment
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s

This commit is contained in:
androidlover5842
2026-01-27 18:50:51 +05:30
parent 03b02a08ca
commit eaee838ca3
7 changed files with 220 additions and 10 deletions

View File

@@ -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
)
}

View File

@@ -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
)
}

View File

@@ -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>
)