Merge pull request 'test' (#383) from test into main

Reviewed-on: #383
This commit is contained in:
2026-01-28 15:40:25 +00:00
44 changed files with 779 additions and 113 deletions

View File

@@ -41,6 +41,8 @@ dependencies {
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("com.nimbusds:nimbus-jose-jwt:9.37.3")
// querydsl (추가 설정)
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")

View File

@@ -148,6 +148,7 @@ class AdminChatCharacterController(
runCatching { CharacterType.valueOf(it) }
.getOrDefault(CharacterType.Character)
} ?: CharacterType.Character,
region = request.region,
tags = request.tags,
values = request.values,
hobbies = request.hobbies,
@@ -203,6 +204,7 @@ class AdminChatCharacterController(
body["name"] = request.name
body["systemPrompt"] = request.systemPrompt
body["description"] = request.description
body["region"] = request.region
request.age?.let { body["age"] = it }
request.gender?.let { body["gender"] = it }
request.mbti?.let { body["mbti"] = it }

View File

@@ -20,6 +20,7 @@ data class ChatCharacterDetailResponse(
val speechPattern: String?,
val speechStyle: String?,
val appearance: String?,
val region: String,
val isActive: Boolean,
val tags: List<String>,
val hobbies: List<String>,
@@ -67,6 +68,7 @@ data class ChatCharacterDetailResponse(
speechPattern = chatCharacter.speechPattern,
speechStyle = chatCharacter.speechStyle,
appearance = chatCharacter.appearance,
region = chatCharacter.region,
isActive = chatCharacter.isActive,
tags = chatCharacter.tagMappings.map { it.tag.tag },
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },

View File

@@ -38,6 +38,7 @@ data class ChatCharacterRegisterRequest(
@JsonProperty("speechPattern") val speechPattern: String?,
@JsonProperty("speechStyle") val speechStyle: String?,
@JsonProperty("appearance") val appearance: String?,
@JsonProperty("region") val region: String = "KR",
@JsonProperty("originalTitle") val originalTitle: String? = null,
@JsonProperty("originalLink") val originalLink: String? = null,
@JsonProperty("originalWorkId") val originalWorkId: Long? = null,

View File

@@ -14,6 +14,7 @@ data class ChatCharacterListResponse(
val mbti: String?,
val speechStyle: String?,
val speechPattern: String?,
val region: String,
val tags: List<String>,
val createdAt: String?,
val updatedAt: String?
@@ -48,6 +49,7 @@ data class ChatCharacterListResponse(
mbti = chatCharacter.mbti,
speechStyle = chatCharacter.speechStyle,
speechPattern = chatCharacter.speechPattern,
region = chatCharacter.region,
tags = chatCharacter.tagMappings.map { it.tag.tag },
createdAt = createdAtStr,
updatedAt = updatedAtStr

View File

@@ -108,6 +108,7 @@ class AdminMemberService(
MemberProvider.KAKAO -> messageSource.getMessage("member.provider.kakao", langContext.lang).orEmpty()
MemberProvider.GOOGLE -> messageSource.getMessage("member.provider.google", 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!!
@@ -126,7 +127,7 @@ class AdminMemberService(
GetAdminMemberListResponseItem(
id = it.id!!,
email = it.email,
email = it.email ?: "",
nickname = it.nickname,
profileUrl = if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
@@ -160,6 +161,7 @@ class AdminMemberService(
val member = repository.findByIdAndActive(memberId = request.memberId)
?: 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])
}
}

View File

@@ -106,7 +106,8 @@ class HomeService(
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
isAdult = isAdult,
contentType = contentType
contentType = contentType,
excludeThemes = listOf("다시듣기")
)
val latestContentList = contentService.getLatestContentByTheme(

View File

@@ -5,7 +5,6 @@ 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.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
@@ -29,6 +28,12 @@ class ChargeTempController(private val service: ChargeTempService) {
@PostMapping("/verify")
fun verify(
@RequestBody request: VerifyRequest,
@AuthenticationPrincipal user: User
) = ApiResponse.ok(service.verify(user, request))
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.verify(member, request))
}
}

View File

@@ -15,10 +15,8 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.userdetails.User
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -26,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional
@Transactional(readOnly = true)
class ChargeTempService(
private val chargeRepository: ChargeRepository,
private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper,
private val messageSource: SodaMessageSource,
@@ -54,11 +51,9 @@ class ChargeTempService(
}
@Transactional
fun verify(user: User, verifyRequest: VerifyRequest) {
fun verify(member: Member, verifyRequest: VerifyRequest) {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: 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) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)

View File

@@ -67,6 +67,10 @@ class ChatCharacter(
@Column(nullable = false)
var characterType: CharacterType = CharacterType.Character,
// 리전 (기본값 KR, 수정 불가)
@Column(nullable = false)
val region: String = "KR",
var isActive: Boolean = true
) : BaseEntity() {
var imagePath: String? = null

View File

@@ -582,6 +582,7 @@ class ChatCharacterService(
originalTitle: String? = null,
originalLink: String? = null,
characterType: CharacterType = CharacterType.Character,
region: String = "KR",
tags: List<String> = emptyList(),
values: List<String> = emptyList(),
hobbies: List<String> = emptyList(),
@@ -600,7 +601,8 @@ class ChatCharacterService(
appearance = appearance,
originalTitle = originalTitle,
originalLink = originalLink,
characterType = characterType
characterType = characterType,
region = region
)
// 관련 엔티티 연결
@@ -630,6 +632,7 @@ class ChatCharacterService(
originalTitle: String? = null,
originalLink: String? = null,
characterType: CharacterType = CharacterType.Character,
region: String = "KR",
tags: List<String> = emptyList(),
values: List<String> = emptyList(),
hobbies: List<String> = emptyList(),
@@ -653,6 +656,7 @@ class ChatCharacterService(
originalTitle = originalTitle,
originalLink = originalLink,
characterType = characterType,
region = region,
tags = tags,
values = values,
hobbies = hobbies,

View File

@@ -1,5 +1,6 @@
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.SodaMessageSource
import org.slf4j.LoggerFactory
@@ -20,10 +21,14 @@ class SodaExceptionHandler(
private val messageSource: SodaMessageSource
) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val logLang = Lang.KO
@ExceptionHandler(SodaException::class)
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) }
?: e.message?.takeIf { it.isNotBlank() }
?: messageSource.getMessage("common.error.unknown", langContext.lang)
@@ -35,35 +40,40 @@ class SodaExceptionHandler(
@ExceptionHandler(MaxUploadSizeExceededException::class)
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)
ApiResponse.error(message = message)
}
@ExceptionHandler(AccessDeniedException::class)
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)
ApiResponse.error(message = message)
}
@ExceptionHandler(InternalAuthenticationServiceException::class)
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)
ApiResponse.error(message)
}
@ExceptionHandler(BadCredentialsException::class)
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)
ApiResponse.error(message)
}
@ExceptionHandler(DataIntegrityViolationException::class)
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)
ApiResponse.error(message)
}
@@ -71,7 +81,10 @@ class SodaExceptionHandler(
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler(AdsChargeException::class)
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) }
?: e.message?.takeIf { it.isNotBlank() }
?: messageSource.getMessage("common.error.invalid_request", langContext.lang)
@@ -81,7 +94,8 @@ class SodaExceptionHandler(
@ExceptionHandler(Exception::class)
fun handleException(e: Exception) = run {
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)
ApiResponse.error(message)
}

View File

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

View File

@@ -34,15 +34,20 @@ class AudioContentThemeService(
isAdult: Boolean = false,
isFree: Boolean = false,
isPointAvailableOnly: Boolean = false,
contentType: ContentType
contentType: ContentType,
excludeThemes: List<String> = emptyList()
): List<String> {
val themesWithIds = queryRepository.getActiveThemeWithIdsOfContent(
var themesWithIds = queryRepository.getActiveThemeWithIdsOfContent(
isAdult = isAdult,
isFree = isFree,
isPointAvailableOnly = isPointAvailableOnly,
contentType = contentType
)
if (excludeThemes.isNotEmpty()) {
themesWithIds = themesWithIds.filter { it.theme !in excludeThemes }
}
/**
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
* 번역이 없으면 번역 API 호출 후 저장하고 반환

View File

@@ -87,7 +87,7 @@ class CreatorAdminMemberService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -1280,6 +1280,16 @@ class SodaMessageSource {
Lang.EN to "Kakao login failed. Please try again.",
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(
Lang.KO to "이메일 제공에 동의하셔야 서비스 이용이 가능합니다.",
Lang.EN to "You must agree to provide your email to use the service.",
@@ -1628,6 +1638,11 @@ class SodaMessageSource {
Lang.KO to "애플",
Lang.EN to "Apple",
Lang.JA to "Apple"
),
"member.provider.line" to mapOf(
Lang.KO to "라인",
Lang.EN to "LINE",
Lang.JA to "LINE"
)
)

View File

@@ -19,7 +19,7 @@ import javax.persistence.OneToOne
@Entity
data class Member(
val email: String,
var email: String? = null,
var password: String,
var nickname: String,
var profileImage: String? = null,
@@ -27,6 +27,7 @@ data class Member(
val kakaoId: Long? = null,
val googleId: String? = null,
val appleId: String? = null,
val lineId: String? = null,
@Enumerated(EnumType.STRING)
val provider: MemberProvider = MemberProvider.EMAIL,
@@ -158,5 +159,5 @@ enum class MemberRole {
}
enum class MemberProvider {
EMAIL, KAKAO, GOOGLE, APPLE
EMAIL, KAKAO, GOOGLE, APPLE, LINE
}

View File

@@ -4,7 +4,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
class MemberAdapter(val member: Member) : User(
member.email,
member.email ?: "member:${member.id}",
member.password,
listOf(SimpleGrantedAuthority("ROLE_${member.role.name}"))
)

View File

@@ -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.notification.UpdateNotificationSettingRequest
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 kr.co.vividnext.sodalive.member.social.SocialAuthServiceResolver
import kr.co.vividnext.sodalive.useraction.ActionType
import kr.co.vividnext.sodalive.useraction.UserActionService
import org.springframework.data.domain.Pageable
@@ -36,8 +35,7 @@ import org.springframework.web.multipart.MultipartFile
@RequestMapping("/member")
class MemberController(
private val service: MemberService,
private val kakaoAuthService: KakaoAuthService,
private val googleAuthService: GoogleAuthService,
private val socialAuthServiceResolver: SocialAuthServiceResolver,
private val trackingService: AdTrackingService,
private val userActionService: UserActionService,
private val messageSource: SodaMessageSource,
@@ -345,31 +343,8 @@ class MemberController(
@RequestHeader("Authorization") authHeader: String,
@RequestBody request: SocialLoginRequest
): ApiResponse<LoginResponse> {
if (!authHeader.startsWith("Bearer ")) {
throw SodaException(messageKey = "member.social.google_login_failed")
}
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)
val token = extractBearerToken(authHeader, MemberProvider.GOOGLE)
return processSocialLogin(MemberProvider.GOOGLE, token, request, null)
}
@PostMapping("/login/kakao")
@@ -377,12 +352,50 @@ class MemberController(
@RequestHeader("Authorization") authHeader: String,
@RequestBody request: SocialLoginRequest
): ApiResponse<LoginResponse> {
if (!authHeader.startsWith("Bearer ")) {
throw SodaException(messageKey = "member.social.kakao_login_failed")
val token = extractBearerToken(authHeader, MemberProvider.KAKAO)
return processSocialLogin(MemberProvider.KAKAO, token, request, null)
}
val token = authHeader.substring(7)
val response = kakaoAuthService.authenticate(token, request.container, request.marketingPid, request.pushToken)
@PostMapping("/login/apple")
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()) {
trackingService.saveTrackingHistory(
@@ -403,4 +416,22 @@ class MemberController(
val message = messageSource.getMessage("member.signup.success", langContext.lang)
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"
}
}
}

View File

@@ -20,10 +20,12 @@ import org.springframework.stereotype.Repository
@Repository
interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
fun findByEmail(email: String): Member?
fun findByEmail(email: String?): Member?
fun findByNickname(nickname: String): Member?
fun findByGoogleId(googleId: String): Member?
fun findByKakaoId(kakaoId: Long): Member?
fun findByAppleId(appleId: String): Member?
fun findByLineId(lineId: String): Member?
}
interface MemberQueryRepository {
@@ -51,7 +53,7 @@ interface MemberQueryRepository {
fun getMessageRecipientPushToken(messageId: Long): PushTokenInfo?
fun getIndividualRecipientPushTokens(recipients: List<Long>, isAuth: Boolean?): List<PushTokenInfo>
fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse
fun getMemberByEmail(email: String): Member?
fun getMemberByEmail(email: String?): Member?
fun getChangeNoticeRecipientPushTokens(creatorId: 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
.selectFrom(member)
.where(member.email.eq(email))

View File

@@ -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.SignUpValidator
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.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.StipulationAgree
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
@@ -346,7 +348,7 @@ class MemberService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
@@ -454,8 +456,16 @@ class MemberService(
}
override fun loadUserByUsername(username: String): UserDetails {
val member = repository.findByEmail(email = username)
?: throw UsernameNotFoundException(username)
val member = if (username.startsWith("member:")) {
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)
}
@@ -592,7 +602,7 @@ class MemberService(
@Transactional
fun signOut(signOutRequest: SignOutRequest, user: User) {
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (
member.provider == MemberProvider.EMAIL &&
@@ -620,11 +630,7 @@ class MemberService(
@Transactional
fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) {
if (profileUpdateRequest.email != user.username) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.nickname != null) {
@@ -652,11 +658,7 @@ class MemberService(
@Transactional
fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse {
if (profileUpdateRequest.email != user.username) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val member = repository.findByEmail(user.username)
val member = findMemberByUsername(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (profileUpdateRequest.modifyPassword != null) {
@@ -729,7 +731,7 @@ class MemberService(
@Transactional
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")
val metadata = ObjectMetadata()
@@ -791,6 +793,7 @@ class MemberService(
MemberProvider.KAKAO -> "member.provider.kakao"
MemberProvider.GOOGLE -> "member.provider.google"
MemberProvider.APPLE -> "member.provider.apple"
MemberProvider.LINE -> "member.provider.line"
}
return messageSource.getMessage(key, langContext.lang) ?: provider.name
}
@@ -932,7 +935,138 @@ class MemberService(
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)
if (member != null) {

View File

@@ -17,7 +17,7 @@ data class ProfileResponse(
) {
constructor(member: Member, cloudFrontHost: String, container: String) : this(
userId = member.id!!,
email = member.email,
email = member.email ?: "",
nickname = member.nickname,
gender = member.gender,
profileUrl = if (member.profileImage != null) {

View File

@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.member
data class ProfileUpdateRequest(
val email: String,
val email: String? = null,
val password: String? = null,
val modifyPassword: String? = null,
val nickname: String? = null,

View File

@@ -10,5 +10,7 @@ data class LoginRequest(
data class SocialLoginRequest(
val container: String,
val pushToken: String? = null,
val marketingPid: String? = null
val marketingPid: String? = null,
val identityToken: String? = null,
val nonce: String? = null
)

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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
)
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.member.social.apple
data class AppleUserInfo(
val sub: String,
val email: String?
)

View File

@@ -3,8 +3,10 @@ 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.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
@@ -18,19 +20,22 @@ class GoogleAuthService(
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun authenticate(
idToken: String,
) : SocialAuthService {
override fun getProvider(): MemberProvider = MemberProvider.GOOGLE
override fun authenticate(
token: String,
container: String,
marketingPid: String?,
pushToken: String?
pushToken: String?,
nonce: String?
): SocialLoginResponse {
val googleUserInfo = googleService.getUserInfo(idToken)
val googleUserInfo = googleService.getUserInfo(token)
?: throw SodaException(messageKey = "member.social.google_login_failed")
val memberResolveResult = memberService.findOrRegister(googleUserInfo, container, marketingPid, pushToken)
val member = memberResolveResult.member
val principal = MemberAdapter(member)
val authToken = GoogleAuthenticationToken(idToken, principal.authorities)
val authToken = GoogleAuthenticationToken(token, principal.authorities)
authToken.setPrincipal(principal)
SecurityContextHolder.getContext().authentication = authToken
@@ -43,7 +48,7 @@ class GoogleAuthService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -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.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
@@ -27,7 +26,7 @@ class GoogleService(
if (token != null) {
val payload = token.payload
val email = payload.email ?: throw SodaException(messageKey = "member.social.email_consent_required")
val email = payload.email
GoogleUserInfo(
sub = payload.subject,

View File

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

View File

@@ -3,8 +3,10 @@ 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.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
@@ -18,19 +20,22 @@ class KakaoAuthService(
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun authenticate(
accessToken: String,
) : SocialAuthService {
override fun getProvider(): MemberProvider = MemberProvider.KAKAO
override fun authenticate(
token: String,
container: String,
marketingPid: String?,
pushToken: String?
pushToken: String?,
nonce: String?
): SocialLoginResponse {
val kakaoUserInfo = kakaoService.getUserInfo(accessToken)
val kakaoUserInfo = kakaoService.getUserInfo(token)
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
val memberResolveResult = memberService.findOrRegister(kakaoUserInfo, container, marketingPid, pushToken)
val member = memberResolveResult.member
val principal = MemberAdapter(member)
val authToken = KakaoAuthenticationToken(accessToken, principal.authorities)
val authToken = KakaoAuthenticationToken(token, principal.authorities)
authToken.setPrincipal(principal)
SecurityContextHolder.getContext().authentication = authToken
@@ -43,7 +48,7 @@ class KakaoAuthService(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
email = member.email ?: "",
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {

View File

@@ -1,7 +1,6 @@
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
@@ -37,7 +36,6 @@ class KakaoService(
val id = jsonNode.get("id").asLong()
val kakaoAccount = jsonNode.get("kakao_account")
val email = kakaoAccount?.get("email")?.asText()
?: throw SodaException(messageKey = "member.social.kakao_login_failed")
val properties = jsonNode.get("properties")
val nickname = properties?.get("nickname")?.asText()

View File

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

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.member.social.line
data class LineUserInfo(
val sub: String,
val email: String? = null
)

View File

@@ -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
)

View File

@@ -1,9 +1,10 @@
package kr.co.vividnext.sodalive.menu
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.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@@ -13,5 +14,13 @@ import org.springframework.web.bind.annotation.RestController
class MenuController(private val service: MenuService) {
@GetMapping
@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))
}
}

View File

@@ -1,18 +1,13 @@
package kr.co.vividnext.sodalive.menu
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.security.core.userdetails.User
import kr.co.vividnext.sodalive.member.Member
import org.springframework.stereotype.Service
@Service
class MenuService(
private val repository: MenuRepository,
private val memberRepository: MemberRepository
private val repository: MenuRepository
) {
fun getMenus(user: User): List<GetMenuResponse> {
val member = memberRepository.findByEmail(user.username)
?: throw SodaException(messageKey = "common.error.bad_credentials")
fun getMenus(member: Member): List<GetMenuResponse> {
return repository.getMenu(member.role)
}
}

View File

@@ -33,6 +33,10 @@ bootpay:
apple:
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
bundleId: ${APPLE_BUNDLE_ID}
line:
channelId: ${LINE_CHANNEL_ID}
agora:
appId: ${AGORA_APP_ID}