Deduplicate logic across controllers, auth, and schema fixes
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package com.android.trisolarisserver.security
|
||||
|
||||
import com.android.trisolarisserver.models.property.AppUser
|
||||
import com.android.trisolarisserver.repo.AppUserRepo
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@Component
|
||||
class AuthResolver(
|
||||
private val appUserRepo: AppUserRepo
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(AuthResolver::class.java)
|
||||
|
||||
fun resolveFromRequest(request: HttpServletRequest, createIfMissing: Boolean): MyPrincipal {
|
||||
val header = request.getHeader(HttpHeaders.AUTHORIZATION)
|
||||
return resolveFromHeader(header, createIfMissing)
|
||||
}
|
||||
|
||||
fun resolveFromHeader(header: String?, createIfMissing: Boolean): MyPrincipal {
|
||||
if (header.isNullOrBlank()) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing Authorization token")
|
||||
}
|
||||
if (!header.startsWith("Bearer ")) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Authorization header")
|
||||
}
|
||||
val token = header.removePrefix("Bearer ").trim()
|
||||
return resolveFromToken(token, createIfMissing)
|
||||
}
|
||||
|
||||
fun resolveFromToken(token: String, createIfMissing: Boolean): MyPrincipal {
|
||||
val decoded = try {
|
||||
FirebaseAuth.getInstance().verifyIdToken(token)
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("Auth verify failed: {}", ex.message)
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token")
|
||||
}
|
||||
val user = resolveUser(decoded.uid, decoded.claims, createIfMissing)
|
||||
return MyPrincipal(
|
||||
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
|
||||
firebaseUid = decoded.uid
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveUser(firebaseUid: String, claims: Map<String, Any>, createIfMissing: Boolean): AppUser {
|
||||
val existing = appUserRepo.findByFirebaseUid(firebaseUid)
|
||||
if (existing != null) return existing
|
||||
if (!createIfMissing) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||
}
|
||||
val phone = claims["phone_number"] as? String
|
||||
val name = claims["name"] as? String
|
||||
val makeSuperAdmin = appUserRepo.count() == 0L
|
||||
val created = appUserRepo.save(
|
||||
AppUser(
|
||||
firebaseUid = firebaseUid,
|
||||
phoneE164 = phone,
|
||||
name = name,
|
||||
superAdmin = makeSuperAdmin
|
||||
)
|
||||
)
|
||||
logger.warn("Auth verify auto-created user uid={}, userId={}", firebaseUid, created.id)
|
||||
return created
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.android.trisolarisserver.security
|
||||
|
||||
import com.android.trisolarisserver.repo.AppUserRepo
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
@@ -11,29 +10,17 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
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
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val authResolver: AuthResolver
|
||||
) : OncePerRequestFilter() {
|
||||
private val logger = LoggerFactory.getLogger(FirebaseAuthFilter::class.java)
|
||||
|
||||
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
|
||||
val path = request.requestURI
|
||||
if (path == "/" || path == "/health" || path.startsWith("/auth/")) {
|
||||
return true
|
||||
}
|
||||
return path.matches(Regex("^/properties/[^/]+/rooms/[^/]+/images/[^/]+/file$"))
|
||||
|| (path.matches(Regex("^/properties/[^/]+/rooms/[^/]+/images$")) && request.method.equals("GET", true))
|
||||
|| (path.matches(Regex("^/properties/[^/]+/rooms/available$")) && request.method.equals("GET", true))
|
||||
|| (path.matches(Regex("^/properties/[^/]+/rooms/by-type/[^/]+$")) && request.method.equals("GET", true))
|
||||
|| (path.matches(Regex("^/properties/[^/]+/room-types$")) && request.method.equals("GET", true))
|
||||
|| path.matches(Regex("^/properties/[^/]+/room-types/[^/]+/images$"))
|
||||
|| (path == "/image-tags" && request.method.equals("GET", true))
|
||||
|| path == "/icons/png"
|
||||
|| path.matches(Regex("^/icons/png/[^/]+$"))
|
||||
return PublicEndpoints.isPublic(request)
|
||||
}
|
||||
|
||||
override fun doFilterInternal(
|
||||
@@ -49,16 +36,9 @@ class FirebaseAuthFilter(
|
||||
}
|
||||
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")
|
||||
logger.debug("Auth verified uid={}, userId={}", firebaseUid, user.id)
|
||||
|
||||
val principal = MyPrincipal(
|
||||
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
|
||||
firebaseUid = firebaseUid
|
||||
)
|
||||
val principal = authResolver.resolveFromToken(token, createIfMissing = false)
|
||||
val user = appUserRepo.findById(principal.userId).orElse(null)
|
||||
logger.debug("Auth verified uid={}, userId={}", principal.firebaseUid, user?.id)
|
||||
val auth = UsernamePasswordAuthenticationToken(principal, token, emptyList())
|
||||
SecurityContextHolder.getContext().authentication = auth
|
||||
filterChain.doFilter(request, response)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.android.trisolarisserver.security
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
|
||||
internal object PublicEndpoints {
|
||||
private val roomImageFile = Regex("^/properties/[^/]+/rooms/[^/]+/images/[^/]+/file$")
|
||||
private val roomImages = Regex("^/properties/[^/]+/rooms/[^/]+/images$")
|
||||
private val roomsAvailable = Regex("^/properties/[^/]+/rooms/available$")
|
||||
private val roomsByType = Regex("^/properties/[^/]+/rooms/by-type/[^/]+$")
|
||||
private val roomTypes = Regex("^/properties/[^/]+/room-types$")
|
||||
private val roomTypeImages = Regex("^/properties/[^/]+/room-types/[^/]+/images$")
|
||||
private val iconPngFile = Regex("^/icons/png/[^/]+$")
|
||||
|
||||
fun isPublic(request: HttpServletRequest): Boolean {
|
||||
val path = request.requestURI
|
||||
if (path == "/" || path == "/health" || path.startsWith("/auth/")) {
|
||||
return true
|
||||
}
|
||||
val method = request.method.uppercase()
|
||||
return roomImageFile.matches(path)
|
||||
|| (roomImages.matches(path) && method == "GET")
|
||||
|| (roomsAvailable.matches(path) && method == "GET")
|
||||
|| (roomsByType.matches(path) && method == "GET")
|
||||
|| (roomTypes.matches(path) && method == "GET")
|
||||
|| roomTypeImages.matches(path)
|
||||
|| (path == "/image-tags" && method == "GET")
|
||||
|| path == "/icons/png"
|
||||
|| iconPngFile.matches(path)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
||||
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.util.matcher.RequestMatcher
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
||||
import org.springframework.http.HttpStatus
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
@@ -25,16 +25,7 @@ class SecurityConfig(
|
||||
.csrf { it.disable() }
|
||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||
.authorizeHttpRequests {
|
||||
it.requestMatchers("/", "/health", "/auth/**").permitAll()
|
||||
it.requestMatchers("/properties/*/rooms/*/images/*/file").permitAll()
|
||||
it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/*/images").permitAll()
|
||||
it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/available").permitAll()
|
||||
it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/rooms/by-type/*").permitAll()
|
||||
it.requestMatchers(org.springframework.http.HttpMethod.GET, "/properties/*/room-types").permitAll()
|
||||
it.requestMatchers("/properties/*/room-types/*/images").permitAll()
|
||||
it.requestMatchers(org.springframework.http.HttpMethod.GET, "/image-tags").permitAll()
|
||||
it.requestMatchers("/icons/png").permitAll()
|
||||
it.requestMatchers("/icons/png/*").permitAll()
|
||||
it.requestMatchers(RequestMatcher { request -> PublicEndpoints.isPublic(request) }).permitAll()
|
||||
it.anyRequest().authenticated()
|
||||
}
|
||||
.exceptionHandling {
|
||||
|
||||
Reference in New Issue
Block a user