Add user search and property access code flow
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s

This commit is contained in:
androidlover5842
2026-02-01 17:55:06 +05:30
parent 9621c2d652
commit 82486bac53
7 changed files with 396 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
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.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 accessCode = accessCodeRepo.findActiveByPropertyAndCode(request.propertyId, code, now)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid code")
val membershipId = PropertyUserId(propertyId = request.propertyId, 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 = accessCode.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 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")
}
}