Skip to content

Commit

Permalink
Merge pull request #166 from SW13-Monstera/dev
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
kshired authored Dec 17, 2023
2 parents 60e3e6f + e1ac17e commit 2679ec4
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 125 deletions.
2 changes: 1 addition & 1 deletion backend-config
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.csbroker.apiserver.common.config

import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.ses.SesClient
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class AwsClientConfig(
@Value("\${aws.mail-url}")
private val url: String,
@Value("\${aws.access-key}")
private val accessKey: String,
@Value("\${aws.secret-key}")
private val secretKey: String,
) {
@Bean
fun sesClient(): SesClient {
return SesClient {
region = url
credentialsProvider = StaticCredentialsProvider {
accessKeyId = accessKey
secretAccessKey = secretKey
}
}
}

@Bean
fun s3Client(): S3Client {
return S3Client {
region = url
credentialsProvider = StaticCredentialsProvider {
accessKeyId = accessKey
secretAccessKey = secretKey
}
}
}
}
19 changes: 14 additions & 5 deletions src/main/kotlin/io/csbroker/apiserver/common/config/RedisConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
class RedisConfig(
@Value("\${spring.redis.host}")
private val host: String,
Expand All @@ -23,11 +27,16 @@ class RedisConfig(

@Bean
fun stringRedisTemplate(): StringRedisTemplate {
val redisTemplate = StringRedisTemplate()
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = StringRedisSerializer()
redisTemplate.setConnectionFactory(redisConnectionFactory())
return StringRedisTemplate().also {
it.keySerializer = StringRedisSerializer()
it.valueSerializer = StringRedisSerializer()
it.connectionFactory = redisConnectionFactory()
it.setEnableTransactionSupport(true)
}
}

return redisTemplate
@Bean
fun transactionManager(): PlatformTransactionManager {
return JpaTransactionManager()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ class AdminController(
),
)
}

@PostMapping("/rank/refresh")
fun refreshRank(): ApiResponse<Unit> {
userService.calculateRank()
return ApiResponse.success()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ data class SubmitLongProblemResponseDto(
val title: String,
val tags: List<String>,
val description: String,
val totalSubmissionCount: Int,
val userSubmissionCount: Int,
val totalSubmission: Int,
val userSubmission: Int,
val userAnswer: String,
val standardAnswer: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.csbroker.apiserver.repository.common
import io.csbroker.apiserver.common.config.properties.AppProperties
import io.csbroker.apiserver.dto.common.RankListDto
import io.csbroker.apiserver.dto.user.RankResultDto
import org.springframework.data.redis.core.RedisOperations
import org.springframework.data.redis.core.SessionCallback
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.core.ZSetOperations.TypedTuple
import org.springframework.stereotype.Repository
Expand All @@ -22,8 +24,7 @@ class RedisRepository(
}

fun setRefreshTokenByEmail(email: String, refreshToken: String) {
redisTemplate.opsForValue()
.set(email, refreshToken, appProperties.auth.refreshTokenExpiry, TimeUnit.MILLISECONDS)
redisTemplate.opsForValue().set(email, refreshToken, appProperties.auth.refreshTokenExpiry, TimeUnit.MILLISECONDS)
}

fun setPasswordVerification(code: String, email: String) {
Expand All @@ -38,64 +39,44 @@ class RedisRepository(
redisTemplate.delete(code)
}

@Suppress("UNCHECKED_CAST")
fun setRank(scoreMap: Map<String, Double>) {
redisTemplate.opsForZSet().add(
RANKING,
scoreMap.map { TypedTuple.of(it.key, it.value) }.toSet(),
redisTemplate.execute(
object : SessionCallback<Unit> {
override fun <K : Any?, V : Any?> execute(operations: RedisOperations<K, V>) {
val stringOperations = operations as RedisOperations<String, String>
stringOperations.multi()
stringOperations.delete(RANKING)
stringOperations.opsForZSet().add(
RANKING,
scoreMap.map { TypedTuple.of(it.key, it.value) }.toSet(),
)
stringOperations.exec()
}
},
)
}

fun getRank(key: String): RankResultDto {
var rank: Long? = null

val score = redisTemplate.opsForZSet().score(RANKING, key) ?: 0.0
val rankKey = redisTemplate.opsForZSet().reverseRangeByScore(RANKING, score, score, 0, 1)?.first()
val score = redisTemplate.opsForZSet().score(RANKING, key)
?: return RankResultDto(null, 0.0)

if (rankKey != null) {
rank = redisTemplate.opsForZSet().reverseRank(RANKING, rankKey)?.plus(1)
val rankKey = redisTemplate.opsForZSet().reverseRangeByScore(RANKING, score, score, 0, 1)?.firstOrNull()
val rank = rankKey?.let {
redisTemplate.opsForZSet().reverseRank(RANKING, it)?.plus(1)
}

return RankResultDto(rank, score)
}

fun getRanks(size: Long, page: Long): RankListDto {
val start = size * page
val end = size * (page + 1) - 1
val end = start + size - 1

val keyWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(RANKING, start, end)
val keyWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(RANKING, start, end) ?: emptySet()
val totalElements = redisTemplate.opsForZSet().size(RANKING) ?: 0
val totalPage = if (totalElements % size > 0) totalElements / size + 1 else totalElements / size
val result = mutableListOf<RankListDto.RankDetail>()
var rank = 1L
var isFirst = true

keyWithScores?.let {
it.forEach { keyWithScore ->
if (!isFirst && result.last().score != keyWithScore.score) {
isFirst = true
}

if (isFirst) {
val score = keyWithScore.score!!
val key = redisTemplate.opsForZSet().reverseRangeByScore(RANKING, score, score, 0, 1)!!.first()
rank = redisTemplate.opsForZSet().reverseRank(RANKING, key)!!.plus(1)
isFirst = false
}

val keys = keyWithScore.value!!.split('@')
val id = UUID.fromString(keys[0])
val username = keys[1]

result.add(
RankListDto.RankDetail(
id,
username,
rank,
keyWithScore.score!!,
),
)
}
}
val result = getRankDetails(keyWithScores)

return RankListDto(
size = size,
Expand All @@ -105,4 +86,35 @@ class RedisRepository(
contents = result,
)
}

private fun getRankDetails(keyWithScores: Set<TypedTuple<String>>): List<RankListDto.RankDetail> {
return keyWithScores.fold(emptyList()) { acc, value ->
val (score, id, username) = value.getRankInfo()

acc + if (acc.isEmpty() || acc.last().score != score) {
val key = redisTemplate.opsForZSet().reverseRangeByScore(RANKING, score, score, 0, 1)!!.first()
RankListDto.RankDetail(
id,
username,
redisTemplate.opsForZSet().reverseRank(RANKING, key)!!.plus(1),
score,
)
} else {
RankListDto.RankDetail(
id,
username,
acc.last().rank,
score,
)
}
}
}

private fun TypedTuple<String>.getRankInfo(): Triple<Double, UUID, String> {
val keys = value!!.split('@')
val id = UUID.fromString(keys[0])
val username = keys[1]

return Triple(this.score!!, id, username)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ interface UserRepository : JpaRepository<User, UUID> {

fun findUserByProviderId(providerId: String): User?

@Query("select count(u.id) > 0 from User u where u.email = :email")
fun existsUserByEmail(@Param("email") email: String): Boolean

@Query("select count(u) from User u where u.isDeleted = FALSE")
fun countUser(): Long

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.csbroker.apiserver.service.common

interface MailService {
suspend fun sendPasswordChangeMail(to: String)
suspend fun sendPasswordChangeMail(email: String)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.csbroker.apiserver.service.common

import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.ObjectCannedAcl
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
Expand All @@ -12,35 +11,20 @@ import java.util.UUID

@Service
class S3ServiceImpl(
@Value("\${aws.access-key}")
private val accessKey: String,

@Value("\${aws.secret-key}")
private val secretKey: String,

private val s3Client: S3Client,
@Value("\${aws.s3-bucket}")
private val bucketName: String,
) : S3Service {
override suspend fun uploadProfileImg(multipartFile: MultipartFile): String {
val s3FileName = createS3FileName(multipartFile)

S3Client {
region = "ap-northeast-2"
credentialsProvider = StaticCredentialsProvider {
accessKeyId = accessKey
secretAccessKey = secretKey
}
}.use {
it.putObject(
PutObjectRequest.invoke {
bucket = bucketName
key = s3FileName
body = ByteStream.fromBytes(multipartFile.bytes)
acl = ObjectCannedAcl.PublicRead
},
)
}

s3Client.putObject(
PutObjectRequest {
bucket = bucketName
key = s3FileName
body = ByteStream.fromBytes(multipartFile.bytes)
acl = ObjectCannedAcl.PublicRead
},
)
return getFullPath(s3FileName)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package io.csbroker.apiserver.service.common

import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.ses.SesClient
import aws.sdk.kotlin.services.ses.model.Body
import aws.sdk.kotlin.services.ses.model.Content
import aws.sdk.kotlin.services.ses.model.Destination
import aws.sdk.kotlin.services.ses.model.Message
import aws.sdk.kotlin.services.ses.model.SendEmailRequest
import io.csbroker.apiserver.auth.ProviderType
import io.csbroker.apiserver.common.exception.EntityNotFoundException
import io.csbroker.apiserver.repository.common.RedisRepository
import io.csbroker.apiserver.repository.user.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.thymeleaf.context.Context
Expand All @@ -23,51 +21,32 @@ class SesMailServiceImpl(
private val templateEngine: SpringTemplateEngine,
private val userRepository: UserRepository,
private val redisRepository: RedisRepository,

@Value("\${aws.mail-url}")
private val url: String,

@Value("\${aws.access-key}")
private val accessKey: String,

@Value("\${aws.secret-key}")
private val secretKey: String,
private val sesClient: SesClient,
) : MailService {

override suspend fun sendPasswordChangeMail(to: String) {
checkUserExist(to)
override suspend fun sendPasswordChangeMail(email: String) {
checkUserExist(email)
val code = UUID.randomUUID().toString()
val emailRequest = createEmailRequest(code, to)
send(emailRequest)
redisRepository.setPasswordVerification(code, to)
val emailRequest = createEmailRequest(code, email)
sesClient.sendEmail(emailRequest)
redisRepository.setPasswordVerification(code, email)
}

private suspend fun checkUserExist(to: String) {
val isExist = withContext(Dispatchers.IO) {
userRepository.existsUserByEmail(to)
}

if (!isExist) {
throw EntityNotFoundException("$to 메일로 가입한 유저를 찾을 수 없습니다.")
}
}
private fun checkUserExist(email: String) {
val user = userRepository.findByEmail(email)
?: throw EntityNotFoundException("$email 메일로 가입한 유저를 찾을 수 없습니다.")

private suspend fun send(emailRequest: SendEmailRequest) {
SesClient {
region = "ap-northeast-2"
credentialsProvider = StaticCredentialsProvider {
accessKeyId = accessKey
secretAccessKey = secretKey
}
}.use {
it.sendEmail(emailRequest)
if (user.providerType != ProviderType.LOCAL) {
throw EntityNotFoundException("소셜 로그인 유저는 비밀번호를 변경할 수 없습니다.")
}
}

private fun createEmailRequest(code: String, to: String): SendEmailRequest {
private fun createEmailRequest(code: String, email: String): SendEmailRequest {
return SendEmailRequest {
destination = Destination {
toAddresses = listOf(to)
toAddresses = listOf(email)
}
message = Message {
subject = Content {
Expand Down
Loading

0 comments on commit 2679ec4

Please sign in to comment.