added auth,property db and room db,org db

This commit is contained in:
androidlover5842
2026-01-24 16:11:40 +05:30
parent c360ff627d
commit 16f279fe5a
22 changed files with 1113 additions and 22 deletions

View File

@@ -30,6 +30,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("tools.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.google.firebase:firebase-admin:9.7.0")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
testImplementation("org.springframework.boot:spring-boot-starter-flyway-test")

View File

@@ -0,0 +1,64 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
import com.android.trisolarisserver.controller.dto.UserResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import org.springframework.http.HttpStatus
@RestController
@RequestMapping("/auth")
class Auth(
private val appUserRepo: AppUserRepo,
private val propertyUserRepo: PropertyUserRepo
) {
@PostMapping("/verify")
fun verify(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
return buildAuthResponse(principal)
}
@GetMapping("/me")
fun me(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
return buildAuthResponse(principal)
}
private fun buildAuthResponse(principal: MyPrincipal?): AuthResponse {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
val user = appUserRepo.findById(principal.userId).orElseThrow {
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
}
val memberships = propertyUserRepo.findByIdUserId(principal.userId).map {
PropertyUserResponse(
userId = it.id.userId!!,
propertyId = it.id.propertyId!!,
roles = it.roles.map { role -> role.name }.toSet()
)
}
return AuthResponse(
user = UserResponse(
id = user.id!!,
orgId = user.org.id!!,
firebaseUid = user.firebaseUid,
phoneE164 = user.phoneE164,
name = user.name,
disabled = user.disabled
),
properties = memberships
)
}
}
data class AuthResponse(
val user: UserResponse,
val properties: List<PropertyUserResponse>
)

View File

@@ -0,0 +1,78 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.controller.dto.OrgCreateRequest
import com.android.trisolarisserver.controller.dto.OrgResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.OrganizationRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.models.property.Organization
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.UUID
@RestController
@RequestMapping("/orgs")
class Orgs(
private val orgRepo: OrganizationRepo,
private val appUserRepo: AppUserRepo,
private val propertyUserRepo: PropertyUserRepo
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createOrg(
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: OrgCreateRequest
): OrgResponse {
val user = requireUser(principal)
val orgId = user.org.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Org missing")
if (!propertyUserRepo.hasAnyRoleInOrg(orgId, user.id!!, setOf(Role.ADMIN))) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
}
val org = Organization().apply {
name = request.name
}
val saved = orgRepo.save(org)
return OrgResponse(
id = saved.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"),
name = saved.name ?: ""
)
}
@GetMapping("/{orgId}")
fun getOrg(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): OrgResponse {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
val org = orgRepo.findById(orgId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
}
return OrgResponse(
id = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"),
name = org.name ?: ""
)
}
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")
}
}
}

View File

@@ -0,0 +1,286 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.PropertyCreateRequest
import com.android.trisolarisserver.controller.dto.PropertyResponse
import com.android.trisolarisserver.controller.dto.PropertyUpdateRequest
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
import com.android.trisolarisserver.controller.dto.PropertyUserRoleRequest
import com.android.trisolarisserver.controller.dto.UserCreateRequest
import com.android.trisolarisserver.controller.dto.UserResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.OrganizationRepo
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.models.property.Property
import com.android.trisolarisserver.models.property.PropertyUser
import com.android.trisolarisserver.models.property.PropertyUserId
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.UUID
@RestController
class Properties(
private val propertyAccess: PropertyAccess,
private val propertyRepo: PropertyRepo,
private val orgRepo: OrganizationRepo,
private val propertyUserRepo: PropertyUserRepo,
private val appUserRepo: AppUserRepo
) {
@PostMapping("/orgs/{orgId}/properties")
@ResponseStatus(HttpStatus.CREATED)
fun createProperty(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: PropertyCreateRequest
): PropertyResponse {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
requireOrgRole(orgId, user.id!!, Role.ADMIN)
if (propertyRepo.existsByOrgIdAndCode(orgId, request.code)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
}
val org = orgRepo.findById(orgId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
}
val property = Property(
org = org,
code = request.code,
name = request.name,
timezone = request.timezone ?: "Asia/Kolkata",
currency = request.currency ?: "INR",
active = request.active ?: true
)
val saved = propertyRepo.save(property)
return saved.toResponse()
}
@GetMapping("/orgs/{orgId}/properties")
fun listProperties(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<PropertyResponse> {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
val propertyIds = propertyUserRepo.findPropertyIdsByOrgAndUser(orgId, user.id!!)
return propertyRepo.findAllById(propertyIds).map { it.toResponse() }
}
@PostMapping("/orgs/{orgId}/users")
@ResponseStatus(HttpStatus.CREATED)
fun createUser(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: UserCreateRequest
): UserResponse {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
requireOrgRole(orgId, user.id!!, Role.ADMIN)
if (appUserRepo.existsByFirebaseUid(request.firebaseUid)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "User already exists")
}
val org = orgRepo.findById(orgId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
}
val newUser = com.android.trisolarisserver.models.property.AppUser(
org = org,
firebaseUid = request.firebaseUid,
phoneE164 = request.phoneE164,
name = request.name,
disabled = request.disabled ?: false
)
val saved = appUserRepo.save(newUser)
return saved.toUserResponse()
}
@GetMapping("/orgs/{orgId}/users")
fun listUsers(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<UserResponse> {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
requireOrgRole(orgId, user.id!!, Role.ADMIN, Role.MANAGER)
return appUserRepo.findByOrgId(orgId).map { it.toUserResponse() }
}
@GetMapping("/properties/{propertyId}/users")
fun listPropertyUsers(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<PropertyUserResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val users = propertyUserRepo.findByIdPropertyId(propertyId)
return users.map {
PropertyUserResponse(
userId = it.id.userId!!,
propertyId = it.id.propertyId!!,
roles = it.roles.map { role -> role.name }.toSet()
)
}
}
@PutMapping("/properties/{propertyId}/users/{userId}/roles")
fun upsertPropertyUserRoles(
@PathVariable propertyId: UUID,
@PathVariable userId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: PropertyUserRoleRequest
): PropertyUserResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val actorRoles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
val allowedRoles = when {
actorRoles.contains(Role.ADMIN) -> Role.entries.toSet()
actorRoles.contains(Role.MANAGER) -> setOf(Role.STAFF, Role.AGENT)
else -> emptySet()
}
if (allowedRoles.isEmpty()) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
}
val requestedRoles = request.roles.map { Role.valueOf(it) }.toSet()
if (!allowedRoles.containsAll(requestedRoles)) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Role not allowed")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val targetUser = appUserRepo.findById(userId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
}
if (targetUser.org.id != property.org.id) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "User not in property org")
}
val propertyUser = PropertyUser(
id = PropertyUserId(propertyId = propertyId, userId = userId),
property = property,
user = targetUser,
roles = requestedRoles.toMutableSet()
)
val saved = propertyUserRepo.save(propertyUser)
return PropertyUserResponse(
userId = saved.id.userId!!,
propertyId = saved.id.propertyId!!,
roles = saved.roles.map { it.name }.toSet()
)
}
@DeleteMapping("/properties/{propertyId}/users/{userId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deletePropertyUser(
@PathVariable propertyId: UUID,
@PathVariable userId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN)
val id = PropertyUserId(propertyId = propertyId, userId = userId)
if (propertyUserRepo.existsById(id)) {
propertyUserRepo.deleteById(id)
}
}
@PutMapping("/properties/{propertyId}")
fun updateProperty(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: PropertyUpdateRequest
): PropertyResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN)
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
if (propertyRepo.existsByOrgIdAndCodeAndIdNot(property.org.id!!, request.code, propertyId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
}
property.code = request.code
property.name = request.name
property.timezone = request.timezone ?: property.timezone
property.currency = request.currency ?: property.currency
property.active = request.active ?: property.active
return propertyRepo.save(property).toResponse()
}
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")
}
}
private fun requireOrgRole(orgId: UUID, userId: UUID, vararg roles: Role) {
if (!propertyUserRepo.hasAnyRoleInOrg(orgId, userId, roles.toSet())) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
}
}
}
private fun Property.toResponse(): PropertyResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
val orgId = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing")
return PropertyResponse(
id = id,
orgId = orgId,
code = code,
name = name,
timezone = timezone,
currency = currency,
active = active
)
}
private fun com.android.trisolarisserver.models.property.AppUser.toUserResponse(): UserResponse {
val id = this.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "User id missing")
val orgId = this.org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing")
return UserResponse(
id = id,
orgId = orgId,
firebaseUid = firebaseUid,
phoneE164 = phoneE164,
name = name,
disabled = disabled
)
}

View File

@@ -0,0 +1,136 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.RoomTypeResponse
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.repo.RoomRepo
import com.android.trisolarisserver.db.repo.RoomTypeRepo
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RoomType
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.UUID
@RestController
@RequestMapping("/properties/{propertyId}/room-types")
class RoomTypes(
private val propertyAccess: PropertyAccess,
private val roomTypeRepo: RoomTypeRepo,
private val roomRepo: RoomRepo,
private val propertyRepo: PropertyRepo
) {
@GetMapping
fun listRoomTypes(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomTypeResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
return roomTypeRepo.findByPropertyIdOrderByCode(propertyId).map { it.toResponse() }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createRoomType(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomTypeUpsertRequest
): RoomTypeResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
if (roomTypeRepo.existsByPropertyIdAndCode(propertyId, request.code)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room type code already exists for property")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val roomType = RoomType(
property = property,
code = request.code,
name = request.name,
baseOccupancy = request.baseOccupancy ?: 2,
maxOccupancy = request.maxOccupancy ?: 3
)
return roomTypeRepo.save(roomType).toResponse()
}
@PutMapping("/{roomTypeId}")
fun updateRoomType(
@PathVariable propertyId: UUID,
@PathVariable roomTypeId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomTypeUpsertRequest
): RoomTypeResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val roomType = roomTypeRepo.findByIdAndPropertyId(roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
if (roomTypeRepo.existsByPropertyIdAndCodeAndIdNot(propertyId, request.code, roomTypeId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room type code already exists for property")
}
roomType.code = request.code
roomType.name = request.name
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
return roomTypeRepo.save(roomType).toResponse()
}
@DeleteMapping("/{roomTypeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteRoomType(
@PathVariable propertyId: UUID,
@PathVariable roomTypeId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val roomType = roomTypeRepo.findByIdAndPropertyId(roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
if (roomRepo.existsByPropertyIdAndRoomTypeId(propertyId, roomTypeId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Cannot delete room type with rooms")
}
roomTypeRepo.delete(roomType)
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
}
private fun RoomType.toResponse(): RoomTypeResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Room type id missing")
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
return RoomTypeResponse(
id = id,
propertyId = propertyId,
code = code,
name = name,
baseOccupancy = baseOccupancy,
maxOccupancy = maxOccupancy
)
}

View File

@@ -1,18 +1,201 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import org.springframework.stereotype.Component
import com.android.trisolarisserver.controller.dto.RoomAvailabilityResponse
import com.android.trisolarisserver.controller.dto.RoomBoardResponse
import com.android.trisolarisserver.controller.dto.RoomBoardStatus
import com.android.trisolarisserver.controller.dto.RoomResponse
import com.android.trisolarisserver.controller.dto.RoomUpsertRequest
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.db.repo.RoomRepo
import com.android.trisolarisserver.db.repo.RoomStayRepo
import com.android.trisolarisserver.db.repo.RoomTypeRepo
import com.android.trisolarisserver.models.room.Room
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.UUID
class Rooms {
// private val propertyAccess: PropertyAccess;
// @GetMapping("/properties/{propertyId}/rooms/free")
// fun freeRooms(@PathVariable propertyId: UUID, principal: MyPrincipal): List<RoomDto> {
// propertyAccess.requireMember(propertyId, principal.userId)
// return roomService.freeRooms(propertyId)
// }
@RestController
@RequestMapping("/properties/{propertyId}/rooms")
class Rooms(
private val propertyAccess: PropertyAccess,
private val roomRepo: RoomRepo,
private val roomStayRepo: RoomStayRepo,
private val propertyRepo: PropertyRepo,
private val roomTypeRepo: RoomTypeRepo,
private val propertyUserRepo: PropertyUserRepo
) {
@GetMapping
fun listRooms(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
if (isAgentOnly(roles)) {
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
return rooms
.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
.map { it.toRoomResponse() }
}
return rooms
.map { it.toRoomResponse() }
}
@GetMapping("/board")
fun roomBoard(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomBoardResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
val mapped = rooms.map { room ->
val status = when {
room.maintenance -> RoomBoardStatus.MAINTENANCE
!room.active -> RoomBoardStatus.INACTIVE
occupiedRoomIds.contains(room.id) -> RoomBoardStatus.OCCUPIED
else -> RoomBoardStatus.FREE
}
RoomBoardResponse(
roomNumber = room.roomNumber,
roomTypeName = room.roomType.name,
status = status
)
}
return if (isAgentOnly(roles)) mapped.filter { it.status == RoomBoardStatus.FREE } else mapped
}
@GetMapping("/availability")
fun roomAvailability(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomAvailabilityResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
val grouped = freeRooms.groupBy { it.roomType.name }
return grouped.entries.map { (typeName, roomList) ->
RoomAvailabilityResponse(
roomTypeName = typeName,
freeRoomNumbers = roomList.map { it.roomNumber }
)
}.sortedBy { it.roomTypeName }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createRoom(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomUpsertRequest
): RoomResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
if (roomRepo.existsByPropertyIdAndRoomNumber(propertyId, request.roomNumber)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
val room = Room(
property = property,
roomType = roomType,
roomNumber = request.roomNumber,
floor = request.floor,
hasNfc = request.hasNfc,
active = request.active,
maintenance = request.maintenance,
notes = request.notes
)
return roomRepo.save(room).toRoomResponse()
}
@PutMapping("/{roomId}")
fun updateRoom(
@PathVariable propertyId: UUID,
@PathVariable roomId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomUpsertRequest
): RoomResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val room = roomRepo.findByIdAndPropertyId(roomId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found for property")
if (roomRepo.existsByPropertyIdAndRoomNumberAndIdNot(propertyId, request.roomNumber, roomId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
}
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
room.roomNumber = request.roomNumber
room.floor = request.floor
room.roomType = roomType
room.hasNfc = request.hasNfc
room.active = request.active
room.maintenance = request.maintenance
room.notes = request.notes
return roomRepo.save(room).toRoomResponse()
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
private fun isAgentOnly(roles: Set<Role>): Boolean {
if (!roles.contains(Role.AGENT)) return false
val privileged = setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.HOUSEKEEPING, Role.FINANCE)
return roles.none { it in privileged }
}
}
private fun Room.toRoomResponse(): RoomResponse {
val roomId = id ?: throw IllegalStateException("Room id is null")
val roomTypeId = roomType.id ?: throw IllegalStateException("Room type id is null")
return RoomResponse(
id = roomId,
roomNumber = roomNumber,
floor = floor,
roomTypeId = roomTypeId,
roomTypeName = roomType.name,
hasNfc = hasNfc,
active = active,
maintenance = maintenance,
notes = notes
)
}

View File

@@ -0,0 +1,64 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class OrgCreateRequest(
val name: String
)
data class OrgResponse(
val id: UUID,
val name: String
)
data class PropertyCreateRequest(
val code: String,
val name: String,
val timezone: String? = null,
val currency: String? = null,
val active: Boolean? = null
)
data class PropertyUpdateRequest(
val code: String,
val name: String,
val timezone: String? = null,
val currency: String? = null,
val active: Boolean? = null
)
data class PropertyResponse(
val id: UUID,
val orgId: UUID,
val code: String,
val name: String,
val timezone: String,
val currency: String,
val active: Boolean
)
data class UserCreateRequest(
val firebaseUid: String,
val phoneE164: String? = null,
val name: String? = null,
val disabled: Boolean? = null
)
data class UserResponse(
val id: UUID,
val orgId: UUID,
val firebaseUid: String?,
val phoneE164: String?,
val name: String?,
val disabled: Boolean
)
data class PropertyUserRoleRequest(
val roles: Set<String>
)
data class PropertyUserResponse(
val userId: UUID,
val propertyId: UUID,
val roles: Set<String>
)

View File

@@ -0,0 +1,43 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class RoomResponse(
val id: UUID,
val roomNumber: Int,
val floor: Int?,
val roomTypeId: UUID,
val roomTypeName: String,
val hasNfc: Boolean,
val active: Boolean,
val maintenance: Boolean,
val notes: String?
)
data class RoomBoardResponse(
val roomNumber: Int,
val roomTypeName: String,
val status: RoomBoardStatus
)
data class RoomAvailabilityResponse(
val roomTypeName: String,
val freeRoomNumbers: List<Int>
)
enum class RoomBoardStatus {
FREE,
OCCUPIED,
MAINTENANCE,
INACTIVE
}
data class RoomUpsertRequest(
val roomNumber: Int,
val floor: Int?,
val roomTypeId: UUID,
val hasNfc: Boolean,
val active: Boolean,
val maintenance: Boolean,
val notes: String?
)

View File

@@ -0,0 +1,19 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class RoomTypeUpsertRequest(
val code: String,
val name: String,
val baseOccupancy: Int? = null,
val maxOccupancy: Int? = null
)
data class RoomTypeResponse(
val id: UUID,
val propertyId: UUID,
val code: String,
val name: String,
val baseOccupancy: Int,
val maxOccupancy: Int
)

View File

@@ -0,0 +1,11 @@
package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.property.AppUser
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface AppUserRepo : JpaRepository<AppUser, UUID> {
fun findByFirebaseUid(firebaseUid: String): AppUser?
fun existsByFirebaseUid(firebaseUid: String): Boolean
fun findByOrgId(orgId: UUID): List<AppUser>
}

View File

@@ -0,0 +1,11 @@
package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.property.Organization
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface OrganizationRepo : JpaRepository<Organization, UUID> {
fun findByName(name: String): Organization?
fun findByNameIgnoreCase(name: String): Organization?
fun existsByNameIgnoreCase(name: String): Boolean
}

View File

@@ -0,0 +1,10 @@
package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.property.Property
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface PropertyRepo : JpaRepository<Property, UUID> {
fun existsByOrgIdAndCode(orgId: UUID, code: String): Boolean
fun existsByOrgIdAndCodeAndIdNot(orgId: UUID, code: String, id: UUID): Boolean
}

View File

@@ -10,6 +10,32 @@ interface PropertyUserRepo : JpaRepository<PropertyUser, PropertyUserId> {
fun existsByIdPropertyIdAndIdUserId(propertyId: UUID, userId: UUID): Boolean
fun findByIdUserId(userId: UUID): List<PropertyUser>
fun findByIdPropertyId(propertyId: UUID): List<PropertyUser>
@Query("""
select r
from PropertyUser pu join pu.roles r
where pu.id.propertyId = :propertyId
and pu.id.userId = :userId
""")
fun findRolesByPropertyAndUser(
@Param("propertyId") propertyId: UUID,
@Param("userId") userId: UUID
): Set<Role>
@Query("""
select pu.property.id
from PropertyUser pu
where pu.user.id = :userId
and pu.property.org.id = :orgId
""")
fun findPropertyIdsByOrgAndUser(
@Param("orgId") orgId: UUID,
@Param("userId") userId: UUID
): List<UUID>
@Query("""
select case when count(pu) > 0 then true else false end
from PropertyUser pu join pu.roles r
@@ -22,4 +48,17 @@ interface PropertyUserRepo : JpaRepository<PropertyUser, PropertyUserId> {
@Param("userId") userId: UUID,
@Param("roles") roles: Set<Role>
): Boolean
@Query("""
select case when count(pu) > 0 then true else false end
from PropertyUser pu join pu.roles r
where pu.user.id = :userId
and pu.property.org.id = :orgId
and r in :roles
""")
fun hasAnyRoleInOrg(
@Param("orgId") orgId: UUID,
@Param("userId") userId: UUID,
@Param("roles") roles: Set<Role>
): Boolean
}

View File

@@ -2,6 +2,7 @@ package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.room.Room
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
@@ -9,6 +10,17 @@ import java.util.UUID
interface RoomRepo : JpaRepository<Room, UUID> {
@EntityGraph(attributePaths = ["roomType"])
fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List<Room>
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): Room?
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean
fun existsByPropertyIdAndRoomNumberAndIdNot(propertyId: UUID, roomNumber: Int, id: UUID): Boolean
fun existsByPropertyIdAndRoomTypeId(propertyId: UUID, roomTypeId: UUID): Boolean
@Query("""
select r
from Room r

View File

@@ -0,0 +1,17 @@
package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.room.RoomStay
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import java.util.UUID
interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
@Query("""
select rs.room.id
from RoomStay rs
where rs.property.id = :propertyId
and rs.toAt is null
""")
fun findOccupiedRoomIds(@Param("propertyId") propertyId: UUID): List<UUID>
}

View File

@@ -0,0 +1,14 @@
package com.android.trisolarisserver.db.repo
import com.android.trisolarisserver.models.room.RoomType
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
@EntityGraph(attributePaths = ["property"])
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean
}

View File

@@ -0,0 +1,49 @@
package com.android.trisolarisserver.security
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.google.firebase.auth.FirebaseAuth
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
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
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val header = request.getHeader(HttpHeaders.AUTHORIZATION)
if (header.isNullOrBlank() || !header.startsWith("Bearer ")) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing Authorization token")
return
}
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")
val principal = MyPrincipal(
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
firebaseUid = firebaseUid
)
val auth = UsernamePasswordAuthenticationToken(principal, token, emptyList())
SecurityContextHolder.getContext().authentication = auth
filterChain.doFilter(request, response)
} catch (ex: Exception) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token")
}
}
}

View File

@@ -0,0 +1,23 @@
package com.android.trisolarisserver.security
import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
@Configuration
class FirebaseConfig {
init {
if (FirebaseApp.getApps().isEmpty()) {
val options = FirebaseOptions.builder()
.setCredentials(
GoogleCredentials.fromStream(
ClassPathResource("firebase-service-account.json").inputStream
)
)
.build()
FirebaseApp.initializeApp(options)
}
}
}

View File

@@ -0,0 +1,8 @@
package com.android.trisolarisserver.security
import java.util.UUID
data class MyPrincipal(
val userId: UUID,
val firebaseUid: String
)

View File

@@ -0,0 +1,23 @@
package com.android.trisolarisserver.security
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
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.authentication.UsernamePasswordAuthenticationFilter
@Configuration
class SecurityConfig(
private val firebaseAuthFilter: FirebaseAuthFilter
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests { it.anyRequest().authenticated() }
.addFilterBefore(firebaseAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
return http.build()
}
}

View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "hoteltrisolaris-b1c34",
"private_key_id": "cae4516aa92df363d16eaa47c7e342fdce96be85",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDD8G//DYD8x82U\n6BY7ZcKVeakU8anRXLMUkgI7E0p1MerU7otlqwawEseH/2YPwfsy3ciig/mVZ0Bl\nj53CQyDhnhBp6MMZmZER/kyIWX97qo2qgLEoehwrqyCfs7cvekar23q9Botb+xOF\nvtlgWxjdHw8zrtEkOOeJ0or3nUhEShVYbWIQ0ZRRsQlOCaDrtMVQF6vY7auYvMj2\nG9bXTHyjbMKygh5W813OBgulW3JcMoWOP3+Umdmyo2mytdOJxY3RP+qN60LVdstw\nGTQuEOvIokSeXVoLmXm/Ms5j95/fp+PPmRrR8P+F32Owh+VzGcQlWMGB1sSNU1wB\nv+1w4GDNAgMBAAECggEAAboBKqSyUcfq8lh3Na/IXqvTRxl4Dx27gD9nIKEjY1P8\nx0KQ3OT8apnHw1WHTzU84u5cYb46+UuPIDX7RGZ2CDbt2xkPew7E3f05LGxpeKwA\nkpOOvBYTYHkiEPYy84qmy8Xj132Sxc05F1EetkAnQG+RITn1otWTiL3ftp3esKdY\ngcRxfTeeOydzibE0q+sl7II59yNeGAWyE4o6ltM9a6wLIo1xckJW2T07lKjq/mLx\nkp46YCvpZD7JOiARXBakoDNrPylMLZLDDg3Zuzz8lTHmVSHk321WuR52c9aJMx+i\ngQzoeQP0HsCvfrT+K7KNZDVwYU2AKTL21NoeiWnlKwKBgQDzmiuXVFVf2eOP6wwx\nS7lfPtsD2Y3wP4KAw2XrM91Ir/XttiuyNrzowvsh1UUqg1BDEUsoqrXKHyNMhl0Q\nV4O6N7OtHzjr0oozo/FHr7Il3o/bN7l8OKOqYregbDqmAHxRSXVHgCpESnyQYpnQ\ncwalVbgABhztXJ76cl6eAV8d3wKBgQDN6Uc4KGD3LFzLHC1fKwiLrW4YNcBEJwHw\nw/l3B3jYrBxDxAFPqmSC+NTpasu9Z99lewxHqia/fiTO+MtcD+air3iJa8utu1wG\nCV81YCE8MTpLr665jbaTF7BXua7GKGLtUPvrSn6i+zH0J8/+FiPtC/2V7wjDJ1cO\nFNUO7Bl+0wKBgQCfeooZO1vdMY96U94ak8GbKlJGFfKHm3x7gfDCZ6TyBkiRxFad\nCJrqI2Q3xSDP8UHldnfm+sOivHnminx4y2Jw0jCuISeps59IqYa3cL3Hbwps8PFc\n8tOrI4+l1dUbgmvg55+BHNYO+VjNSc/7GKL8ML8SPO5JMv7dZWyuMqWrrwKBgQCi\nOeT7cIyckB33g5aXgP71lMjFWCvHRfg4aR300jU6d7a5CQaDblpL+aE82P/1lI2j\nlSMinwJyIf779XW6bWimyZosonnQwWkJ9H5HPhpRIvOrx5jf5a9vCd3L76WrxwvR\nrtkbEhDddQxxMKCkrWrWinjalH2Ryz/B/1WwsQCRMwKBgB40+SlUjH+r4InaalQm\nGV5K9Z98Gc//y55Re2wO8GnrrAZuwcfsFOyhL9Ql+5jAhz+ucfTFT3dpWjbwzB54\nmrge8YCGdAB104udXFNlytdbycidBDhvK0l7AaYAjqowyLiruni5euxNFMuNLLCd\nnXsjgE8vcbW/NiZW7KWS62ye\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@hoteltrisolaris-b1c34.iam.gserviceaccount.com",
"client_id": "118438729250144586337",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40hoteltrisolaris-b1c34.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -1,13 +0,0 @@
package com.android.trisolarisserver
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class TrisolarisServerApplicationTests {
@Test
fun contextLoads() {
}
}