test #383

Merged
klaus merged 7 commits from test into main 2026-01-28 15:40:26 +00:00
44 changed files with 779 additions and 113 deletions
+2
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")
@@ -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 }
@@ -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 },
@@ -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,
@@ -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
@@ -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])
}
}
@@ -106,7 +106,8 @@ class HomeService(
val latestContentThemeList = contentThemeService.getActiveThemeOfContent(
isAdult = isAdult,
contentType = contentType
contentType = contentType,
excludeThemes = listOf("다시듣기")
)
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.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))
}
}
@@ -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)
@@ -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
@@ -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,
@@ -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)
}
@@ -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()
@@ -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 호출 후 저장하고 반환
@@ -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 {
@@ -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"
)
)
@@ -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
}
@@ -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}"))
)
@@ -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"
}
}
}
@@ -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))
@@ -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) {
@@ -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) {
@@ -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,
@@ -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
)
@@ -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.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 {
@@ -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,
@@ -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?
)
@@ -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 {
@@ -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()
@@ -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?
)
@@ -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
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))
}
}
@@ -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)
}
}
+4
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}