diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt index 08055d5..75d8022 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Auth.kt @@ -4,6 +4,7 @@ import com.android.trisolarisserver.controller.dto.PropertyUserResponse import com.android.trisolarisserver.controller.dto.UserResponse import com.android.trisolarisserver.repo.AppUserRepo import com.android.trisolarisserver.repo.OrganizationRepo +import com.android.trisolarisserver.repo.PendingUserRepo import com.android.trisolarisserver.repo.PropertyUserRepo import com.android.trisolarisserver.security.MyPrincipal import com.google.firebase.auth.FirebaseAuth @@ -16,13 +17,15 @@ 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 +import java.util.UUID @RestController @RequestMapping("/auth") class Auth( private val appUserRepo: AppUserRepo, private val propertyUserRepo: PropertyUserRepo, - private val organizationRepo: OrganizationRepo + private val organizationRepo: OrganizationRepo, + private val pendingUserRepo: PendingUserRepo ) { private val logger = LoggerFactory.getLogger(Auth::class.java) @@ -32,12 +35,8 @@ class Auth( request: HttpServletRequest ): AuthResponse { logger.info("Auth verify hit, principalPresent={}", principal != null) - val resolved = principal ?: resolvePrincipalFromHeader(request) - return if (resolved == null) { - AuthResponse(status = "NEEDS_ORG") - } else { - buildAuthResponse(resolved) - } + val resolved = principal?.let { ResolveResult(it, null) } ?: resolvePrincipalFromHeader(request) + return resolved.toResponse() } @GetMapping("/me") @@ -45,12 +44,8 @@ class Auth( @AuthenticationPrincipal principal: MyPrincipal?, request: HttpServletRequest ): AuthResponse { - val resolved = principal ?: resolvePrincipalFromHeader(request) - return if (resolved == null) { - AuthResponse(status = "NEEDS_ORG") - } else { - buildAuthResponse(resolved) - } + val resolved = principal?.let { ResolveResult(it, null) } ?: resolvePrincipalFromHeader(request) + return resolved.toResponse() } private fun buildAuthResponse(principal: MyPrincipal): AuthResponse { @@ -78,7 +73,7 @@ class Auth( ) } - private fun resolvePrincipalFromHeader(request: HttpServletRequest): MyPrincipal? { + private fun resolvePrincipalFromHeader(request: HttpServletRequest): ResolveResult { val header = request.getHeader("Authorization") ?: throw ResponseStatusException( HttpStatus.UNAUTHORIZED, "Missing Authorization token" @@ -99,7 +94,17 @@ class Auth( val orgs = organizationRepo.findAll() if (orgs.isEmpty()) { logger.warn("Auth verify user not found for uid={}, orgCount=0", decoded.uid) - return null + val phone = decoded.claims["phone_number"] as? String + val name = decoded.claims["name"] as? String + val existing = pendingUserRepo.findByFirebaseUid(decoded.uid) + val pending = existing ?: pendingUserRepo.save( + com.android.trisolarisserver.models.property.PendingUser( + firebaseUid = decoded.uid, + phoneE164 = phone, + name = name + ) + ) + return ResolveResult(null, pending.id) } if (orgs.size == 1) { val org = orgs.first() @@ -114,24 +119,45 @@ class Auth( ) ) logger.warn("Auth verify auto-created user uid={}, userId={}, orgId={}", decoded.uid, created.id, org.id) - return MyPrincipal( - userId = created.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), - firebaseUid = decoded.uid + return ResolveResult( + MyPrincipal( + userId = created.id + ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), + firebaseUid = decoded.uid + ), + null ) } logger.warn("Auth verify user not found for uid={}, orgCount={}", decoded.uid, orgs.size) throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } logger.warn("Auth verify resolved uid={}, userId={}", decoded.uid, user.id) - return MyPrincipal( - userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), - firebaseUid = decoded.uid + return ResolveResult( + MyPrincipal( + userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"), + firebaseUid = decoded.uid + ), + null ) } + + private fun ResolveResult.toResponse(): AuthResponse { + return if (principal == null) { + AuthResponse(status = "NEEDS_ORG", pendingUserId = pendingUserId) + } else { + buildAuthResponse(principal) + } + } } data class AuthResponse( val status: String, val user: UserResponse? = null, - val properties: List = emptyList() + val properties: List = emptyList(), + val pendingUserId: UUID? = null +) + +private data class ResolveResult( + val principal: MyPrincipal?, + val pendingUserId: UUID? ) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/property/PendingUser.kt b/src/main/kotlin/com/android/trisolarisserver/models/property/PendingUser.kt new file mode 100644 index 0000000..3b24472 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/property/PendingUser.kt @@ -0,0 +1,33 @@ +package com.android.trisolarisserver.models.property + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table( + name = "pending_user", + uniqueConstraints = [UniqueConstraint(columnNames = ["firebase_uid"])] +) +class PendingUser( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @Column(name = "firebase_uid", nullable = false) + var firebaseUid: String, + + @Column(name = "phone_e164") + var phoneE164: String? = null, + + var name: String? = null, + + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") + val createdAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PendingUserRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PendingUserRepo.kt new file mode 100644 index 0000000..f160b95 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PendingUserRepo.kt @@ -0,0 +1,9 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.property.PendingUser +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface PendingUserRepo : JpaRepository { + fun findByFirebaseUid(firebaseUid: String): PendingUser? +}