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;