Deduplicate logic across controllers, auth, and schema fixes
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s

This commit is contained in:
androidlover5842
2026-01-28 23:03:48 +05:30
parent f8bdb8e759
commit 9b64b34ab9
26 changed files with 412 additions and 510 deletions

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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 {