From 0c32ff41029cd7fef8eb8088add4391458e8b1cc Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sat, 31 Jan 2026 00:19:58 +0530 Subject: [PATCH] Reject duplicate guest documents by hash --- .../config/GuestDocumentSchemaFix.kt | 41 +++++++++++++++++++ .../controller/GuestDocuments.kt | 36 +++++++++++++++- .../db/repo/GuestDocumentRepo.kt | 6 +++ .../models/booking/GuestDocument.kt | 3 ++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/config/GuestDocumentSchemaFix.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/config/GuestDocumentSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/GuestDocumentSchemaFix.kt new file mode 100644 index 0000000..999bd99 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/GuestDocumentSchemaFix.kt @@ -0,0 +1,41 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class GuestDocumentSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : PostgresSchemaFix(jdbcTemplate) { + + override fun runPostgres(jdbcTemplate: JdbcTemplate) { + val hasTable = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.tables + where table_name = 'guest_document' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasTable == 0) return + + val hasHash = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.columns + where table_name = 'guest_document' + and column_name = 'file_hash' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasHash == 0) { + logger.info("Adding file_hash to guest_document table") + jdbcTemplate.execute( + """ + alter table guest_document + add column file_hash varchar + """.trimIndent() + ) + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt index 8a4274b..b44cf5f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt @@ -28,6 +28,7 @@ import java.time.OffsetDateTime import java.nio.file.Files import java.nio.file.Paths import java.util.UUID +import java.security.MessageDigest @RestController @RequestMapping("/properties/{propertyId}/guests/{guestId}/documents") @@ -82,6 +83,17 @@ class GuestDocuments( } val stored = storage.store(propertyId, guestId, bookingId, file) + val fileHash = hashFile(stored.storagePath) + if (fileHash != null && guestDocumentRepo.existsByPropertyIdAndGuestIdAndBookingIdAndFileHash( + propertyId, + guestId, + bookingId, + fileHash + ) + ) { + Files.deleteIfExists(Paths.get(stored.storagePath)) + throw ResponseStatusException(HttpStatus.CONFLICT, "Duplicate document") + } val document = GuestDocument( property = property, guest = guest, @@ -90,7 +102,8 @@ class GuestDocuments( originalFilename = stored.originalFilename, contentType = stored.contentType, sizeBytes = stored.sizeBytes, - storagePath = stored.storagePath + storagePath = stored.storagePath, + fileHash = fileHash ) val saved = guestDocumentRepo.save(document) runExtraction(saved.id!!, propertyId, guestId) @@ -236,6 +249,27 @@ class GuestDocuments( } } + private fun hashFile(storagePath: String): String? { + return try { + val path = Paths.get(storagePath) + if (!Files.exists(path)) return null + val digest = MessageDigest.getInstance("SHA-256") + Files.newInputStream(path).use { input -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read = input.read(buffer) + while (read >= 0) { + if (read > 0) { + digest.update(buffer, 0, read) + } + read = input.read(buffer) + } + } + digest.digest().joinToString("") { "%02x".format(it) } + } catch (_: Exception) { + null + } + } + } data class GuestDocumentResponse( diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt index bdc4b62..1948eec 100644 --- a/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt @@ -8,4 +8,10 @@ interface GuestDocumentRepo : JpaRepository { fun findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId: UUID, guestId: UUID): List fun findByIdAndPropertyIdAndGuestId(id: UUID, propertyId: UUID, guestId: UUID): GuestDocument? fun existsByGuestId(guestId: UUID): Boolean + fun existsByPropertyIdAndGuestIdAndBookingIdAndFileHash( + propertyId: UUID, + guestId: UUID, + bookingId: UUID, + fileHash: String + ): Boolean } diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestDocument.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestDocument.kt index dd0a246..679f608 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestDocument.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestDocument.kt @@ -44,6 +44,9 @@ class GuestDocument( @Column(name = "storage_path", nullable = false) var storagePath: String, + @Column(name = "file_hash") + var fileHash: String? = null, + @Column(name = "extracted_data", columnDefinition = "jsonb") @JdbcTypeCode(SqlTypes.JSON) var extractedData: String? = null,