test #302
@@ -65,6 +65,8 @@ dependencies {
 | 
				
			|||||||
    // android publisher
 | 
					    // android publisher
 | 
				
			||||||
    implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0")
 | 
					    implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    implementation("com.google.api-client:google-api-client:1.32.1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    implementation("org.apache.poi:poi-ooxml:5.2.3")
 | 
					    implementation("org.apache.poi:poi-ooxml:5.2.3")
 | 
				
			||||||
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
 | 
					    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,13 @@ data class Member(
 | 
				
			|||||||
    var nickname: String,
 | 
					    var nickname: String,
 | 
				
			||||||
    var profileImage: String? = null,
 | 
					    var profileImage: String? = null,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val kakaoId: Long? = null,
 | 
				
			||||||
 | 
					    val googleId: String? = null,
 | 
				
			||||||
 | 
					    val appleId: String? = null,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Enumerated(EnumType.STRING)
 | 
				
			||||||
 | 
					    val provider: MemberProvider = MemberProvider.EMAIL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Enumerated(value = EnumType.STRING)
 | 
					    @Enumerated(value = EnumType.STRING)
 | 
				
			||||||
    var gender: Gender = Gender.NONE,
 | 
					    var gender: Gender = Gender.NONE,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,3 +147,7 @@ enum class Gender {
 | 
				
			|||||||
enum class MemberRole {
 | 
					enum class MemberRole {
 | 
				
			||||||
    ADMIN, BOT, USER, CREATOR, AGENT
 | 
					    ADMIN, BOT, USER, CREATOR, AGENT
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum class MemberProvider {
 | 
				
			||||||
 | 
					    EMAIL, KAKAO, GOOGLE, APPLE
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,10 @@ import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
 | 
				
			|||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest
 | 
					import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.login.LoginRequest
 | 
					import kr.co.vividnext.sodalive.member.login.LoginRequest
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
 | 
					import kr.co.vividnext.sodalive.member.login.LoginResponse
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.login.SocialLoginRequest
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
 | 
					import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
 | 
					import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.social.google.GoogleAuthService
 | 
				
			||||||
import org.springframework.data.domain.Pageable
 | 
					import org.springframework.data.domain.Pageable
 | 
				
			||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal
 | 
				
			||||||
import org.springframework.security.core.userdetails.User
 | 
					import org.springframework.security.core.userdetails.User
 | 
				
			||||||
@@ -29,6 +31,7 @@ import org.springframework.web.multipart.MultipartFile
 | 
				
			|||||||
@RequestMapping("/member")
 | 
					@RequestMapping("/member")
 | 
				
			||||||
class MemberController(
 | 
					class MemberController(
 | 
				
			||||||
    private val service: MemberService,
 | 
					    private val service: MemberService,
 | 
				
			||||||
 | 
					    private val googleAuthService: GoogleAuthService,
 | 
				
			||||||
    private val trackingService: AdTrackingService
 | 
					    private val trackingService: AdTrackingService
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    @GetMapping("/check/email")
 | 
					    @GetMapping("/check/email")
 | 
				
			||||||
@@ -316,4 +319,27 @@ class MemberController(
 | 
				
			|||||||
        if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
 | 
					        if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
 | 
				
			||||||
        ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!))
 | 
					        ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/login/google")
 | 
				
			||||||
 | 
					    fun loginGoogle(
 | 
				
			||||||
 | 
					        @RequestHeader("Authorization") authHeader: String,
 | 
				
			||||||
 | 
					        @RequestBody request: SocialLoginRequest
 | 
				
			||||||
 | 
					    ): ApiResponse<LoginResponse> {
 | 
				
			||||||
 | 
					        if (!authHeader.startsWith("Bearer ")) {
 | 
				
			||||||
 | 
					            throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val token = authHeader.substring(7)
 | 
				
			||||||
 | 
					        val response = googleAuthService.authenticate(token, request.container, request.marketingPid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!response.marketingPid.isNullOrBlank()) {
 | 
				
			||||||
 | 
					            trackingService.saveTrackingHistory(
 | 
				
			||||||
 | 
					                pid = response.marketingPid,
 | 
				
			||||||
 | 
					                type = AdTrackingHistoryType.SIGNUP,
 | 
				
			||||||
 | 
					                memberId = response.memberId
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ import org.springframework.stereotype.Repository
 | 
				
			|||||||
interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
 | 
					interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
 | 
				
			||||||
    fun findByEmail(email: String): Member?
 | 
					    fun findByEmail(email: String): Member?
 | 
				
			||||||
    fun findByNickname(nickname: String): Member?
 | 
					    fun findByNickname(nickname: String): Member?
 | 
				
			||||||
 | 
					    fun findByGoogleId(googleId: String): Member?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface MemberQueryRepository {
 | 
					interface MemberQueryRepository {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.member.signUp.SignUpRequest
 | 
				
			|||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
 | 
					import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
 | 
					import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
 | 
					import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.stipulation.Stipulation
 | 
					import kr.co.vividnext.sodalive.member.stipulation.Stipulation
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
 | 
					import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
 | 
				
			||||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
 | 
					import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
 | 
				
			||||||
@@ -736,4 +737,36 @@ class MemberService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return member.activePid
 | 
					        return member.activePid
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional
 | 
				
			||||||
 | 
					    fun findOrRegister(googleUserInfo: GoogleUserInfo, container: String, marketingPid: String?): Member {
 | 
				
			||||||
 | 
					        repository.findByGoogleId(googleUserInfo.sub)?.let { return it }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
 | 
				
			||||||
 | 
					            ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
 | 
				
			||||||
 | 
					            ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val nickname = nicknameGenerateService.generateUniqueNickname()
 | 
				
			||||||
 | 
					        val member = Member(
 | 
				
			||||||
 | 
					            googleId = googleUserInfo.sub,
 | 
				
			||||||
 | 
					            email = googleUserInfo.email,
 | 
				
			||||||
 | 
					            password = "",
 | 
				
			||||||
 | 
					            nickname = nickname,
 | 
				
			||||||
 | 
					            profileImage = "profile/default-profile.png",
 | 
				
			||||||
 | 
					            gender = Gender.NONE,
 | 
				
			||||||
 | 
					            container = container
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!marketingPid.isNullOrBlank()) {
 | 
				
			||||||
 | 
					            member.activePid = marketingPid
 | 
				
			||||||
 | 
					            member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        repository.save(member)
 | 
				
			||||||
 | 
					        agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return member
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,3 +6,5 @@ data class LoginRequest(
 | 
				
			|||||||
    val isAdmin: Boolean = false,
 | 
					    val isAdmin: Boolean = false,
 | 
				
			||||||
    val isCreator: Boolean = false
 | 
					    val isCreator: Boolean = false
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class SocialLoginRequest(val container: String, val marketingPid: String? = null)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					package kr.co.vividnext.sodalive.member.social
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.login.LoginResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class SocialLoginResponse(
 | 
				
			||||||
 | 
					    val memberId: Long,
 | 
				
			||||||
 | 
					    val marketingPid: String?,
 | 
				
			||||||
 | 
					    val loginResponse: LoginResponse
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package kr.co.vividnext.sodalive.member.social.google
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.common.SodaException
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.jwt.TokenProvider
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.MemberAdapter
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.MemberService
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.login.LoginResponse
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value
 | 
				
			||||||
 | 
					import org.springframework.security.core.context.SecurityContextHolder
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					class GoogleAuthService(
 | 
				
			||||||
 | 
					    private val googleService: GoogleService,
 | 
				
			||||||
 | 
					    private val memberService: MemberService,
 | 
				
			||||||
 | 
					    private val tokenProvider: TokenProvider,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Value("\${cloud.aws.cloud-front.host}")
 | 
				
			||||||
 | 
					    private val cloudFrontHost: String
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun authenticate(idToken: String, container: String, marketingPid: String?): SocialLoginResponse {
 | 
				
			||||||
 | 
					        val googleUserInfo = googleService.getUserInfo(idToken)
 | 
				
			||||||
 | 
					            ?: throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요")
 | 
				
			||||||
 | 
					        val member = memberService.findOrRegister(googleUserInfo, container, marketingPid)
 | 
				
			||||||
 | 
					        val principal = MemberAdapter(member)
 | 
				
			||||||
 | 
					        val authToken = GoogleAuthenticationToken(idToken, principal.authorities)
 | 
				
			||||||
 | 
					        authToken.setPrincipal(principal)
 | 
				
			||||||
 | 
					        SecurityContextHolder.getContext().authentication = authToken
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val jwt = tokenProvider.createToken(
 | 
				
			||||||
 | 
					            authentication = authToken,
 | 
				
			||||||
 | 
					            memberId = member.id!!
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val loginResponse = LoginResponse(
 | 
				
			||||||
 | 
					            userId = member.id!!,
 | 
				
			||||||
 | 
					            token = jwt,
 | 
				
			||||||
 | 
					            nickname = member.nickname,
 | 
				
			||||||
 | 
					            email = member.email,
 | 
				
			||||||
 | 
					            profileImage = if (member.profileImage != null) {
 | 
				
			||||||
 | 
					                "$cloudFrontHost/${member.profileImage}"
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                "$cloudFrontHost/profile/default-profile.png"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return SocialLoginResponse(
 | 
				
			||||||
 | 
					            memberId = member.id!!,
 | 
				
			||||||
 | 
					            marketingPid = marketingPid,
 | 
				
			||||||
 | 
					            loginResponse = loginResponse
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package kr.co.vividnext.sodalive.member.social.google
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.security.authentication.AbstractAuthenticationToken
 | 
				
			||||||
 | 
					import org.springframework.security.core.GrantedAuthority
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GoogleAuthenticationToken(
 | 
				
			||||||
 | 
					    private val idToken: String,
 | 
				
			||||||
 | 
					    authorities: Collection<GrantedAuthority>? = null
 | 
				
			||||||
 | 
					) : AbstractAuthenticationToken(authorities) {
 | 
				
			||||||
 | 
					    private var principal: Any? = null
 | 
				
			||||||
 | 
					    init { isAuthenticated = authorities != null }
 | 
				
			||||||
 | 
					    override fun getCredentials(): Any = idToken
 | 
				
			||||||
 | 
					    override fun getPrincipal(): Any? = principal
 | 
				
			||||||
 | 
					    fun setPrincipal(principal: Any) { this.principal = principal }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					package kr.co.vividnext.sodalive.member.social.google
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
 | 
				
			||||||
 | 
					import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
 | 
				
			||||||
 | 
					import com.google.api.client.json.gson.GsonFactory
 | 
				
			||||||
 | 
					import kr.co.vividnext.sodalive.common.SodaException
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					class GoogleService(
 | 
				
			||||||
 | 
					    @Value("\${google.web-client-id}")
 | 
				
			||||||
 | 
					    private val googleWebClientId: String
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    private val transport = GoogleNetHttpTransport.newTrustedTransport()
 | 
				
			||||||
 | 
					    private val jsonFactory = GsonFactory.getDefaultInstance()
 | 
				
			||||||
 | 
					    private val clientIds = listOf(googleWebClientId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun getUserInfo(idToken: String): GoogleUserInfo? {
 | 
				
			||||||
 | 
					        val verifier = GoogleIdTokenVerifier.Builder(transport, jsonFactory)
 | 
				
			||||||
 | 
					            .setAudience(clientIds)
 | 
				
			||||||
 | 
					            .setIssuers(listOf("accounts.google.com", "https://accounts.google.com"))
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return try {
 | 
				
			||||||
 | 
					            val token = verifier.verify(idToken)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (token != null) {
 | 
				
			||||||
 | 
					                val payload = token.payload
 | 
				
			||||||
 | 
					                val email = payload.email ?: throw SodaException("이메일 제공에 동의하셔야 서비스 이용이 가능합니다.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                GoogleUserInfo(
 | 
				
			||||||
 | 
					                    sub = payload.subject,
 | 
				
			||||||
 | 
					                    email = email,
 | 
				
			||||||
 | 
					                    name = payload["name"] as? String
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                null
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (e: Exception) {
 | 
				
			||||||
 | 
					            e.printStackTrace()
 | 
				
			||||||
 | 
					            null
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package kr.co.vividnext.sodalive.member.social.google
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class GoogleUserInfo(
 | 
				
			||||||
 | 
					    val sub: String,
 | 
				
			||||||
 | 
					    val email: String,
 | 
				
			||||||
 | 
					    val name: String?
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -25,6 +25,9 @@ agora:
 | 
				
			|||||||
firebase:
 | 
					firebase:
 | 
				
			||||||
    secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
 | 
					    secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					google:
 | 
				
			||||||
 | 
					    webClientId: ${GOOGLE_WEB_CLIENT_ID}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cloud:
 | 
					cloud:
 | 
				
			||||||
    aws:
 | 
					    aws:
 | 
				
			||||||
        credentials:
 | 
					        credentials:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user