diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index beaba8a..ce9994c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -109,9 +109,9 @@ jobs: KAKAO_REDIRECT_URL=${{ secrets.KAKAO_REDIRECT_URL }} NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }} - GOOGLE_CLIENT_ID=1234 - GOOGLE_CLIENT_SECRET=1234 - GOOGLE_REDIRECT_URI=1234" > .env + GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} + GOOGLE_REDIRECT_URI=${{ secrets.GOOGLE_REDIRECT_URI }}" > .env # 배포 스크립트 EC2로 전송 scp -i private_key.pem -o StrictHostKeyChecking=no deploy.sh .env ubuntu@${{ secrets.EC2_PUBLIC_IP }}:/home/ubuntu/ diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/EmailVerification.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/EmailVerification.kt similarity index 79% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/EmailVerification.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/EmailVerification.kt index 0c0bb7f..a65ae3b 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/EmailVerification.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/EmailVerification.kt @@ -1,6 +1,6 @@ -package com.wafflestudio.toyproject.memoWithTags.user.controller +package com.wafflestudio.toyproject.memoWithTags.mail -import com.wafflestudio.toyproject.memoWithTags.user.persistence.EmailVerificationEntity +import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationEntity import java.time.LocalDateTime class EmailVerification( diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationEntity.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationEntity.kt similarity index 90% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationEntity.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationEntity.kt index 8469f04..a725c26 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationEntity.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationEntity.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.persistence +package com.wafflestudio.toyproject.memoWithTags.mail.persistence import jakarta.persistence.Column import jakarta.persistence.Entity diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationRepository.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationRepository.kt similarity index 83% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationRepository.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationRepository.kt index 3d7605a..dd031f3 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/persistence/EmailVerificationRepository.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/persistence/EmailVerificationRepository.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.persistence +package com.wafflestudio.toyproject.memoWithTags.mail.persistence import org.springframework.data.jpa.repository.JpaRepository import java.time.LocalDateTime diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/MailService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/MailService.kt new file mode 100644 index 0000000..de97f92 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/MailService.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.toyproject.memoWithTags.mail.service + +interface MailService { + /** + * 메일을 보내는 함수. + * 개발 환경에서는 로그만 출력하고, 배포 환경에서만 실제 메일을 보내기 위해 인터페이스를 선언함 + */ + fun sendMail(toEmail: String, title: String, content: String) +} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/NoOpMailService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/NoOpMailService.kt similarity index 88% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/NoOpMailService.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/NoOpMailService.kt index d9031e8..5112996 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/NoOpMailService.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/NoOpMailService.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.service +package com.wafflestudio.toyproject.memoWithTags.mail.service import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SmtpMailService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/SmtpMailService.kt similarity index 95% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SmtpMailService.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/SmtpMailService.kt index d0b600d..5f6d6cc 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SmtpMailService.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/mail/service/SmtpMailService.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.service +package com.wafflestudio.toyproject.memoWithTags.mail.service import jakarta.mail.MessagingException import jakarta.mail.internet.MimeMessage diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/controller/SocialLoginController.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/controller/SocialLoginController.kt new file mode 100644 index 0000000..eb1ae86 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/controller/SocialLoginController.kt @@ -0,0 +1,49 @@ +package com.wafflestudio.toyproject.memoWithTags.social.controller + +import com.wafflestudio.toyproject.memoWithTags.exception.OAuthRequestException +import com.wafflestudio.toyproject.memoWithTags.social.service.SocialLoginService +import com.wafflestudio.toyproject.memoWithTags.user.SocialType +import com.wafflestudio.toyproject.memoWithTags.user.dto.UserResponse.LoginResponse +import io.swagger.v3.oas.annotations.Operation +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1") +class SocialLoginController( + private val socialLoginService: SocialLoginService +) { + @Operation(summary = "소셜 로그인 요청") + @GetMapping("/auth/code/{provider}") + fun oauthCallback( + @RequestParam(value = "code", required = false) code: String?, + @PathVariable provider: String + ): ResponseEntity { + if (code == null) throw OAuthRequestException() + val appLink = "memowithtags://oauth/$provider?code=$code" + return ResponseEntity.status(HttpStatus.FOUND) + .header("Location", appLink) + .build() + } + + @Operation(summary = "소셜 로그인 처리") + @GetMapping("/auth/login/{provider}") + fun oauthLogin( + @RequestParam(value = "code") code: String, + @PathVariable provider: String + ): ResponseEntity { + val socialType = SocialType.from(provider) + val loginResult = when (socialType) { + SocialType.KAKAO -> socialLoginService.kakaoLogin(code) + SocialType.NAVER -> socialLoginService.naverLogin(code) + SocialType.GOOGLE -> socialLoginService.googleLogin(code) + else -> throw OAuthRequestException() + } + return ResponseEntity.ok(LoginResponse(loginResult.second, loginResult.third)) + } +} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/GoogleUser.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/GoogleUser.kt similarity index 86% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/GoogleUser.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/GoogleUser.kt index 84b326f..c8e7460 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/GoogleUser.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/GoogleUser.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.dto +package com.wafflestudio.toyproject.memoWithTags.social.dto data class GoogleOAuthToken( val access_token: String, diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/KakaoUser.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/KakaoUser.kt similarity index 92% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/KakaoUser.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/KakaoUser.kt index 8e16921..ebffc9a 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/KakaoUser.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/KakaoUser.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.dto +package com.wafflestudio.toyproject.memoWithTags.social.dto data class KakaoOAuthToken( val token_type: String, diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/NaverUser.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/NaverUser.kt similarity index 85% rename from src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/NaverUser.kt rename to src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/NaverUser.kt index e4126cd..15301c5 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/dto/NaverUser.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/dto/NaverUser.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.toyproject.memoWithTags.user.dto +package com.wafflestudio.toyproject.memoWithTags.social.dto data class NaverOAuthToken( val token_type: String, diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialLoginService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialLoginService.kt new file mode 100644 index 0000000..4c35f80 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialLoginService.kt @@ -0,0 +1,111 @@ +package com.wafflestudio.toyproject.memoWithTags.social.service + +import com.wafflestudio.toyproject.memoWithTags.exception.EmailAlreadyExistsException +import com.wafflestudio.toyproject.memoWithTags.social.dto.GoogleOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.GoogleProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.KakaoOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.KakaoProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverProfile +import com.wafflestudio.toyproject.memoWithTags.user.GoogleUtil +import com.wafflestudio.toyproject.memoWithTags.user.JwtUtil +import com.wafflestudio.toyproject.memoWithTags.user.KakaoUtil +import com.wafflestudio.toyproject.memoWithTags.user.NaverUtil +import com.wafflestudio.toyproject.memoWithTags.user.SocialType +import com.wafflestudio.toyproject.memoWithTags.user.controller.User +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class SocialLoginService( + private val socialUserService: SocialUserService, + private val kakaoUtil: KakaoUtil, + private val naverUtil: NaverUtil, + private val googleUtil: GoogleUtil +) { + private val logger = LoggerFactory.getLogger(javaClass) + + /** + * 네이버 프로필 정보를 받아온 후, 로그인 또는 회원가입 후 로그인 로직을 처리하는 함수 + */ + fun naverLogin(accessCode: String): Triple { + val oAuthToken: NaverOAuthToken = naverUtil.requestToken(accessCode) + val naverProfile: NaverProfile = naverUtil.requestProfile(oAuthToken) + + val naverEmail = naverProfile.email + val userEntity = socialUserService.findUserByEmail(naverEmail) + + // 기존에 등록된 이메일이 있으면서 다른 서비스로 로그인을 한 경우 예외 발생 + val user: User = if (userEntity != null && userEntity.socialType == SocialType.NAVER) { + logger.info("naver user already exists: ${userEntity.id}, ${userEntity.email}") + User.fromEntity(userEntity) + } else if (userEntity == null) { + logger.info("creating naver user $naverEmail") + socialUserService.createNaverUser(naverProfile) + } else { + throw EmailAlreadyExistsException() + } + + return Triple( + user, + JwtUtil.generateAccessToken(naverEmail), + JwtUtil.generateRefreshToken(naverEmail) + ) + } + + /** + * 카카오 프로필 정보를 받아온 후, 로그인 또는 회원가입 후 로그인 로직을 처리하는 함수 + */ + fun kakaoLogin(accessCode: String): Triple { + val oAuthToken: KakaoOAuthToken = kakaoUtil.requestToken(accessCode) + val kakaoProfile: KakaoProfile = kakaoUtil.requestProfile(oAuthToken) + + val kakaoEmail = kakaoProfile.kakao_account.email + val userEntity = socialUserService.findUserByEmail(kakaoEmail) + + // 기존에 등록된 이메일이 있으면서 다른 서비스로 로그인을 한 경우 예외 발생 + val user: User = if (userEntity != null && userEntity.socialType == SocialType.KAKAO) { + logger.info("kakao user already exists: ${userEntity.id}, ${userEntity.email}") + User.fromEntity(userEntity) + } else if (userEntity == null) { + logger.info("creating kakao user $kakaoEmail") + socialUserService.createKakaoUser(kakaoProfile) + } else { + throw EmailAlreadyExistsException() + } + + return Triple( + user, + JwtUtil.generateAccessToken(kakaoEmail), + JwtUtil.generateRefreshToken(kakaoEmail) + ) + } + + /** + * 구글 프로필 정보를 받아온 후, 로그인 또는 회원가입 후 로그인 로직을 처리하는 함수 + */ + fun googleLogin(accessCode: String): Triple { + val oAuthToken: GoogleOAuthToken = googleUtil.requestToken(accessCode) + val googleProfile: GoogleProfile = googleUtil.requestProfile(oAuthToken) + + val googleEmail = googleProfile.email + val userEntity = socialUserService.findUserByEmail(googleEmail) + + // 기존에 등록된 이메일이 있으면서 다른 서비스로 로그인을 한 경우 예외 발생 + val user: User = if (userEntity != null && userEntity.socialType == SocialType.GOOGLE) { + logger.info("google user already exists: ${userEntity.id}, ${userEntity.email}") + User.fromEntity(userEntity) + } else if (userEntity == null) { + logger.info("creating google user $googleEmail") + socialUserService.createGoogleUser(googleProfile) + } else { + throw EmailAlreadyExistsException() + } + + return Triple( + user, + JwtUtil.generateAccessToken(googleEmail), + JwtUtil.generateRefreshToken(googleEmail) + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialUserService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialUserService.kt new file mode 100644 index 0000000..fcb8577 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/social/service/SocialUserService.kt @@ -0,0 +1,94 @@ +package com.wafflestudio.toyproject.memoWithTags.social.service + +import com.wafflestudio.toyproject.memoWithTags.social.dto.GoogleProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.KakaoProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverProfile +import com.wafflestudio.toyproject.memoWithTags.user.SocialType +import com.wafflestudio.toyproject.memoWithTags.user.controller.User +import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserEntity +import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.Instant + +@Service +class SocialUserService( + private val userRepository: UserRepository +) { + /** + * 네이버 로그인 시 저장되어 있는 User 정보가 없을 경우, DB에 User를 생성하는 함수 + */ + @Transactional + fun createNaverUser(naverProfile: NaverProfile): User { + val naverEmail = naverProfile.email + val naverNickname = naverProfile.nickname + val encryptedPassword = "naver_registered_user" + + val userEntity = userRepository.save( + UserEntity( + email = naverEmail, + nickname = naverNickname, + hashedPassword = encryptedPassword, + verified = true, + socialType = SocialType.NAVER, + createdAt = Instant.now() + ) + ) + + return User.fromEntity(userEntity) + } + + /** + * 카카오 로그인 시 저장되어 있는 User 정보가 없을 경우, DB에 User를 생성하는 함수 + */ + @Transactional + fun createKakaoUser(kakaoProfile: KakaoProfile): User { + val kakaoEmail = kakaoProfile.kakao_account.email + val kakaoNickname = kakaoProfile.kakao_account.profile.nickname + val encryptedPassword = "kakao_registered_user" + + val userEntity = userRepository.save( + UserEntity( + email = kakaoEmail, + nickname = kakaoNickname, + hashedPassword = encryptedPassword, + verified = true, + socialType = SocialType.KAKAO, + createdAt = Instant.now() + ) + ) + + return User.fromEntity(userEntity) + } + + /** + * 구글 로그인 시 저장되어 있는 User 정보가 없을 경우, DB에 User를 생성하는 함수 + */ + @Transactional + fun createGoogleUser(profile: GoogleProfile): User { + val googleEmail = profile.email + val googleNickname = profile.name + val encryptedPassword = "google_registered_user" + + val userEntity = userRepository.save( + UserEntity( + email = googleEmail, + nickname = googleNickname, + hashedPassword = encryptedPassword, + verified = true, + socialType = SocialType.GOOGLE, + createdAt = Instant.now() + ) + ) + + return User.fromEntity(userEntity) + } + + /** + * 해당 메일의 User를 찾는 함수. 없으면 null을 반환한다. + */ + @Transactional + fun findUserByEmail(email: String): UserEntity? { + return userRepository.findByEmail(email) + } +} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/GoogleUtil.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/GoogleUtil.kt index 66c4a58..27131bd 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/GoogleUtil.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/GoogleUtil.kt @@ -1,8 +1,8 @@ package com.wafflestudio.toyproject.memoWithTags.user import com.wafflestudio.toyproject.memoWithTags.exception.OAuthRequestException -import com.wafflestudio.toyproject.memoWithTags.user.dto.GoogleOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.GoogleProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.GoogleOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.GoogleProfile import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpEntity diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/KakaoUtil.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/KakaoUtil.kt index 558a6e3..c5f8587 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/KakaoUtil.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/KakaoUtil.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.wafflestudio.toyproject.memoWithTags.exception.OAuthRequestException -import com.wafflestudio.toyproject.memoWithTags.user.dto.KakaoOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.KakaoProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.KakaoOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.KakaoProfile import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpEntity diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/NaverUtil.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/NaverUtil.kt index f80cad8..673af6d 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/NaverUtil.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/NaverUtil.kt @@ -4,9 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.wafflestudio.toyproject.memoWithTags.exception.OAuthRequestException -import com.wafflestudio.toyproject.memoWithTags.user.dto.NaverOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.NaverProfile -import com.wafflestudio.toyproject.memoWithTags.user.dto.NaverProfileResponse +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverOAuthToken +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverProfile +import com.wafflestudio.toyproject.memoWithTags.social.dto.NaverProfileResponse import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpEntity diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/SocialLoginController.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/SocialLoginController.kt deleted file mode 100644 index 17092b6..0000000 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/SocialLoginController.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.wafflestudio.toyproject.memoWithTags.user.controller - -import com.wafflestudio.toyproject.memoWithTags.exception.OAuthRequestException -import com.wafflestudio.toyproject.memoWithTags.user.dto.UserResponse.LoginResponse -import com.wafflestudio.toyproject.memoWithTags.user.service.SocialLoginService -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController - -@RestController -@RequestMapping("/api/v1") -class SocialLoginController( - private val socialLoginService: SocialLoginService -) { - - @GetMapping("/oauth/naver") - fun naverCallback( - @RequestParam(value = "code", required = false) code: String?, - @RequestParam(value = "state", required = false) state: String?, - @RequestParam(value = "error", required = false) error: String?, - @RequestParam(value = "error_description", required = false) errorDescription: String? - ): ResponseEntity { - if (code == null || error != null) { - throw OAuthRequestException() - } - val (_, accessToken, refreshToken) = socialLoginService.naverCallback(code) - return ResponseEntity.ok(LoginResponse(accessToken, refreshToken)) - } - - @GetMapping("/oauth/kakao") - fun kakaoCallback( - @RequestParam("code") code: String - ): ResponseEntity { - val appLink = "memowithtags://oauth/kakao?code=$code" - return ResponseEntity.status(HttpStatus.FOUND) - .header("Location", appLink) - .build() - } - - @GetMapping("/oauth/kakao/login") - fun kakaoLogin( - @RequestParam("code") code: String - ): ResponseEntity { - val (_, accessToken, refreshToken) = socialLoginService.kakaoCallBack(code) - return ResponseEntity.ok(LoginResponse(accessToken, refreshToken)) - } - - @GetMapping("/oauth/google") - fun googleCallback( - @RequestParam("code") code: String - ): ResponseEntity { - val (_, accessToken, refreshToken) = socialLoginService.googleCallback(code) - return ResponseEntity.ok(LoginResponse(accessToken, refreshToken)) - } -} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/UserController.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/UserController.kt index 3623d13..307b948 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/UserController.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/controller/UserController.kt @@ -74,7 +74,6 @@ class UserController( fun me( @AuthUser user: User ): ResponseEntity { - println("authme start!!!!") return ResponseEntity.ok(user) } } diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/AdminService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/AdminService.kt index 9a70d33..3dec9ee 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/AdminService.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/AdminService.kt @@ -14,6 +14,9 @@ import java.util.UUID class AdminService( private val userRepository: UserRepository ) { + /** + * 관리자 여부를 리턴하는 함수 + */ fun isAdmin(userId: UUID) { val userEntity = userRepository.findById(userId).orElseThrow { UserNotFoundException() } if (userEntity.role != RoleType.ROLE_ADMIN) { @@ -21,15 +24,24 @@ class AdminService( } } + /** + * DB에 저장된 모든 User를 리턴하는 함수 + */ fun getUsers(): List { return userRepository.findAll().map { User.fromEntity(it) } } + /** + * 해당하는 Id의 User를 삭제하는 함수 + */ fun deleteUser(userId: UUID) { val userEntity = userRepository.findById(userId).orElseThrow { UserNotFoundException() } userRepository.delete(userEntity) } + /** + * 해당하는 Id의 User를 userUpdateInfo에 따라 업데이트하는 함수 + */ fun updateUser(userId: UUID, userUpdateInfo: UserUpdateInfo): User { val userEntity = userRepository.findById(userId).orElseThrow { UserNotFoundException() } userEntity.nickname = userUpdateInfo.nickname diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/CustomUserDetailsService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/CustomUserDetailsService.kt index da83c48..8debe8c 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/CustomUserDetailsService.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/CustomUserDetailsService.kt @@ -11,6 +11,9 @@ import org.springframework.stereotype.Service class CustomUserDetailsService( private val userRepository: UserRepository ) : UserDetailsService { + /** + * SecurityConfig에서 쓰이는 쿼리 함수 + */ override fun loadUserByUsername(email: String): UserDetails { val user = userRepository.findByEmail(email) ?: throw UserNotFoundException() return CustomUserDetails(user) diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/MailService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/MailService.kt deleted file mode 100644 index 01676a5..0000000 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/MailService.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wafflestudio.toyproject.memoWithTags.user.service - -interface MailService { - fun sendMail(toEmail: String, title: String, content: String) -} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SocialLoginService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SocialLoginService.kt deleted file mode 100644 index dc181c3..0000000 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/SocialLoginService.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.wafflestudio.toyproject.memoWithTags.user.service - -import com.wafflestudio.toyproject.memoWithTags.user.GoogleUtil -import com.wafflestudio.toyproject.memoWithTags.user.JwtUtil -import com.wafflestudio.toyproject.memoWithTags.user.KakaoUtil -import com.wafflestudio.toyproject.memoWithTags.user.NaverUtil -import com.wafflestudio.toyproject.memoWithTags.user.SocialType -import com.wafflestudio.toyproject.memoWithTags.user.controller.User -import com.wafflestudio.toyproject.memoWithTags.user.dto.GoogleOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.GoogleProfile -import com.wafflestudio.toyproject.memoWithTags.user.dto.KakaoOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.KakaoProfile -import com.wafflestudio.toyproject.memoWithTags.user.dto.NaverOAuthToken -import com.wafflestudio.toyproject.memoWithTags.user.dto.NaverProfile -import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserEntity -import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserRepository -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Service -import java.time.Instant - -@Service -class SocialLoginService( - private val userRepository: UserRepository, - private val kakaoUtil: KakaoUtil, - private val naverUtil: NaverUtil, - private val googleUtil: GoogleUtil -) { - private val logger = LoggerFactory.getLogger(javaClass) - - fun naverCallback(accessCode: String): Triple { - val oAuthToken: NaverOAuthToken = naverUtil.requestToken(accessCode) - val naverProfile: NaverProfile = naverUtil.requestProfile(oAuthToken) - - val naverEmail = naverProfile.email - val userEntity = userRepository.findByEmail(naverEmail) - val user: User = if (userEntity != null && userEntity.socialType == SocialType.NAVER) { - logger.info("user already exists: ${userEntity.id}, ${userEntity.email}") - User.fromEntity(userEntity) - } else { - logger.info("creating user $naverEmail") - createNaverUser(naverProfile) - } - - return Triple( - user, - JwtUtil.generateAccessToken(naverEmail), - JwtUtil.generateRefreshToken(naverEmail) - ) - } - - fun createNaverUser(naverProfile: NaverProfile): User { - val naverEmail = naverProfile.email - val naverNickname = naverProfile.nickname - val encryptedPassword = "naver_registered_user" - - val userEntity = userRepository.save( - UserEntity( - email = naverEmail, - nickname = naverNickname, - hashedPassword = encryptedPassword, - verified = true, - socialType = SocialType.NAVER, - createdAt = Instant.now() - ) - ) - - return User.fromEntity(userEntity) - } - - fun kakaoCallBack(accessCode: String): Triple { - val oAuthToken: KakaoOAuthToken = kakaoUtil.requestToken(accessCode) - val kakaoProfile: KakaoProfile = kakaoUtil.requestProfile(oAuthToken) - - val kakaoEmail = kakaoProfile.kakao_account.email - val userEntity = userRepository.findByEmail(kakaoEmail) - val user: User = if (userEntity != null && userEntity.socialType == SocialType.KAKAO) { - logger.info("user already exists: ${userEntity.id}, ${userEntity.email}") - User.fromEntity(userEntity) - } else { - logger.info("creating user $kakaoEmail") - createKakaoUser(kakaoProfile) - } - - return Triple( - user, - JwtUtil.generateAccessToken(kakaoEmail), - JwtUtil.generateRefreshToken(kakaoEmail) - ) - } - - fun createKakaoUser(kakaoProfile: KakaoProfile): User { - val kakaoEmail = kakaoProfile.kakao_account.email - val kakaoNickname = kakaoProfile.kakao_account.profile.nickname - val encryptedPassword = "kakao_registered_user" - - val userEntity = userRepository.save( - UserEntity( - email = kakaoEmail, - nickname = kakaoNickname, - hashedPassword = encryptedPassword, - verified = true, - socialType = SocialType.KAKAO, - createdAt = Instant.now() - ) - ) - - return User.fromEntity(userEntity) - } - - fun googleCallback(accessCode: String): Triple { - val oAuthToken: GoogleOAuthToken = googleUtil.requestToken(accessCode) - val googleProfile: GoogleProfile = googleUtil.requestProfile(oAuthToken) - - val googleEmail = googleProfile.email - val userEntity = userRepository.findByEmail(googleEmail) - val user: User = if (userEntity != null && userEntity.socialType == SocialType.GOOGLE) { - logger.info("google user already exists: ${userEntity.id}, ${userEntity.email}") - User.fromEntity(userEntity) - } else { - logger.info("creating google user $googleEmail") - createGoogleUser(googleProfile) - } - - return Triple( - user, - JwtUtil.generateAccessToken(googleEmail), - JwtUtil.generateRefreshToken(googleEmail) - ) - } - - fun createGoogleUser(profile: GoogleProfile): User { - val googleEmail = profile.email - val googleNickname = profile.name - val encryptedPassword = "google_registered_user" - - val userEntity = userRepository.save( - UserEntity( - email = googleEmail, - nickname = googleNickname, - hashedPassword = encryptedPassword, - verified = true, - socialType = SocialType.GOOGLE, - createdAt = Instant.now() - ) - ) - - return User.fromEntity(userEntity) - } -} diff --git a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/UserService.kt b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/UserService.kt index 95f6a19..96ec4e8 100644 --- a/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/UserService.kt +++ b/src/main/kotlin/com/wafflestudio/toyproject/memoWithTags/user/service/UserService.kt @@ -7,12 +7,13 @@ import com.wafflestudio.toyproject.memoWithTags.exception.InValidTokenException import com.wafflestudio.toyproject.memoWithTags.exception.MailVerificationException import com.wafflestudio.toyproject.memoWithTags.exception.SignInInvalidException import com.wafflestudio.toyproject.memoWithTags.exception.UserNotFoundException +import com.wafflestudio.toyproject.memoWithTags.mail.EmailVerification +import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationEntity +import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationRepository +import com.wafflestudio.toyproject.memoWithTags.mail.service.MailService import com.wafflestudio.toyproject.memoWithTags.user.JwtUtil -import com.wafflestudio.toyproject.memoWithTags.user.controller.EmailVerification import com.wafflestudio.toyproject.memoWithTags.user.controller.User import com.wafflestudio.toyproject.memoWithTags.user.dto.UserResponse.RefreshTokenResponse -import com.wafflestudio.toyproject.memoWithTags.user.persistence.EmailVerificationEntity -import com.wafflestudio.toyproject.memoWithTags.user.persistence.EmailVerificationRepository import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserEntity import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserRepository import org.mindrot.jbcrypt.BCrypt @@ -31,6 +32,9 @@ class UserService( ) { private val logger = LoggerFactory.getLogger(UserService::class.java) + /** + * 자체 로그인 과정 중 회원가입을 구현한 함수 + */ @Transactional fun register( email: String, @@ -38,6 +42,7 @@ class UserService( ): User { if (userRepository.findByEmail(email) != null) throw EmailAlreadyExistsException() val encryptedPassword = BCrypt.hashpw(password, BCrypt.gensalt()) + // 메일 인증이 이루어지기 전까지 User의 verified 필드는 false이다. val userEntity = userRepository.save( UserEntity( email = email, @@ -49,6 +54,9 @@ class UserService( return User.fromEntity(userEntity) } + /** + * 회원가입 또는 비밀번호 변경 요청 후 인증용 메일을 발송하는 함수 + */ fun sendCodeToEmail( email: String ) { @@ -72,6 +80,9 @@ class UserService( } } + /** + * 인증 메일에 포함될 인증 코드를 랜덤으로 생성하는 함수. 6자리 숫자를 생성한다. + */ private fun createVerificationCode(email: String): EmailVerification { val randomCode: String = (100000..999999).random().toString() val codeEntity = EmailVerificationEntity( @@ -82,6 +93,9 @@ class UserService( return EmailVerification.fromEntity(emailVerificationRepository.save(codeEntity)) } + /** + * 메일로 보내진 인증 번호와 유저가 입력한 인증 번호가 일치하는지 검증하는 함수 + */ @Transactional fun verifyEmail( email: String, @@ -90,10 +104,14 @@ class UserService( val verification = emailVerificationRepository.findByEmailAndCode(email, code) ?: throw MailVerificationException() if (verification.expiryTime.isBefore(LocalDateTime.now())) throw AuthenticationFailedException() val userEntity = userRepository.findByEmail(verification.email) + // 인증 성공 시, user의 Verified 필드가 true로 바뀌어 정식 회원이 된다. userEntity!!.verified = true return true } + /** + * 회원가입된 유저의 자체 로그인 로직을 수행하는 함수 + */ @Transactional fun login( email: String, @@ -109,6 +127,9 @@ class UserService( ) } + /** + * 비밀번호 변경을 위해 보내진 메일 인증을 완료하고, 비밀번호를 변경하는 함수 + */ @Transactional fun resetPassword( email: String, @@ -121,6 +142,9 @@ class UserService( } } + /** + * 유저 토큰을 받아 유저 정보를 반환하는 함수 + */ @Transactional fun authenticate( accessToken: String @@ -132,6 +156,9 @@ class UserService( return User.fromEntity(userEntity) } + /** + * accessToken 만료 시 refreshToken을 통해 유저를 확인하고 새로운 accessToken을 발급하는 함수 + */ fun refreshToken(refreshToken: String): RefreshTokenResponse { if (!JwtUtil.isValidToken(refreshToken)) { throw InValidTokenException() @@ -150,17 +177,26 @@ class UserService( ) } + /** + * 해당 메일의 User를 찾는 함수. 없으면 예외를 발생시킨다. + */ @Transactional fun getUserEntityByEmail(email: String): UserEntity { return userRepository.findByEmail(email) ?: throw UserNotFoundException() } + /** + * 메일 정오에 만료된 인증 코드 엔티티를 삭제하는 함수 + */ @Transactional @Scheduled(cron = "0 0 12 * * ?") // 매일 정오에 만료 코드 삭제 fun deleteExpiredVerificationCode() { emailVerificationRepository.deleteByExpiryTimeBefore(LocalDateTime.now()) } + /** + * 매일 정오에 메일 인증이 되지 않은 유저를 삭제하는 함수 + */ @Transactional @Scheduled(cron = "0 0 12 * * ?") // 매일 정오에 미인증 사용자 삭제 fun deleteUnverifiedUser() {