@@ -41,6 +41,8 @@ dependencies {
|
|||||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
|
||||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
||||||
|
|
||||||
|
implementation("com.nimbusds:nimbus-jose-jwt:9.37.3")
|
||||||
|
|
||||||
// querydsl (추가 설정)
|
// querydsl (추가 설정)
|
||||||
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
|
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
|
||||||
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
|
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ class AdminChatCharacterController(
|
|||||||
runCatching { CharacterType.valueOf(it) }
|
runCatching { CharacterType.valueOf(it) }
|
||||||
.getOrDefault(CharacterType.Character)
|
.getOrDefault(CharacterType.Character)
|
||||||
} ?: CharacterType.Character,
|
} ?: CharacterType.Character,
|
||||||
|
region = request.region,
|
||||||
tags = request.tags,
|
tags = request.tags,
|
||||||
values = request.values,
|
values = request.values,
|
||||||
hobbies = request.hobbies,
|
hobbies = request.hobbies,
|
||||||
@@ -203,6 +204,7 @@ class AdminChatCharacterController(
|
|||||||
body["name"] = request.name
|
body["name"] = request.name
|
||||||
body["systemPrompt"] = request.systemPrompt
|
body["systemPrompt"] = request.systemPrompt
|
||||||
body["description"] = request.description
|
body["description"] = request.description
|
||||||
|
body["region"] = request.region
|
||||||
request.age?.let { body["age"] = it }
|
request.age?.let { body["age"] = it }
|
||||||
request.gender?.let { body["gender"] = it }
|
request.gender?.let { body["gender"] = it }
|
||||||
request.mbti?.let { body["mbti"] = it }
|
request.mbti?.let { body["mbti"] = it }
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
val speechPattern: String?,
|
val speechPattern: String?,
|
||||||
val speechStyle: String?,
|
val speechStyle: String?,
|
||||||
val appearance: String?,
|
val appearance: String?,
|
||||||
|
val region: String,
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val hobbies: List<String>,
|
val hobbies: List<String>,
|
||||||
@@ -67,6 +68,7 @@ data class ChatCharacterDetailResponse(
|
|||||||
speechPattern = chatCharacter.speechPattern,
|
speechPattern = chatCharacter.speechPattern,
|
||||||
speechStyle = chatCharacter.speechStyle,
|
speechStyle = chatCharacter.speechStyle,
|
||||||
appearance = chatCharacter.appearance,
|
appearance = chatCharacter.appearance,
|
||||||
|
region = chatCharacter.region,
|
||||||
isActive = chatCharacter.isActive,
|
isActive = chatCharacter.isActive,
|
||||||
tags = chatCharacter.tagMappings.map { it.tag.tag },
|
tags = chatCharacter.tagMappings.map { it.tag.tag },
|
||||||
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },
|
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ data class ChatCharacterRegisterRequest(
|
|||||||
@JsonProperty("speechPattern") val speechPattern: String?,
|
@JsonProperty("speechPattern") val speechPattern: String?,
|
||||||
@JsonProperty("speechStyle") val speechStyle: String?,
|
@JsonProperty("speechStyle") val speechStyle: String?,
|
||||||
@JsonProperty("appearance") val appearance: String?,
|
@JsonProperty("appearance") val appearance: String?,
|
||||||
|
@JsonProperty("region") val region: String = "KR",
|
||||||
@JsonProperty("originalTitle") val originalTitle: String? = null,
|
@JsonProperty("originalTitle") val originalTitle: String? = null,
|
||||||
@JsonProperty("originalLink") val originalLink: String? = null,
|
@JsonProperty("originalLink") val originalLink: String? = null,
|
||||||
@JsonProperty("originalWorkId") val originalWorkId: Long? = null,
|
@JsonProperty("originalWorkId") val originalWorkId: Long? = null,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data class ChatCharacterListResponse(
|
|||||||
val mbti: String?,
|
val mbti: String?,
|
||||||
val speechStyle: String?,
|
val speechStyle: String?,
|
||||||
val speechPattern: String?,
|
val speechPattern: String?,
|
||||||
|
val region: String,
|
||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val createdAt: String?,
|
val createdAt: String?,
|
||||||
val updatedAt: String?
|
val updatedAt: String?
|
||||||
@@ -48,6 +49,7 @@ data class ChatCharacterListResponse(
|
|||||||
mbti = chatCharacter.mbti,
|
mbti = chatCharacter.mbti,
|
||||||
speechStyle = chatCharacter.speechStyle,
|
speechStyle = chatCharacter.speechStyle,
|
||||||
speechPattern = chatCharacter.speechPattern,
|
speechPattern = chatCharacter.speechPattern,
|
||||||
|
region = chatCharacter.region,
|
||||||
tags = chatCharacter.tagMappings.map { it.tag.tag },
|
tags = chatCharacter.tagMappings.map { it.tag.tag },
|
||||||
createdAt = createdAtStr,
|
createdAt = createdAtStr,
|
||||||
updatedAt = updatedAtStr
|
updatedAt = updatedAtStr
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class AdminMemberService(
|
|||||||
MemberProvider.KAKAO -> messageSource.getMessage("member.provider.kakao", langContext.lang).orEmpty()
|
MemberProvider.KAKAO -> messageSource.getMessage("member.provider.kakao", langContext.lang).orEmpty()
|
||||||
MemberProvider.GOOGLE -> messageSource.getMessage("member.provider.google", langContext.lang).orEmpty()
|
MemberProvider.GOOGLE -> messageSource.getMessage("member.provider.google", langContext.lang).orEmpty()
|
||||||
MemberProvider.APPLE -> messageSource.getMessage("member.provider.apple", langContext.lang).orEmpty()
|
MemberProvider.APPLE -> messageSource.getMessage("member.provider.apple", langContext.lang).orEmpty()
|
||||||
|
MemberProvider.LINE -> messageSource.getMessage("member.provider.line", langContext.lang).orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
val signUpDate = it.createdAt!!
|
val signUpDate = it.createdAt!!
|
||||||
@@ -126,7 +127,7 @@ class AdminMemberService(
|
|||||||
|
|
||||||
GetAdminMemberListResponseItem(
|
GetAdminMemberListResponseItem(
|
||||||
id = it.id!!,
|
id = it.id!!,
|
||||||
email = it.email,
|
email = it.email ?: "",
|
||||||
nickname = it.nickname,
|
nickname = it.nickname,
|
||||||
profileUrl = if (it.profileImage != null) {
|
profileUrl = if (it.profileImage != null) {
|
||||||
"$cloudFrontHost/${it.profileImage}"
|
"$cloudFrontHost/${it.profileImage}"
|
||||||
@@ -160,6 +161,7 @@ class AdminMemberService(
|
|||||||
val member = repository.findByIdAndActive(memberId = request.memberId)
|
val member = repository.findByIdAndActive(memberId = request.memberId)
|
||||||
?: throw SodaException(messageKey = "admin.member.reset_password_invalid")
|
?: throw SodaException(messageKey = "admin.member.reset_password_invalid")
|
||||||
|
|
||||||
member.password = passwordEncoder.encode(member.email.split("@")[0])
|
val email = member.email ?: throw SodaException(message = "이메일이 없는 계정은 비밀번호 재설정이 불가능합니다.")
|
||||||
|
member.password = passwordEncoder.encode(email.split("@")[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ class HomeService(
|
|||||||
|
|
||||||
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
|
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType
|
contentType = contentType,
|
||||||
|
excludeThemes = listOf("다시듣기")
|
||||||
)
|
)
|
||||||
|
|
||||||
val latestContentList = contentService.getLatestContentByTheme(
|
val latestContentList = contentService.getLatestContentByTheme(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
|||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.security.core.userdetails.User
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
@@ -29,6 +28,12 @@ class ChargeTempController(private val service: ChargeTempService) {
|
|||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
fun verify(
|
fun verify(
|
||||||
@RequestBody request: VerifyRequest,
|
@RequestBody request: VerifyRequest,
|
||||||
@AuthenticationPrincipal user: User
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = ApiResponse.ok(service.verify(user, request))
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.verify(member, request))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
|
|||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.security.core.userdetails.User
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@@ -26,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional
|
|||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
class ChargeTempService(
|
class ChargeTempService(
|
||||||
private val chargeRepository: ChargeRepository,
|
private val chargeRepository: ChargeRepository,
|
||||||
private val memberRepository: MemberRepository,
|
|
||||||
|
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val messageSource: SodaMessageSource,
|
private val messageSource: SodaMessageSource,
|
||||||
@@ -54,11 +51,9 @@ class ChargeTempService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun verify(user: User, verifyRequest: VerifyRequest) {
|
fun verify(member: Member, verifyRequest: VerifyRequest) {
|
||||||
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
|
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
|
||||||
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
|
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
|
||||||
val member = memberRepository.findByEmail(user.username)
|
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
|
||||||
|
|
||||||
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
|
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
|
||||||
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
|
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ class ChatCharacter(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var characterType: CharacterType = CharacterType.Character,
|
var characterType: CharacterType = CharacterType.Character,
|
||||||
|
|
||||||
|
// 리전 (기본값 KR, 수정 불가)
|
||||||
|
@Column(nullable = false)
|
||||||
|
val region: String = "KR",
|
||||||
|
|
||||||
var isActive: Boolean = true
|
var isActive: Boolean = true
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
var imagePath: String? = null
|
var imagePath: String? = null
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ class ChatCharacterService(
|
|||||||
originalTitle: String? = null,
|
originalTitle: String? = null,
|
||||||
originalLink: String? = null,
|
originalLink: String? = null,
|
||||||
characterType: CharacterType = CharacterType.Character,
|
characterType: CharacterType = CharacterType.Character,
|
||||||
|
region: String = "KR",
|
||||||
tags: List<String> = emptyList(),
|
tags: List<String> = emptyList(),
|
||||||
values: List<String> = emptyList(),
|
values: List<String> = emptyList(),
|
||||||
hobbies: List<String> = emptyList(),
|
hobbies: List<String> = emptyList(),
|
||||||
@@ -600,7 +601,8 @@ class ChatCharacterService(
|
|||||||
appearance = appearance,
|
appearance = appearance,
|
||||||
originalTitle = originalTitle,
|
originalTitle = originalTitle,
|
||||||
originalLink = originalLink,
|
originalLink = originalLink,
|
||||||
characterType = characterType
|
characterType = characterType,
|
||||||
|
region = region
|
||||||
)
|
)
|
||||||
|
|
||||||
// 관련 엔티티 연결
|
// 관련 엔티티 연결
|
||||||
@@ -630,6 +632,7 @@ class ChatCharacterService(
|
|||||||
originalTitle: String? = null,
|
originalTitle: String? = null,
|
||||||
originalLink: String? = null,
|
originalLink: String? = null,
|
||||||
characterType: CharacterType = CharacterType.Character,
|
characterType: CharacterType = CharacterType.Character,
|
||||||
|
region: String = "KR",
|
||||||
tags: List<String> = emptyList(),
|
tags: List<String> = emptyList(),
|
||||||
values: List<String> = emptyList(),
|
values: List<String> = emptyList(),
|
||||||
hobbies: List<String> = emptyList(),
|
hobbies: List<String> = emptyList(),
|
||||||
@@ -653,6 +656,7 @@ class ChatCharacterService(
|
|||||||
originalTitle = originalTitle,
|
originalTitle = originalTitle,
|
||||||
originalLink = originalLink,
|
originalLink = originalLink,
|
||||||
characterType = characterType,
|
characterType = characterType,
|
||||||
|
region = region,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
values = values,
|
values = values,
|
||||||
hobbies = hobbies,
|
hobbies = hobbies,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.common
|
package kr.co.vividnext.sodalive.common
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -20,10 +21,14 @@ class SodaExceptionHandler(
|
|||||||
private val messageSource: SodaMessageSource
|
private val messageSource: SodaMessageSource
|
||||||
) {
|
) {
|
||||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
private val logLang = Lang.KO
|
||||||
|
|
||||||
@ExceptionHandler(SodaException::class)
|
@ExceptionHandler(SodaException::class)
|
||||||
fun handleSodaException(e: SodaException) = run {
|
fun handleSodaException(e: SodaException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, logLang) }
|
||||||
|
?: e.message?.takeIf { it.isNotBlank() }
|
||||||
|
?: messageSource.getMessage("common.error.unknown", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
|
val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
|
||||||
?: e.message?.takeIf { it.isNotBlank() }
|
?: e.message?.takeIf { it.isNotBlank() }
|
||||||
?: messageSource.getMessage("common.error.unknown", langContext.lang)
|
?: messageSource.getMessage("common.error.unknown", langContext.lang)
|
||||||
@@ -35,35 +40,40 @@ class SodaExceptionHandler(
|
|||||||
|
|
||||||
@ExceptionHandler(MaxUploadSizeExceededException::class)
|
@ExceptionHandler(MaxUploadSizeExceededException::class)
|
||||||
fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run {
|
fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.max_upload_size", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.max_upload_size", langContext.lang)
|
val message = messageSource.getMessage("common.error.max_upload_size", langContext.lang)
|
||||||
ApiResponse.error(message = message)
|
ApiResponse.error(message = message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(AccessDeniedException::class)
|
@ExceptionHandler(AccessDeniedException::class)
|
||||||
fun handleAccessDeniedException(e: AccessDeniedException) = run {
|
fun handleAccessDeniedException(e: AccessDeniedException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.access_denied", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.access_denied", langContext.lang)
|
val message = messageSource.getMessage("common.error.access_denied", langContext.lang)
|
||||||
ApiResponse.error(message = message)
|
ApiResponse.error(message = message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(InternalAuthenticationServiceException::class)
|
@ExceptionHandler(InternalAuthenticationServiceException::class)
|
||||||
fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run {
|
fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.bad_credentials", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
|
val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
|
||||||
ApiResponse.error(message)
|
ApiResponse.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BadCredentialsException::class)
|
@ExceptionHandler(BadCredentialsException::class)
|
||||||
fun handleBadCredentialsException(e: BadCredentialsException) = run {
|
fun handleBadCredentialsException(e: BadCredentialsException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.bad_credentials", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
|
val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
|
||||||
ApiResponse.error(message)
|
ApiResponse.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(DataIntegrityViolationException::class)
|
@ExceptionHandler(DataIntegrityViolationException::class)
|
||||||
fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run {
|
fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run {
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.already_registered", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.already_registered", langContext.lang)
|
val message = messageSource.getMessage("common.error.already_registered", langContext.lang)
|
||||||
ApiResponse.error(message)
|
ApiResponse.error(message)
|
||||||
}
|
}
|
||||||
@@ -71,7 +81,10 @@ class SodaExceptionHandler(
|
|||||||
@ResponseStatus(value = HttpStatus.NOT_FOUND)
|
@ResponseStatus(value = HttpStatus.NOT_FOUND)
|
||||||
@ExceptionHandler(AdsChargeException::class)
|
@ExceptionHandler(AdsChargeException::class)
|
||||||
fun handleAdsChargeException(e: AdsChargeException) = run {
|
fun handleAdsChargeException(e: AdsChargeException) = run {
|
||||||
logger.error("API error - AdsChargeException ::: ", e)
|
val logMessage = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, logLang) }
|
||||||
|
?: e.message?.takeIf { it.isNotBlank() }
|
||||||
|
?: messageSource.getMessage("common.error.invalid_request", logLang)
|
||||||
|
logger.error("API error - AdsChargeException: {}", logMessage, e)
|
||||||
val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
|
val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
|
||||||
?: e.message?.takeIf { it.isNotBlank() }
|
?: e.message?.takeIf { it.isNotBlank() }
|
||||||
?: messageSource.getMessage("common.error.invalid_request", langContext.lang)
|
?: messageSource.getMessage("common.error.invalid_request", langContext.lang)
|
||||||
@@ -81,7 +94,8 @@ class SodaExceptionHandler(
|
|||||||
@ExceptionHandler(Exception::class)
|
@ExceptionHandler(Exception::class)
|
||||||
fun handleException(e: Exception) = run {
|
fun handleException(e: Exception) = run {
|
||||||
if (e is ResponseStatusException) throw e
|
if (e is ResponseStatusException) throw e
|
||||||
logger.error("API error", e)
|
val logMessage = messageSource.getMessage("common.error.unknown", logLang)
|
||||||
|
logger.error("API error: {}", logMessage, e)
|
||||||
val message = messageSource.getMessage("common.error.unknown", langContext.lang)
|
val message = messageSource.getMessage("common.error.unknown", langContext.lang)
|
||||||
ApiResponse.error(message)
|
ApiResponse.error(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class SecurityConfig(
|
|||||||
.antMatchers("/member/login").permitAll()
|
.antMatchers("/member/login").permitAll()
|
||||||
.antMatchers("/member/login/google").permitAll()
|
.antMatchers("/member/login/google").permitAll()
|
||||||
.antMatchers("/member/login/kakao").permitAll()
|
.antMatchers("/member/login/kakao").permitAll()
|
||||||
|
.antMatchers("/member/login/apple").permitAll()
|
||||||
|
.antMatchers("/member/login/line").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()
|
||||||
|
|||||||
@@ -34,15 +34,20 @@ class AudioContentThemeService(
|
|||||||
isAdult: Boolean = false,
|
isAdult: Boolean = false,
|
||||||
isFree: Boolean = false,
|
isFree: Boolean = false,
|
||||||
isPointAvailableOnly: Boolean = false,
|
isPointAvailableOnly: Boolean = false,
|
||||||
contentType: ContentType
|
contentType: ContentType,
|
||||||
|
excludeThemes: List<String> = emptyList()
|
||||||
): List<String> {
|
): List<String> {
|
||||||
val themesWithIds = queryRepository.getActiveThemeWithIdsOfContent(
|
var themesWithIds = queryRepository.getActiveThemeWithIdsOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
isFree = isFree,
|
isFree = isFree,
|
||||||
isPointAvailableOnly = isPointAvailableOnly,
|
isPointAvailableOnly = isPointAvailableOnly,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (excludeThemes.isNotEmpty()) {
|
||||||
|
themesWithIds = themesWithIds.filter { it.theme !in excludeThemes }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
|
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
|
||||||
* 번역이 없으면 번역 API 호출 후 저장하고 반환
|
* 번역이 없으면 번역 API 호출 후 저장하고 반환
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class CreatorAdminMemberService(
|
|||||||
userId = member.id!!,
|
userId = member.id!!,
|
||||||
token = jwt,
|
token = jwt,
|
||||||
nickname = member.nickname,
|
nickname = member.nickname,
|
||||||
email = member.email,
|
email = member.email ?: "",
|
||||||
profileImage = if (member.profileImage != null) {
|
profileImage = if (member.profileImage != null) {
|
||||||
"$cloudFrontHost/${member.profileImage}"
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1280,6 +1280,16 @@ class SodaMessageSource {
|
|||||||
Lang.EN to "Kakao login failed. Please try again.",
|
Lang.EN to "Kakao login failed. Please try again.",
|
||||||
Lang.JA to "Kakaoでログインできませんでした。もう一度お試しください。"
|
Lang.JA to "Kakaoでログインできませんでした。もう一度お試しください。"
|
||||||
),
|
),
|
||||||
|
"member.social.apple_login_failed" to mapOf(
|
||||||
|
Lang.KO to "애플 로그인을 하지 못했습니다. 다시 시도해 주세요",
|
||||||
|
Lang.EN to "Apple sign-in failed. Please try again.",
|
||||||
|
Lang.JA to "Appleでログインできませんでした。もう一度お試しください。"
|
||||||
|
),
|
||||||
|
"member.social.line_login_failed" to mapOf(
|
||||||
|
Lang.KO to "라인 로그인을 하지 못했습니다. 다시 시도해 주세요",
|
||||||
|
Lang.EN to "LINE sign-in failed. Please try again.",
|
||||||
|
Lang.JA to "LINEでログインできませんでした。もう一度お試しください。"
|
||||||
|
),
|
||||||
"member.social.email_consent_required" to mapOf(
|
"member.social.email_consent_required" to mapOf(
|
||||||
Lang.KO to "이메일 제공에 동의하셔야 서비스 이용이 가능합니다.",
|
Lang.KO to "이메일 제공에 동의하셔야 서비스 이용이 가능합니다.",
|
||||||
Lang.EN to "You must agree to provide your email to use the service.",
|
Lang.EN to "You must agree to provide your email to use the service.",
|
||||||
@@ -1628,6 +1638,11 @@ class SodaMessageSource {
|
|||||||
Lang.KO to "애플",
|
Lang.KO to "애플",
|
||||||
Lang.EN to "Apple",
|
Lang.EN to "Apple",
|
||||||
Lang.JA to "Apple"
|
Lang.JA to "Apple"
|
||||||
|
),
|
||||||
|
"member.provider.line" to mapOf(
|
||||||
|
Lang.KO to "라인",
|
||||||
|
Lang.EN to "LINE",
|
||||||
|
Lang.JA to "LINE"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import javax.persistence.OneToOne
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class Member(
|
data class Member(
|
||||||
val email: String,
|
var email: String? = null,
|
||||||
var password: String,
|
var password: String,
|
||||||
var nickname: String,
|
var nickname: String,
|
||||||
var profileImage: String? = null,
|
var profileImage: String? = null,
|
||||||
@@ -27,6 +27,7 @@ data class Member(
|
|||||||
val kakaoId: Long? = null,
|
val kakaoId: Long? = null,
|
||||||
val googleId: String? = null,
|
val googleId: String? = null,
|
||||||
val appleId: String? = null,
|
val appleId: String? = null,
|
||||||
|
val lineId: String? = null,
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
val provider: MemberProvider = MemberProvider.EMAIL,
|
val provider: MemberProvider = MemberProvider.EMAIL,
|
||||||
@@ -158,5 +159,5 @@ enum class MemberRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class MemberProvider {
|
enum class MemberProvider {
|
||||||
EMAIL, KAKAO, GOOGLE, APPLE
|
EMAIL, KAKAO, GOOGLE, APPLE, LINE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
|
|||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.core.userdetails.User
|
||||||
|
|
||||||
class MemberAdapter(val member: Member) : User(
|
class MemberAdapter(val member: Member) : User(
|
||||||
member.email,
|
member.email ?: "member:${member.id}",
|
||||||
member.password,
|
member.password,
|
||||||
listOf(SimpleGrantedAuthority("ROLE_${member.role.name}"))
|
listOf(SimpleGrantedAuthority("ROLE_${member.role.name}"))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import kr.co.vividnext.sodalive.member.login.LoginResponse
|
|||||||
import kr.co.vividnext.sodalive.member.login.SocialLoginRequest
|
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.SocialAuthServiceResolver
|
||||||
import kr.co.vividnext.sodalive.member.social.kakao.KakaoAuthService
|
|
||||||
import kr.co.vividnext.sodalive.useraction.ActionType
|
import kr.co.vividnext.sodalive.useraction.ActionType
|
||||||
import kr.co.vividnext.sodalive.useraction.UserActionService
|
import kr.co.vividnext.sodalive.useraction.UserActionService
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
@@ -36,8 +35,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 kakaoAuthService: KakaoAuthService,
|
private val socialAuthServiceResolver: SocialAuthServiceResolver,
|
||||||
private val googleAuthService: GoogleAuthService,
|
|
||||||
private val trackingService: AdTrackingService,
|
private val trackingService: AdTrackingService,
|
||||||
private val userActionService: UserActionService,
|
private val userActionService: UserActionService,
|
||||||
private val messageSource: SodaMessageSource,
|
private val messageSource: SodaMessageSource,
|
||||||
@@ -345,31 +343,8 @@ class MemberController(
|
|||||||
@RequestHeader("Authorization") authHeader: String,
|
@RequestHeader("Authorization") authHeader: String,
|
||||||
@RequestBody request: SocialLoginRequest
|
@RequestBody request: SocialLoginRequest
|
||||||
): ApiResponse<LoginResponse> {
|
): ApiResponse<LoginResponse> {
|
||||||
if (!authHeader.startsWith("Bearer ")) {
|
val token = extractBearerToken(authHeader, MemberProvider.GOOGLE)
|
||||||
throw SodaException(messageKey = "member.social.google_login_failed")
|
return processSocialLogin(MemberProvider.GOOGLE, token, request, null)
|
||||||
}
|
|
||||||
|
|
||||||
val token = authHeader.substring(7)
|
|
||||||
val response = googleAuthService.authenticate(token, request.container, request.marketingPid, request.pushToken)
|
|
||||||
|
|
||||||
if (!response.marketingPid.isNullOrBlank()) {
|
|
||||||
trackingService.saveTrackingHistory(
|
|
||||||
pid = response.marketingPid,
|
|
||||||
type = AdTrackingHistoryType.SIGNUP,
|
|
||||||
memberId = response.memberId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.isNew) {
|
|
||||||
userActionService.recordAction(
|
|
||||||
memberId = response.memberId,
|
|
||||||
isAuth = false,
|
|
||||||
actionType = ActionType.SIGN_UP
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
|
||||||
return ApiResponse.ok(message = message, data = response.loginResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login/kakao")
|
@PostMapping("/login/kakao")
|
||||||
@@ -377,12 +352,50 @@ class MemberController(
|
|||||||
@RequestHeader("Authorization") authHeader: String,
|
@RequestHeader("Authorization") authHeader: String,
|
||||||
@RequestBody request: SocialLoginRequest
|
@RequestBody request: SocialLoginRequest
|
||||||
): ApiResponse<LoginResponse> {
|
): ApiResponse<LoginResponse> {
|
||||||
if (!authHeader.startsWith("Bearer ")) {
|
val token = extractBearerToken(authHeader, MemberProvider.KAKAO)
|
||||||
throw SodaException(messageKey = "member.social.kakao_login_failed")
|
return processSocialLogin(MemberProvider.KAKAO, token, request, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = authHeader.substring(7)
|
@PostMapping("/login/apple")
|
||||||
val response = kakaoAuthService.authenticate(token, request.container, request.marketingPid, request.pushToken)
|
fun loginApple(
|
||||||
|
@RequestBody request: SocialLoginRequest
|
||||||
|
): ApiResponse<LoginResponse> {
|
||||||
|
val errorKey = socialLoginErrorKey(MemberProvider.APPLE)
|
||||||
|
val token = request.identityToken?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = errorKey)
|
||||||
|
val nonce = request.nonce?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = errorKey)
|
||||||
|
|
||||||
|
return processSocialLogin(MemberProvider.APPLE, token, request, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login/line")
|
||||||
|
fun loginLine(
|
||||||
|
@RequestBody request: SocialLoginRequest
|
||||||
|
): ApiResponse<LoginResponse> {
|
||||||
|
val errorKey = socialLoginErrorKey(MemberProvider.LINE)
|
||||||
|
val token = request.identityToken?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = errorKey)
|
||||||
|
val nonce = request.nonce?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = errorKey)
|
||||||
|
|
||||||
|
return processSocialLogin(MemberProvider.LINE, token, request, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSocialLogin(
|
||||||
|
provider: MemberProvider,
|
||||||
|
token: String,
|
||||||
|
request: SocialLoginRequest,
|
||||||
|
nonce: String?
|
||||||
|
): ApiResponse<LoginResponse> {
|
||||||
|
val authService = socialAuthServiceResolver.resolve(provider)
|
||||||
|
val response = authService.authenticate(
|
||||||
|
token = token,
|
||||||
|
container = request.container,
|
||||||
|
marketingPid = request.marketingPid,
|
||||||
|
pushToken = request.pushToken,
|
||||||
|
nonce = nonce
|
||||||
|
)
|
||||||
|
|
||||||
if (!response.marketingPid.isNullOrBlank()) {
|
if (!response.marketingPid.isNullOrBlank()) {
|
||||||
trackingService.saveTrackingHistory(
|
trackingService.saveTrackingHistory(
|
||||||
@@ -403,4 +416,22 @@ class MemberController(
|
|||||||
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
||||||
return ApiResponse.ok(message = message, data = response.loginResponse)
|
return ApiResponse.ok(message = message, data = response.loginResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractBearerToken(authHeader: String, provider: MemberProvider): String {
|
||||||
|
val errorKey = socialLoginErrorKey(provider)
|
||||||
|
if (!authHeader.startsWith("Bearer ")) {
|
||||||
|
throw SodaException(messageKey = errorKey)
|
||||||
|
}
|
||||||
|
return authHeader.substring(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun socialLoginErrorKey(provider: MemberProvider): String {
|
||||||
|
return when (provider) {
|
||||||
|
MemberProvider.GOOGLE -> "member.social.google_login_failed"
|
||||||
|
MemberProvider.KAKAO -> "member.social.kakao_login_failed"
|
||||||
|
MemberProvider.APPLE -> "member.social.apple_login_failed"
|
||||||
|
MemberProvider.LINE -> "member.social.line_login_failed"
|
||||||
|
else -> "common.error.bad_request"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import org.springframework.stereotype.Repository
|
|||||||
|
|
||||||
@Repository
|
@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 findByGoogleId(googleId: String): Member?
|
||||||
fun findByKakaoId(kakaoId: Long): Member?
|
fun findByKakaoId(kakaoId: Long): Member?
|
||||||
|
fun findByAppleId(appleId: String): Member?
|
||||||
|
fun findByLineId(lineId: String): Member?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MemberQueryRepository {
|
interface MemberQueryRepository {
|
||||||
@@ -51,7 +53,7 @@ interface MemberQueryRepository {
|
|||||||
fun getMessageRecipientPushToken(messageId: Long): PushTokenInfo?
|
fun getMessageRecipientPushToken(messageId: Long): PushTokenInfo?
|
||||||
fun getIndividualRecipientPushTokens(recipients: List<Long>, isAuth: Boolean?): List<PushTokenInfo>
|
fun getIndividualRecipientPushTokens(recipients: List<Long>, isAuth: Boolean?): List<PushTokenInfo>
|
||||||
fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse
|
fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse
|
||||||
fun getMemberByEmail(email: String): Member?
|
fun getMemberByEmail(email: String?): Member?
|
||||||
|
|
||||||
fun getChangeNoticeRecipientPushTokens(creatorId: Long): List<PushTokenInfo>
|
fun getChangeNoticeRecipientPushTokens(creatorId: Long): List<PushTokenInfo>
|
||||||
fun getPushTokenFromReservationList(roomId: Long): List<PushTokenInfo>
|
fun getPushTokenFromReservationList(roomId: Long): List<PushTokenInfo>
|
||||||
@@ -363,7 +365,8 @@ class MemberQueryRepositoryImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMemberByEmail(email: String): Member? {
|
override fun getMemberByEmail(email: String?): Member? {
|
||||||
|
if (email == null) return null
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.selectFrom(member)
|
.selectFrom(member)
|
||||||
.where(member.email.eq(email))
|
.where(member.email.eq(email))
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ 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.MemberResolveResult
|
import kr.co.vividnext.sodalive.member.social.MemberResolveResult
|
||||||
|
import kr.co.vividnext.sodalive.member.social.apple.AppleUserInfo
|
||||||
import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
|
import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
|
||||||
import kr.co.vividnext.sodalive.member.social.kakao.KakaoUserInfo
|
import kr.co.vividnext.sodalive.member.social.kakao.KakaoUserInfo
|
||||||
|
import kr.co.vividnext.sodalive.member.social.line.LineUserInfo
|
||||||
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
|
||||||
@@ -346,7 +348,7 @@ class MemberService(
|
|||||||
userId = member.id!!,
|
userId = member.id!!,
|
||||||
token = jwt,
|
token = jwt,
|
||||||
nickname = member.nickname,
|
nickname = member.nickname,
|
||||||
email = member.email,
|
email = member.email ?: "",
|
||||||
profileImage = if (member.profileImage != null) {
|
profileImage = if (member.profileImage != null) {
|
||||||
"$cloudFrontHost/${member.profileImage}"
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
@@ -454,8 +456,16 @@ class MemberService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun loadUserByUsername(username: String): UserDetails {
|
override fun loadUserByUsername(username: String): UserDetails {
|
||||||
val member = repository.findByEmail(email = username)
|
val member = if (username.startsWith("member:")) {
|
||||||
?: throw UsernameNotFoundException(username)
|
val id = username.substringAfter("member:").toLongOrNull()
|
||||||
|
if (id != null) {
|
||||||
|
repository.findByIdOrNull(id)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
repository.findByEmail(email = username)
|
||||||
|
} ?: throw UsernameNotFoundException(username)
|
||||||
|
|
||||||
return MemberAdapter(member)
|
return MemberAdapter(member)
|
||||||
}
|
}
|
||||||
@@ -592,7 +602,7 @@ class MemberService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun signOut(signOutRequest: SignOutRequest, user: User) {
|
fun signOut(signOutRequest: SignOutRequest, user: User) {
|
||||||
val member = repository.findByEmail(user.username)
|
val member = findMemberByUsername(user.username)
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
if (
|
if (
|
||||||
member.provider == MemberProvider.EMAIL &&
|
member.provider == MemberProvider.EMAIL &&
|
||||||
@@ -620,11 +630,7 @@ class MemberService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) {
|
fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) {
|
||||||
if (profileUpdateRequest.email != user.username) {
|
val member = findMemberByUsername(user.username)
|
||||||
throw SodaException(messageKey = "common.error.bad_credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
val member = repository.findByEmail(user.username)
|
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
|
||||||
if (profileUpdateRequest.nickname != null) {
|
if (profileUpdateRequest.nickname != null) {
|
||||||
@@ -652,11 +658,7 @@ class MemberService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse {
|
fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse {
|
||||||
if (profileUpdateRequest.email != user.username) {
|
val member = findMemberByUsername(user.username)
|
||||||
throw SodaException(messageKey = "common.error.bad_credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
val member = repository.findByEmail(user.username)
|
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
|
||||||
if (profileUpdateRequest.modifyPassword != null) {
|
if (profileUpdateRequest.modifyPassword != null) {
|
||||||
@@ -729,7 +731,7 @@ class MemberService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun profileImageUpdate(multipartFile: MultipartFile, user: User): String {
|
fun profileImageUpdate(multipartFile: MultipartFile, user: User): String {
|
||||||
val member = repository.findByEmail(user.username)
|
val member = findMemberByUsername(user.username)
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
|
||||||
val metadata = ObjectMetadata()
|
val metadata = ObjectMetadata()
|
||||||
@@ -791,6 +793,7 @@ class MemberService(
|
|||||||
MemberProvider.KAKAO -> "member.provider.kakao"
|
MemberProvider.KAKAO -> "member.provider.kakao"
|
||||||
MemberProvider.GOOGLE -> "member.provider.google"
|
MemberProvider.GOOGLE -> "member.provider.google"
|
||||||
MemberProvider.APPLE -> "member.provider.apple"
|
MemberProvider.APPLE -> "member.provider.apple"
|
||||||
|
MemberProvider.LINE -> "member.provider.line"
|
||||||
}
|
}
|
||||||
return messageSource.getMessage(key, langContext.lang) ?: provider.name
|
return messageSource.getMessage(key, langContext.lang) ?: provider.name
|
||||||
}
|
}
|
||||||
@@ -932,7 +935,138 @@ class MemberService(
|
|||||||
return MemberResolveResult(member = member, isNew = true)
|
return MemberResolveResult(member = member, isNew = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkEmail(email: String) {
|
@Transactional
|
||||||
|
fun findOrRegister(
|
||||||
|
appleUserInfo: AppleUserInfo,
|
||||||
|
container: String,
|
||||||
|
marketingPid: String?,
|
||||||
|
pushToken: String?
|
||||||
|
): MemberResolveResult {
|
||||||
|
val findMember = repository.findByAppleId(appleUserInfo.sub)
|
||||||
|
if (findMember != null) {
|
||||||
|
if (findMember.isActive) {
|
||||||
|
return MemberResolveResult(member = findMember, isNew = false)
|
||||||
|
} else {
|
||||||
|
throw SodaException(messageKey = "member.validation.inactive_account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
|
||||||
|
|
||||||
|
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
|
||||||
|
|
||||||
|
val email = appleUserInfo.email
|
||||||
|
checkEmail(email)
|
||||||
|
|
||||||
|
val nickname = nicknameGenerateService.generateUniqueNickname()
|
||||||
|
val member = Member(
|
||||||
|
appleId = appleUserInfo.sub,
|
||||||
|
email = email,
|
||||||
|
password = "",
|
||||||
|
nickname = nickname,
|
||||||
|
profileImage = "profile/default-profile.png",
|
||||||
|
gender = Gender.NONE,
|
||||||
|
provider = MemberProvider.APPLE,
|
||||||
|
container = container,
|
||||||
|
countryCode = countryContext.countryCode
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!marketingPid.isNullOrBlank()) {
|
||||||
|
member.activePid = marketingPid
|
||||||
|
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.save(member)
|
||||||
|
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
|
||||||
|
|
||||||
|
if (pushToken != null) {
|
||||||
|
pushTokenService.registerToken(
|
||||||
|
memberId = member.id!!,
|
||||||
|
token = pushToken,
|
||||||
|
deviceType = container
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemberResolveResult(member = member, isNew = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun findOrRegister(
|
||||||
|
lineUserInfo: LineUserInfo,
|
||||||
|
container: String,
|
||||||
|
marketingPid: String?,
|
||||||
|
pushToken: String?
|
||||||
|
): MemberResolveResult {
|
||||||
|
val findMember = repository.findByLineId(lineUserInfo.sub)
|
||||||
|
if (findMember != null) {
|
||||||
|
if (findMember.isActive) {
|
||||||
|
return MemberResolveResult(member = findMember, isNew = false)
|
||||||
|
} else {
|
||||||
|
throw SodaException(messageKey = "member.validation.inactive_account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
|
||||||
|
|
||||||
|
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.invalid_request_retry")
|
||||||
|
|
||||||
|
val email = lineUserInfo.email
|
||||||
|
checkEmail(email)
|
||||||
|
|
||||||
|
val nickname = nicknameGenerateService.generateUniqueNickname()
|
||||||
|
val member = Member(
|
||||||
|
lineId = lineUserInfo.sub,
|
||||||
|
email = email,
|
||||||
|
password = "",
|
||||||
|
nickname = nickname,
|
||||||
|
profileImage = "profile/default-profile.png",
|
||||||
|
gender = Gender.NONE,
|
||||||
|
provider = MemberProvider.LINE,
|
||||||
|
container = container,
|
||||||
|
countryCode = countryContext.countryCode
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!marketingPid.isNullOrBlank()) {
|
||||||
|
member.activePid = marketingPid
|
||||||
|
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.save(member)
|
||||||
|
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
|
||||||
|
|
||||||
|
if (pushToken != null) {
|
||||||
|
pushTokenService.registerToken(
|
||||||
|
memberId = member.id!!,
|
||||||
|
token = pushToken,
|
||||||
|
deviceType = container
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MemberResolveResult(member = member, isNew = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findMemberByUsername(username: String): Member? {
|
||||||
|
return if (username.startsWith("member:")) {
|
||||||
|
val id = username.substringAfter("member:").toLongOrNull()
|
||||||
|
if (id != null) {
|
||||||
|
repository.findByIdOrNull(id)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
repository.findByEmail(email = username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkEmail(email: String?) {
|
||||||
|
if (email.isNullOrBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val member = repository.findByEmail(email)
|
val member = repository.findByEmail(email)
|
||||||
|
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class ProfileResponse(
|
|||||||
) {
|
) {
|
||||||
constructor(member: Member, cloudFrontHost: String, container: String) : this(
|
constructor(member: Member, cloudFrontHost: String, container: String) : this(
|
||||||
userId = member.id!!,
|
userId = member.id!!,
|
||||||
email = member.email,
|
email = member.email ?: "",
|
||||||
nickname = member.nickname,
|
nickname = member.nickname,
|
||||||
gender = member.gender,
|
gender = member.gender,
|
||||||
profileUrl = if (member.profileImage != null) {
|
profileUrl = if (member.profileImage != null) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.member
|
package kr.co.vividnext.sodalive.member
|
||||||
|
|
||||||
data class ProfileUpdateRequest(
|
data class ProfileUpdateRequest(
|
||||||
val email: String,
|
val email: String? = null,
|
||||||
val password: String? = null,
|
val password: String? = null,
|
||||||
val modifyPassword: String? = null,
|
val modifyPassword: String? = null,
|
||||||
val nickname: String? = null,
|
val nickname: String? = null,
|
||||||
|
|||||||
@@ -10,5 +10,7 @@ data class LoginRequest(
|
|||||||
data class SocialLoginRequest(
|
data class SocialLoginRequest(
|
||||||
val container: String,
|
val container: String,
|
||||||
val pushToken: String? = null,
|
val pushToken: String? = null,
|
||||||
val marketingPid: String? = null
|
val marketingPid: String? = null,
|
||||||
|
val identityToken: String? = null,
|
||||||
|
val nonce: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
|
|
||||||
|
interface SocialAuthService {
|
||||||
|
fun getProvider(): MemberProvider
|
||||||
|
fun authenticate(
|
||||||
|
token: String,
|
||||||
|
container: String,
|
||||||
|
marketingPid: String?,
|
||||||
|
pushToken: String?,
|
||||||
|
nonce: String?
|
||||||
|
): SocialLoginResponse
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class SocialAuthServiceResolver(
|
||||||
|
val services: List<SocialAuthService>
|
||||||
|
) {
|
||||||
|
private val serviceMap: Map<MemberProvider, SocialAuthService> = services.associateBy { it.getProvider() }
|
||||||
|
|
||||||
|
fun resolve(provider: MemberProvider): SocialAuthService {
|
||||||
|
return serviceMap[provider] ?: throw IllegalArgumentException("Unsupported social provider: $provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.apple
|
||||||
|
|
||||||
|
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.MemberProvider
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||||
|
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 AppleAuthService(
|
||||||
|
private val appleIdentityTokenVerifier: AppleIdentityTokenVerifier,
|
||||||
|
private val memberService: MemberService,
|
||||||
|
private val tokenProvider: TokenProvider,
|
||||||
|
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) : SocialAuthService {
|
||||||
|
override fun getProvider(): MemberProvider = MemberProvider.APPLE
|
||||||
|
|
||||||
|
override fun authenticate(
|
||||||
|
token: String,
|
||||||
|
container: String,
|
||||||
|
marketingPid: String?,
|
||||||
|
pushToken: String?,
|
||||||
|
nonce: String?
|
||||||
|
): SocialLoginResponse {
|
||||||
|
val rawNonce = nonce?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
|
||||||
|
val appleUserInfo = appleIdentityTokenVerifier.verify(token, rawNonce)
|
||||||
|
val memberResolveResult = memberService.findOrRegister(appleUserInfo, container, marketingPid, pushToken)
|
||||||
|
val member = memberResolveResult.member
|
||||||
|
val principal = MemberAdapter(member)
|
||||||
|
val authToken = AppleAuthenticationToken(token, 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,
|
||||||
|
isNew = memberResolveResult.isNew
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.apple
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
|
||||||
|
class AppleAuthenticationToken(
|
||||||
|
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,93 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.apple
|
||||||
|
|
||||||
|
import com.nimbusds.jose.JWSAlgorithm
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSourceBuilder
|
||||||
|
import com.nimbusds.jose.proc.JWSVerificationKeySelector
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext
|
||||||
|
import com.nimbusds.jwt.JWTClaimsSet
|
||||||
|
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor
|
||||||
|
import com.nimbusds.jwt.proc.DefaultJWTProcessor
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.Base64
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AppleIdentityTokenVerifier(
|
||||||
|
@Value("\${apple.bundle-id}")
|
||||||
|
private val bundleId: String
|
||||||
|
) {
|
||||||
|
private val jwkUrl = URL("https://appleid.apple.com/auth/keys")
|
||||||
|
private val jwkSource: JWKSource<SecurityContext> = JWKSourceBuilder.create<SecurityContext>(jwkUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val jwtProcessor: ConfigurableJWTProcessor<SecurityContext> =
|
||||||
|
DefaultJWTProcessor<SecurityContext>().apply {
|
||||||
|
jwsKeySelector = JWSVerificationKeySelector(JWSAlgorithm.RS256, jwkSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verify(identityToken: String, rawNonce: String): AppleUserInfo {
|
||||||
|
if (bundleId.isBlank()) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawNonce.isBlank()) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val claims = try {
|
||||||
|
jwtProcessor.process(identityToken, null)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateClaims(claims, rawNonce)
|
||||||
|
|
||||||
|
return AppleUserInfo(
|
||||||
|
sub = claims.subject ?: throw SodaException(messageKey = "member.social.apple_login_failed"),
|
||||||
|
email = claims.getStringClaim("email")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateClaims(claims: JWTClaimsSet, rawNonce: String) {
|
||||||
|
if (claims.issuer != ISSUER) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claims.audience.contains(bundleId)) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Date()
|
||||||
|
val expirationTime = claims.expirationTime ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
if (expirationTime.before(now)) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val issuedAt = claims.issueTime ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
if (issuedAt.after(now)) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val nonce = claims.getStringClaim("nonce") ?: throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
val expectedNonce = hashNonce(rawNonce)
|
||||||
|
if (nonce != expectedNonce) {
|
||||||
|
throw SodaException(messageKey = "member.social.apple_login_failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hashNonce(rawNonce: String): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hashed = digest.digest(rawNonce.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(hashed)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ISSUER = "https://appleid.apple.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.apple
|
||||||
|
|
||||||
|
data class AppleUserInfo(
|
||||||
|
val sub: String,
|
||||||
|
val email: String?
|
||||||
|
)
|
||||||
@@ -3,8 +3,10 @@ package kr.co.vividnext.sodalive.member.social.google
|
|||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
import kr.co.vividnext.sodalive.member.MemberService
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||||
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
@@ -18,19 +20,22 @@ class GoogleAuthService(
|
|||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) : SocialAuthService {
|
||||||
fun authenticate(
|
override fun getProvider(): MemberProvider = MemberProvider.GOOGLE
|
||||||
idToken: String,
|
|
||||||
|
override fun authenticate(
|
||||||
|
token: String,
|
||||||
container: String,
|
container: String,
|
||||||
marketingPid: String?,
|
marketingPid: String?,
|
||||||
pushToken: String?
|
pushToken: String?,
|
||||||
|
nonce: String?
|
||||||
): SocialLoginResponse {
|
): SocialLoginResponse {
|
||||||
val googleUserInfo = googleService.getUserInfo(idToken)
|
val googleUserInfo = googleService.getUserInfo(token)
|
||||||
?: throw SodaException(messageKey = "member.social.google_login_failed")
|
?: throw SodaException(messageKey = "member.social.google_login_failed")
|
||||||
val memberResolveResult = memberService.findOrRegister(googleUserInfo, container, marketingPid, pushToken)
|
val memberResolveResult = memberService.findOrRegister(googleUserInfo, container, marketingPid, pushToken)
|
||||||
val member = memberResolveResult.member
|
val member = memberResolveResult.member
|
||||||
val principal = MemberAdapter(member)
|
val principal = MemberAdapter(member)
|
||||||
val authToken = GoogleAuthenticationToken(idToken, principal.authorities)
|
val authToken = GoogleAuthenticationToken(token, principal.authorities)
|
||||||
authToken.setPrincipal(principal)
|
authToken.setPrincipal(principal)
|
||||||
SecurityContextHolder.getContext().authentication = authToken
|
SecurityContextHolder.getContext().authentication = authToken
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ class GoogleAuthService(
|
|||||||
userId = member.id!!,
|
userId = member.id!!,
|
||||||
token = jwt,
|
token = jwt,
|
||||||
nickname = member.nickname,
|
nickname = member.nickname,
|
||||||
email = member.email,
|
email = member.email ?: "",
|
||||||
profileImage = if (member.profileImage != null) {
|
profileImage = if (member.profileImage != null) {
|
||||||
"$cloudFrontHost/${member.profileImage}"
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.member.social.google
|
|||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
|
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
|
||||||
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
||||||
import com.google.api.client.json.gson.GsonFactory
|
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.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ class GoogleService(
|
|||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
val payload = token.payload
|
val payload = token.payload
|
||||||
val email = payload.email ?: throw SodaException(messageKey = "member.social.email_consent_required")
|
val email = payload.email
|
||||||
|
|
||||||
GoogleUserInfo(
|
GoogleUserInfo(
|
||||||
sub = payload.subject,
|
sub = payload.subject,
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.member.social.google
|
|||||||
|
|
||||||
data class GoogleUserInfo(
|
data class GoogleUserInfo(
|
||||||
val sub: String,
|
val sub: String,
|
||||||
val email: String,
|
val email: String?,
|
||||||
val name: String?
|
val name: String?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package kr.co.vividnext.sodalive.member.social.kakao
|
|||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
import kr.co.vividnext.sodalive.member.MemberService
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||||
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
@@ -18,19 +20,22 @@ class KakaoAuthService(
|
|||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) : SocialAuthService {
|
||||||
fun authenticate(
|
override fun getProvider(): MemberProvider = MemberProvider.KAKAO
|
||||||
accessToken: String,
|
|
||||||
|
override fun authenticate(
|
||||||
|
token: String,
|
||||||
container: String,
|
container: String,
|
||||||
marketingPid: String?,
|
marketingPid: String?,
|
||||||
pushToken: String?
|
pushToken: String?,
|
||||||
|
nonce: String?
|
||||||
): SocialLoginResponse {
|
): SocialLoginResponse {
|
||||||
val kakaoUserInfo = kakaoService.getUserInfo(accessToken)
|
val kakaoUserInfo = kakaoService.getUserInfo(token)
|
||||||
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
|
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
|
||||||
val memberResolveResult = memberService.findOrRegister(kakaoUserInfo, container, marketingPid, pushToken)
|
val memberResolveResult = memberService.findOrRegister(kakaoUserInfo, container, marketingPid, pushToken)
|
||||||
val member = memberResolveResult.member
|
val member = memberResolveResult.member
|
||||||
val principal = MemberAdapter(member)
|
val principal = MemberAdapter(member)
|
||||||
val authToken = KakaoAuthenticationToken(accessToken, principal.authorities)
|
val authToken = KakaoAuthenticationToken(token, principal.authorities)
|
||||||
authToken.setPrincipal(principal)
|
authToken.setPrincipal(principal)
|
||||||
SecurityContextHolder.getContext().authentication = authToken
|
SecurityContextHolder.getContext().authentication = authToken
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ class KakaoAuthService(
|
|||||||
userId = member.id!!,
|
userId = member.id!!,
|
||||||
token = jwt,
|
token = jwt,
|
||||||
nickname = member.nickname,
|
nickname = member.nickname,
|
||||||
email = member.email,
|
email = member.email ?: "",
|
||||||
profileImage = if (member.profileImage != null) {
|
profileImage = if (member.profileImage != null) {
|
||||||
"$cloudFrontHost/${member.profileImage}"
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.member.social.kakao
|
package kr.co.vividnext.sodalive.member.social.kakao
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
@@ -37,7 +36,6 @@ class KakaoService(
|
|||||||
val id = jsonNode.get("id").asLong()
|
val id = jsonNode.get("id").asLong()
|
||||||
val kakaoAccount = jsonNode.get("kakao_account")
|
val kakaoAccount = jsonNode.get("kakao_account")
|
||||||
val email = kakaoAccount?.get("email")?.asText()
|
val email = kakaoAccount?.get("email")?.asText()
|
||||||
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
|
|
||||||
val properties = jsonNode.get("properties")
|
val properties = jsonNode.get("properties")
|
||||||
val nickname = properties?.get("nickname")?.asText()
|
val nickname = properties?.get("nickname")?.asText()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.member.social.kakao
|
|||||||
|
|
||||||
data class KakaoUserInfo(
|
data class KakaoUserInfo(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val email: String,
|
val email: String?,
|
||||||
val nickname: String?
|
val nickname: String?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.line
|
||||||
|
|
||||||
|
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.MemberProvider
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||||
|
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
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class LineAuthService(
|
||||||
|
private val lineService: LineService,
|
||||||
|
private val memberService: MemberService,
|
||||||
|
private val tokenProvider: TokenProvider,
|
||||||
|
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String,
|
||||||
|
|
||||||
|
@Value("\${line.channel-id}")
|
||||||
|
private val lineChannelId: String
|
||||||
|
) : SocialAuthService {
|
||||||
|
override fun getProvider(): MemberProvider = MemberProvider.LINE
|
||||||
|
|
||||||
|
override fun authenticate(
|
||||||
|
token: String,
|
||||||
|
container: String,
|
||||||
|
marketingPid: String?,
|
||||||
|
pushToken: String?,
|
||||||
|
nonce: String?
|
||||||
|
): SocialLoginResponse {
|
||||||
|
val rawNonce = nonce?.takeIf { it.isNotBlank() }
|
||||||
|
?: throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
|
||||||
|
if (lineChannelId.isBlank()) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val verifyResponse = lineService.verifyIdToken(token, lineChannelId, rawNonce)
|
||||||
|
?: throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
|
||||||
|
validateVerifyResponse(verifyResponse, lineChannelId, rawNonce)
|
||||||
|
|
||||||
|
val lineUserInfo = LineUserInfo(
|
||||||
|
sub = verifyResponse.sub,
|
||||||
|
email = verifyResponse.email
|
||||||
|
)
|
||||||
|
|
||||||
|
val memberResolveResult = memberService.findOrRegister(lineUserInfo, container, marketingPid, pushToken)
|
||||||
|
val member = memberResolveResult.member
|
||||||
|
val principal = MemberAdapter(member)
|
||||||
|
val authToken = LineAuthenticationToken(token, 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,
|
||||||
|
isNew = memberResolveResult.isNew
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateVerifyResponse(response: LineVerifyResponse, clientId: String, nonce: String) {
|
||||||
|
if (response.iss != ISSUER) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.aud != clientId) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Instant.now().epochSecond
|
||||||
|
if (response.exp <= now) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.iat > now) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.nonce != null && response.nonce != nonce) {
|
||||||
|
throw SodaException(messageKey = "member.social.line_login_failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ISSUER = "https://access.line.me"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.line
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
|
||||||
|
class LineAuthenticationToken(
|
||||||
|
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,48 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.line
|
||||||
|
|
||||||
|
import org.springframework.http.HttpEntity
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.util.LinkedMultiValueMap
|
||||||
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class LineService(
|
||||||
|
private val restTemplate: RestTemplate = RestTemplate()
|
||||||
|
) {
|
||||||
|
fun verifyIdToken(idToken: String, clientId: String, nonce: String?): LineVerifyResponse? {
|
||||||
|
val url = "https://api.line.me/oauth2/v2.1/verify"
|
||||||
|
val headers = HttpHeaders().apply {
|
||||||
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = LinkedMultiValueMap<String, String>().apply {
|
||||||
|
add("id_token", idToken)
|
||||||
|
add("client_id", clientId)
|
||||||
|
if (!nonce.isNullOrBlank()) {
|
||||||
|
add("nonce", nonce)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val entity = HttpEntity(body, headers)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val response: ResponseEntity<LineVerifyResponse> = restTemplate.postForEntity(
|
||||||
|
url,
|
||||||
|
entity,
|
||||||
|
LineVerifyResponse::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.statusCode.is2xxSuccessful) {
|
||||||
|
response.body
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
ex.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.line
|
||||||
|
|
||||||
|
data class LineUserInfo(
|
||||||
|
val sub: String,
|
||||||
|
val email: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package kr.co.vividnext.sodalive.member.social.line
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class LineVerifyResponse(
|
||||||
|
val iss: String,
|
||||||
|
val sub: String,
|
||||||
|
val aud: String,
|
||||||
|
val exp: Long,
|
||||||
|
val iat: Long,
|
||||||
|
val nonce: String? = null,
|
||||||
|
val name: String? = null,
|
||||||
|
val picture: String? = null,
|
||||||
|
val email: String? = null
|
||||||
|
)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.menu
|
package kr.co.vividnext.sodalive.menu
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.security.access.prepost.PreAuthorize
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.security.core.userdetails.User
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@@ -13,5 +14,13 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
class MenuController(private val service: MenuService) {
|
class MenuController(private val service: MenuService) {
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN', 'CREATOR')")
|
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN', 'CREATOR')")
|
||||||
fun getMenus(@AuthenticationPrincipal user: User) = ApiResponse.ok(service.getMenus(user))
|
fun getMenus(
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getMenus(member))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package kr.co.vividnext.sodalive.menu
|
package kr.co.vividnext.sodalive.menu
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
|
||||||
import org.springframework.security.core.userdetails.User
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class MenuService(
|
class MenuService(
|
||||||
private val repository: MenuRepository,
|
private val repository: MenuRepository
|
||||||
private val memberRepository: MemberRepository
|
|
||||||
) {
|
) {
|
||||||
fun getMenus(user: User): List<GetMenuResponse> {
|
fun getMenus(member: Member): List<GetMenuResponse> {
|
||||||
val member = memberRepository.findByEmail(user.username)
|
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
|
||||||
return repository.getMenu(member.role)
|
return repository.getMenu(member.role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ bootpay:
|
|||||||
apple:
|
apple:
|
||||||
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
||||||
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
||||||
|
bundleId: ${APPLE_BUNDLE_ID}
|
||||||
|
|
||||||
|
line:
|
||||||
|
channelId: ${LINE_CHANNEL_ID}
|
||||||
|
|
||||||
agora:
|
agora:
|
||||||
appId: ${AGORA_APP_ID}
|
appId: ${AGORA_APP_ID}
|
||||||
|
|||||||
Reference in New Issue
Block a user