Compare commits

..

No commits in common. "5639d8ac8ea551767c45ae4992b90ebcd45f8f88" and "9aac5915912561dcbff38b376894e3edb7ec6984" have entirely different histories.

20 changed files with 3 additions and 404 deletions

View File

@ -65,8 +65,6 @@ 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")

View File

@ -38,7 +38,6 @@ class AdminRecommendSeriesQueryRepositoryImpl(
.and(series.isActive.isTrue) .and(series.isActive.isTrue)
.and(recommendSeries.isFree.eq(isFree)) .and(recommendSeries.isFree.eq(isFree))
) )
.orderBy(recommendSeries.orders.asc())
.fetch() .fetch()
} }
} }

View File

@ -156,8 +156,8 @@ class AdminEventBannerService(
) )
} }
if (event.link != link) { if (!link.isNullOrBlank() && event.link != link) {
event.link = if (link.isNullOrBlank()) null else link event.link = link
} }
if (!title.isNullOrBlank() && event.title != title) { if (!title.isNullOrBlank() && event.title != title) {

View File

@ -70,8 +70,6 @@ class SecurityConfig(
.antMatchers("/member/signup").permitAll() .antMatchers("/member/signup").permitAll()
.antMatchers("/member/signup/v2").permitAll() .antMatchers("/member/signup/v2").permitAll()
.antMatchers("/member/login").permitAll() .antMatchers("/member/login").permitAll()
.antMatchers("/member/login/google").permitAll()
.antMatchers("/member/login/kakao").permitAll()
.antMatchers("/creator-admin/member/login").permitAll() .antMatchers("/creator-admin/member/login").permitAll()
.antMatchers("/member/forgot-password").permitAll() .antMatchers("/member/forgot-password").permitAll()
.antMatchers("/stplat/terms_of_service").permitAll() .antMatchers("/stplat/terms_of_service").permitAll()

View File

@ -202,7 +202,7 @@ class EventService(
) )
} }
if (event.link != link) { if (!link.isNullOrBlank() && event.link != link) {
event.link = link event.link = link
} }

View File

@ -24,13 +24,6 @@ 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,
@ -147,7 +140,3 @@ 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
}

View File

@ -8,11 +8,8 @@ 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 kr.co.vividnext.sodalive.member.social.kakao.KakaoAuthService
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
@ -32,8 +29,6 @@ import org.springframework.web.multipart.MultipartFile
@RequestMapping("/member") @RequestMapping("/member")
class MemberController( class MemberController(
private val service: MemberService, private val service: MemberService,
private val kakaoAuthService: KakaoAuthService,
private val googleAuthService: GoogleAuthService,
private val trackingService: AdTrackingService private val trackingService: AdTrackingService
) { ) {
@GetMapping("/check/email") @GetMapping("/check/email")
@ -321,50 +316,4 @@ 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)
}
@PostMapping("/login/kakao")
fun loginKakao(
@RequestHeader("Authorization") authHeader: String,
@RequestBody request: SocialLoginRequest
): ApiResponse<LoginResponse> {
if (!authHeader.startsWith("Bearer ")) {
throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요")
}
val token = authHeader.substring(7)
val response = kakaoAuthService.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)
}
} }

View File

@ -21,8 +21,6 @@ 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?
fun findByKakaoId(kakaoId: Long): Member?
} }
interface MemberQueryRepository { interface MemberQueryRepository {

View File

@ -32,8 +32,6 @@ 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.social.kakao.KakaoUserInfo
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
@ -738,70 +736,4 @@ 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,
provider = MemberProvider.GOOGLE,
container = container
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
return member
}
@Transactional
fun findOrRegister(kakaoUserInfo: KakaoUserInfo, container: String, marketingPid: String?): Member {
repository.findByKakaoId(kakaoUserInfo.id)?.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(
kakaoId = kakaoUserInfo.id,
email = kakaoUserInfo.email,
password = "",
nickname = nickname,
profileImage = "profile/default-profile.png",
gender = Gender.NONE,
provider = MemberProvider.KAKAO,
container = container
)
if (!marketingPid.isNullOrBlank()) {
member.activePid = marketingPid
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
}
repository.save(member)
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
return member
}
} }

View File

@ -6,5 +6,3 @@ 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)

View File

@ -1,9 +0,0 @@
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
)

View File

@ -1,54 +0,0 @@
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
)
}
}

View File

@ -1,15 +0,0 @@
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 }
}

View File

@ -1,45 +0,0 @@
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
}
}
}

View File

@ -1,7 +0,0 @@
package kr.co.vividnext.sodalive.member.social.google
data class GoogleUserInfo(
val sub: String,
val email: String,
val name: String?
)

View File

@ -1,54 +0,0 @@
package kr.co.vividnext.sodalive.member.social.kakao
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 KakaoAuthService(
private val kakaoService: KakaoService,
private val memberService: MemberService,
private val tokenProvider: TokenProvider,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun authenticate(accessToken: String, container: String, marketingPid: String?): SocialLoginResponse {
val kakaoUserInfo = kakaoService.getUserInfo(accessToken)
?: throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요")
val member = memberService.findOrRegister(kakaoUserInfo, container, marketingPid)
val principal = MemberAdapter(member)
val authToken = KakaoAuthenticationToken(accessToken, 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
)
}
}

View File

@ -1,15 +0,0 @@
package kr.co.vividnext.sodalive.member.social.kakao
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
class KakaoAuthenticationToken(
val accessToken: String,
authorities: Collection<GrantedAuthority>? = null
) : AbstractAuthenticationToken(authorities) {
private var principal: Any? = null
init { isAuthenticated = authorities != null }
override fun getCredentials(): Any = accessToken
override fun getPrincipal(): Any? = principal
fun setPrincipal(principal: Any) { this.principal = principal }
}

View File

@ -1,53 +0,0 @@
package kr.co.vividnext.sodalive.member.social.kakao
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class KakaoService(
private val restTemplate: RestTemplate = RestTemplate(),
private val objectMapper: ObjectMapper = ObjectMapper()
) {
fun getUserInfo(accessToken: String): KakaoUserInfo? {
val url = "https://kapi.kakao.com/v2/user/me"
val headers = HttpHeaders().apply {
set("Authorization", "Bearer $accessToken")
}
val entity = HttpEntity<Any>(headers)
try {
val response: ResponseEntity<String> = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String::class.java
)
if (response.statusCode == HttpStatus.OK) {
// 응답 JSON을 KakaoUserInfo와 직접 매핑하려면 DTO의 구조와 JSON 구조가 일치해야 합니다.
// 실제 응답 JSON은 중첩 구조를 가지므로, 필요한 필드만 추출하는 방법을 사용합니다.
val jsonNode = objectMapper.readTree(response.body ?: return null)
val id = jsonNode.get("id").asLong()
val kakaoAccount = jsonNode.get("kakao_account")
val email = kakaoAccount?.get("email")?.asText()
?: throw SodaException("카카오 로그인을 하지 못했습니다. 다시 시도해 주세요")
val properties = jsonNode.get("properties")
val nickname = properties?.get("nickname")?.asText()
return KakaoUserInfo(id, email, nickname)
} else {
println("카카오 사용자 정보 요청 실패: ${response.statusCode}")
}
} catch (ex: Exception) {
ex.printStackTrace()
}
return null
}
}

View File

@ -1,7 +0,0 @@
package kr.co.vividnext.sodalive.member.social.kakao
data class KakaoUserInfo(
val id: Long,
val email: String,
val nickname: String?
)

View File

@ -25,9 +25,6 @@ agora:
firebase: firebase:
secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS} secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
google:
webClientId: ${GOOGLE_WEB_CLIENT_ID}
cloud: cloud:
aws: aws:
credentials: credentials: