From 3cb52c019f23d24f8f177c2869d232dd165caaf4 Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 21 Apr 2026 15:30:58 +0300 Subject: [PATCH 01/22] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20V2=5F=5Fadd=5Foauth2=5Ffields.sql=20=D0=92=D1=81=D0=B5?= =?UTF-8?q?=20UserRepositoryIntegrationTest=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BF=D1=80=D0=BE=D1=88=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ .../resources/db/migration/V2__add_oauth2_fields.sql | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V2__add_oauth2_fields.sql diff --git a/build.gradle.kts b/build.gradle.kts index 18d7d23..0adabc9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.actuator) -// implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.cache) implementation(libs.spring.boot.starter.data.jdbc) implementation(libs.spring.boot.starter.validation) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e91484..e43dfa6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ spring-grpc = "1.0.1" protoc = "3.25.1" grpc-java = "1.60.0" springdoc = "2.8.6" +mockk = "1.13.13" [libraries] spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } @@ -33,6 +34,7 @@ flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry" } sentry-spring-boot-starter = { module = "io.sentry:sentry-spring-boot-starter-jakarta" } diff --git a/src/main/resources/db/migration/V2__add_oauth2_fields.sql b/src/main/resources/db/migration/V2__add_oauth2_fields.sql new file mode 100644 index 0000000..0db4084 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_oauth2_fields.sql @@ -0,0 +1,9 @@ +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); + -- 2.54.0 From c64fc35353fec000c6b2264f789cceef79801b59 Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Wed, 22 Apr 2026 08:46:58 +0300 Subject: [PATCH 02/22] =?UTF-8?q?OAuth2=20(=D0=B1=D0=B5=D0=B7=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B0=20=D1=81=20java)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 + gradle/libs.versions.toml | 1 + .../persistence/jdbc/UserRepository.kt | 10 ++++ .../security/CustomOAuth2UserService.kt | 57 +++++++++++++++++++ .../adapters/security/GoogleOAuth2UserInfo.kt | 16 ++++++ .../adapters/security/OAuth2UserInfo.kt | 9 +++ .../security/OAuth2UserInfoFactory.kt | 18 ++++++ .../adapters/security/UserPrincipal.kt | 42 ++++++++++++++ .../adapters/security/VkOAuth2UserInfo.kt | 26 +++++++++ .../adapters/security/YandexOAuth2UserInfo.kt | 20 +++++++ .../ports/output/UserRepositoryPort.kt | 2 + .../application/services/UserService.kt | 1 + .../project/movienight/domain/model/User.kt | 1 + 13 files changed, 205 insertions(+) create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index 18d7d23..609f72b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) + implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.3") + runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) runtimeOnly(libs.postgresql) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e91484..28cf8d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ grpc-java = "1.60.0" springdoc = "2.8.6" [libraries] +spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index a6607e5..8eee586 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -16,6 +16,7 @@ class UserRepository( id = UUID.fromString(rs.getString("id")), name = rs.getString("name"), email = rs.getString("email"), + password = rs.getString("password"), library = null, ) } @@ -56,6 +57,15 @@ class UserRepository( return users.firstOrNull() } + override fun findByEmail(email: String): User? { + val users = jdbc.query( + "SELECT id, name, email, password FROM users WHERE email = ?", + userRowMapper, + email, + ) + return users.firstOrNull() + } + override fun findAll(): List = jdbc.query( "SELECT id, name, email FROM users", diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt new file mode 100644 index 0000000..189e927 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -0,0 +1,57 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.domain.model.User +import org.slf4j.LoggerFactory +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service + +@Service +class CustomOAuth2UserService( + private val userRepository: UserRepositoryPort, + private val idGenerator: IdGenerator, +) : DefaultOAuth2UserService() { + + companion object { + private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) + } + + override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User { + val oAuth2User = super.loadUser(userRequest) + val registrationId = userRequest.clientRegistration.registrationId + + log.debug("Processing OAuth2 login for provider: {}", registrationId) + + return try { + val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) + val user = findOrCreateUser(userInfo) + UserPrincipal.create(user, oAuth2User.attributes) + } catch (e: Exception) { + log.error("OAuth2 authentication failed: ${e.message}", e) + throw OAuth2AuthenticationException("Failed to process OAuth2 user data") + } + } + + private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { + val existingUser = userRepository.findByEmail(userInfo.getEmail()) + + return if (existingUser != null) { + log.debug("User found by email: {}", userInfo.getEmail()) + existingUser + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", // OAuth2 пользователи не имеют пароля + library = null, + ) + userRepository.save(newUser) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt new file mode 100644 index 0000000..dcff86e --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -0,0 +1,16 @@ +package com.project.movienight.adapters.security.oauth2 + +class GoogleOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["sub"] as String + + override fun getEmail(): String = attributes["email"] as String + + override fun getName(): String = attributes["name"] as String + + override fun getProvider(): String = "google" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt new file mode 100644 index 0000000..5a155b9 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -0,0 +1,9 @@ +package com.project.movienight.adapters.security.oauth2 + +interface OAuth2UserInfo { + fun getProviderId(): String + fun getEmail(): String + fun getName(): String + fun getProvider(): String + fun getAttributes(): Map +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt new file mode 100644 index 0000000..001b789 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -0,0 +1,18 @@ +package com.project.movienight.adapters.security.oauth2 + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User + +object OAuth2UserInfoFactory { + + fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { + val attributes = user.attributes + + return when (registrationId.lowercase()) { + "google" -> GoogleOAuth2UserInfo(attributes) + "yandex" -> YandexOAuth2UserInfo(attributes) + "vk" -> VkOAuth2UserInfo(attributes) + else -> throw OAuth2AuthenticationException("Unknown provider: $registrationId") + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt new file mode 100644 index 0000000..5fbbee1 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -0,0 +1,42 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.domain.model.User +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.oauth2.core.user.OAuth2User +import java.util.* + +class UserPrincipal( + private val user: User, + private val attributes: Map? = null, +) : OAuth2User, UserDetails { + + fun getId(): UUID = user.id + + override fun getName(): String = user.name + + override fun getAttributes(): Map = attributes ?: emptyMap() + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority("ROLE_USER")) + } + + override fun getPassword(): String = user.password + + override fun getUsername(): String = user.email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = true + + companion object { + fun create(user: User, attributes: Map? = null): UserPrincipal { + return UserPrincipal(user, attributes) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt new file mode 100644 index 0000000..492e74c --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -0,0 +1,26 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class VkOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String { + val response = attributes["response"] as? List> + return response?.firstOrNull()?.get("id")?.toString() ?: "" + } + + override fun getEmail(): String = attributes["email"] as? String ?: "" + + override fun getName(): String { + val response = attributes["response"] as? List> + val first = response?.firstOrNull() + val firstName = first?.get("first_name") as? String ?: "" + val lastName = first?.get("last_name") as? String ?: "" + return "$firstName $lastName".trim() + } + + override fun getProvider(): String = "vk" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt new file mode 100644 index 0000000..2929389 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -0,0 +1,20 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class YandexOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["id"]?.toString() ?: "" + + override fun getEmail(): String { + val emails = attributes["emails"] as? List> + return emails?.firstOrNull()?.get("value") ?: "" + } + + override fun getName(): String = attributes["display_name"] as? String ?: "" + + override fun getProvider(): String = "yandex" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index af8ef0a..cfefe9b 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -8,6 +8,8 @@ interface UserRepositoryPort { fun findById(id: UUID): User? + fun findByEmail(email: String): User? + fun findAll(): List fun deleteById(id: UUID) diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index be32d6a..5eb754e 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -32,6 +32,7 @@ class UserService( id = idGenerator.generateId(), name = command.name, email = command.email, + password = "", library = null, ) return userRepository.save(user) diff --git a/src/main/kotlin/com/project/movienight/domain/model/User.kt b/src/main/kotlin/com/project/movienight/domain/model/User.kt index b4f2d9b..db9142b 100644 --- a/src/main/kotlin/com/project/movienight/domain/model/User.kt +++ b/src/main/kotlin/com/project/movienight/domain/model/User.kt @@ -6,5 +6,6 @@ data class User( val id: UUID, val name: String, val email: String, + val password: String, val library: FilmLibrary?, ) -- 2.54.0 From d50e2e640cf7ee48c8771d41b5144dd298a76900 Mon Sep 17 00:00:00 2001 From: skettiks Date: Thu, 23 Apr 2026 18:47:30 +0300 Subject: [PATCH 03/22] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20V1=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=8F=D1=82=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20email=20=D0=A0=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20saveWithOAuth2()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20findByProviderAndProviderId()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20CustomOAuth2UserService=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20OAuth2=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20package=20declaration=20=D0=B2=D0=BE=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=D1=85=207=20OAuth2=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jdbc/UserRepository.kt | 52 +++++++++++++++++-- .../security/CustomOAuth2UserService.kt | 37 ++++++++----- .../adapters/security/GoogleOAuth2UserInfo.kt | 2 +- .../adapters/security/OAuth2UserInfo.kt | 2 +- .../security/OAuth2UserInfoFactory.kt | 2 +- .../adapters/security/UserPrincipal.kt | 2 +- .../adapters/security/VkOAuth2UserInfo.kt | 2 +- .../adapters/security/YandexOAuth2UserInfo.kt | 2 +- .../ports/output/UserRepositoryPort.kt | 4 ++ src/main/resources/db/migration/V1__init.sql | 3 +- 10 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 8eee586..420dbd0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -26,22 +26,24 @@ class UserRepository( jdbc.update( """ UPDATE users - SET name = ?, email = ? + SET name = ?, email = ?, password = ? WHERE id = ? """.trimIndent(), user.name, user.email, + user.password, user.id, ) if (updatedRows == 0) { jdbc.update( """ - INSERT INTO users (id, name, email) - VALUES (?, ?, ?) + INSERT INTO users (id, name, email, password) + VALUES (?, ?, ?, ?) """.trimIndent(), user.id, user.name, user.email, + user.password, ) } return user @@ -50,7 +52,7 @@ class UserRepository( override fun findById(id: UUID): User? { val users = jdbc.query( - "SELECT id, name, email FROM users WHERE id = ?", + "SELECT id, name, email, password FROM users WHERE id = ?", userRowMapper, id, ) @@ -68,11 +70,51 @@ class UserRepository( override fun findAll(): List = jdbc.query( - "SELECT id, name, email FROM users", + "SELECT id, name, email, password FROM users", userRowMapper, ) override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) } + + override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { + val updatedRows = jdbc.update(""" + UPDATE users + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + user.name, + user.email, + user.password, + provider, + providerId, + user.id, + ) + if (updatedRows == 0) { + jdbc.update( + """ + INSERT INTO users (id, name, email, password, provider, provider_id) + VALUES (?, ?, ?, ?, ?, ?) + """.trimIndent(), + user.id, + user.name, + user.email, + user.password, + provider, + providerId, + ) + } + return user + } + + override fun findByProviderAndProviderId(provider: String, providerId: String): User? { + val users = jdbc.query( + "SELECT id, name, email, password FROM users WHERE provider = ? AND provider_id = ?", + userRowMapper, + provider, + providerId, + ) + return users.firstOrNull() + } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt index 189e927..968424a 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort @@ -37,21 +37,34 @@ class CustomOAuth2UserService( } private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { - val existingUser = userRepository.findByEmail(userInfo.getEmail()) + // Сначала ищем по provider + provider_id (основной способ для OAuth2) + val existingUser = userRepository.findByProviderAndProviderId( + userInfo.getProvider(), + userInfo.getProviderId() + ) return if (existingUser != null) { - log.debug("User found by email: {}", userInfo.getEmail()) + log.debug("User found by provider: {}", userInfo.getProvider()) existingUser } else { - log.debug("Creating new user for provider: {}", userInfo.getProvider()) - val newUser = User( - id = idGenerator.generateId(), - name = userInfo.getName(), - email = userInfo.getEmail(), - password = "", // OAuth2 пользователи не имеют пароля - library = null, - ) - userRepository.save(newUser) + // Проверяем нет ли пользователя с таким email (связывание аккаунтов) + val userByEmail = userRepository.findByEmail(userInfo.getEmail()) + + if (userByEmail != null) { + // Пользователь существует, обновляем его OAuth2 данными + log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) + userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", + library = null, + ) + userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) + } } } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt index dcff86e..fa41de5 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security class GoogleOAuth2UserInfo( private val attributes: Map diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt index 5a155b9..b6abf09 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security interface OAuth2UserInfo { fun getProviderId(): String diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt index 001b789..e2db545 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index 5fbbee1..a4d94ea 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.domain.model.User import org.springframework.security.core.GrantedAuthority diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt index 492e74c..47c41d2 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class VkOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt index 2929389..467aa85 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class YandexOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index cfefe9b..0e45c1e 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -6,6 +6,10 @@ import java.util.UUID interface UserRepositoryPort { 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 findByEmail(email: String): User? diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 11017d1..a94ddaf 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,7 +1,8 @@ CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, - email VARCHAR(320) NOT NULL UNIQUE + email VARCHAR(320) NOT NULL UNIQUE, + password VARCHAR(255) ); CREATE TABLE IF NOT EXISTS public.films ( -- 2.54.0 From 312c18645b405d01a36ad6a2d3a5cdb8cb82baa0 Mon Sep 17 00:00:00 2001 From: skettiks Date: Thu, 23 Apr 2026 23:59:16 +0300 Subject: [PATCH 04/22] =?UTF-8?q?-=20=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=91=D0=BD=20OAuth2UserInfo.kt=20=D0=B8=D0=B7=20adapter?= =?UTF-8?q?s/security/=20=D0=B2=20application/ports/input/security/;=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - В UserPrincipal.kt заменён star import java.util.* на явный java.util.UUID. - В YandexOAuth2UserInfo.kt и VkOAuth2UserInfo.kt убрана аннотация @Suppress("UNCHECKED_CAST") - В build.gradle.kts хардкод версии заменён на version catalog - Из CustomOAuth2UserService.kt удалены комментарии на русском языке --- build.gradle.kts | 3 +-- .../security/CustomOAuth2UserService.kt | 4 +--- .../adapters/security/GoogleOAuth2UserInfo.kt | 2 ++ .../security/OAuth2UserInfoFactory.kt | 1 + .../adapters/security/UserPrincipal.kt | 2 +- .../adapters/security/VkOAuth2UserInfo.kt | 20 +++++++++++-------- .../adapters/security/YandexOAuth2UserInfo.kt | 12 +++++++---- .../ports/input}/security/OAuth2UserInfo.kt | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) rename src/main/kotlin/com/project/movienight/{adapters => application/ports/input}/security/OAuth2UserInfo.kt (74%) diff --git a/build.gradle.kts b/build.gradle.kts index 2d2ed27..a5b9744 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,8 +46,7 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) - - implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.3") + implementation(libs.spring.boot.starter.oauth2.client) runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt index 968424a..cc1cb8d 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -1,5 +1,6 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort import com.project.movienight.domain.model.User @@ -37,7 +38,6 @@ class CustomOAuth2UserService( } private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { - // Сначала ищем по provider + provider_id (основной способ для OAuth2) val existingUser = userRepository.findByProviderAndProviderId( userInfo.getProvider(), userInfo.getProviderId() @@ -47,11 +47,9 @@ class CustomOAuth2UserService( log.debug("User found by provider: {}", userInfo.getProvider()) existingUser } else { - // Проверяем нет ли пользователя с таким email (связывание аккаунтов) val userByEmail = userRepository.findByEmail(userInfo.getEmail()) if (userByEmail != null) { - // Пользователь существует, обновляем его OAuth2 данными log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) } else { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt index fa41de5..c463ac6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -1,5 +1,7 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class GoogleOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt index e2db545..89d6d30 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -1,5 +1,6 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index a4d94ea..c602211 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -5,7 +5,7 @@ import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.oauth2.core.user.OAuth2User -import java.util.* +import java.util.UUID class UserPrincipal( private val user: User, diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt index 47c41d2..e2c55c0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -1,22 +1,26 @@ package com.project.movienight.adapters.security -@Suppress("UNCHECKED_CAST") +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class VkOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { override fun getProviderId(): String { - val response = attributes["response"] as? List> - return response?.firstOrNull()?.get("id")?.toString() ?: "" + return (attributes["response"] as? List<*>) + ?.firstOrNull() + ?.let { it as? Map<*, *> } + ?.get("id") + ?.toString() ?: "" } - override fun getEmail(): String = attributes["email"] as? String ?: "" + override fun getEmail(): String = attributes["email"]?.toString() ?: "" override fun getName(): String { - val response = attributes["response"] as? List> - val first = response?.firstOrNull() - val firstName = first?.get("first_name") as? String ?: "" - val lastName = first?.get("last_name") as? String ?: "" + val response = attributes["response"] as? List<*> + val first = response?.firstOrNull() as? Map<*, *> + val firstName = first?.get("first_name")?.toString() ?: "" + val lastName = first?.get("last_name")?.toString() ?: "" return "$firstName $lastName".trim() } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt index 467aa85..59bf3eb 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -1,6 +1,7 @@ package com.project.movienight.adapters.security -@Suppress("UNCHECKED_CAST") +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class YandexOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { @@ -8,11 +9,14 @@ class YandexOAuth2UserInfo( override fun getProviderId(): String = attributes["id"]?.toString() ?: "" override fun getEmail(): String { - val emails = attributes["emails"] as? List> - return emails?.firstOrNull()?.get("value") ?: "" + return (attributes["emails"] as? List<*>) + ?.firstOrNull() + ?.let { it as? Map<*, *> } + ?.get("value") + ?.toString() ?: "" } - override fun getName(): String = attributes["display_name"] as? String ?: "" + override fun getName(): String = attributes["display_name"]?.toString() ?: "" override fun getProvider(): String = "yandex" diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt similarity index 74% rename from src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt index b6abf09..c081592 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security +package com.project.movienight.application.ports.input.security interface OAuth2UserInfo { fun getProviderId(): String -- 2.54.0 From 27317c00e42b4325de6642281a03c972cdc7ceae Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 5 May 2026 00:58:20 +0300 Subject: [PATCH 05/22] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=20=D1=81=D0=BB=D0=B8=D1=8F=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=20develop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - После слияния с develop возникли конфликты в реализации OAuth2 - Две ветки независимо реализовали OAuth2 функциональность по-разному - Сборка проекта падала из-за отсутствия зависимостей OAuth2 Изменения: - Временно отключена OAuth2 зависимость в build.gradle.kts - Перенесен OAuth2 код в папку security.disabled для сохранения - Добавлены исключения security.disabled из компиляции, ktlint и detekt - Удалена миграция V2__add_oauth2_fields.sql (OAuth2 поля теперь в V1) - Удалено поле password из User domain модели - Обновлены репозитории и сервисы для работы с новой схемой БД Результат: - Проект успешно собирается (./gradlew build) - Все 35 тестов проходят - OAuth2 код сохранен для будущего использования --- build.gradle.kts | 9 +- .../movienight/MovieNightApplication.kt | 3 +- .../persistence/jdbc/FilmRepository.kt | 11 ++- .../persistence/jdbc/UserRepository.kt | 99 ++++++++----------- .../CustomOAuth2UserService.kt | 48 ++++++--- .../GoogleOAuth2UserInfo.kt | 3 +- .../OAuth2UserInfoFactory.kt | 6 +- .../UserPrincipal.kt | 17 ++-- .../VkOAuth2UserInfo.kt | 8 +- .../YandexOAuth2UserInfo.kt | 8 +- .../movienight/adapters/web/FilmController.kt | 18 ++-- .../adapters/web/FilmLibraryController.kt | 14 +-- .../movienight/adapters/web/UserController.kt | 13 ++- .../OAuth2UserInfo.kt | 4 + .../ports/output/UserRepositoryPort.kt | 4 - .../application/services/FilmService.kt | 22 +++-- .../application/services/UserService.kt | 7 +- .../project/movienight/domain/model/User.kt | 1 - src/main/resources/db/migration/V1__init.sql | 4 +- .../db/migration/V2__add_oauth2_fields.sql | 9 -- .../entity/UserEntityMappingTest.kt | 59 +++++------ 21 files changed, 180 insertions(+), 187 deletions(-) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/CustomOAuth2UserService.kt (61%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/GoogleOAuth2UserInfo.kt (91%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/OAuth2UserInfoFactory.kt (85%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/UserPrincipal.kt (77%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/VkOAuth2UserInfo.kt (84%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/YandexOAuth2UserInfo.kt (80%) rename src/main/kotlin/com/project/movienight/application/ports/input/{security => security.disabled}/OAuth2UserInfo.kt (98%) delete mode 100644 src/main/resources/db/migration/V2__add_oauth2_fields.sql diff --git a/build.gradle.kts b/build.gradle.kts index f20ddff..6fcffe6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,8 @@ plugins { jacoco } -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") @@ -48,7 +49,8 @@ dependencies { implementation(libs.spring.grpc.starter) 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.h2) @@ -76,6 +78,7 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) allWarningsAsErrors.set(false) } + exclude("**/security.disabled/**") } tasks.withType { @@ -158,6 +161,7 @@ ktlint { filter { exclude("**/build/**") exclude("**/generated/**") + exclude("**/security.disabled/**") } } @@ -171,6 +175,7 @@ detekt { tasks.withType().configureEach { jvmTarget = "21" + exclude("**/security.disabled/**") reports { html.required.set(true) xml.required.set(true) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index 39274c8..f6c7d33 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,10 +1,11 @@ package com.project.movienight 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.runApplication -@SpringBootApplication +@SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class]) @ConfigurationPropertiesScan("com.project.movienight.config") class MovieNightApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt index a13f721..243b8d7 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt @@ -62,11 +62,12 @@ class FilmRepository( ) override fun findByTitle(title: String): Film? { - val films = jdbc.query( - "SELECT id, title, description FROM films WHERE title = ?", - filmRowMapper, - title - ) + val films = + jdbc.query( + "SELECT id, title, description FROM films WHERE title = ?", + filmRowMapper, + title, + ) return films.firstOrNull() } diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index cb866d9..cd1a860 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -20,8 +20,9 @@ class UserRepository( id = UUID.fromString(rs.getString("id")), name = rs.getString("name"), email = rs.getString("email"), - password = rs.getString("password"), - library = null, + provider = rs.getString("provider"), + providerId = rs.getString("provider_id"), + createdAt = rs.getTimestamp("created_at").toLocalDateTime(), ) } @@ -31,24 +32,25 @@ class UserRepository( jdbc.update( """ UPDATE users - SET name = ?, email = ?, password = ? + SET name = ?, email = ? WHERE id = ? """.trimIndent(), - user.name, - user.email, - user.password, - user.id, + entity.name, + entity.email, + entity.id, ) if (updatedRows == 0) { jdbc.update( """ - INSERT INTO users (id, name, email, password) - VALUES (?, ?, ?, ?) + INSERT INTO users (id, name, email, provider, provider_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) """.trimIndent(), - user.id, - user.name, - user.email, - user.password, + entity.id, + entity.name, + entity.email, + entity.provider, + entity.providerId, + entity.createdAt, ) } return user @@ -57,69 +59,48 @@ class UserRepository( override fun findById(id: UUID): User? { val entities = jdbc.query( - "SELECT id, name, email, password FROM users WHERE id = ?", - userRowMapper, + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", + userEntityRowMapper, id, ) return entities.firstOrNull()?.toDomain() } override fun findByEmail(email: String): User? { - val users = jdbc.query( - "SELECT id, name, email, password FROM users WHERE email = ?", - userRowMapper, - email, - ) - return users.firstOrNull() + val entities = + jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", + userEntityRowMapper, + email, + ) + return entities.firstOrNull()?.toDomain() } override fun findAll(): List = - jdbc.query( - "SELECT id, name, email, password FROM users", - userRowMapper, - ) + jdbc + .query( + "SELECT id, name, email, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) } - override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { - val updatedRows = jdbc.update(""" - UPDATE users - SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? - WHERE id = ? - """.trimIndent(), - user.name, - user.email, - user.password, - provider, - providerId, - user.id, - ) - if (updatedRows == 0) { - jdbc.update( + override fun findByProviderAndProviderId( + provider: AuthProvider, + providerId: String, + ): User? { + val entities = + jdbc.query( """ - INSERT INTO users (id, name, email, password, provider, provider_id) - VALUES (?, ?, ?, ?, ?, ?) + SELECT id, name, email, provider, provider_id, created_at FROM users + WHERE provider = ? AND provider_id = ? """.trimIndent(), - user.id, - user.name, - user.email, - user.password, - provider, + userEntityRowMapper, + provider.name, providerId, ) - } - return user - } - - override fun findByProviderAndProviderId(provider: String, providerId: String): User? { - val users = jdbc.query( - "SELECT id, name, email, password FROM users WHERE provider = ? AND provider_id = ?", - userRowMapper, - provider, - providerId, - ) - return users.firstOrNull() + return entities.firstOrNull()?.toDomain() } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt similarity index 61% rename from src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt index cc1cb8d..b64ea71 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt @@ -1,8 +1,11 @@ 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.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.domain.model.AuthProvider import com.project.movienight.domain.model.User import org.slf4j.LoggerFactory import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService @@ -16,7 +19,6 @@ class CustomOAuth2UserService( private val userRepository: UserRepositoryPort, private val idGenerator: IdGenerator, ) : DefaultOAuth2UserService() { - companion object { private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) } @@ -31,17 +33,23 @@ class CustomOAuth2UserService( val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) val user = findOrCreateUser(userInfo) UserPrincipal.create(user, oAuth2User.attributes) - } catch (e: Exception) { + } catch (e: IllegalArgumentException) { log.error("OAuth2 authentication failed: ${e.message}", e) 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 { - val existingUser = userRepository.findByProviderAndProviderId( - userInfo.getProvider(), - userInfo.getProviderId() - ) + val provider = AuthProvider.valueOf(userInfo.getProvider().uppercase()) + + val existingUser = + userRepository.findByProviderAndProviderId( + provider, + userInfo.getProviderId(), + ) return if (existingUser != null) { log.debug("User found by provider: {}", userInfo.getProvider()) @@ -51,17 +59,27 @@ class CustomOAuth2UserService( if (userByEmail != null) { 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 { log.debug("Creating new user for provider: {}", userInfo.getProvider()) - val newUser = User( - id = idGenerator.generateId(), - name = userInfo.getName(), - email = userInfo.getEmail(), - password = "", - library = null, - ) - userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) + val newUser = + User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + library = null, + ) + val entity = + newUser.toEntity( + provider = provider, + providerId = userInfo.getProviderId(), + ) + userRepository.save(entity.toDomain()) } } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt similarity index 91% rename from src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt index c463ac6..123f9a0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt @@ -3,9 +3,8 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class GoogleOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - override fun getProviderId(): String = attributes["sub"] as String override fun getEmail(): String = attributes["email"] as String diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt similarity index 85% rename from src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt index 89d6d30..1faf660 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt @@ -5,8 +5,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User object OAuth2UserInfoFactory { - - fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { + fun getOAuth2UserInfo( + registrationId: String, + user: OAuth2User, + ): OAuth2UserInfo { val attributes = user.attributes return when (registrationId.lowercase()) { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt similarity index 77% rename from src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt index c602211..3ea81b4 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt @@ -10,19 +10,17 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, UserDetails { - +) : OAuth2User, + UserDetails { fun getId(): UUID = user.id override fun getName(): String = user.name override fun getAttributes(): Map = attributes ?: emptyMap() - override fun getAuthorities(): Collection { - return listOf(SimpleGrantedAuthority("ROLE_USER")) - } + override fun getAuthorities(): Collection = listOf(SimpleGrantedAuthority("ROLE_USER")) - override fun getPassword(): String = user.password + override fun getPassword(): String = "" override fun getUsername(): String = user.email @@ -35,8 +33,9 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create(user: User, attributes: Map? = null): UserPrincipal { - return UserPrincipal(user, attributes) - } + fun create( + user: User, + attributes: Map? = null, + ): UserPrincipal = UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt similarity index 84% rename from src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt index e2c55c0..9b55f35 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt @@ -3,16 +3,14 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class VkOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - - override fun getProviderId(): String { - return (attributes["response"] as? List<*>) + override fun getProviderId(): String = + (attributes["response"] as? List<*>) ?.firstOrNull() ?.let { it as? Map<*, *> } ?.get("id") ?.toString() ?: "" - } override fun getEmail(): String = attributes["email"]?.toString() ?: "" diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt similarity index 80% rename from src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt index 59bf3eb..dc71df9 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt @@ -3,18 +3,16 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class YandexOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - override fun getProviderId(): String = attributes["id"]?.toString() ?: "" - override fun getEmail(): String { - return (attributes["emails"] as? List<*>) + override fun getEmail(): String = + (attributes["emails"] as? List<*>) ?.firstOrNull() ?.let { it as? Map<*, *> } ?.get("value") ?.toString() ?: "" - } override fun getName(): String = attributes["display_name"]?.toString() ?: "" diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt index 12ffa79..c2d1eb2 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -56,10 +56,11 @@ class FilmController( FilmResponse.fromDomain( editFilmUseCase.edit( id = id, - command = EditFilmCommand( - title = request.title, - description = request.description, - ), + command = + EditFilmCommand( + title = request.title, + description = request.description, + ), ), ) @@ -72,16 +73,13 @@ class FilmController( @GetMapping("/{id}") fun getById( @PathVariable id: UUID, - ): FilmResponse = - FilmResponse.fromDomain(getFilmByIdUseCase.getById(id)) + ): FilmResponse = FilmResponse.fromDomain(getFilmByIdUseCase.getById(id)) @GetMapping("/search") fun searchByTitle( @RequestParam title: String, - ): FilmResponse? = - searchFilmByTitleUseCase.searchByTitle(title)?.let { FilmResponse.fromDomain(it) } + ): FilmResponse? = searchFilmByTitleUseCase.searchByTitle(title)?.let { FilmResponse.fromDomain(it) } @GetMapping - fun getAll(): List = - getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) } + fun getAll(): List = getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt index f5246cc..ec44e2f 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -63,9 +63,10 @@ class FilmLibraryController( fun getAllFilmsInLibrary( @PathVariable userId: UUID, ): List { - val library = getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId) - ) + val library = + getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) val film = getFilmByIdUseCase.getById(library.filmId) @@ -103,9 +104,10 @@ class FilmLibraryController( fun getAvailableFilms( @PathVariable userId: UUID, ): List { - val userLibrary = getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId) - ) + val userLibrary = + getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) val allFilms = getAllFilmsUseCase.getAll() diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index 6c1a996..bccf5bc 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -46,14 +46,12 @@ class UserController( ) @GetMapping - fun getAll(): List = - getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) } + fun getAll(): List = getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) } @GetMapping("/{id}") fun getById( @PathVariable id: UUID, - ): UserResponse = - UserResponse.fromDomain(getUserByIdUseCase.getById(id)) + ): UserResponse = UserResponse.fromDomain(getUserByIdUseCase.getById(id)) @PatchMapping("/{id}") fun edit( @@ -63,9 +61,10 @@ class UserController( UserResponse.fromDomain( editUserUseCase.edit( id = id, - command = EditUserCommand( - name = request.name, - ), + command = + EditUserCommand( + name = request.name, + ), ), ) diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt similarity index 98% rename from src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt index c081592..e45db2b 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt @@ -2,8 +2,12 @@ package com.project.movienight.application.ports.input.security interface OAuth2UserInfo { fun getProviderId(): String + fun getEmail(): String + fun getName(): String + fun getProvider(): String + fun getAttributes(): Map } diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index 678cea0..dd69728 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -7,10 +7,6 @@ import java.util.UUID interface UserRepositoryPort { 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 findByEmail(email: String): User? diff --git a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt index 555655a..f775de3 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -28,7 +28,6 @@ class FilmService( GetFilmByIdUseCase, GetAllFilmsUseCase, SearchFilmByTitleUseCase { - override fun create(command: CreateFilmCommand): Film { if (filmConfig.isBlocked(command.title)) { throw BlockedValueException(target = "Film", field = "title") @@ -37,15 +36,19 @@ class FilmService( throw BlockedValueException(target = "Film", field = "description") } - val film = Film( - id = idGenerator.generateId(), - title = command.title, - description = command.description, - ) + val film = + Film( + id = idGenerator.generateId(), + title = command.title, + description = command.description, + ) return filmRepository.save(film) } - override fun edit(id: UUID, command: EditFilmCommand): Film { + override fun edit( + id: UUID, + command: EditFilmCommand, + ): Film { if (filmConfig.isBlocked(command.title)) { throw BlockedValueException(target = "Film", field = "title") } @@ -63,9 +66,8 @@ class FilmService( filmRepository.deleteById(id) } - override fun getById(id: UUID): Film { - return filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - } + override fun getById(id: UUID): Film = + filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) override fun getAll(): List = filmRepository.findAll() diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index 840208e..da2a84b 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -26,7 +26,6 @@ class UserService( DeleteUserUseCase, GetUserByIdUseCase, GetAllUsersUseCase { - override fun create(command: CreateUserCommand): User { if (userConfig.isBlocked(command.name)) { throw BlockedValueException(target = "User", field = "name") @@ -37,7 +36,6 @@ class UserService( id = idGenerator.generateId(), name = command.name, email = command.email, - password = "", library = null, ) return userRepository.save(user) @@ -61,9 +59,8 @@ class UserService( userRepository.deleteById(id) } - override fun getById(id: UUID): User { - return userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) - } + override fun getById(id: UUID): User = + userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) override fun getAll(): List = userRepository.findAll() } diff --git a/src/main/kotlin/com/project/movienight/domain/model/User.kt b/src/main/kotlin/com/project/movienight/domain/model/User.kt index db9142b..b4f2d9b 100644 --- a/src/main/kotlin/com/project/movienight/domain/model/User.kt +++ b/src/main/kotlin/com/project/movienight/domain/model/User.kt @@ -6,6 +6,5 @@ data class User( val id: UUID, val name: String, val email: String, - val password: String, val library: FilmLibrary?, ) diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index a94ddaf..900f6b5 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(320) NOT NULL UNIQUE, - password VARCHAR(255) + provider VARCHAR(64), + provider_id VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS public.films ( diff --git a/src/main/resources/db/migration/V2__add_oauth2_fields.sql b/src/main/resources/db/migration/V2__add_oauth2_fields.sql deleted file mode 100644 index 0db4084..0000000 --- a/src/main/resources/db/migration/V2__add_oauth2_fields.sql +++ /dev/null @@ -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); - diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt index bec7f20..cdf0c42 100644 --- a/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt @@ -9,17 +9,17 @@ import kotlin.test.assertEquals import kotlin.test.assertNull class UserEntityMappingTest { - @Test fun `toDomain maps UserEntity correctly`() { - val entity = UserEntity( - id = UUID.randomUUID(), - name = "John Pork", - email = "john@email.com", - provider = "GOOGLE", - providerId = "google1234", - createdAt = LocalDateTime.now(), - ) + val entity = + UserEntity( + id = UUID.randomUUID(), + name = "John Pork", + email = "john@email.com", + provider = "GOOGLE", + providerId = "google1234", + createdAt = LocalDateTime.now(), + ) val user = entity.toDomain() assertEquals(entity.id, user.id) @@ -30,12 +30,13 @@ class UserEntityMappingTest { @Test fun `toEntity maps User with OAuth provider`() { - val user = User( - id = UUID.randomUUID(), - name = "Jane", - email = "jane@mail.com", - library = null - ) + val user = + User( + id = UUID.randomUUID(), + name = "Jane", + email = "jane@mail.com", + library = null, + ) val entity = user.toEntity(AuthProvider.YANDEX, "yandex456") @@ -48,12 +49,13 @@ class UserEntityMappingTest { @Test fun `toEntity maps User without OAuth provider`() { - val user = User( - id = UUID.randomUUID(), - name = "Bob", - email = "bob@mail.com", - library = null - ) + val user = + User( + id = UUID.randomUUID(), + name = "Bob", + email = "bob@mail.com", + library = null, + ) val entity = user.toEntity() @@ -63,12 +65,13 @@ class UserEntityMappingTest { @Test fun `mapping is reversible for basic fields`() { - val original = User( - id = UUID.randomUUID(), - name = "Alice", - email = "alice@email.com", - library = null - ) + val original = + User( + id = UUID.randomUUID(), + name = "Alice", + email = "alice@email.com", + library = null, + ) val mapped = original.toEntity().toDomain() @@ -76,6 +79,4 @@ class UserEntityMappingTest { assertEquals(original.name, mapped.name) assertEquals(original.email, mapped.email) } - - } -- 2.54.0 From 6158d3543c75834f2b2e0de2e0942676ac51ed31 Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Thu, 7 May 2026 07:58:11 +0300 Subject: [PATCH 06/22] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BB=D0=B0=20security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 +- .../movienight/MovieNightApplication.kt | 2 +- .../persistence/jdbc/UserRepository.kt | 88 +++++++++++-------- .../SecurityConfiguration.kt | 42 +++++++++ .../security.disabled/UserPrincipal.kt | 13 ++- .../movienight/adapters/web/UserController.kt | 9 ++ src/main/resources/application.yaml | 32 +++++++ .../db/migration/V2__add_oauth2_index.sql | 3 + 8 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt create mode 100644 src/main/resources/db/migration/V2__add_oauth2_index.sql diff --git a/build.gradle.kts b/build.gradle.kts index 6fcffe6..8468c3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,8 +49,8 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) - // Temporarily disabled due to OAuth2 configuration issues - // implementation(libs.spring.boot.starter.oauth2.client) + + implementation(libs.spring.boot.starter.oauth2.client) runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index f6c7d33..a792cd2 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -5,7 +5,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAu import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication -@SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class]) +@SpringBootApplication @ConfigurationPropertiesScan("com.project.movienight.config") class MovieNightApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index cd1a860..3f73bd0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,6 +9,7 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet +import java.time.LocalDateTime import java.util.UUID @Repository @@ -27,18 +28,32 @@ class UserRepository( } override fun save(user: User): User { - val entity = user.toEntity() - val updatedRows = - jdbc.update( - """ - UPDATE users - SET name = ?, email = ? - WHERE id = ? - """.trimIndent(), - entity.name, - entity.email, - entity.id, + val existingUser = findById(user.id) + + val entity = if (existingUser != null) { + val existingEntity = existingUser.toEntity() + user.toEntity( + provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, + providerId = existingEntity.providerId, + createdAt = existingEntity.createdAt ) + } else { + user.toEntity() + } + + val updatedRows = jdbc.update( + """ + UPDATE users + SET name = ?, email = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + entity.name, + entity.email, + entity.provider, + entity.providerId, + entity.id, + ) + if (updatedRows == 0) { jdbc.update( """ @@ -57,31 +72,28 @@ class UserRepository( } override fun findById(id: UUID): User? { - val entities = - jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", - userEntityRowMapper, - id, - ) + val entities = jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", + userEntityRowMapper, + id, + ) return entities.firstOrNull()?.toDomain() } override fun findByEmail(email: String): User? { - val entities = - jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", - userEntityRowMapper, - email, - ) + val entities = jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", + userEntityRowMapper, + email, + ) return entities.firstOrNull()?.toDomain() } override fun findAll(): List = - jdbc - .query( - "SELECT id, name, email, provider, provider_id, created_at FROM users", - userEntityRowMapper, - ).map { it.toDomain() } + jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) @@ -91,16 +103,16 @@ class UserRepository( provider: AuthProvider, providerId: String, ): User? { - val entities = - jdbc.query( - """ - SELECT id, name, email, provider, provider_id, created_at FROM users - WHERE provider = ? AND provider_id = ? - """.trimIndent(), - userEntityRowMapper, - provider.name, - providerId, - ) + val entities = jdbc.query( + """ + SELECT id, name, email, provider, provider_id, created_at + FROM users + WHERE provider = ? AND provider_id = ? + """.trimIndent(), + userEntityRowMapper, + provider.name, + providerId, + ) return entities.firstOrNull()?.toDomain() } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt new file mode 100644 index 0000000..d885b91 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt @@ -0,0 +1,42 @@ +package com.project.movienight.adapters.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class SecurityConfiguration( + private val customOAuth2UserService: CustomOAuth2UserService, +) { + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http + .oauth2Login { oauth2 -> + oauth2 + .userInfoEndpoint { userInfo -> + userInfo.userService(customOAuth2UserService) + } + .defaultSuccessUrl("/api/users/me", true) + } + .authorizeHttpRequests { auth -> + auth + .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health").permitAll() + .requestMatchers("/api/users/me").authenticated() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated() + } + .headers { headers -> + headers.frameOptions { frameOptions -> + frameOptions.sameOrigin() + } + } + .csrf { csrf -> + csrf.disable() + } + + return http.build() + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt index 3ea81b4..b5d7eb9 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt @@ -10,15 +10,16 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, - UserDetails { +) : OAuth2User, UserDetails { + fun getId(): UUID = user.id override fun getName(): String = user.name override fun getAttributes(): Map = attributes ?: emptyMap() - override fun getAuthorities(): Collection = listOf(SimpleGrantedAuthority("ROLE_USER")) + override fun getAuthorities(): Collection = + listOf(SimpleGrantedAuthority("ROLE_USER")) override fun getPassword(): String = "" @@ -33,9 +34,7 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create( - user: User, - attributes: Map? = null, - ): UserPrincipal = UserPrincipal(user, attributes) + fun create(user: User, attributes: Map? = null): UserPrincipal = + UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index bccf5bc..4d838fb 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController +//import com.project.movienight.adapters.security.UserPrincipal import java.util.UUID @RestController @@ -73,4 +74,12 @@ class UserController( fun delete( @PathVariable id: UUID, ) = deleteUserUseCase.delete(id) + + /* + @GetMapping("/me") + fun getCurrentUser(principal: UserPrincipal): UserResponse = + UserResponse.fromDomain( + getUserByIdUseCase.getById(principal.getId()) + ) + */ } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f36c77d..86c806c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,6 +25,38 @@ spring: console: enabled: ${SPRING_H2_CONSOLE_ENABLED:true} path: /h2-console + security: + oauth2: + client: + registration: + google: + client-id: ${OAUTH2_GOOGLE_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET:test-secret} + scope: email,profile + yandex: + client-id: ${OAUTH2_YANDEX_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_YANDEX_CLIENT_SECRET:test-secret} + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: login:email,login:avatar + vk: + client-id: ${OAUTH2_VK_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_VK_CLIENT_SECRET:test-secret} + authorization-grant-type: authorization_code + client-authentication-method: client_secret_post + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: email + provider: + yandex: + authorization-uri: https://oauth.yandex.ru/authorize + token-uri: https://oauth.yandex.ru/token + user-info-uri: https://login.yandex.ru/info + user-name-attribute: id + vk: + authorization-uri: https://oauth.vk.com/authorize + token-uri: https://oauth.vk.com/access_token + user-info-uri: https://api.vk.com/method/users.get?v=5.131&fields=photo_200 + user-name-attribute: response server: shutdown: graceful diff --git a/src/main/resources/db/migration/V2__add_oauth2_index.sql b/src/main/resources/db/migration/V2__add_oauth2_index.sql new file mode 100644 index 0000000..d416108 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_oauth2_index.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_provider_provider_id +ON users(provider, provider_id) +WHERE provider IS NOT NULL AND provider_id IS NOT NULL; -- 2.54.0 From 0aa7dcaf98ca6918c12d3b8c09b0f473672533cf Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Thu, 7 May 2026 08:01:24 +0300 Subject: [PATCH 07/22] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20=D0=B8=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/project/movienight/MovieNightApplication.kt | 2 +- .../movienight/adapters/persistence/jdbc/UserRepository.kt | 2 +- .../{security.disabled => security}/CustomOAuth2UserService.kt | 0 .../{security.disabled => security}/GoogleOAuth2UserInfo.kt | 0 .../{security.disabled => security}/OAuth2UserInfoFactory.kt | 0 .../{security.disabled => security}/SecurityConfiguration.kt | 0 .../adapters/{security.disabled => security}/UserPrincipal.kt | 0 .../{security.disabled => security}/VkOAuth2UserInfo.kt | 0 .../{security.disabled => security}/YandexOAuth2UserInfo.kt | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/CustomOAuth2UserService.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/GoogleOAuth2UserInfo.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/OAuth2UserInfoFactory.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/SecurityConfiguration.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/UserPrincipal.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/VkOAuth2UserInfo.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/YandexOAuth2UserInfo.kt (100%) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index a792cd2..6ac4f10 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,7 +1,7 @@ package com.project.movienight import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration +//import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 3f73bd0..f6598a6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,7 +9,7 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet -import java.time.LocalDateTime +//import java.time.LocalDateTime import java.util.UUID @Repository diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt rename to src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt rename to src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt rename to src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt rename to src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt -- 2.54.0 From 0b57c3161125c2e2c41bf7cafdf8ea65426030fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:37:35 +0000 Subject: [PATCH 08/22] fix: resolve CI compile failure and complete OAuth2 review fixes Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/62eaa8e4-560b-4737-be4b-478f1a4c484a Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../movienight/MovieNightApplication.kt | 1 - .../persistence/jdbc/UserRepository.kt | 99 ++++++++++--------- .../security/SecurityConfiguration.kt | 24 ++--- .../adapters/security/UserPrincipal.kt | 14 ++- .../movienight/adapters/web/UserController.kt | 9 -- .../OAuth2UserInfo.kt | 0 6 files changed, 73 insertions(+), 74 deletions(-) rename src/main/kotlin/com/project/movienight/application/ports/input/{security.disabled => security}/OAuth2UserInfo.kt (100%) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index 6ac4f10..39274c8 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,7 +1,6 @@ package com.project.movienight 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.runApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index f6598a6..82fcb93 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,7 +9,6 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet -//import java.time.LocalDateTime import java.util.UUID @Repository @@ -30,29 +29,31 @@ class UserRepository( override fun save(user: User): User { val existingUser = findById(user.id) - val entity = if (existingUser != null) { - val existingEntity = existingUser.toEntity() - user.toEntity( - provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, - providerId = existingEntity.providerId, - createdAt = existingEntity.createdAt - ) - } else { - user.toEntity() - } + val entity = + if (existingUser != null) { + val existingEntity = existingUser.toEntity() + user.toEntity( + provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, + providerId = existingEntity.providerId, + createdAt = existingEntity.createdAt, + ) + } else { + user.toEntity() + } - val updatedRows = jdbc.update( - """ - UPDATE users - SET name = ?, email = ?, provider = ?, provider_id = ? - WHERE id = ? - """.trimIndent(), - entity.name, - entity.email, - entity.provider, - entity.providerId, - entity.id, - ) + val updatedRows = + jdbc.update( + """ + UPDATE users + SET name = ?, email = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + entity.name, + entity.email, + entity.provider, + entity.providerId, + entity.id, + ) if (updatedRows == 0) { jdbc.update( @@ -72,28 +73,31 @@ class UserRepository( } override fun findById(id: UUID): User? { - val entities = jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", - userEntityRowMapper, - id, - ) + val entities = + jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", + userEntityRowMapper, + id, + ) return entities.firstOrNull()?.toDomain() } override fun findByEmail(email: String): User? { - val entities = jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", - userEntityRowMapper, - email, - ) + val entities = + jdbc.query( + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", + userEntityRowMapper, + email, + ) return entities.firstOrNull()?.toDomain() } override fun findAll(): List = - jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users", - userEntityRowMapper, - ).map { it.toDomain() } + jdbc + .query( + "SELECT id, name, email, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) @@ -103,16 +107,17 @@ class UserRepository( provider: AuthProvider, providerId: String, ): User? { - val entities = jdbc.query( - """ - SELECT id, name, email, provider, provider_id, created_at - FROM users - WHERE provider = ? AND provider_id = ? - """.trimIndent(), - userEntityRowMapper, - provider.name, - providerId, - ) + val entities = + jdbc.query( + """ + SELECT id, name, email, provider, provider_id, created_at + FROM users + WHERE provider = ? AND provider_id = ? + """.trimIndent(), + userEntityRowMapper, + provider.name, + providerId, + ) return entities.firstOrNull()?.toDomain() } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt index d885b91..0bcb1b3 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt @@ -18,22 +18,22 @@ class SecurityConfiguration( oauth2 .userInfoEndpoint { userInfo -> userInfo.userService(customOAuth2UserService) - } - .defaultSuccessUrl("/api/users/me", true) - } - .authorizeHttpRequests { auth -> + }.defaultSuccessUrl("/api/users/me", true) + }.authorizeHttpRequests { auth -> auth - .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health").permitAll() - .requestMatchers("/api/users/me").authenticated() - .requestMatchers("/api/**").authenticated() - .anyRequest().authenticated() - } - .headers { headers -> + .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health") + .permitAll() + .requestMatchers("/api/users/me") + .authenticated() + .requestMatchers("/api/**") + .authenticated() + .anyRequest() + .authenticated() + }.headers { headers -> headers.frameOptions { frameOptions -> frameOptions.sameOrigin() } - } - .csrf { csrf -> + }.csrf { csrf -> csrf.disable() } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index b5d7eb9..dd8eb93 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -10,8 +10,8 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, UserDetails { - +) : OAuth2User, + UserDetails { fun getId(): UUID = user.id override fun getName(): String = user.name @@ -19,7 +19,9 @@ class UserPrincipal( override fun getAttributes(): Map = attributes ?: emptyMap() override fun getAuthorities(): Collection = - listOf(SimpleGrantedAuthority("ROLE_USER")) + listOf( + SimpleGrantedAuthority("ROLE_USER"), + ) override fun getPassword(): String = "" @@ -34,7 +36,9 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create(user: User, attributes: Map? = null): UserPrincipal = - UserPrincipal(user, attributes) + fun create( + user: User, + attributes: Map? = null, + ): UserPrincipal = UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index 4d838fb..bccf5bc 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -20,7 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController -//import com.project.movienight.adapters.security.UserPrincipal import java.util.UUID @RestController @@ -74,12 +73,4 @@ class UserController( fun delete( @PathVariable id: UUID, ) = deleteUserUseCase.delete(id) - - /* - @GetMapping("/me") - fun getCurrentUser(principal: UserPrincipal): UserResponse = - UserResponse.fromDomain( - getUserByIdUseCase.getById(principal.getId()) - ) - */ } diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt -- 2.54.0 From 06e2b50ba517b301c866406c6b5e1c3ffe474b6e Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Wed, 22 Apr 2026 08:46:58 +0300 Subject: [PATCH 09/22] =?UTF-8?q?OAuth2=20(=D0=B1=D0=B5=D0=B7=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B0=20=D1=81=20java)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 + gradle/libs.versions.toml | 1 + .../security/CustomOAuth2UserService.kt | 57 +++++++++++++++++++ .../adapters/security/GoogleOAuth2UserInfo.kt | 16 ++++++ .../adapters/security/OAuth2UserInfo.kt | 9 +++ .../security/OAuth2UserInfoFactory.kt | 18 ++++++ .../adapters/security/UserPrincipal.kt | 42 ++++++++++++++ .../adapters/security/VkOAuth2UserInfo.kt | 26 +++++++++ .../adapters/security/YandexOAuth2UserInfo.kt | 20 +++++++ .../ports/output/UserRepositoryPort.kt | 2 + .../application/services/UserService.kt | 1 + .../project/movienight/domain/model/User.kt | 1 + 12 files changed, 195 insertions(+) create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index 72e9333..1b4b205 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,8 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) + implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.3") + runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) runtimeOnly(libs.postgresql) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7314bb3..66e6c13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ springdoc = "2.8.6" mockk = "1.13.12" [libraries] +spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt new file mode 100644 index 0000000..189e927 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -0,0 +1,57 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.domain.model.User +import org.slf4j.LoggerFactory +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service + +@Service +class CustomOAuth2UserService( + private val userRepository: UserRepositoryPort, + private val idGenerator: IdGenerator, +) : DefaultOAuth2UserService() { + + companion object { + private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) + } + + override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User { + val oAuth2User = super.loadUser(userRequest) + val registrationId = userRequest.clientRegistration.registrationId + + log.debug("Processing OAuth2 login for provider: {}", registrationId) + + return try { + val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) + val user = findOrCreateUser(userInfo) + UserPrincipal.create(user, oAuth2User.attributes) + } catch (e: Exception) { + log.error("OAuth2 authentication failed: ${e.message}", e) + throw OAuth2AuthenticationException("Failed to process OAuth2 user data") + } + } + + private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { + val existingUser = userRepository.findByEmail(userInfo.getEmail()) + + return if (existingUser != null) { + log.debug("User found by email: {}", userInfo.getEmail()) + existingUser + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", // OAuth2 пользователи не имеют пароля + library = null, + ) + userRepository.save(newUser) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt new file mode 100644 index 0000000..dcff86e --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -0,0 +1,16 @@ +package com.project.movienight.adapters.security.oauth2 + +class GoogleOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["sub"] as String + + override fun getEmail(): String = attributes["email"] as String + + override fun getName(): String = attributes["name"] as String + + override fun getProvider(): String = "google" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt new file mode 100644 index 0000000..5a155b9 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -0,0 +1,9 @@ +package com.project.movienight.adapters.security.oauth2 + +interface OAuth2UserInfo { + fun getProviderId(): String + fun getEmail(): String + fun getName(): String + fun getProvider(): String + fun getAttributes(): Map +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt new file mode 100644 index 0000000..001b789 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -0,0 +1,18 @@ +package com.project.movienight.adapters.security.oauth2 + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User + +object OAuth2UserInfoFactory { + + fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { + val attributes = user.attributes + + return when (registrationId.lowercase()) { + "google" -> GoogleOAuth2UserInfo(attributes) + "yandex" -> YandexOAuth2UserInfo(attributes) + "vk" -> VkOAuth2UserInfo(attributes) + else -> throw OAuth2AuthenticationException("Unknown provider: $registrationId") + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt new file mode 100644 index 0000000..5fbbee1 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -0,0 +1,42 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.domain.model.User +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.oauth2.core.user.OAuth2User +import java.util.* + +class UserPrincipal( + private val user: User, + private val attributes: Map? = null, +) : OAuth2User, UserDetails { + + fun getId(): UUID = user.id + + override fun getName(): String = user.name + + override fun getAttributes(): Map = attributes ?: emptyMap() + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority("ROLE_USER")) + } + + override fun getPassword(): String = user.password + + override fun getUsername(): String = user.email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = true + + companion object { + fun create(user: User, attributes: Map? = null): UserPrincipal { + return UserPrincipal(user, attributes) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt new file mode 100644 index 0000000..492e74c --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -0,0 +1,26 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class VkOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String { + val response = attributes["response"] as? List> + return response?.firstOrNull()?.get("id")?.toString() ?: "" + } + + override fun getEmail(): String = attributes["email"] as? String ?: "" + + override fun getName(): String { + val response = attributes["response"] as? List> + val first = response?.firstOrNull() + val firstName = first?.get("first_name") as? String ?: "" + val lastName = first?.get("last_name") as? String ?: "" + return "$firstName $lastName".trim() + } + + override fun getProvider(): String = "vk" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt new file mode 100644 index 0000000..2929389 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -0,0 +1,20 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class YandexOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["id"]?.toString() ?: "" + + override fun getEmail(): String { + val emails = attributes["emails"] as? List> + return emails?.firstOrNull()?.get("value") ?: "" + } + + override fun getName(): String = attributes["display_name"] as? String ?: "" + + override fun getProvider(): String = "yandex" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index e3c902c..dd69728 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -9,6 +9,8 @@ interface UserRepositoryPort { fun findById(id: UUID): User? + fun findByEmail(email: String): User? + fun findAll(): List fun deleteById(id: UUID) diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index da2a84b..e6f22be 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -36,6 +36,7 @@ class UserService( id = idGenerator.generateId(), name = command.name, email = command.email, + password = "", library = null, ) return userRepository.save(user) diff --git a/src/main/kotlin/com/project/movienight/domain/model/User.kt b/src/main/kotlin/com/project/movienight/domain/model/User.kt index b4f2d9b..db9142b 100644 --- a/src/main/kotlin/com/project/movienight/domain/model/User.kt +++ b/src/main/kotlin/com/project/movienight/domain/model/User.kt @@ -6,5 +6,6 @@ data class User( val id: UUID, val name: String, val email: String, + val password: String, val library: FilmLibrary?, ) -- 2.54.0 From 88794e6a963a840a5cc65eb405fdbe5031ea90cf Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 21 Apr 2026 15:30:58 +0300 Subject: [PATCH 10/22] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20V2=5F=5Fadd=5Foauth2=5Ffields.sql=20=D0=92=D1=81=D0=B5?= =?UTF-8?q?=20UserRepositoryIntegrationTest=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BF=D1=80=D0=BE=D1=88=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- .../resources/db/migration/V2__add_oauth2_fields.sql | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V2__add_oauth2_fields.sql diff --git a/build.gradle.kts b/build.gradle.kts index 1b4b205..24a137a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.actuator) -// implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.cache) implementation(libs.spring.boot.starter.data.jdbc) implementation(libs.spring.boot.starter.validation) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66e6c13..72e81e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ spring-grpc = "1.0.1" protoc = "3.25.1" grpc-java = "1.60.0" springdoc = "2.8.6" -mockk = "1.13.12" +mockk = "1.13.13" [libraries] spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } diff --git a/src/main/resources/db/migration/V2__add_oauth2_fields.sql b/src/main/resources/db/migration/V2__add_oauth2_fields.sql new file mode 100644 index 0000000..0db4084 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_oauth2_fields.sql @@ -0,0 +1,9 @@ +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); + -- 2.54.0 From 166c5be863c71e7d65856c39e3ffbb1b22944fac Mon Sep 17 00:00:00 2001 From: skettiks Date: Thu, 23 Apr 2026 18:47:30 +0300 Subject: [PATCH 11/22] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20V1=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=8F=D1=82=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20email=20=D0=A0=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20saveWithOAuth2()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20findByProviderAndProviderId()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20CustomOAuth2UserService=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20OAuth2=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20package=20declaration=20=D0=B2=D0=BE=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=D1=85=207=20OAuth2=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/persistence/entity/UserEntity.kt | 2 + .../persistence/jdbc/UserRepository.kt | 56 +++++++++++++++++-- .../security/CustomOAuth2UserService.kt | 37 ++++++++---- .../adapters/security/GoogleOAuth2UserInfo.kt | 2 +- .../adapters/security/OAuth2UserInfo.kt | 2 +- .../security/OAuth2UserInfoFactory.kt | 2 +- .../adapters/security/UserPrincipal.kt | 2 +- .../adapters/security/VkOAuth2UserInfo.kt | 2 +- .../adapters/security/YandexOAuth2UserInfo.kt | 2 +- .../ports/output/UserRepositoryPort.kt | 4 ++ src/main/resources/db/migration/V1__init.sql | 1 + 11 files changed, 88 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt index 0beda74..f606ea1 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt @@ -9,6 +9,7 @@ data class UserEntity( val id: UUID, val name: String, val email: String, + val password: String?, val provider: String?, val providerId: String?, val createdAt: LocalDateTime, @@ -31,6 +32,7 @@ fun User.toEntity( id = id, name = name, email = email, + password = password, provider = provider?.name, providerId = providerId, createdAt = createdAt, diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 686899f..f3ea7ad 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -20,6 +20,7 @@ class UserRepository( id = UUID.fromString(rs.getString("id")), name = rs.getString("name"), email = rs.getString("email"), + password = rs.getString("password"), provider = rs.getString("provider"), providerId = rs.getString("provider_id"), createdAt = rs.getTimestamp("created_at").toLocalDateTime(), @@ -32,11 +33,12 @@ class UserRepository( jdbc.update( """ UPDATE users - SET name = ?, email = ?, provider = ?, provider_id = ? + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? WHERE id = ? """.trimIndent(), entity.name, entity.email, + user.password, entity.provider, entity.providerId, entity.id, @@ -44,12 +46,13 @@ class UserRepository( if (updatedRows == 0) { jdbc.update( """ - INSERT INTO users (id, name, email, provider, provider_id, created_at) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO users (id, name, email, password, provider, provider_id, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) """.trimIndent(), entity.id, entity.name, entity.email, + user.password, entity.provider, entity.providerId, entity.createdAt, @@ -61,7 +64,7 @@ class UserRepository( override fun findById(id: UUID): User? { val entities = jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", + "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", userEntityRowMapper, id, ) @@ -71,7 +74,7 @@ class UserRepository( override fun findAll(): List = jdbc .query( - "SELECT id, name, email, provider, provider_id, created_at FROM users", + "SELECT id, name, email, password, provider, provider_id, created_at FROM users", userEntityRowMapper, ).map { it.toDomain() } @@ -79,6 +82,47 @@ class UserRepository( jdbc.update("DELETE FROM users WHERE id = ?", id) } + override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { + val updatedRows = jdbc.update( + """ + UPDATE users + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + user.name, + user.email, + user.password, + provider, + providerId, + user.id, + ) + if (updatedRows == 0) { + jdbc.update( + """ + INSERT INTO users (id, name, email, password, provider, provider_id) + VALUES (?, ?, ?, ?, ?, ?) + """.trimIndent(), + user.id, + user.name, + user.email, + user.password, + provider, + providerId, + ) + } + return user + } + + override fun findByProviderAndProviderId(provider: String, providerId: String): User? { + val entities = jdbc.query( + "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ?", + userEntityRowMapper, + provider, + providerId, + ) + return entities.firstOrNull()?.toDomain() + } + override fun findByProviderAndProviderId( provider: AuthProvider, providerId: String, @@ -86,7 +130,7 @@ class UserRepository( val entities = jdbc.query( """ - SELECT id, name, email, provider, provider_id, created_at + SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ? """.trimIndent(), diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt index 189e927..968424a 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort @@ -37,21 +37,34 @@ class CustomOAuth2UserService( } private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { - val existingUser = userRepository.findByEmail(userInfo.getEmail()) + // Сначала ищем по provider + provider_id (основной способ для OAuth2) + val existingUser = userRepository.findByProviderAndProviderId( + userInfo.getProvider(), + userInfo.getProviderId() + ) return if (existingUser != null) { - log.debug("User found by email: {}", userInfo.getEmail()) + log.debug("User found by provider: {}", userInfo.getProvider()) existingUser } else { - log.debug("Creating new user for provider: {}", userInfo.getProvider()) - val newUser = User( - id = idGenerator.generateId(), - name = userInfo.getName(), - email = userInfo.getEmail(), - password = "", // OAuth2 пользователи не имеют пароля - library = null, - ) - userRepository.save(newUser) + // Проверяем нет ли пользователя с таким email (связывание аккаунтов) + val userByEmail = userRepository.findByEmail(userInfo.getEmail()) + + if (userByEmail != null) { + // Пользователь существует, обновляем его OAuth2 данными + log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) + userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", + library = null, + ) + userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) + } } } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt index dcff86e..fa41de5 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security class GoogleOAuth2UserInfo( private val attributes: Map diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt index 5a155b9..b6abf09 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security interface OAuth2UserInfo { fun getProviderId(): String diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt index 001b789..e2db545 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index 5fbbee1..a4d94ea 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.domain.model.User import org.springframework.security.core.GrantedAuthority diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt index 492e74c..47c41d2 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class VkOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt index 2929389..467aa85 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class YandexOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index dd69728..678cea0 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -7,6 +7,10 @@ import java.util.UUID interface UserRepositoryPort { 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 findByEmail(email: String): User? diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 900f6b5..ee51933 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(320) NOT NULL UNIQUE, + password VARCHAR(255), provider VARCHAR(64), provider_id VARCHAR(255), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -- 2.54.0 From cf7e3411da760157840c0c5162a42f41eaa0e200 Mon Sep 17 00:00:00 2001 From: skettiks Date: Thu, 23 Apr 2026 23:59:16 +0300 Subject: [PATCH 12/22] =?UTF-8?q?-=20=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=91=D0=BD=20OAuth2UserInfo.kt=20=D0=B8=D0=B7=20adapter?= =?UTF-8?q?s/security/=20=D0=B2=20application/ports/input/security/;=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - В UserPrincipal.kt заменён star import java.util.* на явный java.util.UUID. - В YandexOAuth2UserInfo.kt и VkOAuth2UserInfo.kt убрана аннотация @Suppress("UNCHECKED_CAST") - В build.gradle.kts хардкод версии заменён на version catalog - Из CustomOAuth2UserService.kt удалены комментарии на русском языке --- build.gradle.kts | 3 +-- .../security/CustomOAuth2UserService.kt | 4 +--- .../adapters/security/GoogleOAuth2UserInfo.kt | 2 ++ .../security/OAuth2UserInfoFactory.kt | 1 + .../adapters/security/UserPrincipal.kt | 2 +- .../adapters/security/VkOAuth2UserInfo.kt | 20 +++++++++++-------- .../adapters/security/YandexOAuth2UserInfo.kt | 12 +++++++---- .../ports/input}/security/OAuth2UserInfo.kt | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) rename src/main/kotlin/com/project/movienight/{adapters => application/ports/input}/security/OAuth2UserInfo.kt (74%) diff --git a/build.gradle.kts b/build.gradle.kts index 24a137a..f20ddff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,8 +48,7 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) - - implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.3") + implementation(libs.spring.boot.starter.oauth2.client) runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt index 968424a..cc1cb8d 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -1,5 +1,6 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort import com.project.movienight.domain.model.User @@ -37,7 +38,6 @@ class CustomOAuth2UserService( } private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { - // Сначала ищем по provider + provider_id (основной способ для OAuth2) val existingUser = userRepository.findByProviderAndProviderId( userInfo.getProvider(), userInfo.getProviderId() @@ -47,11 +47,9 @@ class CustomOAuth2UserService( log.debug("User found by provider: {}", userInfo.getProvider()) existingUser } else { - // Проверяем нет ли пользователя с таким email (связывание аккаунтов) val userByEmail = userRepository.findByEmail(userInfo.getEmail()) if (userByEmail != null) { - // Пользователь существует, обновляем его OAuth2 данными log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) } else { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt index fa41de5..c463ac6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -1,5 +1,7 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class GoogleOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt index e2db545..89d6d30 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -1,5 +1,6 @@ package com.project.movienight.adapters.security +import com.project.movienight.application.ports.input.security.OAuth2UserInfo import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index a4d94ea..c602211 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -5,7 +5,7 @@ import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.oauth2.core.user.OAuth2User -import java.util.* +import java.util.UUID class UserPrincipal( private val user: User, diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt index 47c41d2..e2c55c0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -1,22 +1,26 @@ package com.project.movienight.adapters.security -@Suppress("UNCHECKED_CAST") +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class VkOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { override fun getProviderId(): String { - val response = attributes["response"] as? List> - return response?.firstOrNull()?.get("id")?.toString() ?: "" + return (attributes["response"] as? List<*>) + ?.firstOrNull() + ?.let { it as? Map<*, *> } + ?.get("id") + ?.toString() ?: "" } - override fun getEmail(): String = attributes["email"] as? String ?: "" + override fun getEmail(): String = attributes["email"]?.toString() ?: "" override fun getName(): String { - val response = attributes["response"] as? List> - val first = response?.firstOrNull() - val firstName = first?.get("first_name") as? String ?: "" - val lastName = first?.get("last_name") as? String ?: "" + val response = attributes["response"] as? List<*> + val first = response?.firstOrNull() as? Map<*, *> + val firstName = first?.get("first_name")?.toString() ?: "" + val lastName = first?.get("last_name")?.toString() ?: "" return "$firstName $lastName".trim() } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt index 467aa85..59bf3eb 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -1,6 +1,7 @@ package com.project.movienight.adapters.security -@Suppress("UNCHECKED_CAST") +import com.project.movienight.application.ports.input.security.OAuth2UserInfo + class YandexOAuth2UserInfo( private val attributes: Map ) : OAuth2UserInfo { @@ -8,11 +9,14 @@ class YandexOAuth2UserInfo( override fun getProviderId(): String = attributes["id"]?.toString() ?: "" override fun getEmail(): String { - val emails = attributes["emails"] as? List> - return emails?.firstOrNull()?.get("value") ?: "" + return (attributes["emails"] as? List<*>) + ?.firstOrNull() + ?.let { it as? Map<*, *> } + ?.get("value") + ?.toString() ?: "" } - override fun getName(): String = attributes["display_name"] as? String ?: "" + override fun getName(): String = attributes["display_name"]?.toString() ?: "" override fun getProvider(): String = "yandex" diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt similarity index 74% rename from src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt index b6abf09..c081592 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security +package com.project.movienight.application.ports.input.security interface OAuth2UserInfo { fun getProviderId(): String -- 2.54.0 From a10737a7f7d45b18dadf81ca4a25a96e39b5244f Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 5 May 2026 00:58:20 +0300 Subject: [PATCH 13/22] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=20=D1=81=D0=BB=D0=B8=D1=8F=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=20develop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - После слияния с develop возникли конфликты в реализации OAuth2 - Две ветки независимо реализовали OAuth2 функциональность по-разному - Сборка проекта падала из-за отсутствия зависимостей OAuth2 Изменения: - Временно отключена OAuth2 зависимость в build.gradle.kts - Перенесен OAuth2 код в папку security.disabled для сохранения - Добавлены исключения security.disabled из компиляции, ktlint и detekt - Удалена миграция V2__add_oauth2_fields.sql (OAuth2 поля теперь в V1) - Удалено поле password из User domain модели - Обновлены репозитории и сервисы для работы с новой схемой БД Результат: - Проект успешно собирается (./gradlew build) - Все 35 тестов проходят - OAuth2 код сохранен для будущего использования --- build.gradle.kts | 9 +++- .../movienight/MovieNightApplication.kt | 3 +- .../persistence/jdbc/UserRepository.kt | 10 ++++ .../CustomOAuth2UserService.kt | 48 +++++++++++++------ .../GoogleOAuth2UserInfo.kt | 3 +- .../OAuth2UserInfoFactory.kt | 6 ++- .../UserPrincipal.kt | 17 ++++--- .../VkOAuth2UserInfo.kt | 8 ++-- .../YandexOAuth2UserInfo.kt | 8 ++-- .../adapters/web/FilmLibraryController.kt | 1 + .../OAuth2UserInfo.kt | 4 ++ .../ports/output/UserRepositoryPort.kt | 4 -- .../application/services/UserService.kt | 1 - .../project/movienight/domain/model/User.kt | 1 - .../db/migration/V2__add_oauth2_fields.sql | 9 ---- 15 files changed, 76 insertions(+), 56 deletions(-) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/CustomOAuth2UserService.kt (61%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/GoogleOAuth2UserInfo.kt (91%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/OAuth2UserInfoFactory.kt (85%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/UserPrincipal.kt (77%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/VkOAuth2UserInfo.kt (84%) rename src/main/kotlin/com/project/movienight/adapters/{security => security.disabled}/YandexOAuth2UserInfo.kt (80%) rename src/main/kotlin/com/project/movienight/application/ports/input/{security => security.disabled}/OAuth2UserInfo.kt (98%) delete mode 100644 src/main/resources/db/migration/V2__add_oauth2_fields.sql diff --git a/build.gradle.kts b/build.gradle.kts index f20ddff..6fcffe6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,8 @@ plugins { jacoco } -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") @@ -48,7 +49,8 @@ dependencies { implementation(libs.spring.grpc.starter) 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.h2) @@ -76,6 +78,7 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) allWarningsAsErrors.set(false) } + exclude("**/security.disabled/**") } tasks.withType { @@ -158,6 +161,7 @@ ktlint { filter { exclude("**/build/**") exclude("**/generated/**") + exclude("**/security.disabled/**") } } @@ -171,6 +175,7 @@ detekt { tasks.withType().configureEach { jvmTarget = "21" + exclude("**/security.disabled/**") reports { html.required.set(true) xml.required.set(true) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index 39274c8..f6c7d33 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,10 +1,11 @@ package com.project.movienight 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.runApplication -@SpringBootApplication +@SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class]) @ConfigurationPropertiesScan("com.project.movienight.config") class MovieNightApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index f3ea7ad..95d123f 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -71,6 +71,16 @@ class UserRepository( 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 = jdbc .query( diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt similarity index 61% rename from src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt index cc1cb8d..b64ea71 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt @@ -1,8 +1,11 @@ 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.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.domain.model.AuthProvider import com.project.movienight.domain.model.User import org.slf4j.LoggerFactory import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService @@ -16,7 +19,6 @@ class CustomOAuth2UserService( private val userRepository: UserRepositoryPort, private val idGenerator: IdGenerator, ) : DefaultOAuth2UserService() { - companion object { private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) } @@ -31,17 +33,23 @@ class CustomOAuth2UserService( val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) val user = findOrCreateUser(userInfo) UserPrincipal.create(user, oAuth2User.attributes) - } catch (e: Exception) { + } catch (e: IllegalArgumentException) { log.error("OAuth2 authentication failed: ${e.message}", e) 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 { - val existingUser = userRepository.findByProviderAndProviderId( - userInfo.getProvider(), - userInfo.getProviderId() - ) + val provider = AuthProvider.valueOf(userInfo.getProvider().uppercase()) + + val existingUser = + userRepository.findByProviderAndProviderId( + provider, + userInfo.getProviderId(), + ) return if (existingUser != null) { log.debug("User found by provider: {}", userInfo.getProvider()) @@ -51,17 +59,27 @@ class CustomOAuth2UserService( if (userByEmail != null) { 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 { log.debug("Creating new user for provider: {}", userInfo.getProvider()) - val newUser = User( - id = idGenerator.generateId(), - name = userInfo.getName(), - email = userInfo.getEmail(), - password = "", - library = null, - ) - userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) + val newUser = + User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + library = null, + ) + val entity = + newUser.toEntity( + provider = provider, + providerId = userInfo.getProviderId(), + ) + userRepository.save(entity.toDomain()) } } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt similarity index 91% rename from src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt index c463ac6..123f9a0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt @@ -3,9 +3,8 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class GoogleOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - override fun getProviderId(): String = attributes["sub"] as String override fun getEmail(): String = attributes["email"] as String diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt similarity index 85% rename from src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt index 89d6d30..1faf660 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt @@ -5,8 +5,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User object OAuth2UserInfoFactory { - - fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { + fun getOAuth2UserInfo( + registrationId: String, + user: OAuth2User, + ): OAuth2UserInfo { val attributes = user.attributes return when (registrationId.lowercase()) { diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt similarity index 77% rename from src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt index c602211..3ea81b4 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt @@ -10,19 +10,17 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, UserDetails { - +) : OAuth2User, + UserDetails { fun getId(): UUID = user.id override fun getName(): String = user.name override fun getAttributes(): Map = attributes ?: emptyMap() - override fun getAuthorities(): Collection { - return listOf(SimpleGrantedAuthority("ROLE_USER")) - } + override fun getAuthorities(): Collection = listOf(SimpleGrantedAuthority("ROLE_USER")) - override fun getPassword(): String = user.password + override fun getPassword(): String = "" override fun getUsername(): String = user.email @@ -35,8 +33,9 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create(user: User, attributes: Map? = null): UserPrincipal { - return UserPrincipal(user, attributes) - } + fun create( + user: User, + attributes: Map? = null, + ): UserPrincipal = UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt similarity index 84% rename from src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt index e2c55c0..9b55f35 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt @@ -3,16 +3,14 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class VkOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - - override fun getProviderId(): String { - return (attributes["response"] as? List<*>) + override fun getProviderId(): String = + (attributes["response"] as? List<*>) ?.firstOrNull() ?.let { it as? Map<*, *> } ?.get("id") ?.toString() ?: "" - } override fun getEmail(): String = attributes["email"]?.toString() ?: "" diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt similarity index 80% rename from src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt index 59bf3eb..dc71df9 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt @@ -3,18 +3,16 @@ package com.project.movienight.adapters.security import com.project.movienight.application.ports.input.security.OAuth2UserInfo class YandexOAuth2UserInfo( - private val attributes: Map + private val attributes: Map, ) : OAuth2UserInfo { - override fun getProviderId(): String = attributes["id"]?.toString() ?: "" - override fun getEmail(): String { - return (attributes["emails"] as? List<*>) + override fun getEmail(): String = + (attributes["emails"] as? List<*>) ?.firstOrNull() ?.let { it as? Map<*, *> } ?.get("value") ?.toString() ?: "" - } override fun getName(): String = attributes["display_name"]?.toString() ?: "" diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt index 998a330..c2ed037 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -68,6 +68,7 @@ class FilmLibraryController( getFilmLibraryUseCase.getLibrary( GetFilmLibraryQuery(userId = userId), ) + val film = getFilmByIdUseCase.getById(library.filmId) return listOf(FilmResponse.fromDomain(film)) } diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt similarity index 98% rename from src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt index c081592..e45db2b 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt @@ -2,8 +2,12 @@ package com.project.movienight.application.ports.input.security interface OAuth2UserInfo { fun getProviderId(): String + fun getEmail(): String + fun getName(): String + fun getProvider(): String + fun getAttributes(): Map } diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index 678cea0..dd69728 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -7,10 +7,6 @@ import java.util.UUID interface UserRepositoryPort { 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 findByEmail(email: String): User? diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index e6f22be..da2a84b 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -36,7 +36,6 @@ class UserService( id = idGenerator.generateId(), name = command.name, email = command.email, - password = "", library = null, ) return userRepository.save(user) diff --git a/src/main/kotlin/com/project/movienight/domain/model/User.kt b/src/main/kotlin/com/project/movienight/domain/model/User.kt index db9142b..b4f2d9b 100644 --- a/src/main/kotlin/com/project/movienight/domain/model/User.kt +++ b/src/main/kotlin/com/project/movienight/domain/model/User.kt @@ -6,6 +6,5 @@ data class User( val id: UUID, val name: String, val email: String, - val password: String, val library: FilmLibrary?, ) diff --git a/src/main/resources/db/migration/V2__add_oauth2_fields.sql b/src/main/resources/db/migration/V2__add_oauth2_fields.sql deleted file mode 100644 index 0db4084..0000000 --- a/src/main/resources/db/migration/V2__add_oauth2_fields.sql +++ /dev/null @@ -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); - -- 2.54.0 From 82a0cec1ccd81a2813bc108502b88c386f9ae258 Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Thu, 7 May 2026 07:58:11 +0300 Subject: [PATCH 14/22] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BB=D0=B0=20security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 +- .../movienight/MovieNightApplication.kt | 2 +- .../persistence/jdbc/UserRepository.kt | 93 ++++++++++--------- .../SecurityConfiguration.kt | 42 +++++++++ .../security.disabled/UserPrincipal.kt | 13 ++- .../movienight/adapters/web/UserController.kt | 9 ++ src/main/resources/application.yaml | 32 +++++++ .../db/migration/V2__add_oauth2_index.sql | 3 + 8 files changed, 146 insertions(+), 52 deletions(-) create mode 100644 src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt create mode 100644 src/main/resources/db/migration/V2__add_oauth2_index.sql diff --git a/build.gradle.kts b/build.gradle.kts index 6fcffe6..8468c3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,8 +49,8 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) - // Temporarily disabled due to OAuth2 configuration issues - // implementation(libs.spring.boot.starter.oauth2.client) + + implementation(libs.spring.boot.starter.oauth2.client) runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index f6c7d33..a792cd2 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -5,7 +5,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAu import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication -@SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class]) +@SpringBootApplication @ConfigurationPropertiesScan("com.project.movienight.config") class MovieNightApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 95d123f..dc5364e 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,6 +9,7 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet +import java.time.LocalDateTime import java.util.UUID @Repository @@ -28,21 +29,33 @@ class UserRepository( } override fun save(user: User): User { - val entity = user.toEntity() - val updatedRows = - jdbc.update( - """ - UPDATE users - SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? - WHERE id = ? - """.trimIndent(), - entity.name, - entity.email, - user.password, - entity.provider, - entity.providerId, - entity.id, + val existingUser = findById(user.id) + + val entity = if (existingUser != null) { + val existingEntity = existingUser.toEntity() + user.toEntity( + provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, + providerId = existingEntity.providerId, + createdAt = existingEntity.createdAt, ) + } else { + user.toEntity() + } + + val updatedRows = jdbc.update( + """ + UPDATE users + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + entity.name, + entity.email, + user.password, + entity.provider, + entity.providerId, + entity.id, + ) + if (updatedRows == 0) { jdbc.update( """ @@ -62,31 +75,28 @@ class UserRepository( } override fun findById(id: UUID): User? { - val entities = - jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", - userEntityRowMapper, - id, - ) + val entities = jdbc.query( + "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", + userEntityRowMapper, + id, + ) 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, - ) + 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 = - jdbc - .query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users", - userEntityRowMapper, - ).map { it.toDomain() } + jdbc.query( + "SELECT id, name, email, password, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) @@ -137,17 +147,16 @@ class UserRepository( provider: AuthProvider, providerId: String, ): User? { - val entities = - jdbc.query( - """ - SELECT id, name, email, password, provider, provider_id, created_at - FROM users - WHERE provider = ? AND provider_id = ? - """.trimIndent(), - userEntityRowMapper, - provider.name, - providerId, - ) + val entities = jdbc.query( + """ + SELECT id, name, email, password, provider, provider_id, created_at + FROM users + WHERE provider = ? AND provider_id = ? + """.trimIndent(), + userEntityRowMapper, + provider.name, + providerId, + ) return entities.firstOrNull()?.toDomain() } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt new file mode 100644 index 0000000..d885b91 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt @@ -0,0 +1,42 @@ +package com.project.movienight.adapters.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class SecurityConfiguration( + private val customOAuth2UserService: CustomOAuth2UserService, +) { + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http + .oauth2Login { oauth2 -> + oauth2 + .userInfoEndpoint { userInfo -> + userInfo.userService(customOAuth2UserService) + } + .defaultSuccessUrl("/api/users/me", true) + } + .authorizeHttpRequests { auth -> + auth + .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health").permitAll() + .requestMatchers("/api/users/me").authenticated() + .requestMatchers("/api/**").authenticated() + .anyRequest().authenticated() + } + .headers { headers -> + headers.frameOptions { frameOptions -> + frameOptions.sameOrigin() + } + } + .csrf { csrf -> + csrf.disable() + } + + return http.build() + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt index 3ea81b4..b5d7eb9 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt @@ -10,15 +10,16 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, - UserDetails { +) : OAuth2User, UserDetails { + fun getId(): UUID = user.id override fun getName(): String = user.name override fun getAttributes(): Map = attributes ?: emptyMap() - override fun getAuthorities(): Collection = listOf(SimpleGrantedAuthority("ROLE_USER")) + override fun getAuthorities(): Collection = + listOf(SimpleGrantedAuthority("ROLE_USER")) override fun getPassword(): String = "" @@ -33,9 +34,7 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create( - user: User, - attributes: Map? = null, - ): UserPrincipal = UserPrincipal(user, attributes) + fun create(user: User, attributes: Map? = null): UserPrincipal = + UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index bccf5bc..4d838fb 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController +//import com.project.movienight.adapters.security.UserPrincipal import java.util.UUID @RestController @@ -73,4 +74,12 @@ class UserController( fun delete( @PathVariable id: UUID, ) = deleteUserUseCase.delete(id) + + /* + @GetMapping("/me") + fun getCurrentUser(principal: UserPrincipal): UserResponse = + UserResponse.fromDomain( + getUserByIdUseCase.getById(principal.getId()) + ) + */ } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f36c77d..86c806c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,6 +25,38 @@ spring: console: enabled: ${SPRING_H2_CONSOLE_ENABLED:true} path: /h2-console + security: + oauth2: + client: + registration: + google: + client-id: ${OAUTH2_GOOGLE_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET:test-secret} + scope: email,profile + yandex: + client-id: ${OAUTH2_YANDEX_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_YANDEX_CLIENT_SECRET:test-secret} + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: login:email,login:avatar + vk: + client-id: ${OAUTH2_VK_CLIENT_ID:test-client-id} + client-secret: ${OAUTH2_VK_CLIENT_SECRET:test-secret} + authorization-grant-type: authorization_code + client-authentication-method: client_secret_post + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: email + provider: + yandex: + authorization-uri: https://oauth.yandex.ru/authorize + token-uri: https://oauth.yandex.ru/token + user-info-uri: https://login.yandex.ru/info + user-name-attribute: id + vk: + authorization-uri: https://oauth.vk.com/authorize + token-uri: https://oauth.vk.com/access_token + user-info-uri: https://api.vk.com/method/users.get?v=5.131&fields=photo_200 + user-name-attribute: response server: shutdown: graceful diff --git a/src/main/resources/db/migration/V2__add_oauth2_index.sql b/src/main/resources/db/migration/V2__add_oauth2_index.sql new file mode 100644 index 0000000..d416108 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_oauth2_index.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_provider_provider_id +ON users(provider, provider_id) +WHERE provider IS NOT NULL AND provider_id IS NOT NULL; -- 2.54.0 From aecf19aa116e7fd13475acbf2f76b0714ee6221f Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Thu, 7 May 2026 08:01:24 +0300 Subject: [PATCH 15/22] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20=D0=B8=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/project/movienight/MovieNightApplication.kt | 2 +- .../movienight/adapters/persistence/jdbc/UserRepository.kt | 2 +- .../{security.disabled => security}/CustomOAuth2UserService.kt | 0 .../{security.disabled => security}/GoogleOAuth2UserInfo.kt | 0 .../{security.disabled => security}/OAuth2UserInfoFactory.kt | 0 .../{security.disabled => security}/SecurityConfiguration.kt | 0 .../adapters/{security.disabled => security}/UserPrincipal.kt | 0 .../{security.disabled => security}/VkOAuth2UserInfo.kt | 0 .../{security.disabled => security}/YandexOAuth2UserInfo.kt | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/CustomOAuth2UserService.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/GoogleOAuth2UserInfo.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/OAuth2UserInfoFactory.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/SecurityConfiguration.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/UserPrincipal.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/VkOAuth2UserInfo.kt (100%) rename src/main/kotlin/com/project/movienight/adapters/{security.disabled => security}/YandexOAuth2UserInfo.kt (100%) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index a792cd2..6ac4f10 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,7 +1,7 @@ package com.project.movienight import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration +//import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index dc5364e..fe1a044 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,7 +9,7 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet -import java.time.LocalDateTime +//import java.time.LocalDateTime import java.util.UUID @Repository diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/CustomOAuth2UserService.kt rename to src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/GoogleOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/OAuth2UserInfoFactory.kt rename to src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/SecurityConfiguration.kt rename to src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/UserPrincipal.kt rename to src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/VkOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/adapters/security.disabled/YandexOAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt -- 2.54.0 From d4dbc108c8d0131263e1fa87e9b2ae982bd7c00e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:37:35 +0000 Subject: [PATCH 16/22] fix: resolve CI compile failure and complete OAuth2 review fixes Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/62eaa8e4-560b-4737-be4b-478f1a4c484a Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../movienight/MovieNightApplication.kt | 1 - .../persistence/jdbc/UserRepository.kt | 101 +++++++++--------- .../security/SecurityConfiguration.kt | 24 ++--- .../adapters/security/UserPrincipal.kt | 14 ++- .../movienight/adapters/web/UserController.kt | 9 -- .../OAuth2UserInfo.kt | 0 6 files changed, 74 insertions(+), 75 deletions(-) rename src/main/kotlin/com/project/movienight/application/ports/input/{security.disabled => security}/OAuth2UserInfo.kt (100%) diff --git a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt index 6ac4f10..39274c8 100644 --- a/src/main/kotlin/com/project/movienight/MovieNightApplication.kt +++ b/src/main/kotlin/com/project/movienight/MovieNightApplication.kt @@ -1,7 +1,6 @@ package com.project.movienight 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.runApplication diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index fe1a044..1d05ec5 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -9,7 +9,6 @@ import com.project.movienight.domain.model.User import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository import java.sql.ResultSet -//import java.time.LocalDateTime import java.util.UUID @Repository @@ -31,30 +30,32 @@ class UserRepository( override fun save(user: User): User { val existingUser = findById(user.id) - val entity = if (existingUser != null) { - val existingEntity = existingUser.toEntity() - user.toEntity( - provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, - providerId = existingEntity.providerId, - createdAt = existingEntity.createdAt, - ) - } else { - user.toEntity() - } + val entity = + if (existingUser != null) { + val existingEntity = existingUser.toEntity() + user.toEntity( + provider = existingEntity.provider?.let { AuthProvider.valueOf(it) }, + providerId = existingEntity.providerId, + createdAt = existingEntity.createdAt, + ) + } else { + user.toEntity() + } - val updatedRows = jdbc.update( - """ - UPDATE users - SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? - WHERE id = ? - """.trimIndent(), - entity.name, - entity.email, - user.password, - entity.provider, - entity.providerId, - entity.id, - ) + val updatedRows = + jdbc.update( + """ + UPDATE users + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + entity.name, + entity.email, + user.password, + entity.provider, + entity.providerId, + entity.id, + ) if (updatedRows == 0) { jdbc.update( @@ -75,28 +76,31 @@ class UserRepository( } override fun findById(id: UUID): User? { - val entities = jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", - userEntityRowMapper, - id, - ) + val entities = + jdbc.query( + "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", + userEntityRowMapper, + id, + ) 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, - ) + 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 = - jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users", - userEntityRowMapper, - ).map { it.toDomain() } + jdbc + .query( + "SELECT id, name, email, password, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) @@ -147,16 +151,17 @@ class UserRepository( provider: AuthProvider, providerId: String, ): User? { - val entities = jdbc.query( - """ - SELECT id, name, email, password, provider, provider_id, created_at - FROM users - WHERE provider = ? AND provider_id = ? - """.trimIndent(), - userEntityRowMapper, - provider.name, - providerId, - ) + val entities = + jdbc.query( + """ + SELECT id, name, email, password, provider, provider_id, created_at + FROM users + WHERE provider = ? AND provider_id = ? + """.trimIndent(), + userEntityRowMapper, + provider.name, + providerId, + ) return entities.firstOrNull()?.toDomain() } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt index d885b91..0bcb1b3 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/SecurityConfiguration.kt @@ -18,22 +18,22 @@ class SecurityConfiguration( oauth2 .userInfoEndpoint { userInfo -> userInfo.userService(customOAuth2UserService) - } - .defaultSuccessUrl("/api/users/me", true) - } - .authorizeHttpRequests { auth -> + }.defaultSuccessUrl("/api/users/me", true) + }.authorizeHttpRequests { auth -> auth - .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health").permitAll() - .requestMatchers("/api/users/me").authenticated() - .requestMatchers("/api/**").authenticated() - .anyRequest().authenticated() - } - .headers { headers -> + .requestMatchers("/", "/login/**", "/oauth2/**", "/h2-console/**", "/actuator/health") + .permitAll() + .requestMatchers("/api/users/me") + .authenticated() + .requestMatchers("/api/**") + .authenticated() + .anyRequest() + .authenticated() + }.headers { headers -> headers.frameOptions { frameOptions -> frameOptions.sameOrigin() } - } - .csrf { csrf -> + }.csrf { csrf -> csrf.disable() } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index b5d7eb9..dd8eb93 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -10,8 +10,8 @@ import java.util.UUID class UserPrincipal( private val user: User, private val attributes: Map? = null, -) : OAuth2User, UserDetails { - +) : OAuth2User, + UserDetails { fun getId(): UUID = user.id override fun getName(): String = user.name @@ -19,7 +19,9 @@ class UserPrincipal( override fun getAttributes(): Map = attributes ?: emptyMap() override fun getAuthorities(): Collection = - listOf(SimpleGrantedAuthority("ROLE_USER")) + listOf( + SimpleGrantedAuthority("ROLE_USER"), + ) override fun getPassword(): String = "" @@ -34,7 +36,9 @@ class UserPrincipal( override fun isEnabled(): Boolean = true companion object { - fun create(user: User, attributes: Map? = null): UserPrincipal = - UserPrincipal(user, attributes) + fun create( + user: User, + attributes: Map? = null, + ): UserPrincipal = UserPrincipal(user, attributes) } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index 4d838fb..bccf5bc 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -20,7 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController -//import com.project.movienight.adapters.security.UserPrincipal import java.util.UUID @RestController @@ -74,12 +73,4 @@ class UserController( fun delete( @PathVariable id: UUID, ) = deleteUserUseCase.delete(id) - - /* - @GetMapping("/me") - fun getCurrentUser(principal: UserPrincipal): UserResponse = - UserResponse.fromDomain( - getUserByIdUseCase.getById(principal.getId()) - ) - */ } diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt similarity index 100% rename from src/main/kotlin/com/project/movienight/application/ports/input/security.disabled/OAuth2UserInfo.kt rename to src/main/kotlin/com/project/movienight/application/ports/input/security/OAuth2UserInfo.kt -- 2.54.0 From a4f99bbe8a127d99428587dd865cdb0a721b6420 Mon Sep 17 00:00:00 2001 From: skettiks Date: Fri, 15 May 2026 21:18:46 +0300 Subject: [PATCH 17/22] =?UTF-8?q?*=20=D0=A1=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20`UserE?= =?UTF-8?q?ntity`=20=D0=B8=20`UserRepository`=20=D1=81=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D1=8E=20`User`=20=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=BE=D0=BC=20`UserReposit?= =?UTF-8?q?oryPort`:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D1=83=D1=8E=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D1=81=20`password`=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BD=D0=B5=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B4=D0=BD=D1=8B=D0=B5=20`override`.=20*=20?= =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BA=D0=BE=D0=BD=D1=84=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D1=82=D1=83=D1=8E=D1=89=D0=B8=D0=B5=20gRPC-=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8,?= =?UTF-8?q?=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D1=82=D1=8F?= =?UTF-8?q?=D0=BD=D1=83=D0=BB=D0=B8=20=D0=BD=D0=B5=D1=81=D0=BE=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D1=8B=D0=B9=20runtime/test=20sta?= =?UTF-8?q?ck.=20*=20=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20Flyway-=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20`V2`=20=D1=81=20OA?= =?UTF-8?q?uth2-=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=BE=D0=BC=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=20H2.=20*=20=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B8?= =?UTF-8?q?=D0=BB=20security-=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B2=20controller-=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20`MockMvc`,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B8=20=D0=BD=D0=B5=D1=81=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2.=20*?= =?UTF-8?q?=20=D0=92=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D0=B8=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 5 -- .../adapters/persistence/entity/UserEntity.kt | 2 - .../persistence/jdbc/UserRepository.kt | 58 +++---------------- .../db/migration/V2__add_oauth2_index.sql | 3 +- .../controllers/FilmControllerTest.kt | 2 +- .../controllers/FilmLibraryControllerTest.kt | 2 +- .../controllers/UserControllerTest.kt | 2 +- 7 files changed, 11 insertions(+), 63 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8468c3a..a3b3bf6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,6 @@ java { dependencies { implementation(platform(libs.sentry.bom)) - implementation(platform(libs.spring.grpc.bom)) implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.actuator) @@ -47,9 +46,6 @@ dependencies { implementation(libs.opentelemetry.exporter.otlp) implementation(libs.sentry.spring.boot.starter) - implementation(libs.spring.grpc.starter) - implementation(libs.grpc.services) - implementation(libs.spring.boot.starter.oauth2.client) runtimeOnly(libs.micrometer.registry.prometheus) @@ -60,7 +56,6 @@ dependencies { testImplementation(libs.spring.boot.starter.test) testImplementation(libs.kotlin.test.junit5) - testImplementation(libs.spring.grpc.test) testImplementation(libs.mockk) testRuntimeOnly(libs.junit.platform.launcher) } diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt index f606ea1..0beda74 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt @@ -9,7 +9,6 @@ data class UserEntity( val id: UUID, val name: String, val email: String, - val password: String?, val provider: String?, val providerId: String?, val createdAt: LocalDateTime, @@ -32,7 +31,6 @@ fun User.toEntity( id = id, name = name, email = email, - password = password, provider = provider?.name, providerId = providerId, createdAt = createdAt, diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 1d05ec5..82fcb93 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -20,7 +20,6 @@ class UserRepository( id = UUID.fromString(rs.getString("id")), name = rs.getString("name"), email = rs.getString("email"), - password = rs.getString("password"), provider = rs.getString("provider"), providerId = rs.getString("provider_id"), createdAt = rs.getTimestamp("created_at").toLocalDateTime(), @@ -46,12 +45,11 @@ class UserRepository( jdbc.update( """ UPDATE users - SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + SET name = ?, email = ?, provider = ?, provider_id = ? WHERE id = ? """.trimIndent(), entity.name, entity.email, - user.password, entity.provider, entity.providerId, entity.id, @@ -60,13 +58,12 @@ class UserRepository( if (updatedRows == 0) { jdbc.update( """ - INSERT INTO users (id, name, email, password, provider, provider_id, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (id, name, email, provider, provider_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) """.trimIndent(), entity.id, entity.name, entity.email, - user.password, entity.provider, entity.providerId, entity.createdAt, @@ -78,7 +75,7 @@ class UserRepository( override fun findById(id: UUID): User? { val entities = jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE id = ?", + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", userEntityRowMapper, id, ) @@ -88,7 +85,7 @@ class UserRepository( override fun findByEmail(email: String): User? { val entities = jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE email = ?", + "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?", userEntityRowMapper, email, ) @@ -98,7 +95,7 @@ class UserRepository( override fun findAll(): List = jdbc .query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users", + "SELECT id, name, email, provider, provider_id, created_at FROM users", userEntityRowMapper, ).map { it.toDomain() } @@ -106,47 +103,6 @@ class UserRepository( jdbc.update("DELETE FROM users WHERE id = ?", id) } - override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { - val updatedRows = jdbc.update( - """ - UPDATE users - SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? - WHERE id = ? - """.trimIndent(), - user.name, - user.email, - user.password, - provider, - providerId, - user.id, - ) - if (updatedRows == 0) { - jdbc.update( - """ - INSERT INTO users (id, name, email, password, provider, provider_id) - VALUES (?, ?, ?, ?, ?, ?) - """.trimIndent(), - user.id, - user.name, - user.email, - user.password, - provider, - providerId, - ) - } - return user - } - - override fun findByProviderAndProviderId(provider: String, providerId: String): User? { - val entities = jdbc.query( - "SELECT id, name, email, password, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ?", - userEntityRowMapper, - provider, - providerId, - ) - return entities.firstOrNull()?.toDomain() - } - override fun findByProviderAndProviderId( provider: AuthProvider, providerId: String, @@ -154,7 +110,7 @@ class UserRepository( val entities = jdbc.query( """ - SELECT id, name, email, password, provider, provider_id, created_at + SELECT id, name, email, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ? """.trimIndent(), diff --git a/src/main/resources/db/migration/V2__add_oauth2_index.sql b/src/main/resources/db/migration/V2__add_oauth2_index.sql index d416108..b92102c 100644 --- a/src/main/resources/db/migration/V2__add_oauth2_index.sql +++ b/src/main/resources/db/migration/V2__add_oauth2_index.sql @@ -1,3 +1,2 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_provider_provider_id -ON users(provider, provider_id) -WHERE provider IS NOT NULL AND provider_id IS NOT NULL; +ON users(provider, provider_id); diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt index 18a952a..c91e49e 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt @@ -17,7 +17,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPat import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class FilmControllerTest { @Autowired private lateinit var mockMvc: MockMvc diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt index 560cf38..16d835d 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt @@ -17,7 +17,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.transaction.annotation.Transactional @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) @Transactional class FilmLibraryControllerTest { @Autowired diff --git a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt index 7a2dca3..342fda9 100644 --- a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt @@ -17,7 +17,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.transaction.annotation.Transactional @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) @Transactional class UserControllerTest { @Autowired -- 2.54.0 From bc06654c84650df27adccbdc4890741527b11d2e Mon Sep 17 00:00:00 2001 From: skettiks Date: Fri, 15 May 2026 22:20:03 +0300 Subject: [PATCH 18/22] =?UTF-8?q?-=20=20=20=20=20exclude("**/security.disa?= =?UTF-8?q?bled/**")=20-=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20-=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=B3=D0=B8=D0=BD=20org.graalvm.buildtools.native=20=D0=B2=20b?= =?UTF-8?q?uild.gradle.kts,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=8F=D0=B2=D0=B8=D0=BB=D0=B8=D1=81=D1=8C=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B8=20processAot=20=D0=B8=20nativeCompile.?= =?UTF-8?q?=20-=20=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=20confi?= =?UTF-8?q?guration-cache=20=D0=B2=20gradle.properties,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=BB=D0=B8=D0=BA=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20nativeCompile=20=D0=BD=D0=B0=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=83=D1=89=D0=B5=D0=BC=20toolchain.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 7 +------ gradle.properties | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a3b3bf6..630e545 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,15 +9,13 @@ plugins { alias(libs.plugins.kotlin.spring) alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependency.management) + id("org.graalvm.buildtools.native") version "0.10.5" alias(libs.plugins.protobuf) alias(libs.plugins.ktlint) alias(libs.plugins.detekt) jacoco } -// Temporarily disabled due to OAuth2 AOT processing issues -// apply(plugin = "org.springframework.boot.aot") - apply(from = "$rootDir/gradle/docker.gradle.kts") group = "com.project" @@ -73,7 +71,6 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) allWarningsAsErrors.set(false) } - exclude("**/security.disabled/**") } tasks.withType { @@ -156,7 +153,6 @@ ktlint { filter { exclude("**/build/**") exclude("**/generated/**") - exclude("**/security.disabled/**") } } @@ -170,7 +166,6 @@ detekt { tasks.withType().configureEach { jvmTarget = "21" - exclude("**/security.disabled/**") reports { html.required.set(true) xml.required.set(true) diff --git a/gradle.properties b/gradle.properties index b185c69..7ef89b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,5 +8,5 @@ kotlin.incremental=true kotlin.incremental.js=true kotlin.incremental.multiplatform=true -org.gradle.configuration-cache=true +org.gradle.configuration-cache=false org.gradle.unsafe.configuration-cache-problems=warn -- 2.54.0 From d2821a843906043b0e75fa4814e2cc48973bad49 Mon Sep 17 00:00:00 2001 From: skettiks Date: Fri, 15 May 2026 22:40:50 +0300 Subject: [PATCH 19/22] =?UTF-8?q?Revert=20"-=20=20=20=20=20exclude("**/sec?= =?UTF-8?q?urity.disabled/**")=20-=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bc06654c84650df27adccbdc4890741527b11d2e. --- build.gradle.kts | 7 ++++++- gradle.properties | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 630e545..a3b3bf6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,13 +9,15 @@ plugins { alias(libs.plugins.kotlin.spring) alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependency.management) - id("org.graalvm.buildtools.native") version "0.10.5" alias(libs.plugins.protobuf) alias(libs.plugins.ktlint) alias(libs.plugins.detekt) jacoco } +// Temporarily disabled due to OAuth2 AOT processing issues +// apply(plugin = "org.springframework.boot.aot") + apply(from = "$rootDir/gradle/docker.gradle.kts") group = "com.project" @@ -71,6 +73,7 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) allWarningsAsErrors.set(false) } + exclude("**/security.disabled/**") } tasks.withType { @@ -153,6 +156,7 @@ ktlint { filter { exclude("**/build/**") exclude("**/generated/**") + exclude("**/security.disabled/**") } } @@ -166,6 +170,7 @@ detekt { tasks.withType().configureEach { jvmTarget = "21" + exclude("**/security.disabled/**") reports { html.required.set(true) xml.required.set(true) diff --git a/gradle.properties b/gradle.properties index 7ef89b3..b185c69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,5 +8,5 @@ kotlin.incremental=true kotlin.incremental.js=true kotlin.incremental.multiplatform=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.unsafe.configuration-cache-problems=warn -- 2.54.0 From 56f1f0a46b063d24dfa7ba33371fef4407320cf7 Mon Sep 17 00:00:00 2001 From: skettiks Date: Fri, 15 May 2026 22:46:38 +0300 Subject: [PATCH 20/22] . --- build.gradle.kts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a3b3bf6..c0f9dfd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,8 +15,7 @@ plugins { jacoco } -// Temporarily disabled due to OAuth2 AOT processing issues -// apply(plugin = "org.springframework.boot.aot") +apply(plugin = "org.springframework.boot.aot") apply(from = "$rootDir/gradle/docker.gradle.kts") @@ -73,7 +72,6 @@ tasks.withType { jvmTarget.set(JvmTarget.JVM_21) allWarningsAsErrors.set(false) } - exclude("**/security.disabled/**") } tasks.withType { @@ -156,7 +154,6 @@ ktlint { filter { exclude("**/build/**") exclude("**/generated/**") - exclude("**/security.disabled/**") } } @@ -170,7 +167,6 @@ detekt { tasks.withType().configureEach { jvmTarget = "21" - exclude("**/security.disabled/**") reports { html.required.set(true) xml.required.set(true) -- 2.54.0 From 66a4add814c7af5fc79e1c6b3360b0170bf9b42f Mon Sep 17 00:00:00 2001 From: skettiks Date: Mon, 18 May 2026 00:03:08 +0300 Subject: [PATCH 21/22] add Film observability with trace id and metrics --- .../adapters/web/ApiExceptionHandler.kt | 52 +++++- .../movienight/adapters/web/TraceIdFilter.kt | 27 +++ .../application/services/FilmService.kt | 163 +++++++++++++++--- src/main/resources/application.yaml | 3 + 4 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/project/movienight/adapters/web/TraceIdFilter.kt diff --git a/src/main/kotlin/com/project/movienight/adapters/web/ApiExceptionHandler.kt b/src/main/kotlin/com/project/movienight/adapters/web/ApiExceptionHandler.kt index 27a236d..40b3362 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/ApiExceptionHandler.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/ApiExceptionHandler.kt @@ -3,6 +3,8 @@ package com.project.movienight.adapters.web import com.project.movienight.domain.exception.BlockedValueException import com.project.movienight.domain.exception.DomainException import com.project.movienight.domain.exception.EntityNotFoundException +import org.slf4j.LoggerFactory +import org.slf4j.MDC import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ResponseStatus @@ -10,22 +12,60 @@ import org.springframework.web.bind.annotation.RestControllerAdvice @RestControllerAdvice class ApiExceptionHandler { + private val log = LoggerFactory.getLogger(javaClass) + @ExceptionHandler(EntityNotFoundException::class) @ResponseStatus(HttpStatus.NOT_FOUND) - fun handleNotFound(exception: EntityNotFoundException): ErrorResponse = - ErrorResponse(message = exception.message ?: "Entity not found") + fun handleNotFound(exception: EntityNotFoundException): ErrorResponse { + val traceId = currentTraceId() + log.warn("Entity not found: traceId='{}', message='{}'", traceId, exception.message) + + return ErrorResponse( + message = exception.message ?: "Entity not found", + traceId = traceId, + ) + } @ExceptionHandler(BlockedValueException::class) @ResponseStatus(HttpStatus.BAD_REQUEST) - fun handleBlockedValue(exception: BlockedValueException): ErrorResponse = - ErrorResponse(message = exception.message ?: "Blocked value") + fun handleBlockedValue(exception: BlockedValueException): ErrorResponse { + val traceId = currentTraceId() + log.warn("Blocked value: traceId='{}', message='{}'", traceId, exception.message) + + return ErrorResponse( + message = exception.message ?: "Blocked value", + traceId = traceId, + ) + } @ExceptionHandler(DomainException::class) @ResponseStatus(HttpStatus.BAD_REQUEST) - fun handleDomainException(exception: DomainException): ErrorResponse = - ErrorResponse(message = exception.message ?: "Domain error") + fun handleDomainException(exception: DomainException): ErrorResponse { + val traceId = currentTraceId() + log.warn("Domain error: traceId='{}', message='{}'", traceId, exception.message) + + return ErrorResponse( + message = exception.message ?: "Domain error", + traceId = traceId, + ) + } + + @ExceptionHandler(Exception::class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + fun handleUnexpectedException(exception: Exception): ErrorResponse { + val traceId = currentTraceId() + log.error("Unexpected error: traceId='{}'", traceId, exception) + + return ErrorResponse( + message = "Internal server error", + traceId = traceId, + ) + } + + private fun currentTraceId(): String = MDC.get("traceId") ?: "unknown" } data class ErrorResponse( val message: String, + val traceId: String, ) diff --git a/src/main/kotlin/com/project/movienight/adapters/web/TraceIdFilter.kt b/src/main/kotlin/com/project/movienight/adapters/web/TraceIdFilter.kt new file mode 100644 index 0000000..596cb47 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/web/TraceIdFilter.kt @@ -0,0 +1,27 @@ +package com.project.movienight.adapters.web + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.MDC +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import java.util.UUID + +@Component +class TraceIdFilter : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val traceId = UUID.randomUUID().toString() + MDC.put("traceId", traceId) + + try { + filterChain.doFilter(request, response) + } finally { + MDC.remove("traceId") + } + } +} diff --git a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt index f775de3..bbd32b1 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -14,6 +14,10 @@ import com.project.movienight.config.FilmServiceProperties import com.project.movienight.domain.exception.BlockedValueException import com.project.movienight.domain.exception.EntityNotFoundException import com.project.movienight.domain.model.Film +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.Timer +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.util.UUID @@ -22,48 +26,117 @@ class FilmService( private val filmRepository: FilmRepositoryPort, private val idGenerator: IdGenerator, private val filmConfig: FilmServiceProperties, + private val meterRegistry: MeterRegistry, ) : CreateFilmUseCase, EditFilmUseCase, DeleteFilmUseCase, GetFilmByIdUseCase, GetAllFilmsUseCase, SearchFilmByTitleUseCase { - override fun create(command: CreateFilmCommand): Film { - if (filmConfig.isBlocked(command.title)) { - throw BlockedValueException(target = "Film", field = "title") - } - if (filmConfig.isBlocked(command.description)) { - throw BlockedValueException(target = "Film", field = "description") - } + private val log = LoggerFactory.getLogger(javaClass) - val film = - Film( - id = idGenerator.generateId(), - title = command.title, - description = command.description, + override fun create(command: CreateFilmCommand): Film { + val sample = Timer.start(meterRegistry) + + try { + log.debug( + "Create film request received: title='{}', descriptionLength={}", + command.title, + command.description.length, ) - return filmRepository.save(film) + + if (filmConfig.isBlocked(command.title)) { + log.debug("Create film blocked by title policy: title='{}'", command.title) + filmBlockedCounter.increment() + throw BlockedValueException(target = "Film", field = "title") + } + if (filmConfig.isBlocked(command.description)) { + log.debug("Create film blocked by description policy") + filmBlockedCounter.increment() + throw BlockedValueException(target = "Film", field = "description") + } + + val film = + Film( + id = idGenerator.generateId(), + title = command.title, + description = command.description, + ) + val saved = filmRepository.save(film) + + filmCreatedCounter.increment() + + log.info("Film created: id='{}', title='{}'", saved.id, saved.title) + return saved + } finally { + sample.stop(createFilmTimer) + } } override fun edit( id: UUID, command: EditFilmCommand, ): Film { - if (filmConfig.isBlocked(command.title)) { - throw BlockedValueException(target = "Film", field = "title") - } - if (filmConfig.isBlocked(command.description)) { - throw BlockedValueException(target = "Film", field = "description") - } + val sample = Timer.start(meterRegistry) - var film = filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - film = film.copy(title = command.title, description = command.description) - return filmRepository.save(film) + try { + log.debug("Edit film with id: {}", id) + + if (filmConfig.isBlocked(command.title)) { + log.debug("Edit film blocked by title policy: title='{}'", command.title) + filmBlockedCounter.increment() + throw BlockedValueException(target = "Film", field = "title") + } + if (filmConfig.isBlocked(command.description)) { + log.debug("Edit film blocked by description policy") + filmBlockedCounter.increment() + throw BlockedValueException(target = "Film", field = "description") + } + + val film = filmRepository.findById(id) + + if (film == null) { + log.debug("Film not found for edit: id='{}'", id) + throw EntityNotFoundException(entity = "Film", id = id.toString()) + } + + val updatedFilm = + film.copy( + title = command.title, + description = command.description, + ) + val saved = filmRepository.save(updatedFilm) + + filmEditedCounter.increment() + + log.info("Film edited: id='{}'", saved.id) + return saved + } finally { + sample.stop(editFilmTimer) + } } override fun delete(id: UUID) { - filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - filmRepository.deleteById(id) + val sample = Timer.start(meterRegistry) + + try { + log.debug("Delete film with id: {}", id) + + val film = filmRepository.findById(id) + + if (film == null) { + log.debug("Film not found for delete: id='{}'", id) + throw EntityNotFoundException(entity = "Film", id = id.toString()) + } + + filmRepository.deleteById(id) + + filmDeletedCounter.increment() + + log.info("Film deleted: id='{}'", id) + } finally { + sample.stop(deleteFilmTimer) + } } override fun getById(id: UUID): Film = @@ -72,4 +145,46 @@ class FilmService( override fun getAll(): List = filmRepository.findAll() override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title) + + private val filmCreatedCounter = + Counter + .builder("film_created_total") + .description("Total number of created films") + .register(meterRegistry) + + private val filmEditedCounter = + Counter + .builder("film_edited_total") + .description("Total number of successfully edited films") + .register(meterRegistry) + + private val filmDeletedCounter = + Counter + .builder("film_deleted_total") + .description("Total number of successfully deleted films") + .register(meterRegistry) + + private val filmBlockedCounter = + Counter + .builder("films.blocked") + .description("Total blocked film operations") + .register(meterRegistry) + + private val createFilmTimer = + Timer + .builder("films.create.duration") + .description("Film creation duration") + .register(meterRegistry) + + private val editFilmTimer = + Timer + .builder("films.edit.duration") + .description("Film edit duration") + .register(meterRegistry) + + private val deleteFilmTimer = + Timer + .builder("films.delete.duration") + .description("Film deletion duration") + .register(meterRegistry) } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f36c77d..4ccd637 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -74,3 +74,6 @@ services: - censored - epstein - python +logging: + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{traceId}] %logger{36} - %msg%n" -- 2.54.0 From 6e009339fb38bd5c61135d6ee374aff09da5c53d Mon Sep 17 00:00:00 2001 From: skettiks Date: Mon, 18 May 2026 00:40:02 +0300 Subject: [PATCH 22/22] test: provide meter registry in FilmServiceTest --- .../movienight/application/services/FilmServiceTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt index fcc5613..e0f96be 100644 --- a/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt +++ b/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt @@ -8,6 +8,7 @@ import com.project.movienight.config.FilmServiceProperties import com.project.movienight.domain.exception.BlockedValueException import com.project.movienight.domain.exception.EntityNotFoundException import com.project.movienight.domain.model.Film +import io.micrometer.core.instrument.simple.SimpleMeterRegistry import io.mockk.every import io.mockk.justRun import io.mockk.mockk @@ -23,6 +24,7 @@ class FilmServiceTest { private lateinit var filmRepository: FilmRepositoryPort private lateinit var idGenerator: IdGenerator private lateinit var filmConfig: FilmServiceProperties + private lateinit var meterRegistry: SimpleMeterRegistry private lateinit var filmService: FilmService @BeforeEach @@ -30,7 +32,8 @@ class FilmServiceTest { filmRepository = mockk() idGenerator = mockk() filmConfig = mockk() - filmService = FilmService(filmRepository, idGenerator, filmConfig) + meterRegistry = SimpleMeterRegistry() + filmService = FilmService(filmRepository, idGenerator, filmConfig, meterRegistry) } @Test -- 2.54.0