chore(release): first stable release #45

Merged
devitq merged 134 commits from develop into main 2026-05-23 07:28:37 +00:00
12 changed files with 195 additions and 0 deletions
Showing only changes of commit 06e2b50ba5 - Show all commits
+2
View File
@@ -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)
+1
View File
@@ -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" }
@@ -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)
}
}
}
@@ -0,0 +1,16 @@
package com.project.movienight.adapters.security.oauth2
class GoogleOAuth2UserInfo(
private val attributes: Map<String, Any>
) : 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<String, Any> = attributes
}
@@ -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<String, Any>
}
@@ -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")
}
}
}
@@ -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<String, Any>? = null,
) : OAuth2User, UserDetails {
fun getId(): UUID = user.id
override fun getName(): String = user.name
override fun getAttributes(): Map<String, Any> = attributes ?: emptyMap()
override fun getAuthorities(): Collection<GrantedAuthority> {
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<String, Any>? = null): UserPrincipal {
return UserPrincipal(user, attributes)
}
}
}
@@ -0,0 +1,26 @@
package com.project.movienight.adapters.security.oauth2
@Suppress("UNCHECKED_CAST")
class VkOAuth2UserInfo(
private val attributes: Map<String, Any>
) : OAuth2UserInfo {
override fun getProviderId(): String {
val response = attributes["response"] as? List<Map<String, Any>>
return response?.firstOrNull()?.get("id")?.toString() ?: ""
}
override fun getEmail(): String = attributes["email"] as? String ?: ""
override fun getName(): String {
val response = attributes["response"] as? List<Map<String, Any>>
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<String, Any> = attributes
}
@@ -0,0 +1,20 @@
package com.project.movienight.adapters.security.oauth2
@Suppress("UNCHECKED_CAST")
class YandexOAuth2UserInfo(
private val attributes: Map<String, Any>
) : OAuth2UserInfo {
override fun getProviderId(): String = attributes["id"]?.toString() ?: ""
override fun getEmail(): String {
val emails = attributes["emails"] as? List<Map<String, String>>
return emails?.firstOrNull()?.get("value") ?: ""
}
override fun getName(): String = attributes["display_name"] as? String ?: ""
override fun getProvider(): String = "yandex"
override fun getAttributes(): Map<String, Any> = attributes
}
@@ -9,6 +9,8 @@ interface UserRepositoryPort {
fun findById(id: UUID): User?
fun findByEmail(email: String): User?
fun findAll(): List<User>
fun deleteById(id: UUID)
@@ -36,6 +36,7 @@ class UserService(
id = idGenerator.generateId(),
name = command.name,
email = command.email,
password = "",
library = null,
)
return userRepository.save(user)
@@ -6,5 +6,6 @@ data class User(
val id: UUID,
val name: String,
val email: String,
val password: String,
val library: FilmLibrary?,
)