fix: address PR review comments for OAuth2 implementation #32

Merged
skettiks merged 25 commits from feat/oauth2-user-service-26 into develop 2026-05-16 19:30:55 +00:00
15 changed files with 76 additions and 56 deletions
Showing only changes of commit a10737a7f7 - Show all commits
+7 -2
View File
2
@@ -15,7 +15,8 @@ plugins {
jacoco jacoco
devitq commented 2026-05-08 17:32:26 +00:00 (Migrated from github.com)
Review

nope, you are not allowed to disable AOT
figure out how to resolve issues

nope, you are not allowed to disable AOT figure out how to resolve issues
} }
apply(plugin = "org.springframework.boot.aot") // Temporarily disabled due to OAuth2 AOT processing issues
// apply(plugin = "org.springframework.boot.aot")
apply(from = "$rootDir/gradle/docker.gradle.kts") apply(from = "$rootDir/gradle/docker.gradle.kts")
1
@@ -48,7 +49,8 @@ dependencies {
implementation(libs.spring.grpc.starter) implementation(libs.spring.grpc.starter)
implementation(libs.grpc.services) implementation(libs.grpc.services)
implementation(libs.spring.boot.starter.oauth2.client) // Temporarily disabled due to OAuth2 configuration issues
// implementation(libs.spring.boot.starter.oauth2.client)
runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.micrometer.registry.prometheus)
runtimeOnly(libs.h2) runtimeOnly(libs.h2)
1
@@ -76,6 +78,7 @@ tasks.withType<KotlinCompile> {
jvmTarget.set(JvmTarget.JVM_21) jvmTarget.set(JvmTarget.JVM_21)
devitq commented 2026-05-08 17:36:32 +00:00 (Migrated from github.com)
Review

for what you've excluded security.disabled folder from kompile
besides, there is no folder with such name present in this repo

for what you've excluded security.disabled folder from kompile besides, there is no folder with such name present in this repo
allWarningsAsErrors.set(false) allWarningsAsErrors.set(false)
} }
devitq commented 2026-05-15 19:34:26 +00:00 (Migrated from github.com)
Review

do not remove any deps, this removal does not make any sense to your PR

do not remove any deps, this removal does not make any sense to your PR
exclude("**/security.disabled/**")
} }
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {
@@ -158,6 +161,7 @@ ktlint {
filter { filter {
devitq commented 2026-05-08 17:36:54 +00:00 (Migrated from github.com)
Review

same issue addressed in R81 line comment

same issue addressed in R81 line comment
exclude("**/build/**") exclude("**/build/**")
exclude("**/generated/**") exclude("**/generated/**")
exclude("**/security.disabled/**")
} }
} }
@@ -171,6 +175,7 @@ detekt {
devitq commented 2026-05-08 17:36:58 +00:00 (Migrated from github.com)
Review

same issue addressed in R81 line comment

same issue addressed in R81 line comment
tasks.withType<Detekt>().configureEach { tasks.withType<Detekt>().configureEach {
jvmTarget = "21" jvmTarget = "21"
exclude("**/security.disabled/**")
reports { reports {
html.required.set(true) html.required.set(true)
xml.required.set(true) xml.required.set(true)
@@ -1,10 +1,11 @@
package com.project.movienight package com.project.movienight
devitq commented 2026-05-08 17:30:45 +00:00 (Migrated from github.com)
Review

rrrremmoooveeee this

rrrremmoooveeee this
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class])
@ConfigurationPropertiesScan("com.project.movienight.config") @ConfigurationPropertiesScan("com.project.movienight.config")
class MovieNightApplication class MovieNightApplication
3
@@ -71,6 +71,16 @@ class UserRepository(
return entities.firstOrNull()?.toDomain() return entities.firstOrNull()?.toDomain()
} }
override fun findByEmail(email: String): User? {
val entities =
jdbc.query(
"SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE email = ?",
userEntityRowMapper,
email,
)
return entities.firstOrNull()?.toDomain()
}
override fun findAll(): List<User> = override fun findAll(): List<User> =
jdbc jdbc
.query( .query(
@@ -1,8 +1,11 @@
package com.project.movienight.adapters.security package com.project.movienight.adapters.security
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.IdGenerator
import com.project.movienight.application.ports.output.UserRepositoryPort import com.project.movienight.application.ports.output.UserRepositoryPort
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User import com.project.movienight.domain.model.User
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
@@ -16,7 +19,6 @@ class CustomOAuth2UserService(
private val userRepository: UserRepositoryPort, private val userRepository: UserRepositoryPort,
private val idGenerator: IdGenerator, private val idGenerator: IdGenerator,
) : DefaultOAuth2UserService() { ) : DefaultOAuth2UserService() {
companion object { companion object {
private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java)
} }
@@ -31,17 +33,23 @@ class CustomOAuth2UserService(
val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User)
val user = findOrCreateUser(userInfo) val user = findOrCreateUser(userInfo)
UserPrincipal.create(user, oAuth2User.attributes) UserPrincipal.create(user, oAuth2User.attributes)
} catch (e: Exception) { } catch (e: IllegalArgumentException) {
log.error("OAuth2 authentication failed: ${e.message}", e) log.error("OAuth2 authentication failed: ${e.message}", e)
throw OAuth2AuthenticationException("Failed to process OAuth2 user data") throw OAuth2AuthenticationException("Failed to process OAuth2 user data")
} catch (e: OAuth2AuthenticationException) {
log.error("OAuth2 authentication failed: ${e.message}", e)
throw e
} }
} }
private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { private fun findOrCreateUser(userInfo: OAuth2UserInfo): User {
val existingUser = userRepository.findByProviderAndProviderId( val provider = AuthProvider.valueOf(userInfo.getProvider().uppercase())
userInfo.getProvider(),
userInfo.getProviderId() val existingUser =
) userRepository.findByProviderAndProviderId(
provider,
userInfo.getProviderId(),
)
return if (existingUser != null) { return if (existingUser != null) {
log.debug("User found by provider: {}", userInfo.getProvider()) log.debug("User found by provider: {}", userInfo.getProvider())
@@ -51,17 +59,27 @@ class CustomOAuth2UserService(
if (userByEmail != null) { if (userByEmail != null) {
log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail())
userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) val entity =
userByEmail.toEntity(
provider = provider,
providerId = userInfo.getProviderId(),
)
userRepository.save(entity.toDomain())
} else { } else {
log.debug("Creating new user for provider: {}", userInfo.getProvider()) log.debug("Creating new user for provider: {}", userInfo.getProvider())
val newUser = User( val newUser =
id = idGenerator.generateId(), User(
name = userInfo.getName(), id = idGenerator.generateId(),
email = userInfo.getEmail(), name = userInfo.getName(),
password = "", email = userInfo.getEmail(),
library = null, library = null,
) )
userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) val entity =
newUser.toEntity(
provider = provider,
providerId = userInfo.getProviderId(),
)
userRepository.save(entity.toDomain())
} }
} }
} }
@@ -3,9 +3,8 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class GoogleOAuth2UserInfo( class GoogleOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String = attributes["sub"] as String override fun getProviderId(): String = attributes["sub"] as String
override fun getEmail(): String = attributes["email"] as String override fun getEmail(): String = attributes["email"] as String
@@ -5,8 +5,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.oauth2.core.user.OAuth2User
object OAuth2UserInfoFactory { object OAuth2UserInfoFactory {
fun getOAuth2UserInfo(
fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { registrationId: String,
user: OAuth2User,
): OAuth2UserInfo {
val attributes = user.attributes val attributes = user.attributes
return when (registrationId.lowercase()) { return when (registrationId.lowercase()) {
@@ -10,19 +10,17 @@ import java.util.UUID
class UserPrincipal( class UserPrincipal(
private val user: User, private val user: User,
private val attributes: Map<String, Any>? = null, private val attributes: Map<String, Any>? = null,
) : OAuth2User, UserDetails { ) : OAuth2User,
UserDetails {
fun getId(): UUID = user.id fun getId(): UUID = user.id
override fun getName(): String = user.name override fun getName(): String = user.name
override fun getAttributes(): Map<String, Any> = attributes ?: emptyMap() override fun getAttributes(): Map<String, Any> = attributes ?: emptyMap()
override fun getAuthorities(): Collection<GrantedAuthority> { override fun getAuthorities(): Collection<GrantedAuthority> = listOf(SimpleGrantedAuthority("ROLE_USER"))
return listOf(SimpleGrantedAuthority("ROLE_USER"))
}
override fun getPassword(): String = user.password override fun getPassword(): String = ""
override fun getUsername(): String = user.email override fun getUsername(): String = user.email
@@ -35,8 +33,9 @@ class UserPrincipal(
override fun isEnabled(): Boolean = true override fun isEnabled(): Boolean = true
companion object { companion object {
fun create(user: User, attributes: Map<String, Any>? = null): UserPrincipal { fun create(
return UserPrincipal(user, attributes) user: User,
} attributes: Map<String, Any>? = null,
): UserPrincipal = UserPrincipal(user, attributes)
} }
} }
@@ -3,16 +3,14 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class VkOAuth2UserInfo( class VkOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String =
override fun getProviderId(): String { (attributes["response"] as? List<*>)
return (attributes["response"] as? List<*>)
?.firstOrNull() ?.firstOrNull()
?.let { it as? Map<*, *> } ?.let { it as? Map<*, *> }
?.get("id") ?.get("id")
?.toString() ?: "" ?.toString() ?: ""
}
override fun getEmail(): String = attributes["email"]?.toString() ?: "" override fun getEmail(): String = attributes["email"]?.toString() ?: ""
@@ -3,18 +3,16 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class YandexOAuth2UserInfo( class YandexOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String = attributes["id"]?.toString() ?: "" override fun getProviderId(): String = attributes["id"]?.toString() ?: ""
override fun getEmail(): String { override fun getEmail(): String =
return (attributes["emails"] as? List<*>) (attributes["emails"] as? List<*>)
?.firstOrNull() ?.firstOrNull()
?.let { it as? Map<*, *> } ?.let { it as? Map<*, *> }
?.get("value") ?.get("value")
?.toString() ?: "" ?.toString() ?: ""
}
override fun getName(): String = attributes["display_name"]?.toString() ?: "" override fun getName(): String = attributes["display_name"]?.toString() ?: ""
@@ -68,6 +68,7 @@ class FilmLibraryController(
getFilmLibraryUseCase.getLibrary( getFilmLibraryUseCase.getLibrary(
GetFilmLibraryQuery(userId = userId), GetFilmLibraryQuery(userId = userId),
) )
val film = getFilmByIdUseCase.getById(library.filmId) val film = getFilmByIdUseCase.getById(library.filmId)
return listOf(FilmResponse.fromDomain(film)) return listOf(FilmResponse.fromDomain(film))
} }
@@ -2,8 +2,12 @@ package com.project.movienight.application.ports.input.security
interface OAuth2UserInfo { interface OAuth2UserInfo {
fun getProviderId(): String fun getProviderId(): String
fun getEmail(): String fun getEmail(): String
fun getName(): String fun getName(): String
fun getProvider(): String fun getProvider(): String
fun getAttributes(): Map<String, Any> fun getAttributes(): Map<String, Any>
} }
@@ -7,10 +7,6 @@ import java.util.UUID
interface UserRepositoryPort { interface UserRepositoryPort {
fun save(user: User): User fun save(user: User): User
fun saveWithOAuth2(user: User, provider: String, providerId: String): User
fun findByProviderAndProviderId(provider: String, providerId: String): User?
fun findById(id: UUID): User? fun findById(id: UUID): User?
fun findByEmail(email: String): User? fun findByEmail(email: String): User?
@@ -36,7 +36,6 @@ class UserService(
id = idGenerator.generateId(), id = idGenerator.generateId(),
name = command.name, name = command.name,
email = command.email, email = command.email,
password = "",
library = null, library = null,
) )
return userRepository.save(user) return userRepository.save(user)
@@ -6,6 +6,5 @@ data class User(
val id: UUID, val id: UUID,
val name: String, val name: String,
val email: String, val email: String,
val password: String,
val library: FilmLibrary?, val library: FilmLibrary?,
) )
@@ -1,9 +0,0 @@
ALTER TABLE public.users ADD COLUMN provider VARCHAR(20);
ALTER TABLE public.users ADD COLUMN provider_id VARCHAR(255);
ALTER TABLE public.users ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
CREATE UNIQUE INDEX idx_users_provider_provider_id
ON public.users(provider, provider_id);
CREATE INDEX idx_users_email ON public.users(email);