From 9b64b34ab9c13aea6efefb246e92ad88888b3fb0 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Wed, 28 Jan 2026 23:03:48 +0530 Subject: [PATCH] Deduplicate logic across controllers, auth, and schema fixes --- .../component/EmailStorage.kt | 9 -- .../component/FileStorageUtils.kt | 17 +++ .../trisolarisserver/component/LlamaClient.kt | 12 +- .../component/RoomImageStorage.kt | 8 -- .../config/IssuedCardSchemaFix.kt | 14 +- .../config/PostgresSchemaFix.kt | 23 ++++ .../config/RoomImageSchemaFix.kt | 14 +- .../config/RoomImageTagSchemaFix.kt | 14 +- .../config/RoomTypeSchemaFix.kt | 14 +- .../trisolarisserver/controller/Auth.kt | 54 ++------ .../controller/BookingFlow.kt | 20 +-- .../controller/CardEncoding.kt | 63 +++++++++ .../controller/ControllerAccess.kt | 42 ++++++ .../controller/ControllerLookups.kt | 85 +++++++++++++ .../controller/GuestDocuments.kt | 34 +---- .../controller/GuestRatings.kt | 33 +---- .../trisolarisserver/controller/Guests.kt | 25 +--- .../controller/IssuedCardMappings.kt | 19 +++ .../controller/IssuedCards.kt | 120 +----------------- .../trisolarisserver/controller/RoomImages.kt | 32 ++--- .../controller/RoomStayFlow.kt | 32 ++--- .../controller/TemporaryRoomCards.kt | 94 -------------- .../trisolarisserver/security/AuthResolver.kt | 69 ++++++++++ .../security/FirebaseAuthFilter.kt | 32 +---- .../security/PublicEndpoints.kt | 30 +++++ .../security/SecurityConfig.kt | 13 +- 26 files changed, 412 insertions(+), 510 deletions(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/component/FileStorageUtils.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/config/PostgresSchemaFix.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/CardEncoding.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/IssuedCardMappings.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/security/AuthResolver.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/component/EmailStorage.kt b/src/main/kotlin/com/android/trisolarisserver/component/EmailStorage.kt index d22a494..2df2749 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/EmailStorage.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/EmailStorage.kt @@ -8,7 +8,6 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths import java.time.OffsetDateTime import java.util.UUID @@ -112,12 +111,4 @@ class EmailStorage( atomicMove(tmp, path) return path.toString() } - - private fun atomicMove(tmp: Path, target: Path) { - try { - Files.move(tmp, target, java.nio.file.StandardCopyOption.ATOMIC_MOVE, java.nio.file.StandardCopyOption.REPLACE_EXISTING) - } catch (_: Exception) { - Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING) - } - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/component/FileStorageUtils.kt b/src/main/kotlin/com/android/trisolarisserver/component/FileStorageUtils.kt new file mode 100644 index 0000000..639c0e1 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/FileStorageUtils.kt @@ -0,0 +1,17 @@ +package com.android.trisolarisserver.component + +import java.nio.file.Files +import java.nio.file.Path + +internal fun atomicMove(tmp: Path, target: Path) { + try { + Files.move( + tmp, + target, + java.nio.file.StandardCopyOption.ATOMIC_MOVE, + java.nio.file.StandardCopyOption.REPLACE_EXISTING + ) + } catch (_: Exception) { + Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING) + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/component/LlamaClient.kt b/src/main/kotlin/com/android/trisolarisserver/component/LlamaClient.kt index da94413..2172c4f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/LlamaClient.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/LlamaClient.kt @@ -38,13 +38,7 @@ class LlamaClient( ) ) ) - val headers = HttpHeaders() - headers.contentType = MediaType.APPLICATION_JSON - val entity = HttpEntity(payload, headers) - val response = restTemplate.postForEntity(baseUrl, entity, String::class.java) - val body = response.body ?: return "" - val node = objectMapper.readTree(body) - return node.path("choices").path(0).path("message").path("content").asText() + return post(payload) } fun askText(content: String, question: String): String { @@ -61,6 +55,10 @@ class LlamaClient( ) ) ) + return post(payload) + } + + private fun post(payload: Map): String { val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON val entity = HttpEntity(payload, headers) diff --git a/src/main/kotlin/com/android/trisolarisserver/component/RoomImageStorage.kt b/src/main/kotlin/com/android/trisolarisserver/component/RoomImageStorage.kt index 0ccef4b..2aaa6ce 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/RoomImageStorage.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/RoomImageStorage.kt @@ -6,7 +6,6 @@ import org.springframework.web.multipart.MultipartFile import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths import java.time.OffsetDateTime import java.util.UUID @@ -102,13 +101,6 @@ class RoomImageStorage( } } - private fun atomicMove(tmp: Path, target: Path) { - try { - Files.move(tmp, target, java.nio.file.StandardCopyOption.ATOMIC_MOVE, java.nio.file.StandardCopyOption.REPLACE_EXISTING) - } catch (_: Exception) { - Files.move(tmp, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING) - } - } } data class StoredRoomImage( diff --git a/src/main/kotlin/com/android/trisolarisserver/config/IssuedCardSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/IssuedCardSchemaFix.kt index a041557..f63b3bb 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/IssuedCardSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/IssuedCardSchemaFix.kt @@ -1,24 +1,14 @@ package com.android.trisolarisserver.config -import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component class IssuedCardSchemaFix( private val jdbcTemplate: JdbcTemplate -) : ApplicationRunner { - - private val logger = LoggerFactory.getLogger(IssuedCardSchemaFix::class.java) - - override fun run(args: ApplicationArguments) { - val version = jdbcTemplate.queryForObject("select version()", String::class.java) ?: return - if (!version.contains("PostgreSQL", ignoreCase = true)) { - return - } +) : PostgresSchemaFix(jdbcTemplate) { + override fun runPostgres(jdbcTemplate: JdbcTemplate) { val isNullable = jdbcTemplate.queryForObject( """ select count(*) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PostgresSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PostgresSchemaFix.kt new file mode 100644 index 0000000..42170ec --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/PostgresSchemaFix.kt @@ -0,0 +1,23 @@ +package com.android.trisolarisserver.config + +import org.slf4j.LoggerFactory +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.jdbc.core.JdbcTemplate + +abstract class PostgresSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : ApplicationRunner { + + protected val logger = LoggerFactory.getLogger(this::class.java) + + override fun run(args: ApplicationArguments) { + val version = jdbcTemplate.queryForObject("select version()", String::class.java) ?: return + if (!version.contains("PostgreSQL", ignoreCase = true)) { + return + } + runPostgres(jdbcTemplate) + } + + protected abstract fun runPostgres(jdbcTemplate: JdbcTemplate) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RoomImageSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RoomImageSchemaFix.kt index 3246851..7769728 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/RoomImageSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RoomImageSchemaFix.kt @@ -1,24 +1,14 @@ package com.android.trisolarisserver.config -import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component class RoomImageSchemaFix( private val jdbcTemplate: JdbcTemplate -) : ApplicationRunner { - - private val logger = LoggerFactory.getLogger(RoomImageSchemaFix::class.java) - - override fun run(args: ApplicationArguments) { - val version = jdbcTemplate.queryForObject("select version()", String::class.java) ?: return - if (!version.contains("PostgreSQL", ignoreCase = true)) { - return - } +) : PostgresSchemaFix(jdbcTemplate) { + override fun runPostgres(jdbcTemplate: JdbcTemplate) { val hasContentHash = jdbcTemplate.queryForObject( """ select count(*) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RoomImageTagSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RoomImageTagSchemaFix.kt index 0223308..8d44cd3 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/RoomImageTagSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RoomImageTagSchemaFix.kt @@ -1,24 +1,14 @@ package com.android.trisolarisserver.config -import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component class RoomImageTagSchemaFix( private val jdbcTemplate: JdbcTemplate -) : ApplicationRunner { - - private val logger = LoggerFactory.getLogger(RoomImageTagSchemaFix::class.java) - - override fun run(args: ApplicationArguments) { - val version = jdbcTemplate.queryForObject("select version()", String::class.java) ?: return - if (!version.contains("PostgreSQL", ignoreCase = true)) { - return - } +) : PostgresSchemaFix(jdbcTemplate) { + override fun runPostgres(jdbcTemplate: JdbcTemplate) { val hasOldRoomImageId = jdbcTemplate.queryForObject( """ select count(*) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RoomTypeSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RoomTypeSchemaFix.kt index af7a9cc..0188859 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/RoomTypeSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RoomTypeSchemaFix.kt @@ -1,24 +1,14 @@ package com.android.trisolarisserver.config -import org.slf4j.LoggerFactory -import org.springframework.boot.ApplicationArguments -import org.springframework.boot.ApplicationRunner import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Component @Component class RoomTypeSchemaFix( private val jdbcTemplate: JdbcTemplate -) : ApplicationRunner { - - private val logger = LoggerFactory.getLogger(RoomTypeSchemaFix::class.java) - - override fun run(args: ApplicationArguments) { - val version = jdbcTemplate.queryForObject("select version()", String::class.java) ?: return - if (!version.contains("PostgreSQL", ignoreCase = true)) { - return - } +) : PostgresSchemaFix(jdbcTemplate) { + override fun runPostgres(jdbcTemplate: JdbcTemplate) { val hasActive = jdbcTemplate.queryForObject( """ select count(*) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt index f82be33..395e63d 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt @@ -5,7 +5,6 @@ import com.android.trisolarisserver.controller.dto.UserResponse import com.android.trisolarisserver.repo.AppUserRepo import com.android.trisolarisserver.repo.PropertyUserRepo import com.android.trisolarisserver.security.MyPrincipal -import com.google.firebase.auth.FirebaseAuth import jakarta.servlet.http.HttpServletRequest import org.slf4j.LoggerFactory import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -19,12 +18,14 @@ import org.springframework.web.server.ResponseStatusException import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import java.util.UUID +import com.android.trisolarisserver.security.AuthResolver @RestController @RequestMapping("/auth") class Auth( private val appUserRepo: AppUserRepo, - private val propertyUserRepo: PropertyUserRepo + private val propertyUserRepo: PropertyUserRepo, + private val authResolver: AuthResolver ) { private val logger = LoggerFactory.getLogger(Auth::class.java) @@ -34,7 +35,8 @@ class Auth( request: HttpServletRequest ): ResponseEntity { logger.info("Auth verify hit, principalPresent={}", principal != null) - val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request) + val resolved = principal?.let { ResolveResult(it) } + ?: ResolveResult(authResolver.resolveFromRequest(request, createIfMissing = true)) return resolved.toResponseEntity() } @@ -43,7 +45,8 @@ class Auth( @AuthenticationPrincipal principal: MyPrincipal?, request: HttpServletRequest ): ResponseEntity { - val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request) + val resolved = principal?.let { ResolveResult(it) } + ?: ResolveResult(authResolver.resolveFromRequest(request, createIfMissing = true)) return resolved.toResponseEntity() } @@ -53,7 +56,8 @@ class Auth( request: HttpServletRequest, @RequestBody body: UpdateMeRequest ): ResponseEntity { - val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request) + val resolved = principal?.let { ResolveResult(it) } + ?: ResolveResult(authResolver.resolveFromRequest(request, createIfMissing = true)) if (resolved.principal == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(AuthResponse(status = "UNAUTHORIZED")) @@ -98,46 +102,6 @@ class Auth( ) } - private fun resolvePrincipalFromHeader(request: HttpServletRequest): ResolveResult { - val header = request.getHeader("Authorization") ?: throw ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "Missing Authorization token" - ) - if (!header.startsWith("Bearer ")) { - logger.warn("Auth verify invalid Authorization header") - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Authorization header") - } - val token = header.removePrefix("Bearer ").trim() - val decoded = try { - FirebaseAuth.getInstance().verifyIdToken(token) - } catch (ex: Exception) { - logger.warn("Auth verify failed: {}", ex.message) - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token") - } - val user = appUserRepo.findByFirebaseUid(decoded.uid) ?: run { - val phone = decoded.claims["phone_number"] as? String - val name = decoded.claims["name"] as? String - val makeSuperAdmin = appUserRepo.count() == 0L - val created = appUserRepo.save( - com.android.trisolarisserver.models.property.AppUser( - firebaseUid = decoded.uid, - phoneE164 = phone, - name = name, - superAdmin = makeSuperAdmin - ) - ) - logger.warn("Auth verify auto-created user uid={}, userId={}", decoded.uid, created.id) - created - } - logger.warn("Auth verify resolved uid={}, userId={}", decoded.uid, user.id) - return ResolveResult( - MyPrincipal( - userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), - firebaseUid = decoded.uid - ) - ) - } - private fun ResolveResult.toResponseEntity(): ResponseEntity { return if (principal == null) { ResponseEntity.status(HttpStatus.UNAUTHORIZED) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt index afdaf69..a841333 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt @@ -232,23 +232,12 @@ class BookingFlow( } private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER, Role.STAFF) - return appUserRepo.findById(principal.userId).orElseThrow { + val resolved = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + return appUserRepo.findById(resolved.userId).orElseThrow { ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } - private fun parseOffset(value: String?): OffsetDateTime? { - if (value.isNullOrBlank()) return null - return try { - OffsetDateTime.parse(value.trim()) - } catch (_: Exception) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") - } - } - private fun parseTransportMode(value: String): TransportMode { return try { TransportMode.valueOf(value) @@ -269,9 +258,4 @@ class BookingFlow( return allowed.contains(mode) } - private fun requirePrincipal(principal: MyPrincipal?) { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/CardEncoding.kt b/src/main/kotlin/com/android/trisolarisserver/controller/CardEncoding.kt new file mode 100644 index 0000000..2ebf8c4 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/CardEncoding.kt @@ -0,0 +1,63 @@ +package com.android.trisolarisserver.controller + +import java.time.OffsetDateTime + +internal fun encodeBlock(value: String?): String { + val raw = (value ?: "").padEnd(16).take(16) + val bytes = raw.toByteArray(Charsets.UTF_8) + val sb = StringBuilder(bytes.size * 2) + for (b in bytes) { + sb.append(String.format("%02X", b)) + } + return sb.toString() +} + +internal fun buildSector0Block2(roomNumber: Int, cardID: Int): String { + val guestID = cardID + 1 + val cardIdStr = cardID.toString().padStart(6, '0') + val guestIdStr = guestID.toString().padStart(6, '0') + val finalRoom = roomNumber.toString().padStart(2, '0') + return "472F${cardIdStr}2F${guestIdStr}00010000${finalRoom}0000" +} + +internal fun formatDateComponents(time: OffsetDateTime): String { + val minute = time.minute.toString().padStart(2, '0') + val hour = time.hour.toString().padStart(2, '0') + val day = time.dayOfMonth.toString().padStart(2, '0') + val month = time.monthValue.toString().padStart(2, '0') + val year = time.year.toString().takeLast(2) + return "${minute}${hour}${day}${month}${year}" +} + +internal fun calculateChecksum(dataHex: String): String { + val data = hexStringToByteArray(dataHex) + var checksum = 0 + for (byte in data) { + checksum = calculateByteChecksum(byte, checksum) + } + return String.format("%02X", checksum) +} + +private fun calculateByteChecksum(byte: Byte, currentChecksum: Int): Int { + var checksum = currentChecksum + var b = byte.toInt() + for (i in 0 until 8) { + checksum = if ((checksum xor b) and 1 != 0) { + (checksum xor 0x18) shr 1 or 0x80 + } else { + checksum shr 1 + } + b = b shr 1 + } + return checksum +} + +private fun hexStringToByteArray(hexString: String): ByteArray { + val len = hexString.length + val data = ByteArray(len / 2) + for (i in 0 until len step 2) { + data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + + Character.digit(hexString[i + 1], 16)).toByte() + } + return data +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt new file mode 100644 index 0000000..9fc80f1 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt @@ -0,0 +1,42 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.models.property.AppUser +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.repo.AppUserRepo +import com.android.trisolarisserver.security.MyPrincipal +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.util.UUID + +internal fun requirePrincipal(principal: MyPrincipal?): MyPrincipal { + return principal ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") +} + +internal fun requireUser(appUserRepo: AppUserRepo, principal: MyPrincipal?): AppUser { + val resolved = requirePrincipal(principal) + return appUserRepo.findById(resolved.userId).orElseThrow { + ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") + } +} + +internal fun requireMember( + propertyAccess: PropertyAccess, + propertyId: UUID, + principal: MyPrincipal? +): MyPrincipal { + val resolved = requirePrincipal(principal) + propertyAccess.requireMember(propertyId, resolved.userId) + return resolved +} + +internal fun requireRole( + propertyAccess: PropertyAccess, + propertyId: UUID, + principal: MyPrincipal?, + vararg roles: Role +): MyPrincipal { + val resolved = requireMember(propertyAccess, propertyId, principal) + propertyAccess.requireAnyRole(propertyId, resolved.userId, *roles) + return resolved +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt new file mode 100644 index 0000000..36c4df6 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt @@ -0,0 +1,85 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.db.repo.GuestRepo +import com.android.trisolarisserver.models.booking.Guest +import com.android.trisolarisserver.models.property.Property +import com.android.trisolarisserver.models.room.RoomStay +import com.android.trisolarisserver.repo.PropertyRepo +import com.android.trisolarisserver.repo.RoomStayRepo +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.time.OffsetDateTime +import java.time.ZoneId +import java.util.UUID + +internal data class PropertyGuest( + val property: Property, + val guest: Guest +) + +internal fun requireProperty(propertyRepo: PropertyRepo, propertyId: UUID): Property { + return propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") + } +} + +internal fun requirePropertyGuest( + propertyRepo: PropertyRepo, + guestRepo: GuestRepo, + propertyId: UUID, + guestId: UUID +): PropertyGuest { + val property = requireProperty(propertyRepo, propertyId) + val guest = guestRepo.findById(guestId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") + } + if (guest.property.id != property.id) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property") + } + return PropertyGuest(property, guest) +} + +internal fun requireRoomStayForProperty( + roomStayRepo: RoomStayRepo, + propertyId: UUID, + roomStayId: UUID +): RoomStay { + val stay = roomStayRepo.findById(roomStayId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") + } + if (stay.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property") + } + return stay +} + +internal fun requireOpenRoomStayForProperty( + roomStayRepo: RoomStayRepo, + propertyId: UUID, + roomStayId: UUID, + closedMessage: String +): RoomStay { + val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId) + if (stay.toAt != null) { + throw ResponseStatusException(HttpStatus.CONFLICT, closedMessage) + } + return stay +} + +internal fun parseOffset(value: String?): OffsetDateTime? { + if (value.isNullOrBlank()) return null + return try { + OffsetDateTime.parse(value.trim()) + } catch (_: Exception) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") + } +} + +internal fun nowForProperty(timezone: String?): OffsetDateTime { + val zone = try { + if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone) + } catch (_: Exception) { + ZoneId.of("Asia/Kolkata") + } + return OffsetDateTime.now(zone) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt index b4cbad7..895aeef 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt @@ -53,7 +53,7 @@ class GuestDocuments( @RequestParam("bookingId") bookingId: UUID, @RequestPart("file") file: MultipartFile ): GuestDocumentResponse { - val user = requireUser(principal) + val user = requireUser(appUserRepo, principal) propertyAccess.requireMember(propertyId, user.id!!) propertyAccess.requireAnyRole(propertyId, user.id!!, Role.ADMIN, Role.MANAGER) @@ -65,15 +65,7 @@ class GuestDocuments( throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Video files are not allowed") } - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val guest = guestRepo.findById(guestId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") - } - if (guest.property.id != property.id) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property") - } + val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId) val booking = bookingRepo.findById(bookingId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") } @@ -106,9 +98,7 @@ class GuestDocuments( @PathVariable guestId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) return guestDocumentRepo .findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId) @@ -124,9 +114,7 @@ class GuestDocuments( @AuthenticationPrincipal principal: MyPrincipal? ): ResponseEntity { if (token == null) { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) } else if (!tokenService.validateToken(token, documentId.toString())) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token") } @@ -207,20 +195,6 @@ class GuestDocuments( } } - private fun requirePrincipal(principal: MyPrincipal?) { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - } - - private fun requireUser(principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - return appUserRepo.findById(principal.userId).orElseThrow { - ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") - } - } } data class GuestDocumentResponse( diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/GuestRatings.kt b/src/main/kotlin/com/android/trisolarisserver/controller/GuestRatings.kt index 2b3c364..a918d21 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/GuestRatings.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/GuestRatings.kt @@ -42,18 +42,9 @@ class GuestRatings( @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: GuestRatingCreateRequest ): GuestRatingResponse { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) + val resolved = requireMember(propertyAccess, propertyId, principal) - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val guest = guestRepo.findById(guestId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") - } - if (guest.property.id != property.id) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property") - } + val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId) val booking = bookingRepo.findById(request.bookingId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") @@ -75,7 +66,7 @@ class GuestRatings( booking = booking, score = score, notes = request.notes?.trim(), - createdBy = appUserRepo.findById(principal.userId).orElse(null) + createdBy = appUserRepo.findById(resolved.userId).orElse(null) ) guestRatingRepo.save(rating) return rating.toResponse() @@ -87,18 +78,9 @@ class GuestRatings( @PathVariable guestId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) + requireMember(propertyAccess, propertyId, principal) - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val guest = guestRepo.findById(guestId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") - } - if (guest.property.id != property.id) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property") - } + val (_, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId) return guestRatingRepo.findByGuestIdOrderByCreatedAtDesc(guestId).map { it.toResponse() } } @@ -126,9 +108,4 @@ class GuestRatings( ) } - private fun requirePrincipal(principal: MyPrincipal?) { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt index 1ca3dcc..b9f6089 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt @@ -33,16 +33,13 @@ class Guests( @RequestParam(required = false) phone: String?, @RequestParam(required = false) vehicleNumber: String? ): List { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) + requireMember(propertyAccess, propertyId, principal) if (phone.isNullOrBlank() && vehicleNumber.isNullOrBlank()) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "phone or vehicleNumber required") } - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } + requireProperty(propertyRepo, propertyId) val guests = mutableSetOf() if (!phone.isNullOrBlank()) { @@ -64,18 +61,9 @@ class Guests( @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: GuestVehicleRequest ): GuestResponse { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) + requireMember(propertyAccess, propertyId, principal) - val property = propertyRepo.findById(propertyId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") - } - val guest = guestRepo.findById(guestId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") - } - if (guest.property.id != property.id) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property") - } + val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId) if (guestVehicleRepo.existsByPropertyIdAndVehicleNumberIgnoreCase(property.id!!, request.vehicleNumber)) { throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists") } @@ -89,11 +77,6 @@ class Guests( return setOf(guest).toResponse(guestVehicleRepo, guestRatingRepo).first() } - private fun requirePrincipal(principal: MyPrincipal?) { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - } } private fun Set.toResponse( diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCardMappings.kt b/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCardMappings.kt new file mode 100644 index 0000000..8638670 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCardMappings.kt @@ -0,0 +1,19 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.controller.dto.IssuedCardResponse +import com.android.trisolarisserver.models.room.IssuedCard + +internal fun IssuedCard.toResponse(): IssuedCardResponse { + return IssuedCardResponse( + id = id!!, + propertyId = property.id!!, + roomId = room.id!!, + roomStayId = roomStay?.id, + cardId = cardId, + cardIndex = cardIndex, + issuedAt = issuedAt.toString(), + expiresAt = expiresAt.toString(), + issuedByUserId = issuedBy?.id, + revokedAt = revokedAt?.toString() + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCards.kt b/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCards.kt index 7041119..1ec6380 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCards.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/IssuedCards.kt @@ -26,7 +26,6 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException import java.time.OffsetDateTime -import java.time.ZoneId import java.util.UUID @RestController @@ -50,15 +49,7 @@ class IssuedCards( @RequestBody request: CardPrepareRequest ): CardPrepareResponse { val actor = requireIssueActor(propertyId, principal) - val stay = roomStayRepo.findById(roomStayId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") - } - if (stay.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property") - } - if (stay.toAt != null) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay closed") - } + val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed") val issuedAt = nowForProperty(stay.property.timezone) val expiresAt = request.expiresAt?.let { parseOffset(it) } ?: stay.toAt @@ -97,15 +88,7 @@ class IssuedCards( if (request.cardIndex <= 0) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardIndex required") } - val stay = roomStayRepo.findById(roomStayId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") - } - if (stay.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property") - } - if (stay.toAt != null) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay closed") - } + val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed") val issuedAt = parseOffset(request.issuedAt) ?: nowForProperty(stay.property.timezone) val expiresAt = parseOffset(request.expiresAt) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt required") @@ -140,12 +123,7 @@ class IssuedCards( @AuthenticationPrincipal principal: MyPrincipal? ): List { requireViewActor(propertyId, principal) - val stay = roomStayRepo.findById(roomStayId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") - } - if (stay.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property") - } + val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId) return issuedCardRepo.findByRoomStayIdOrderByIssuedAtDesc(roomStayId) .map { it.toResponse() } } @@ -183,34 +161,6 @@ class IssuedCards( return card.toResponse() } - private fun parseOffset(value: String?): OffsetDateTime? { - if (value.isNullOrBlank()) return null - return try { - OffsetDateTime.parse(value.trim()) - } catch (_: Exception) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") - } - } - - private fun nowForProperty(timezone: String?): OffsetDateTime { - val zone = try { - if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone) - } catch (_: Exception) { - ZoneId.of("Asia/Kolkata") - } - return OffsetDateTime.now(zone) - } - - private fun encodeBlock(value: String?): String { - val raw = (value ?: "").padEnd(16).take(16) - val bytes = raw.toByteArray(Charsets.UTF_8) - val sb = StringBuilder(bytes.size * 2) - for (b in bytes) { - sb.append(String.format("%02X", b)) - } - return sb.toString() - } - private fun requireMember(propertyId: UUID, principal: MyPrincipal?) { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") @@ -303,73 +253,9 @@ class IssuedCards( val checkSum = calculateChecksum((key ?: "") + newData) return newData + checkSum } - - private fun buildSector0Block2(roomNumber: Int, cardID: Int): String { - val guestID = cardID + 1 - val cardIdStr = cardID.toString().padStart(6, '0') - val guestIdStr = guestID.toString().padStart(6, '0') - val finalRoom = roomNumber.toString().padStart(2, '0') - return "472F${cardIdStr}2F${guestIdStr}00010000${finalRoom}0000" - } - - private fun formatDateComponents(time: OffsetDateTime): String { - val minute = time.minute.toString().padStart(2, '0') - val hour = time.hour.toString().padStart(2, '0') - val day = time.dayOfMonth.toString().padStart(2, '0') - val month = time.monthValue.toString().padStart(2, '0') - val year = time.year.toString().takeLast(2) - return "${minute}${hour}${day}${month}${year}" - } - - private fun calculateChecksum(dataHex: String): String { - val data = hexStringToByteArray(dataHex) - var checksum = 0 - for (byte in data) { - checksum = calculateByteChecksum(byte, checksum) - } - return String.format("%02X", checksum) - } - - private fun calculateByteChecksum(byte: Byte, currentChecksum: Int): Int { - var checksum = currentChecksum - var b = byte.toInt() - for (i in 0 until 8) { - checksum = if ((checksum xor b) and 1 != 0) { - (checksum xor 0x18) shr 1 or 0x80 - } else { - checksum shr 1 - } - b = b shr 1 - } - return checksum - } - - private fun hexStringToByteArray(hexString: String): ByteArray { - val len = hexString.length - val data = ByteArray(len / 2) - for (i in 0 until len step 2) { - data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) - + Character.digit(hexString[i + 1], 16)).toByte() - } - return data - } } private data class Sector0Payload( val key: String, val timeData: String ) -private fun IssuedCard.toResponse(): IssuedCardResponse { - return IssuedCardResponse( - id = id!!, - propertyId = property.id!!, - roomId = room.id!!, - roomStayId = roomStay?.id, - cardId = cardId, - cardIndex = cardIndex, - issuedAt = issuedAt.toString(), - expiresAt = expiresAt.toString(), - issuedByUserId = issuedBy?.id, - revokedAt = revokedAt?.toString() - ) -} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt index 1f98f20..4da7e4f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImages.kt @@ -86,10 +86,7 @@ class RoomImages( @RequestParam("file") file: MultipartFile, @RequestParam(required = false) tagIds: List? ): RoomImageResponse { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) - val room = ensureRoom(propertyId, roomId) + val room = requireRoomAdmin(propertyId, roomId, principal) if (file.isEmpty) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty") @@ -132,10 +129,7 @@ class RoomImages( @PathVariable imageId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ) { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) - ensureRoom(propertyId, roomId) + requireRoomAdmin(propertyId, roomId, principal) val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") @@ -187,10 +181,7 @@ class RoomImages( @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) + requireRoomAdmin(propertyId, roomId, principal) val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") @@ -208,10 +199,7 @@ class RoomImages( @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: RoomImageReorderRequest ) { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) - ensureRoom(propertyId, roomId) + requireRoomAdmin(propertyId, roomId, principal) if (request.imageIds.isEmpty()) { return @@ -239,10 +227,7 @@ class RoomImages( @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: RoomImageReorderRequest ) { - requirePrincipal(principal) - propertyAccess.requireMember(propertyId, principal!!.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) - val room = ensureRoom(propertyId, roomId) + val room = requireRoomAdmin(propertyId, roomId, principal) if (request.imageIds.isEmpty()) { return @@ -299,10 +284,9 @@ class RoomImages( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found") } - private fun requirePrincipal(principal: MyPrincipal?) { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } + private fun requireRoomAdmin(propertyId: UUID, roomId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.room.Room { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) + return ensureRoom(propertyId, roomId) } private fun resolveTags(tagIds: List?): Set { diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomStayFlow.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomStayFlow.kt index d4831b1..f9bc14c 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomStayFlow.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomStayFlow.kt @@ -47,15 +47,12 @@ class RoomStayFlow( ): RoomChangeResponse { val actor = requireActor(propertyId, principal) - val stay = roomStayRepo.findById(roomStayId).orElseThrow { - ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") - } - if (stay.property.id != propertyId) { - throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property") - } - if (stay.toAt != null) { - throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay already closed") - } + val stay = requireOpenRoomStayForProperty( + roomStayRepo, + propertyId, + roomStayId, + "Room stay already closed" + ) if (request.idempotencyKey.isBlank()) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "idempotencyKey required") } @@ -114,22 +111,9 @@ class RoomStayFlow( ) } - private fun parseOffset(value: String?): OffsetDateTime? { - if (value.isNullOrBlank()) return null - return try { - OffsetDateTime.parse(value.trim()) - } catch (_: Exception) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") - } - } - private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { - if (principal == null) { - throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") - } - propertyAccess.requireMember(propertyId, principal.userId) - propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER, Role.STAFF) - return appUserRepo.findById(principal.userId).orElseThrow { + val resolved = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + return appUserRepo.findById(resolved.userId).orElseThrow { ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/TemporaryRoomCards.kt b/src/main/kotlin/com/android/trisolarisserver/controller/TemporaryRoomCards.kt index 20c599b..589871e 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/TemporaryRoomCards.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/TemporaryRoomCards.kt @@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException import java.time.OffsetDateTime -import java.time.ZoneId import java.util.UUID @RestController @@ -105,34 +104,6 @@ class TemporaryRoomCards( return issuedCardRepo.save(card).toResponse() } - private fun parseOffset(value: String?): OffsetDateTime? { - if (value.isNullOrBlank()) return null - return try { - OffsetDateTime.parse(value.trim()) - } catch (_: Exception) { - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") - } - } - - private fun encodeBlock(value: String?): String { - val raw = (value ?: "").padEnd(16).take(16) - val bytes = raw.toByteArray(Charsets.UTF_8) - val sb = StringBuilder(bytes.size * 2) - for (b in bytes) { - sb.append(String.format("%02X", b)) - } - return sb.toString() - } - - private fun nowForProperty(timezone: String?): OffsetDateTime { - val zone = try { - if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone) - } catch (_: Exception) { - ZoneId.of("Asia/Kolkata") - } - return OffsetDateTime.now(zone) - } - private fun requireIssueActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") @@ -175,74 +146,9 @@ class TemporaryRoomCards( val finalData = newData + checkSum return TempSector0Payload(key, finalData) } - - private fun buildSector0Block2(roomNumber: Int, cardID: Int): String { - val guestID = cardID + 1 - val cardIdStr = cardID.toString().padStart(6, '0') - val guestIdStr = guestID.toString().padStart(6, '0') - val finalRoom = roomNumber.toString().padStart(2, '0') - return "472F${cardIdStr}2F${guestIdStr}00010000${finalRoom}0000" - } - - private fun formatDateComponents(time: OffsetDateTime): String { - val minute = time.minute.toString().padStart(2, '0') - val hour = time.hour.toString().padStart(2, '0') - val day = time.dayOfMonth.toString().padStart(2, '0') - val month = time.monthValue.toString().padStart(2, '0') - val year = time.year.toString().takeLast(2) - return "${minute}${hour}${day}${month}${year}" - } - - private fun calculateChecksum(dataHex: String): String { - val data = hexStringToByteArray(dataHex) - var checksum = 0 - for (byte in data) { - checksum = calculateByteChecksum(byte, checksum) - } - return String.format("%02X", checksum) - } - - private fun calculateByteChecksum(byte: Byte, currentChecksum: Int): Int { - var checksum = currentChecksum - var b = byte.toInt() - for (i in 0 until 8) { - checksum = if ((checksum xor b) and 1 != 0) { - (checksum xor 0x18) shr 1 or 0x80 - } else { - checksum shr 1 - } - b = b shr 1 - } - return checksum - } - - private fun hexStringToByteArray(hexString: String): ByteArray { - val len = hexString.length - val data = ByteArray(len / 2) - for (i in 0 until len step 2) { - data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) - + Character.digit(hexString[i + 1], 16)).toByte() - } - return data - } } private data class TempSector0Payload( val key: String, val timeData: String ) - -private fun IssuedCard.toResponse(): IssuedCardResponse { - return IssuedCardResponse( - id = id!!, - propertyId = property.id!!, - roomId = room.id!!, - roomStayId = roomStay?.id, - cardId = cardId, - cardIndex = cardIndex, - issuedAt = issuedAt.toString(), - expiresAt = expiresAt.toString(), - issuedByUserId = issuedBy?.id, - revokedAt = revokedAt?.toString() - ) -} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/AuthResolver.kt b/src/main/kotlin/com/android/trisolarisserver/security/AuthResolver.kt new file mode 100644 index 0000000..4ba0c97 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/AuthResolver.kt @@ -0,0 +1,69 @@ +package com.android.trisolarisserver.security + +import com.android.trisolarisserver.models.property.AppUser +import com.android.trisolarisserver.repo.AppUserRepo +import com.google.firebase.auth.FirebaseAuth +import jakarta.servlet.http.HttpServletRequest +import org.slf4j.LoggerFactory +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.server.ResponseStatusException + +@Component +class AuthResolver( + private val appUserRepo: AppUserRepo +) { + private val logger = LoggerFactory.getLogger(AuthResolver::class.java) + + fun resolveFromRequest(request: HttpServletRequest, createIfMissing: Boolean): MyPrincipal { + val header = request.getHeader(HttpHeaders.AUTHORIZATION) + return resolveFromHeader(header, createIfMissing) + } + + fun resolveFromHeader(header: String?, createIfMissing: Boolean): MyPrincipal { + if (header.isNullOrBlank()) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing Authorization token") + } + if (!header.startsWith("Bearer ")) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Authorization header") + } + val token = header.removePrefix("Bearer ").trim() + return resolveFromToken(token, createIfMissing) + } + + fun resolveFromToken(token: String, createIfMissing: Boolean): MyPrincipal { + val decoded = try { + FirebaseAuth.getInstance().verifyIdToken(token) + } catch (ex: Exception) { + logger.warn("Auth verify failed: {}", ex.message) + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token") + } + val user = resolveUser(decoded.uid, decoded.claims, createIfMissing) + return MyPrincipal( + userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), + firebaseUid = decoded.uid + ) + } + + private fun resolveUser(firebaseUid: String, claims: Map, createIfMissing: Boolean): AppUser { + val existing = appUserRepo.findByFirebaseUid(firebaseUid) + if (existing != null) return existing + if (!createIfMissing) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") + } + val phone = claims["phone_number"] as? String + val name = claims["name"] as? String + val makeSuperAdmin = appUserRepo.count() == 0L + val created = appUserRepo.save( + AppUser( + firebaseUid = firebaseUid, + phoneE164 = phone, + name = name, + superAdmin = makeSuperAdmin + ) + ) + logger.warn("Auth verify auto-created user uid={}, userId={}", firebaseUid, created.id) + return created + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt index df86109..56847b2 100644 --- a/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt +++ b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt @@ -1,7 +1,6 @@ package com.android.trisolarisserver.security import com.android.trisolarisserver.repo.AppUserRepo -import com.google.firebase.auth.FirebaseAuth import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse @@ -11,29 +10,17 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter -import org.springframework.web.server.ResponseStatusException import org.springframework.http.HttpStatus @Component class FirebaseAuthFilter( - private val appUserRepo: AppUserRepo + private val appUserRepo: AppUserRepo, + private val authResolver: AuthResolver ) : OncePerRequestFilter() { private val logger = LoggerFactory.getLogger(FirebaseAuthFilter::class.java) override fun shouldNotFilter(request: HttpServletRequest): Boolean { - val path = request.requestURI - if (path == "/" || path == "/health" || path.startsWith("/auth/")) { - return true - } - return path.matches(Regex("^/properties/[^/]+/rooms/[^/]+/images/[^/]+/file$")) - || (path.matches(Regex("^/properties/[^/]+/rooms/[^/]+/images$")) && request.method.equals("GET", true)) - || (path.matches(Regex("^/properties/[^/]+/rooms/available$")) && request.method.equals("GET", true)) - || (path.matches(Regex("^/properties/[^/]+/rooms/by-type/[^/]+$")) && request.method.equals("GET", true)) - || (path.matches(Regex("^/properties/[^/]+/room-types$")) && request.method.equals("GET", true)) - || path.matches(Regex("^/properties/[^/]+/room-types/[^/]+/images$")) - || (path == "/image-tags" && request.method.equals("GET", true)) - || path == "/icons/png" - || path.matches(Regex("^/icons/png/[^/]+$")) + return PublicEndpoints.isPublic(request) } override fun doFilterInternal( @@ -49,16 +36,9 @@ class FirebaseAuthFilter( } val token = header.removePrefix("Bearer ").trim() try { - val decoded = FirebaseAuth.getInstance().verifyIdToken(token) - val firebaseUid = decoded.uid - val user = appUserRepo.findByFirebaseUid(firebaseUid) - ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") - logger.debug("Auth verified uid={}, userId={}", firebaseUid, user.id) - - val principal = MyPrincipal( - userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), - firebaseUid = firebaseUid - ) + val principal = authResolver.resolveFromToken(token, createIfMissing = false) + val user = appUserRepo.findById(principal.userId).orElse(null) + logger.debug("Auth verified uid={}, userId={}", principal.firebaseUid, user?.id) val auth = UsernamePasswordAuthenticationToken(principal, token, emptyList()) SecurityContextHolder.getContext().authentication = auth filterChain.doFilter(request, response) diff --git a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt new file mode 100644 index 0000000..0af8a3f --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt @@ -0,0 +1,30 @@ +package com.android.trisolarisserver.security + +import jakarta.servlet.http.HttpServletRequest + +internal object PublicEndpoints { + private val roomImageFile = Regex("^/properties/[^/]+/rooms/[^/]+/images/[^/]+/file$") + private val roomImages = Regex("^/properties/[^/]+/rooms/[^/]+/images$") + private val roomsAvailable = Regex("^/properties/[^/]+/rooms/available$") + private val roomsByType = Regex("^/properties/[^/]+/rooms/by-type/[^/]+$") + private val roomTypes = Regex("^/properties/[^/]+/room-types$") + private val roomTypeImages = Regex("^/properties/[^/]+/room-types/[^/]+/images$") + private val iconPngFile = Regex("^/icons/png/[^/]+$") + + fun isPublic(request: HttpServletRequest): Boolean { + val path = request.requestURI + if (path == "/" || path == "/health" || path.startsWith("/auth/")) { + return true + } + val method = request.method.uppercase() + return roomImageFile.matches(path) + || (roomImages.matches(path) && method == "GET") + || (roomsAvailable.matches(path) && method == "GET") + || (roomsByType.matches(path) && method == "GET") + || (roomTypes.matches(path) && method == "GET") + || roomTypeImages.matches(path) + || (path == "/image-tags" && method == "GET") + || path == "/icons/png" + || iconPngFile.matches(path) + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt b/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt index c459c53..cbb6565 100644 --- a/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt +++ b/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt @@ -6,8 +6,8 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.util.matcher.RequestMatcher import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.HttpStatusEntryPoint import org.springframework.http.HttpStatus import com.fasterxml.jackson.databind.ObjectMapper import jakarta.servlet.http.HttpServletRequest @@ -25,16 +25,7 @@ class SecurityConfig( .csrf { it.disable() } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .authorizeHttpRequests { - it.requestMatchers("/", "/health", "/auth/**").permitAll() - it.requestMatchers("/properties/*/rooms/*/images/*/file").permitAll() - it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/*/images").permitAll() - it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/available").permitAll() - it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/by-type/*").permitAll() - it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/room-types").permitAll() - it.requestMatchers("/properties/*/room-types/*/images").permitAll() - it.requestMatchers(org.springframework.http.HttpMethod.GET, "/image-tags").permitAll() - it.requestMatchers("/icons/png").permitAll() - it.requestMatchers("/icons/png/*").permitAll() + it.requestMatchers(RequestMatcher { request -> PublicEndpoints.isPublic(request) }).permitAll() it.anyRequest().authenticated() } .exceptionHandling {