added auth,property db and room db,org db
This commit is contained in:
@@ -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>
|
||||
)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
@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
|
||||
) {
|
||||
|
||||
// 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)
|
||||
// }
|
||||
@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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.android.trisolarisserver.security
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class MyPrincipal(
|
||||
val userId: UUID,
|
||||
val firebaseUid: String
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
13
src/main/resources/firebase-service-account.json
Normal file
13
src/main/resources/firebase-service-account.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user