diff --git a/src/main/kotlin/com/snuxi/config/SecurityConfig.kt b/src/main/kotlin/com/snuxi/config/SecurityConfig.kt new file mode 100644 index 0000000..96a883e --- /dev/null +++ b/src/main/kotlin/com/snuxi/config/SecurityConfig.kt @@ -0,0 +1,42 @@ +package com.snuxi.config + +import com.snuxi.user.service.GoogleOAuth2UserService +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +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 SecurityConfig( + val googleOAuth2UserService: GoogleOAuth2UserService +) { + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + .csrf { it.disable() } + .authorizeHttpRequests { + it.requestMatchers("/", "/login").permitAll() + it.anyRequest().authenticated() + } + .oauth2Login { + it.loginPage("/login") + it.userInfoEndpoint { + endpoint -> + endpoint.userService(googleOAuth2UserService) + } + it.defaultSuccessUrl("/", true) + } + .logout { + it.logoutSuccessUrl("/") + it.logoutUrl("/logout") + it.invalidateHttpSession(true) + } + return http.build() + } + + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/snuxi/user/controller/AuthController.kt b/src/main/kotlin/com/snuxi/user/controller/AuthController.kt new file mode 100644 index 0000000..93cfa9c --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/controller/AuthController.kt @@ -0,0 +1,12 @@ +package com.snuxi.user.controller + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class AuthController { + @GetMapping("/login") + fun login(): String { + return "redirect:/oauth2/authorization/google" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/snuxi/user/controller/UserController.kt b/src/main/kotlin/com/snuxi/user/controller/UserController.kt new file mode 100644 index 0000000..0a0fd33 --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/controller/UserController.kt @@ -0,0 +1,26 @@ +package com.snuxi.user.controller + +import com.snuxi.user.dto.UserResponse +import com.snuxi.user.service.UserService +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/users") +class UserController( + private val userService: UserService +) { + @GetMapping("/profile") + fun getMyProfile( + @AuthenticationPrincipal + oAuth2User: OAuth2User + ): ResponseEntity { + val email = oAuth2User.attributes["email"] as String + val profile = userService.getProfile(email) + return ResponseEntity.ok(profile) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/snuxi/user/dto/UserResponse.kt b/src/main/kotlin/com/snuxi/user/dto/UserResponse.kt new file mode 100644 index 0000000..749762f --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/dto/UserResponse.kt @@ -0,0 +1,18 @@ +package com.snuxi.user.dto + +import com.snuxi.user.model.Role +import com.snuxi.user.model.User + +data class UserResponse( + val email: String, + val username: String, + val profileImageUrl: String?, + val role: Role +) { + constructor(user: User) : this( + email = user.email, + username = user.username, + profileImageUrl = user.profileImageUrl, + role = user.role + ) +} diff --git a/src/main/kotlin/com/snuxi/user/model/User.kt b/src/main/kotlin/com/snuxi/user/model/User.kt new file mode 100644 index 0000000..7024304 --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/model/User.kt @@ -0,0 +1,37 @@ +package com.snuxi.user.model + +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import java.time.Instant + + +@Entity +@Table(name = "users") +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @Column(nullable = false, unique = true) + val email: String, + + @Column(nullable = false) + val username: String, + + var profileImageUrl: String? = null, + + @Enumerated(EnumType.STRING) + val role: Role = Role.USER +) { + @CreatedDate + var createdAt: Instant? = null + + @LastModifiedDate + var updatedAt: Instant? = null +} + +enum class Role { + USER, ADMIN +} + diff --git a/src/main/kotlin/com/snuxi/user/repository/UserRepository.kt b/src/main/kotlin/com/snuxi/user/repository/UserRepository.kt new file mode 100644 index 0000000..f5fa50f --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/repository/UserRepository.kt @@ -0,0 +1,8 @@ +package com.snuxi.user.repository + +import com.snuxi.user.model.User +import org.springframework.data.jpa.repository.JpaRepository + +interface UserRepository: JpaRepository { + fun findByEmail(email: String): User? +} \ No newline at end of file diff --git a/src/main/kotlin/com/snuxi/user/service/GoogleOAuth2UserService.kt b/src/main/kotlin/com/snuxi/user/service/GoogleOAuth2UserService.kt new file mode 100644 index 0000000..647b2e5 --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/service/GoogleOAuth2UserService.kt @@ -0,0 +1,64 @@ +package com.snuxi.user.service + +import com.snuxi.user.model.User +import com.snuxi.user.repository.UserRepository +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.OAuth2Error +import org.springframework.security.oauth2.core.user.DefaultOAuth2User +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service +import java.util.Collections + +@Service +class GoogleOAuth2UserService( + private val userRepository: UserRepository, +) : OAuth2UserService { + override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User { + val delegate = DefaultOAuth2UserService() + val oAuth2User = delegate.loadUser(userRequest) + val attributes = oAuth2User.attributes + + val email = attributes["email"] as String + if (!email.endsWith("@snu.ac.kr")) { + throw OAuth2AuthenticationException( + OAuth2Error("Email is not valid"), + "서울대학교(@snu.ac.kr) 계정만 로그인 가능합니다." + ) + } + + val user = getOrSave(attributes) + val userNameAttributeName = userRequest.clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName + + return DefaultOAuth2User( + Collections.singleton(SimpleGrantedAuthority("ROLE_${user.role.name}")), + attributes, + userNameAttributeName + ) + } + + private fun getOrSave(attributes: Map): User { + val email = attributes["email"] as String + + val existingUser = userRepository.findByEmail(email) + + if(existingUser != null) { + return existingUser + } + + val name = attributes["name"] as String + val picture = attributes["picture"] as? String + + return userRepository.save( + User( + email = email, + username = name, + profileImageUrl = picture + ) + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/snuxi/user/service/UserService.kt b/src/main/kotlin/com/snuxi/user/service/UserService.kt new file mode 100644 index 0000000..40586e1 --- /dev/null +++ b/src/main/kotlin/com/snuxi/user/service/UserService.kt @@ -0,0 +1,19 @@ +package com.snuxi.user.service + +import com.snuxi.user.dto.UserResponse +import com.snuxi.user.repository.UserRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class UserService( + val userRepository: UserRepository +) { + fun getProfile(email: String): UserResponse { + val user = userRepository.findByEmail(email) + ?: throw IllegalStateException("User not found") + + return UserResponse(user) + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 401a385..3317089 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -40,4 +40,4 @@ spring: ### app: s3: - bucket: snuxi-dev-assets \ No newline at end of file + bucket: snuxi-dev-assets