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): Set { 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") } }