diff --git a/build.gradle.kts b/build.gradle.kts index 8ca53a9..b244991 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt new file mode 100644 index 0000000..33da3c3 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt @@ -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 +) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt new file mode 100644 index 0000000..2032074 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt @@ -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") + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt new file mode 100644 index 0000000..5bf2a85 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt @@ -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 { + 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 { + 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 { + 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 + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt new file mode 100644 index 0000000..3f1b741 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt @@ -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 { + 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 + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt index 018ec66..d9e00e2 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt @@ -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 { -// propertyAccess.requireMember(propertyId, principal.userId) -// return roomService.freeRooms(propertyId) -// } + @GetMapping + fun listRooms( + @PathVariable propertyId: UUID, + @AuthenticationPrincipal principal: MyPrincipal? + ): List { + 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() } + } -} \ No newline at end of file + @GetMapping("/board") + fun roomBoard( + @PathVariable propertyId: UUID, + @AuthenticationPrincipal principal: MyPrincipal? + ): List { + 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 { + 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): 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 + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt new file mode 100644 index 0000000..c7afa85 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt @@ -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 +) + +data class PropertyUserResponse( + val userId: UUID, + val propertyId: UUID, + val roles: Set +) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomDtos.kt new file mode 100644 index 0000000..833c731 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomDtos.kt @@ -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 +) + +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? +) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt new file mode 100644 index 0000000..2383a87 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt @@ -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 +) diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/AppUserRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/AppUserRepo.kt new file mode 100644 index 0000000..b3813fa --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/AppUserRepo.kt @@ -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 { + fun findByFirebaseUid(firebaseUid: String): AppUser? + fun existsByFirebaseUid(firebaseUid: String): Boolean + fun findByOrgId(orgId: UUID): List +} diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/OrganizationRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/OrganizationRepo.kt new file mode 100644 index 0000000..86d8492 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/OrganizationRepo.kt @@ -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 { + fun findByName(name: String): Organization? + fun findByNameIgnoreCase(name: String): Organization? + fun existsByNameIgnoreCase(name: String): Boolean +} diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyRepo.kt new file mode 100644 index 0000000..7146c69 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyRepo.kt @@ -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 { + fun existsByOrgIdAndCode(orgId: UUID, code: String): Boolean + fun existsByOrgIdAndCodeAndIdNot(orgId: UUID, code: String, id: UUID): Boolean +} diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyUserRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyUserRepo.kt index 8813bce..bdde252 100644 --- a/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyUserRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/PropertyUserRepo.kt @@ -10,6 +10,32 @@ interface PropertyUserRepo : JpaRepository { fun existsByIdPropertyIdAndIdUserId(propertyId: UUID, userId: UUID): Boolean + fun findByIdUserId(userId: UUID): List + + fun findByIdPropertyId(propertyId: UUID): List + + @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 + + @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 + @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 { @Param("userId") userId: UUID, @Param("roles") roles: Set ): 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 + ): Boolean } diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomRepo.kt index 3e06b31..bf21d52 100644 --- a/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomRepo.kt @@ -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 { + @EntityGraph(attributePaths = ["roomType"]) + fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List + + 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 diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomStayRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomStayRepo.kt new file mode 100644 index 0000000..7928430 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomStayRepo.kt @@ -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 { + @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 +} diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomTypeRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomTypeRepo.kt new file mode 100644 index 0000000..8b2feb2 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/RoomTypeRepo.kt @@ -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 { + fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType? + @EntityGraph(attributePaths = ["property"]) + fun findByPropertyIdOrderByCode(propertyId: UUID): List + fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean + fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt new file mode 100644 index 0000000..a434c9c --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseAuthFilter.kt @@ -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") + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/FirebaseConfig.kt b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseConfig.kt new file mode 100644 index 0000000..8e7237a --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/FirebaseConfig.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/MyPrincipal.kt b/src/main/kotlin/com/android/trisolarisserver/security/MyPrincipal.kt new file mode 100644 index 0000000..22e3f6f --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/MyPrincipal.kt @@ -0,0 +1,8 @@ +package com.android.trisolarisserver.security + +import java.util.UUID + +data class MyPrincipal( + val userId: UUID, + val firebaseUid: String +) diff --git a/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt b/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt new file mode 100644 index 0000000..dba6826 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/security/SecurityConfig.kt @@ -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() + } +} diff --git a/src/main/resources/firebase-service-account.json b/src/main/resources/firebase-service-account.json new file mode 100644 index 0000000..f04cd8e --- /dev/null +++ b/src/main/resources/firebase-service-account.json @@ -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" +} diff --git a/src/test/kotlin/com/android/trisolarisserver/TrisolarisServerApplicationTests.kt b/src/test/kotlin/com/android/trisolarisserver/TrisolarisServerApplicationTests.kt deleted file mode 100644 index 8fc885f..0000000 --- a/src/test/kotlin/com/android/trisolarisserver/TrisolarisServerApplicationTests.kt +++ /dev/null @@ -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() { - } - -}