Deduplicate logic across controllers, auth, and schema fixes
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:
@@ -8,7 +8,6 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font
|
|||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -112,12 +111,4 @@ class EmailStorage(
|
|||||||
atomicMove(tmp, path)
|
atomicMove(tmp, path)
|
||||||
return path.toString()
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,13 +38,7 @@ class LlamaClient(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val headers = HttpHeaders()
|
return post(payload)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askText(content: String, question: String): String {
|
fun askText(content: String, question: String): String {
|
||||||
@@ -61,6 +55,10 @@ class LlamaClient(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
return post(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun post(payload: Map<String, Any>): String {
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
val entity = HttpEntity(payload, headers)
|
val entity = HttpEntity(payload, headers)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.springframework.web.multipart.MultipartFile
|
|||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
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(
|
data class StoredRoomImage(
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
package com.android.trisolarisserver.config
|
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.jdbc.core.JdbcTemplate
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class IssuedCardSchemaFix(
|
class IssuedCardSchemaFix(
|
||||||
private val jdbcTemplate: JdbcTemplate
|
private val jdbcTemplate: JdbcTemplate
|
||||||
) : ApplicationRunner {
|
) : PostgresSchemaFix(jdbcTemplate) {
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun runPostgres(jdbcTemplate: JdbcTemplate) {
|
||||||
val isNullable = jdbcTemplate.queryForObject(
|
val isNullable = jdbcTemplate.queryForObject(
|
||||||
"""
|
"""
|
||||||
select count(*)
|
select count(*)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,24 +1,14 @@
|
|||||||
package com.android.trisolarisserver.config
|
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.jdbc.core.JdbcTemplate
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class RoomImageSchemaFix(
|
class RoomImageSchemaFix(
|
||||||
private val jdbcTemplate: JdbcTemplate
|
private val jdbcTemplate: JdbcTemplate
|
||||||
) : ApplicationRunner {
|
) : PostgresSchemaFix(jdbcTemplate) {
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun runPostgres(jdbcTemplate: JdbcTemplate) {
|
||||||
val hasContentHash = jdbcTemplate.queryForObject(
|
val hasContentHash = jdbcTemplate.queryForObject(
|
||||||
"""
|
"""
|
||||||
select count(*)
|
select count(*)
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
package com.android.trisolarisserver.config
|
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.jdbc.core.JdbcTemplate
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class RoomImageTagSchemaFix(
|
class RoomImageTagSchemaFix(
|
||||||
private val jdbcTemplate: JdbcTemplate
|
private val jdbcTemplate: JdbcTemplate
|
||||||
) : ApplicationRunner {
|
) : PostgresSchemaFix(jdbcTemplate) {
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun runPostgres(jdbcTemplate: JdbcTemplate) {
|
||||||
val hasOldRoomImageId = jdbcTemplate.queryForObject(
|
val hasOldRoomImageId = jdbcTemplate.queryForObject(
|
||||||
"""
|
"""
|
||||||
select count(*)
|
select count(*)
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
package com.android.trisolarisserver.config
|
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.jdbc.core.JdbcTemplate
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class RoomTypeSchemaFix(
|
class RoomTypeSchemaFix(
|
||||||
private val jdbcTemplate: JdbcTemplate
|
private val jdbcTemplate: JdbcTemplate
|
||||||
) : ApplicationRunner {
|
) : PostgresSchemaFix(jdbcTemplate) {
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun runPostgres(jdbcTemplate: JdbcTemplate) {
|
||||||
val hasActive = jdbcTemplate.queryForObject(
|
val hasActive = jdbcTemplate.queryForObject(
|
||||||
"""
|
"""
|
||||||
select count(*)
|
select count(*)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.android.trisolarisserver.controller.dto.UserResponse
|
|||||||
import com.android.trisolarisserver.repo.AppUserRepo
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
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.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import com.android.trisolarisserver.security.AuthResolver
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
class Auth(
|
class Auth(
|
||||||
private val appUserRepo: AppUserRepo,
|
private val appUserRepo: AppUserRepo,
|
||||||
private val propertyUserRepo: PropertyUserRepo
|
private val propertyUserRepo: PropertyUserRepo,
|
||||||
|
private val authResolver: AuthResolver
|
||||||
) {
|
) {
|
||||||
private val logger = LoggerFactory.getLogger(Auth::class.java)
|
private val logger = LoggerFactory.getLogger(Auth::class.java)
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ class Auth(
|
|||||||
request: HttpServletRequest
|
request: HttpServletRequest
|
||||||
): ResponseEntity<AuthResponse> {
|
): ResponseEntity<AuthResponse> {
|
||||||
logger.info("Auth verify hit, principalPresent={}", principal != null)
|
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()
|
return resolved.toResponseEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +45,8 @@ class Auth(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
request: HttpServletRequest
|
request: HttpServletRequest
|
||||||
): ResponseEntity<AuthResponse> {
|
): ResponseEntity<AuthResponse> {
|
||||||
val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request)
|
val resolved = principal?.let { ResolveResult(it) }
|
||||||
|
?: ResolveResult(authResolver.resolveFromRequest(request, createIfMissing = true))
|
||||||
return resolved.toResponseEntity()
|
return resolved.toResponseEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +56,8 @@ class Auth(
|
|||||||
request: HttpServletRequest,
|
request: HttpServletRequest,
|
||||||
@RequestBody body: UpdateMeRequest
|
@RequestBody body: UpdateMeRequest
|
||||||
): ResponseEntity<AuthResponse> {
|
): ResponseEntity<AuthResponse> {
|
||||||
val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request)
|
val resolved = principal?.let { ResolveResult(it) }
|
||||||
|
?: ResolveResult(authResolver.resolveFromRequest(request, createIfMissing = true))
|
||||||
if (resolved.principal == null) {
|
if (resolved.principal == null) {
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
.body(AuthResponse(status = "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<AuthResponse> {
|
private fun ResolveResult.toResponseEntity(): ResponseEntity<AuthResponse> {
|
||||||
return if (principal == null) {
|
return if (principal == null) {
|
||||||
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
|||||||
@@ -232,23 +232,12 @@ class BookingFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
||||||
requirePrincipal(principal)
|
val resolved = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
return appUserRepo.findById(resolved.userId).orElseThrow {
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
|
||||||
return appUserRepo.findById(principal.userId).orElseThrow {
|
|
||||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
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 {
|
private fun parseTransportMode(value: String): TransportMode {
|
||||||
return try {
|
return try {
|
||||||
TransportMode.valueOf(value)
|
TransportMode.valueOf(value)
|
||||||
@@ -269,9 +258,4 @@ class BookingFlow(
|
|||||||
return allowed.contains(mode)
|
return allowed.contains(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
|
||||||
if (principal == null) {
|
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ class GuestDocuments(
|
|||||||
@RequestParam("bookingId") bookingId: UUID,
|
@RequestParam("bookingId") bookingId: UUID,
|
||||||
@RequestPart("file") file: MultipartFile
|
@RequestPart("file") file: MultipartFile
|
||||||
): GuestDocumentResponse {
|
): GuestDocumentResponse {
|
||||||
val user = requireUser(principal)
|
val user = requireUser(appUserRepo, principal)
|
||||||
propertyAccess.requireMember(propertyId, user.id!!)
|
propertyAccess.requireMember(propertyId, user.id!!)
|
||||||
propertyAccess.requireAnyRole(propertyId, user.id!!, Role.ADMIN, Role.MANAGER)
|
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")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Video files are not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
|
||||||
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 booking = bookingRepo.findById(bookingId).orElseThrow {
|
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
}
|
}
|
||||||
@@ -106,9 +98,7 @@ class GuestDocuments(
|
|||||||
@PathVariable guestId: UUID,
|
@PathVariable guestId: UUID,
|
||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
): List<GuestDocumentResponse> {
|
): List<GuestDocumentResponse> {
|
||||||
requirePrincipal(principal)
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
||||||
|
|
||||||
return guestDocumentRepo
|
return guestDocumentRepo
|
||||||
.findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId)
|
.findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId)
|
||||||
@@ -124,9 +114,7 @@ class GuestDocuments(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
): ResponseEntity<FileSystemResource> {
|
): ResponseEntity<FileSystemResource> {
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
requirePrincipal(principal)
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
||||||
} else if (!tokenService.validateToken(token, documentId.toString())) {
|
} else if (!tokenService.validateToken(token, documentId.toString())) {
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token")
|
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(
|
data class GuestDocumentResponse(
|
||||||
|
|||||||
@@ -42,18 +42,9 @@ class GuestRatings(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: GuestRatingCreateRequest
|
@RequestBody request: GuestRatingCreateRequest
|
||||||
): GuestRatingResponse {
|
): GuestRatingResponse {
|
||||||
requirePrincipal(principal)
|
val resolved = requireMember(propertyAccess, propertyId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
|
|
||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
|
||||||
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 booking = bookingRepo.findById(request.bookingId).orElseThrow {
|
val booking = bookingRepo.findById(request.bookingId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
@@ -75,7 +66,7 @@ class GuestRatings(
|
|||||||
booking = booking,
|
booking = booking,
|
||||||
score = score,
|
score = score,
|
||||||
notes = request.notes?.trim(),
|
notes = request.notes?.trim(),
|
||||||
createdBy = appUserRepo.findById(principal.userId).orElse(null)
|
createdBy = appUserRepo.findById(resolved.userId).orElse(null)
|
||||||
)
|
)
|
||||||
guestRatingRepo.save(rating)
|
guestRatingRepo.save(rating)
|
||||||
return rating.toResponse()
|
return rating.toResponse()
|
||||||
@@ -87,18 +78,9 @@ class GuestRatings(
|
|||||||
@PathVariable guestId: UUID,
|
@PathVariable guestId: UUID,
|
||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
): List<GuestRatingResponse> {
|
): List<GuestRatingResponse> {
|
||||||
requirePrincipal(principal)
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
|
|
||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val (_, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return guestRatingRepo.findByGuestIdOrderByCreatedAtDesc(guestId).map { it.toResponse() }
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,16 +33,13 @@ class Guests(
|
|||||||
@RequestParam(required = false) phone: String?,
|
@RequestParam(required = false) phone: String?,
|
||||||
@RequestParam(required = false) vehicleNumber: String?
|
@RequestParam(required = false) vehicleNumber: String?
|
||||||
): List<GuestResponse> {
|
): List<GuestResponse> {
|
||||||
requirePrincipal(principal)
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
|
|
||||||
if (phone.isNullOrBlank() && vehicleNumber.isNullOrBlank()) {
|
if (phone.isNullOrBlank() && vehicleNumber.isNullOrBlank()) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "phone or vehicleNumber required")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "phone or vehicleNumber required")
|
||||||
}
|
}
|
||||||
|
|
||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
requireProperty(propertyRepo, propertyId)
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
val guests = mutableSetOf<Guest>()
|
val guests = mutableSetOf<Guest>()
|
||||||
if (!phone.isNullOrBlank()) {
|
if (!phone.isNullOrBlank()) {
|
||||||
@@ -64,18 +61,9 @@ class Guests(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: GuestVehicleRequest
|
@RequestBody request: GuestVehicleRequest
|
||||||
): GuestResponse {
|
): GuestResponse {
|
||||||
requirePrincipal(principal)
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
|
|
||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
|
||||||
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")
|
|
||||||
}
|
|
||||||
if (guestVehicleRepo.existsByPropertyIdAndVehicleNumberIgnoreCase(property.id!!, request.vehicleNumber)) {
|
if (guestVehicleRepo.existsByPropertyIdAndVehicleNumberIgnoreCase(property.id!!, request.vehicleNumber)) {
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists")
|
||||||
}
|
}
|
||||||
@@ -89,11 +77,6 @@ class Guests(
|
|||||||
return setOf(guest).toResponse(guestVehicleRepo, guestRatingRepo).first()
|
return setOf(guest).toResponse(guestVehicleRepo, guestRatingRepo).first()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
|
||||||
if (principal == null) {
|
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Set<Guest>.toResponse(
|
private fun Set<Guest>.toResponse(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
|||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneId
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -50,15 +49,7 @@ class IssuedCards(
|
|||||||
@RequestBody request: CardPrepareRequest
|
@RequestBody request: CardPrepareRequest
|
||||||
): CardPrepareResponse {
|
): CardPrepareResponse {
|
||||||
val actor = requireIssueActor(propertyId, principal)
|
val actor = requireIssueActor(propertyId, principal)
|
||||||
val stay = roomStayRepo.findById(roomStayId).orElseThrow {
|
val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed")
|
||||||
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 issuedAt = nowForProperty(stay.property.timezone)
|
val issuedAt = nowForProperty(stay.property.timezone)
|
||||||
val expiresAt = request.expiresAt?.let { parseOffset(it) } ?: stay.toAt
|
val expiresAt = request.expiresAt?.let { parseOffset(it) } ?: stay.toAt
|
||||||
@@ -97,15 +88,7 @@ class IssuedCards(
|
|||||||
if (request.cardIndex <= 0) {
|
if (request.cardIndex <= 0) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardIndex required")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardIndex required")
|
||||||
}
|
}
|
||||||
val stay = roomStayRepo.findById(roomStayId).orElseThrow {
|
val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed")
|
||||||
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 issuedAt = parseOffset(request.issuedAt) ?: nowForProperty(stay.property.timezone)
|
val issuedAt = parseOffset(request.issuedAt) ?: nowForProperty(stay.property.timezone)
|
||||||
val expiresAt = parseOffset(request.expiresAt)
|
val expiresAt = parseOffset(request.expiresAt)
|
||||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt required")
|
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt required")
|
||||||
@@ -140,12 +123,7 @@ class IssuedCards(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
): List<IssuedCardResponse> {
|
): List<IssuedCardResponse> {
|
||||||
requireViewActor(propertyId, principal)
|
requireViewActor(propertyId, principal)
|
||||||
val stay = roomStayRepo.findById(roomStayId).orElseThrow {
|
val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId)
|
||||||
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 issuedCardRepo.findByRoomStayIdOrderByIssuedAtDesc(roomStayId)
|
return issuedCardRepo.findByRoomStayIdOrderByIssuedAtDesc(roomStayId)
|
||||||
.map { it.toResponse() }
|
.map { it.toResponse() }
|
||||||
}
|
}
|
||||||
@@ -183,34 +161,6 @@ class IssuedCards(
|
|||||||
return card.toResponse()
|
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?) {
|
private fun requireMember(propertyId: UUID, principal: MyPrincipal?) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
@@ -303,73 +253,9 @@ class IssuedCards(
|
|||||||
val checkSum = calculateChecksum((key ?: "") + newData)
|
val checkSum = calculateChecksum((key ?: "") + newData)
|
||||||
return newData + checkSum
|
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(
|
private data class Sector0Payload(
|
||||||
val key: String,
|
val key: String,
|
||||||
val timeData: 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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -86,10 +86,7 @@ class RoomImages(
|
|||||||
@RequestParam("file") file: MultipartFile,
|
@RequestParam("file") file: MultipartFile,
|
||||||
@RequestParam(required = false) tagIds: List<UUID>?
|
@RequestParam(required = false) tagIds: List<UUID>?
|
||||||
): RoomImageResponse {
|
): RoomImageResponse {
|
||||||
requirePrincipal(principal)
|
val room = requireRoomAdmin(propertyId, roomId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
||||||
val room = ensureRoom(propertyId, roomId)
|
|
||||||
|
|
||||||
if (file.isEmpty) {
|
if (file.isEmpty) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
|
||||||
@@ -132,10 +129,7 @@ class RoomImages(
|
|||||||
@PathVariable imageId: UUID,
|
@PathVariable imageId: UUID,
|
||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
) {
|
) {
|
||||||
requirePrincipal(principal)
|
requireRoomAdmin(propertyId, roomId, 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)
|
val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId)
|
||||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
||||||
@@ -187,10 +181,7 @@ class RoomImages(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: RoomImageTagUpdateRequest
|
@RequestBody request: RoomImageTagUpdateRequest
|
||||||
) {
|
) {
|
||||||
requirePrincipal(principal)
|
requireRoomAdmin(propertyId, roomId, 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)
|
val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId)
|
||||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
|
||||||
@@ -208,10 +199,7 @@ class RoomImages(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: RoomImageReorderRequest
|
@RequestBody request: RoomImageReorderRequest
|
||||||
) {
|
) {
|
||||||
requirePrincipal(principal)
|
requireRoomAdmin(propertyId, roomId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
||||||
ensureRoom(propertyId, roomId)
|
|
||||||
|
|
||||||
if (request.imageIds.isEmpty()) {
|
if (request.imageIds.isEmpty()) {
|
||||||
return
|
return
|
||||||
@@ -239,10 +227,7 @@ class RoomImages(
|
|||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: RoomImageReorderRequest
|
@RequestBody request: RoomImageReorderRequest
|
||||||
) {
|
) {
|
||||||
requirePrincipal(principal)
|
val room = requireRoomAdmin(propertyId, roomId, principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
||||||
val room = ensureRoom(propertyId, roomId)
|
|
||||||
|
|
||||||
if (request.imageIds.isEmpty()) {
|
if (request.imageIds.isEmpty()) {
|
||||||
return
|
return
|
||||||
@@ -299,10 +284,9 @@ class RoomImages(
|
|||||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found")
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
private fun requireRoomAdmin(propertyId: UUID, roomId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.room.Room {
|
||||||
if (principal == null) {
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
return ensureRoom(propertyId, roomId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveTags(tagIds: List<UUID>?): Set<RoomImageTag> {
|
private fun resolveTags(tagIds: List<UUID>?): Set<RoomImageTag> {
|
||||||
|
|||||||
@@ -47,15 +47,12 @@ class RoomStayFlow(
|
|||||||
): RoomChangeResponse {
|
): RoomChangeResponse {
|
||||||
val actor = requireActor(propertyId, principal)
|
val actor = requireActor(propertyId, principal)
|
||||||
|
|
||||||
val stay = roomStayRepo.findById(roomStayId).orElseThrow {
|
val stay = requireOpenRoomStayForProperty(
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found")
|
roomStayRepo,
|
||||||
}
|
propertyId,
|
||||||
if (stay.property.id != propertyId) {
|
roomStayId,
|
||||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property")
|
"Room stay already closed"
|
||||||
}
|
)
|
||||||
if (stay.toAt != null) {
|
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay already closed")
|
|
||||||
}
|
|
||||||
if (request.idempotencyKey.isBlank()) {
|
if (request.idempotencyKey.isBlank()) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "idempotencyKey required")
|
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 {
|
private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
||||||
if (principal == null) {
|
val resolved = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
return appUserRepo.findById(resolved.userId).orElseThrow {
|
||||||
}
|
|
||||||
propertyAccess.requireMember(propertyId, principal.userId)
|
|
||||||
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
|
||||||
return appUserRepo.findById(principal.userId).orElseThrow {
|
|
||||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
|||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneId
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -105,34 +104,6 @@ class TemporaryRoomCards(
|
|||||||
return issuedCardRepo.save(card).toResponse()
|
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 {
|
private fun requireIssueActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
@@ -175,74 +146,9 @@ class TemporaryRoomCards(
|
|||||||
val finalData = newData + checkSum
|
val finalData = newData + checkSum
|
||||||
return TempSector0Payload(key, finalData)
|
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(
|
private data class TempSector0Payload(
|
||||||
val key: String,
|
val key: String,
|
||||||
val timeData: 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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<String, Any>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.android.trisolarisserver.security
|
package com.android.trisolarisserver.security
|
||||||
|
|
||||||
import com.android.trisolarisserver.repo.AppUserRepo
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
|
||||||
import jakarta.servlet.FilterChain
|
import jakarta.servlet.FilterChain
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
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.security.core.context.SecurityContextHolder
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import org.springframework.web.filter.OncePerRequestFilter
|
import org.springframework.web.filter.OncePerRequestFilter
|
||||||
import org.springframework.web.server.ResponseStatusException
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class FirebaseAuthFilter(
|
class FirebaseAuthFilter(
|
||||||
private val appUserRepo: AppUserRepo
|
private val appUserRepo: AppUserRepo,
|
||||||
|
private val authResolver: AuthResolver
|
||||||
) : OncePerRequestFilter() {
|
) : OncePerRequestFilter() {
|
||||||
private val logger = LoggerFactory.getLogger(FirebaseAuthFilter::class.java)
|
private val logger = LoggerFactory.getLogger(FirebaseAuthFilter::class.java)
|
||||||
|
|
||||||
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
|
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
|
||||||
val path = request.requestURI
|
return PublicEndpoints.isPublic(request)
|
||||||
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/[^/]+$"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doFilterInternal(
|
override fun doFilterInternal(
|
||||||
@@ -49,16 +36,9 @@ class FirebaseAuthFilter(
|
|||||||
}
|
}
|
||||||
val token = header.removePrefix("Bearer ").trim()
|
val token = header.removePrefix("Bearer ").trim()
|
||||||
try {
|
try {
|
||||||
val decoded = FirebaseAuth.getInstance().verifyIdToken(token)
|
val principal = authResolver.resolveFromToken(token, createIfMissing = false)
|
||||||
val firebaseUid = decoded.uid
|
val user = appUserRepo.findById(principal.userId).orElse(null)
|
||||||
val user = appUserRepo.findByFirebaseUid(firebaseUid)
|
logger.debug("Auth verified uid={}, userId={}", principal.firebaseUid, user?.id)
|
||||||
?: 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 auth = UsernamePasswordAuthenticationToken(principal, token, emptyList())
|
val auth = UsernamePasswordAuthenticationToken(principal, token, emptyList())
|
||||||
SecurityContextHolder.getContext().authentication = auth
|
SecurityContextHolder.getContext().authentication = auth
|
||||||
filterChain.doFilter(request, response)
|
filterChain.doFilter(request, response)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
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.UsernamePasswordAuthenticationFilter
|
||||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
@@ -25,16 +25,7 @@ class SecurityConfig(
|
|||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers("/", "/health", "/auth/**").permitAll()
|
it.requestMatchers(RequestMatcher { request -> PublicEndpoints.isPublic(request) }).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.anyRequest().authenticated()
|
it.anyRequest().authenticated()
|
||||||
}
|
}
|
||||||
.exceptionHandling {
|
.exceptionHandling {
|
||||||
|
|||||||
Reference in New Issue
Block a user