173 lines
7.2 KiB
Kotlin
173 lines
7.2 KiB
Kotlin
package com.android.trisolarisserver.controller.property
|
|
|
|
import com.android.trisolarisserver.component.auth.PropertyAccess
|
|
import com.android.trisolarisserver.controller.common.requirePrincipal
|
|
import com.android.trisolarisserver.controller.dto.property.PropertyAccessCodeCreateRequest
|
|
import com.android.trisolarisserver.controller.dto.property.PropertyAccessCodeJoinRequest
|
|
import com.android.trisolarisserver.controller.dto.property.PropertyAccessCodeResponse
|
|
import com.android.trisolarisserver.controller.dto.property.PropertyUserResponse
|
|
import com.android.trisolarisserver.models.property.PropertyAccessCode
|
|
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.repo.property.AppUserRepo
|
|
import com.android.trisolarisserver.repo.property.PropertyAccessCodeRepo
|
|
import com.android.trisolarisserver.repo.property.PropertyRepo
|
|
import com.android.trisolarisserver.repo.property.PropertyUserRepo
|
|
import com.android.trisolarisserver.security.MyPrincipal
|
|
import org.springframework.http.HttpStatus
|
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
|
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.ResponseStatus
|
|
import org.springframework.web.bind.annotation.RestController
|
|
import org.springframework.web.server.ResponseStatusException
|
|
import org.springframework.transaction.annotation.Transactional
|
|
import java.security.SecureRandom
|
|
import java.time.OffsetDateTime
|
|
import java.util.UUID
|
|
|
|
@RestController
|
|
class PropertyAccessCodes(
|
|
private val propertyAccess: PropertyAccess,
|
|
private val propertyRepo: PropertyRepo,
|
|
private val propertyUserRepo: PropertyUserRepo,
|
|
private val accessCodeRepo: PropertyAccessCodeRepo,
|
|
private val appUserRepo: AppUserRepo
|
|
) {
|
|
|
|
private val secureRandom = SecureRandom()
|
|
|
|
@PostMapping("/properties/{propertyId}/access-codes")
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
fun createAccessCode(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: PropertyAccessCodeCreateRequest
|
|
): PropertyAccessCodeResponse {
|
|
val resolved = requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, resolved.userId)
|
|
propertyAccess.requireAnyRole(propertyId, resolved.userId, Role.ADMIN)
|
|
|
|
val roles = parseRoles(request.roles)
|
|
if (roles.contains(Role.ADMIN)) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "ADMIN cannot be invited by code")
|
|
}
|
|
if (roles.isEmpty()) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one role is required")
|
|
}
|
|
|
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
val actor = appUserRepo.findById(resolved.userId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
|
}
|
|
val now = OffsetDateTime.now()
|
|
val expiresAt = now.plusMinutes(1)
|
|
val code = generateCode(propertyId, now)
|
|
|
|
val accessCode = PropertyAccessCode(
|
|
property = property,
|
|
createdBy = actor,
|
|
code = code,
|
|
expiresAt = expiresAt,
|
|
roles = roles.toMutableSet()
|
|
)
|
|
accessCodeRepo.save(accessCode)
|
|
|
|
return PropertyAccessCodeResponse(
|
|
propertyId = propertyId,
|
|
code = code,
|
|
expiresAt = expiresAt,
|
|
roles = roles.map { it.name }.toSet()
|
|
)
|
|
}
|
|
|
|
@PostMapping("/properties/access-codes/join")
|
|
@Transactional
|
|
fun joinWithAccessCode(
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: PropertyAccessCodeJoinRequest
|
|
): PropertyUserResponse {
|
|
val resolved = requirePrincipal(principal)
|
|
val code = request.code.trim()
|
|
if (code.length != 6 || !code.all { it.isDigit() }) {
|
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid code")
|
|
}
|
|
val now = OffsetDateTime.now()
|
|
val property = resolveProperty(request)
|
|
val accessCode = accessCodeRepo.findActiveByPropertyAndCode(property.id!!, code, now)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid code")
|
|
|
|
val membershipId = PropertyUserId(propertyId = property.id, userId = resolved.userId)
|
|
if (propertyUserRepo.existsById(membershipId)) {
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "User already a member")
|
|
}
|
|
|
|
val user = appUserRepo.findById(resolved.userId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
|
}
|
|
|
|
val propertyUser = PropertyUser(
|
|
id = membershipId,
|
|
property = property,
|
|
user = user,
|
|
roles = accessCode.roles.toMutableSet()
|
|
)
|
|
propertyUserRepo.save(propertyUser)
|
|
|
|
accessCode.usedAt = now
|
|
accessCode.usedBy = user
|
|
accessCodeRepo.save(accessCode)
|
|
|
|
return PropertyUserResponse(
|
|
userId = membershipId.userId!!,
|
|
propertyId = membershipId.propertyId!!,
|
|
roles = propertyUser.roles.map { it.name }.toSet()
|
|
)
|
|
}
|
|
|
|
private fun resolveProperty(request: PropertyAccessCodeJoinRequest): Property {
|
|
val code = request.propertyCode?.trim().orEmpty()
|
|
if (code.isNotBlank()) {
|
|
return propertyRepo.findByCode(code)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
val rawId = request.propertyId?.trim().orEmpty()
|
|
if (rawId.isBlank()) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Property code required")
|
|
}
|
|
val asUuid = runCatching { UUID.fromString(rawId) }.getOrNull()
|
|
return if (asUuid != null) {
|
|
propertyRepo.findById(asUuid).orElseThrow {
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
} else {
|
|
propertyRepo.findByCode(rawId)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
}
|
|
|
|
private fun parseRoles(input: Set<String>): Set<Role> {
|
|
return try {
|
|
input.map { Role.valueOf(it) }.toSet()
|
|
} catch (ex: IllegalArgumentException) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown role")
|
|
}
|
|
}
|
|
|
|
private fun generateCode(propertyId: UUID, now: OffsetDateTime): String {
|
|
repeat(6) {
|
|
val value = secureRandom.nextInt(1_000_000)
|
|
val code = String.format("%06d", value)
|
|
if (!accessCodeRepo.existsActiveByPropertyAndCode(propertyId, code, now)) {
|
|
return code
|
|
}
|
|
}
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Unable to generate code, try again")
|
|
}
|
|
}
|