Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 주요 API 로깅 구현 #239

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
Expand All @@ -17,6 +18,7 @@ import uoslife.servermeeting.admin.dto.request.ResetEmailRequest
import uoslife.servermeeting.admin.service.AdminService
import uoslife.servermeeting.global.config.SwaggerConfig
import uoslife.servermeeting.global.error.ErrorResponse
import uoslife.servermeeting.global.util.RequestUtils
import uoslife.servermeeting.payment.dto.response.PaymentResponseDto

@RestController
Expand Down Expand Up @@ -50,7 +52,10 @@ import uoslife.servermeeting.payment.dto.response.PaymentResponseDto
)]
)]
)
class AdminApi(private val adminService: AdminService) {
class AdminApi(
private val adminService: AdminService,
private val requestUtils: RequestUtils,
) {
@Operation(summary = "이메일 발송 횟수 초기화", description = "특정 이메일의 일일 발송 횟수를 초기화합니다.")
@ApiResponses(
value =
Expand All @@ -63,8 +68,12 @@ class AdminApi(private val adminService: AdminService) {
]
)
@PostMapping("/verification/send-email/reset")
fun resetEmailSendCount(@RequestBody request: ResetEmailRequest): ResponseEntity<Unit> {
adminService.resetEmailSendCount(request.email)
fun resetEmailSendCount(
request: HttpServletRequest,
@RequestBody body: ResetEmailRequest
): ResponseEntity<Unit> {
val requestInfo = requestUtils.toRequestInfoDto(request)
adminService.resetEmailSendCount(body.email, requestInfo)
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

Expand Down Expand Up @@ -95,10 +104,12 @@ class AdminApi(private val adminService: AdminService) {
)
@DeleteMapping("/user")
fun deleteUser(
@RequestBody request: DeleteUserRequest,
@RequestBody body: DeleteUserRequest,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<Unit> {
adminService.deleteUserById(request.userId, response)
val requestInfo = requestUtils.toRequestInfoDto(request)
adminService.deleteUserById(body.userId, response, requestInfo)
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

Expand Down Expand Up @@ -144,7 +155,10 @@ class AdminApi(private val adminService: AdminService) {
]
)
@PostMapping("/refund/match")
fun refundPayment(): ResponseEntity<PaymentResponseDto.NotMatchedPaymentRefundResponse> {
return ResponseEntity.status(HttpStatus.OK).body(adminService.refundPayment())
fun refundPayment(
request: HttpServletRequest
): ResponseEntity<PaymentResponseDto.NotMatchedPaymentRefundResponse> {
val requestInfo = requestUtils.toRequestInfoDto(request)
return ResponseEntity.status(HttpStatus.OK).body(adminService.refundPayment(requestInfo))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import uoslife.servermeeting.global.common.dto.RequestInfoDto
import uoslife.servermeeting.payment.dto.response.PaymentResponseDto
import uoslife.servermeeting.payment.service.PaymentService
import uoslife.servermeeting.user.service.UserService
Expand All @@ -22,21 +23,26 @@ class AdminService(
private val logger = LoggerFactory.getLogger(AdminService::class.java)
}

fun resetEmailSendCount(email: String) {
fun resetEmailSendCount(email: String, requestInfo: RequestInfoDto) {
val sendCountKey =
VerificationUtils.generateRedisKey(VerificationConstants.SEND_COUNT_PREFIX, email, true)

redisTemplate.opsForValue().set(sendCountKey, "0")
redisTemplate.expire(sendCountKey, Duration.ofDays(1))

logger.info("[이메일 발송 횟수 초기화] email: $email")
logger.info("[ADMIN-이메일 발송 횟수 초기화] targetEmail: $email, $requestInfo")
}

fun deleteUserById(userId: Long, response: HttpServletResponse) {
fun deleteUserById(userId: Long, response: HttpServletResponse, requestInfo: RequestInfoDto) {
userService.deleteUserById(userId, response)
logger.info("[ADMIN-유저 삭제] targetUserId: $userId, $requestInfo")
}

fun refundPayment(): PaymentResponseDto.NotMatchedPaymentRefundResponse {
return paymentService.refundPayment()
fun refundPayment(
requestInfo: RequestInfoDto
): PaymentResponseDto.NotMatchedPaymentRefundResponse {
val result = paymentService.refundPayment()
logger.info("[ADMIN-매칭 실패 유저 환불] $requestInfo")
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import uoslife.servermeeting.global.auth.exception.*
import uoslife.servermeeting.global.auth.security.JwtTokenProvider
import uoslife.servermeeting.global.auth.security.SecurityConstants
import uoslife.servermeeting.global.auth.util.CookieUtils
import uoslife.servermeeting.global.util.RequestUtils

@Service
class AuthService(
private val jwtTokenProvider: JwtTokenProvider,
private val cookieUtils: CookieUtils,
private val requestUtils: RequestUtils,
@Value("\${jwt.refresh.expiration}") private val refreshTokenExpiration: Long,
) {
companion object {
Expand All @@ -43,15 +45,19 @@ class AuthService(
}

fun reissueTokens(request: HttpServletRequest, response: HttpServletResponse): JwtResponse {
val requestInfo = requestUtils.toRequestInfoDto(request)
val refreshToken =
cookieUtils.getRefreshTokenFromCookie(request)
?: throw JwtRefreshTokenNotFoundException()

?: run {
logger.warn("[재발급 실패(토큰없음)] $requestInfo")
throw JwtRefreshTokenNotFoundException()
}
try {
val userId = jwtTokenProvider.getUserIdFromRefreshToken(refreshToken)

val storedToken = jwtTokenProvider.getStoredRefreshToken(userId)
if (storedToken != refreshToken) {
logger.warn("[재발급 실패(재사용)] $requestInfo")
throw JwtRefreshTokenReusedException()
}

Expand All @@ -61,26 +67,30 @@ class AuthService(
jwtTokenProvider.saveRefreshToken(userId, newRefreshToken)

cookieUtils.addRefreshTokenCookie(response, newRefreshToken, refreshTokenExpiration)
logger.info("[토큰 재발급 성공] USER ID: $userId")
logger.info("[재발급 성공] userId: $userId")
return JwtResponse(newAccessToken)
} catch (e: ExpiredJwtException) {
logger.warn("[재발급 실패(만료)] $requestInfo")
throw JwtRefreshTokenExpiredException()
} catch (e: JwtException) {
logger.warn("[재발급 실패(서명)] $requestInfo")
throw JwtTokenInvalidSignatureException()
}
}

fun logout(request: HttpServletRequest, response: HttpServletResponse) {
val requestInfo = requestUtils.toRequestInfoDto(request)

val refreshToken = cookieUtils.getRefreshTokenFromCookie(request)
if (refreshToken != null) {
try {
val userId = jwtTokenProvider.getUserIdFromRefreshToken(refreshToken)
jwtTokenProvider.deleteRefreshToken(userId)
} catch (e: JwtException) {
logger.warn("[로그아웃 요청] 유효하지 않은 리프레시 토큰으로 로그아웃 시도")
logger.warn("[로그아웃 요청] 유효하지 않은 리프레시 토큰 사용 $requestInfo")
}
} else {
logger.warn("[로그아웃 요청] 리프레시 토큰 없이 로그아웃 시도")
logger.warn("[로그아웃 요청] 리프레시 토큰 없음 $requestInfo")
}
cookieUtils.deleteRefreshTokenCookie(response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uoslife.servermeeting.global.common.dto

data class RequestInfoDto(val ip: String, val userAgent: String) {
override fun toString(): String {
return "ip: $ip, userAgent: $userAgent"
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/uoslife/servermeeting/global/util/RequestUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package uoslife.servermeeting.global.util

import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Component
import uoslife.servermeeting.global.common.dto.RequestInfoDto

@Component
class RequestUtils {
companion object {
private const val X_FORWARDED_FOR = "X-Forwarded-For"
private const val PROXY_CLIENT_IP = "Proxy-Client-IP"
private const val WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP"
}

fun toRequestInfoDto(request: HttpServletRequest) =
RequestInfoDto(ip = getClientIp(request), userAgent = getUserAgent(request))

fun getClientIp(request: HttpServletRequest): String =
when {
request.getHeader(X_FORWARDED_FOR) != null ->
request.getHeader(X_FORWARDED_FOR).split(",")[0]
request.getHeader(PROXY_CLIENT_IP) != null -> request.getHeader(PROXY_CLIENT_IP)
request.getHeader(WL_PROXY_CLIENT_IP) != null -> request.getHeader(WL_PROXY_CLIENT_IP)
else -> request.remoteAddr
}

fun getUserAgent(request: HttpServletRequest): String {
return request.getHeader("User-Agent") ?: "Unknown"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class UserService(

deleteRefreshInfo(user, response)
userRepository.delete(user)
logger.info("[유저 삭제 완료] User Email : $deletedEmail")
logger.info("[유저 삭제] email: $deletedEmail")
}

private fun deleteRefreshInfo(user: User, response: HttpServletResponse) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import uoslife.servermeeting.global.auth.dto.response.JwtResponse
import uoslife.servermeeting.global.auth.service.AuthService
import uoslife.servermeeting.global.error.ErrorResponse
import uoslife.servermeeting.global.util.RequestUtils
import uoslife.servermeeting.user.service.UserService
import uoslife.servermeeting.verification.dto.request.VerifyEmailRequest
import uoslife.servermeeting.verification.dto.response.SendVerificationEmailResponse
Expand All @@ -26,6 +28,7 @@ class VerificationApi(
private val emailVerificationService: EmailVerificationService,
private val userService: UserService,
private val authService: AuthService,
private val requestUtils: RequestUtils,
) {
@Operation(summary = "인증메일 전송", description = "이메일로 인증코드를 전송합니다.")
@ApiResponses(
Expand Down Expand Up @@ -80,9 +83,11 @@ class VerificationApi(
)
@PostMapping("/send-email")
fun sendVerificationEmail(
@RequestParam email: String
@RequestParam email: String,
request: HttpServletRequest
): ResponseEntity<SendVerificationEmailResponse> {
val response = emailVerificationService.sendVerificationEmail(email)
val requestInfo = requestUtils.toRequestInfoDto(request)
val response = emailVerificationService.sendVerificationEmail(email, requestInfo)
return ResponseEntity.ok(response)
}

Expand Down Expand Up @@ -180,16 +185,19 @@ class VerificationApi(
// TODO: Service layer로 이동해야함
@PostMapping("/verify-email")
fun verifyEmail(
@Valid @RequestBody request: VerifyEmailRequest,
@Valid @RequestBody body: VerifyEmailRequest,
@PathVariable userId: Long,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<JwtResponse> {
emailVerificationService.verifyEmail(request.email, request.code)
val requestInfo = requestUtils.toRequestInfoDto(request)
emailVerificationService.verifyEmail(body.email, body.code, requestInfo)

val user =
try {
userService.getUserByEmail(request.email)
userService.getUserByEmail(body.email)
} catch (e: Exception) {
userService.createUserByEmail(request.email)
userService.createUserByEmail(body.email)
}

val accessToken = authService.issueTokens(user.id!!, response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import uoslife.servermeeting.global.common.dto.RequestInfoDto
import uoslife.servermeeting.verification.dto.response.SendVerificationEmailResponse
import uoslife.servermeeting.verification.exception.*
import uoslife.servermeeting.verification.util.VerificationConstants
Expand All @@ -24,7 +25,10 @@ class EmailVerificationService(
private val logger = LoggerFactory.getLogger(EmailVerificationService::class.java)
}

fun sendVerificationEmail(email: String): SendVerificationEmailResponse {
fun sendVerificationEmail(
email: String,
requestInfo: RequestInfoDto
): SendVerificationEmailResponse {
// 이메일 형식 검증
validateEmail(email)
// 발송 제한 확인
Expand All @@ -33,9 +37,9 @@ class EmailVerificationService(
val asyncResult = asyncEmailService.sendEmailAsync(email)
asyncResult.whenComplete { _, exception ->
if (exception != null) {
logger.warn("[이메일 전송 실패] EMAIL : $email")
logger.warn("[이메일 전송 실패] email: $email, $requestInfo")
} else {
logger.info("[이메일 전송 성공] EMAIL : $email")
logger.info("[이메일 전송 성공] email: $email, $requestInfo")
}
}
// 코드 만료 시각 계산
Expand All @@ -47,13 +51,13 @@ class EmailVerificationService(
)
}

fun verifyEmail(email: String, code: String) {
fun verifyEmail(email: String, code: String, requestInfo: RequestInfoDto) {
validateVerificationAttempts(email)
incrementVerificationAttempts(email)
// Redis에서 인증 코드 조회
val redisCode = getVerificationCode(email)
// 인증 코드 검증
validateVerificationCode(redisCode, code)
validateVerificationCode(redisCode, code, email, requestInfo)
// 검증 성공한 코드 삭제
clearVerificationData(email)
}
Expand All @@ -64,10 +68,17 @@ class EmailVerificationService(
return redisTemplate.opsForValue().get(verificationCodeKey).toString()
}

private fun validateVerificationCode(redisCode: String, code: String) {
private fun validateVerificationCode(
redisCode: String,
code: String,
email: String,
requestInfo: RequestInfoDto
) {
if (redisCode != code) {
logger.warn("[이메일 인증 실패] email: $email, $requestInfo")
throw EmailVerificationCodeMismatchException()
}
logger.info("[이메일 인증 성공] email: $email")
}

private fun validateSendCount(email: String) {
Expand Down
Loading