Skip to content

Commit

Permalink
Merge pull request #21 from EntryDSM/feature/19-exception-handling
Browse files Browse the repository at this point in the history
Feature/19 exception handling
  • Loading branch information
kangeunchan authored Jan 9, 2025
2 parents d44cfd9 + beeb2f9 commit 1fdf736
Show file tree
Hide file tree
Showing 34 changed files with 175 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object AuthorizedClientNotFoundException : CustomException(ErrorCode.AUTHORIZED_CLIENT_NOT_FOUND)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object EmptyAccessTokenException : CustomException(ErrorCode.EMPTY_ACCESS_TOKEN)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object InvalidAccessTokenException : CustomException(ErrorCode.INVALID_ACCESS_TOKEN)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object MissingRequiredAttributeException : CustomException(ErrorCode.MISSING_REQUIRED_ATTRIBUTE)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object OrganizationMembershipErrorException : CustomException(ErrorCode.ORGANIZATION_MEMBERSHIP_ERROR)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object TokenExpirationMissingException : CustomException(ErrorCode.TOKEN_EXPIRATION_MISSING)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object UserInfoFetchFailureException : CustomException(ErrorCode.USER_INFO_FETCH_ERROR)
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.service

import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.AuthorizedClientNotFoundException
import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.MissingRequiredAttributeException
import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.OrganizationMembershipErrorException
import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.TokenExpirationMissingException
import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.UserInfoFetchFailureException
import entry.dsm.gitauth.equusgithubauth.domain.auth.presentation.dto.GithubUserInformation
import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
import org.springframework.security.oauth2.core.user.OAuth2User
Expand All @@ -16,7 +20,6 @@ class GithubUserService(
private val githubUserValidationService: GithubUserValidationService,
private val githubUserTokenValidationService: GithubUserTokenValidationService,
) {
private val logger = LoggerFactory.getLogger(GithubUserService::class.java)
private val timestampFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME

fun getGithubUserInformation(oAuth2User: OAuth2User): GithubUserInformation {
Expand All @@ -29,14 +32,13 @@ class GithubUserService(

createGithubUserInformation(oAuth2User, client)
} catch (e: Exception) {
logger.error("GitHub 사용자 정보 취득 중 오류 발생: ${e.message}", e)
throw IllegalStateException("GitHub 사용자 정보를 가져올 수 없습니다.", e)
throw UserInfoFetchFailureException
}
}

private fun getAuthorizedClient(oAuth2User: OAuth2User): OAuth2AuthorizedClient {
return authorizedClientService.loadAuthorizedClient("github", oAuth2User.name)
?: throw IllegalArgumentException("사용자 ${oAuth2User.name}에 대한 인증된 클라이언트를 찾을 수 없습니다.")
?: throw AuthorizedClientNotFoundException
}

private fun validateOrganizationMembership(
Expand All @@ -45,10 +47,8 @@ class GithubUserService(
) {
val username = oAuth2User.attributes["login"].toString()
if (!githubUserValidationService.validateUserMembership(client.accessToken.tokenValue, username)) {
logger.warn("조직 멤버십 검증 실패: $username")
throw IllegalArgumentException("사용자가 조직의 멤버가 아닙니다.")
throw OrganizationMembershipErrorException
}
logger.info("조직 멤버십 검증 성공: $username")
}

private fun createGithubUserInformation(
Expand Down Expand Up @@ -77,15 +77,15 @@ class GithubUserService(

private fun parseTokenExpirationTime(client: OAuth2AuthorizedClient): LocalDateTime {
return client.accessToken.expiresAt?.atZone(ZoneId.systemDefault())?.toLocalDateTime()
?: throw IllegalStateException("토큰 만료 시간이 누락되었습니다.")
?: throw TokenExpirationMissingException
}

private fun getRequiredAttributeValue(
oAuth2User: OAuth2User,
attributeName: String,
): String {
return oAuth2User.attributes[attributeName]?.toString()
?: throw IllegalStateException("사용자 ${oAuth2User.attributes["login"]}의 필수 속성 '$attributeName'이(가) 누락되었습니다.")
?: throw MissingRequiredAttributeException
}

private fun getOptionalAttributeValue(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package entry.dsm.gitauth.equusgithubauth.domain.auth.service

import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.EmptyAccessTokenException
import entry.dsm.gitauth.equusgithubauth.domain.auth.exception.InvalidAccessTokenException
import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.controller.GithubApiClient
import entry.dsm.gitauth.equusgithubauth.global.external.service.TokenAuthenticator
import org.springframework.stereotype.Service
Expand All @@ -10,11 +12,11 @@ class GithubUserTokenValidationService(
private val tokenAuthenticator: TokenAuthenticator,
) {
fun validateAccessToken(token: String) {
require(token.isNotBlank()) { "Access token is empty." }
require(token.isNotBlank()) { EmptyAccessTokenException }
try {
githubClient.getUser(tokenAuthenticator.createAuthorizationHeader(token))
} catch (ex: Exception) {
throw IllegalArgumentException("Access token is expired or invalid.")
throw InvalidAccessTokenException
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package entry.dsm.gitauth.equusgithubauth.domain.auth.service
import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.controller.GithubApiClient
import entry.dsm.gitauth.equusgithubauth.global.external.github.presentation.dto.GithubOrganizationResponse
import entry.dsm.gitauth.equusgithubauth.global.external.service.TokenAuthenticator
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class GithubUserValidationService(
private val githubApiClient: GithubApiClient,
private val tokenAuthenticator: TokenAuthenticator,
) {
private val logger = LoggerFactory.getLogger(GithubUserValidationService::class.java)

companion object {
private const val TARGET_ORGANIZATION = "EntryDSM"
}
Expand All @@ -23,21 +20,15 @@ class GithubUserValidationService(
): Boolean {
return try {
val authorizationHeader = tokenAuthenticator.createAuthorizationHeader(token)

val currentUsername = githubApiClient.getUser(authorizationHeader).login
if (currentUsername != username) {
logger.error("Token username mismatch: $currentUsername != $username")
return false
}

val isMemberOfOrg =
githubApiClient.getUserOrganizations(authorizationHeader, username)
.any { org: GithubOrganizationResponse -> org.login == TARGET_ORGANIZATION }

logger.info("Membership status for $username in $TARGET_ORGANIZATION: $isMemberOfOrg")
isMemberOfOrg
} catch (e: Exception) {
logger.error("Error validating GitHub user membership for $username", e)
false
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package entry.dsm.gitauth.equusgithubauth.domain.notice.command.service

import entry.dsm.gitauth.equusgithubauth.domain.notice.command.repository.NoticeRepository
import entry.dsm.gitauth.equusgithubauth.domain.notice.exception.NoticeNotFoundException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -13,8 +14,7 @@ class DeleteNoticeService(
fun deleteNotice(noticeId: Long) {
val notice =
noticeRepository.findByIdOrNull(noticeId)
?: throw IllegalArgumentException("Notice with id $noticeId not found")

noticeRepository.delete(notice) // 공지사항 삭제
?: throw NoticeNotFoundException
noticeRepository.delete(notice)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package entry.dsm.gitauth.equusgithubauth.domain.notice.command.service

import entry.dsm.gitauth.equusgithubauth.domain.notice.command.dto.request.UpdateNoticeCommand
import entry.dsm.gitauth.equusgithubauth.domain.notice.command.repository.NoticeRepository
import entry.dsm.gitauth.equusgithubauth.domain.notice.exception.NoticeNotFoundException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -17,8 +18,7 @@ class UpdateNoticeService(
) {
val notice =
noticeRepository.findByIdOrNull(noticeId)
?: throw IllegalArgumentException("Notice with id $noticeId not found")

?: throw NoticeNotFoundException
notice.update(command)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.notice.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object NoticeNotFoundException : CustomException(ErrorCode.NOTICE_NOT_FOUND)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.notice.query.service

import entry.dsm.gitauth.equusgithubauth.domain.notice.exception.NoticeNotFoundException
import entry.dsm.gitauth.equusgithubauth.domain.notice.query.dto.response.NoticeQueryResponse
import entry.dsm.gitauth.equusgithubauth.domain.notice.query.repository.NoticeQueryRepository
import org.springframework.data.repository.findByIdOrNull
Expand All @@ -14,8 +15,7 @@ class NoticeQueryService(
fun getNotice(noticeId: Long): NoticeQueryResponse {
val notice =
noticeQueryRepository.findByIdOrNull(noticeId)
?: throw IllegalArgumentException("Notice id $noticeId not found")

?: throw NoticeNotFoundException
return NoticeQueryResponse.from(notice)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package entry.dsm.gitauth.equusgithubauth.domain.report.command.service

import entry.dsm.gitauth.equusgithubauth.domain.notice.command.repository.NoticeRepository
import entry.dsm.gitauth.equusgithubauth.domain.notice.exception.NoticeNotFoundException
import entry.dsm.gitauth.equusgithubauth.domain.report.command.dto.request.CreateReportCommand
import entry.dsm.gitauth.equusgithubauth.domain.report.command.repository.ReportRepository
import entry.dsm.gitauth.equusgithubauth.domain.report.entity.Report
Expand All @@ -17,7 +18,7 @@ class CreateReportService(
// Notice 엔티티 가져오기
val notice =
noticeRepository.findById(command.noticeId)
.orElseThrow { IllegalArgumentException("Notice with id ${command.noticeId} not found") }
.orElseThrow { NoticeNotFoundException }

// Report 엔티티 생성
val report =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package entry.dsm.gitauth.equusgithubauth.domain.report.command.service

import entry.dsm.gitauth.equusgithubauth.domain.report.command.repository.ReportRepository
import entry.dsm.gitauth.equusgithubauth.domain.report.exception.ReportNotFoundException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -13,8 +14,7 @@ class DeleteReportService(
fun deleteReport(reportId: Long) {
val report =
reportRepository.findByIdOrNull(reportId)
?: throw IllegalArgumentException("Report with id $reportId not found")

?: throw ReportNotFoundException
reportRepository.delete(report)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package entry.dsm.gitauth.equusgithubauth.domain.report.command.service
import entry.dsm.gitauth.equusgithubauth.domain.report.command.dto.request.UpdateReportCommand
import entry.dsm.gitauth.equusgithubauth.domain.report.command.repository.ReportRepository
import entry.dsm.gitauth.equusgithubauth.domain.report.entity.Report
import entry.dsm.gitauth.equusgithubauth.domain.report.exception.ReportNotFoundException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -18,7 +19,7 @@ class UpdateReportService(
): Report {
val report =
reportRepository.findByIdOrNull(reportId)
?: throw IllegalArgumentException("Report with id $reportId not found")
?: throw ReportNotFoundException

report.apply {
applicantName = command.applicantName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.report.exception

import entry.dsm.gitauth.equusgithubauth.global.exception.CustomException
import entry.dsm.gitauth.equusgithubauth.global.exception.ErrorCode

object ReportNotFoundException : CustomException(ErrorCode.REPORT_NOT_FOUND)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package entry.dsm.gitauth.equusgithubauth.domain.report.query.service

import entry.dsm.gitauth.equusgithubauth.domain.report.exception.ReportNotFoundException
import entry.dsm.gitauth.equusgithubauth.domain.report.query.dto.response.ReportQueryResponse
import entry.dsm.gitauth.equusgithubauth.domain.report.query.repository.ReportQueryRepository
import org.springframework.data.repository.findByIdOrNull
Expand All @@ -14,7 +15,7 @@ class ReportQueryService(
fun getReport(reportId: Long): ReportQueryResponse {
val report =
reportQueryRepository.findByIdOrNull(reportId)
?: throw IllegalArgumentException("Report with id $reportId not found")
?: throw ReportNotFoundException
return ReportQueryResponse.from(report)
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package entry.dsm.gitauth.equusgithubauth.global.exception

abstract class CustomException(
private val errorCode: ErrorCode,
) : RuntimeException() {
val statusCode: Int get() = errorCode.status
override val message: String get() = errorCode.message
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package entry.dsm.gitauth.equusgithubauth.global.exception

enum class ErrorCode(
val status: Int,
val message: String,
) {
USER_NOT_FOUND(404, "사용자를 찾을 수 없습니다."),
USER_INFO_FETCH_ERROR(500, "GitHub 사용자 정보를 가져올 수 없습니다."),
AUTHORIZED_CLIENT_NOT_FOUND(400, "사용자에 대한 인증된 클라이언트를 찾을 수 없습니다."),
ORGANIZATION_MEMBERSHIP_ERROR(400, "사용자가 조직의 멤버가 아닙니다."),
TOKEN_EXPIRATION_MISSING(400, "토큰 만료 시간이 누락되었습니다."),
MISSING_REQUIRED_ATTRIBUTE(400, "사용자의 필수 속성이 누락되었습니다."),
INVALID_ACCESS_TOKEN(400, "액세스 토큰이 만료되었거나 유효하지 않습니다."),
EMPTY_ACCESS_TOKEN(400, "액세스 토큰이 비어 있습니다."),

NOTICE_NOT_FOUND(404, "공지사항을 찾을 수 없습니다."),

REPORT_NOT_FOUND(404, "보고서를 찾을 수 없습니다."),

JWT_TOKEN_EXPIRED(401, "JWT 토큰이 만료되었습니다."),
JWT_TOKEN_INVALID(401, "JWT 토큰이 유효하지 않습니다."),
}
Loading

0 comments on commit 1fdf736

Please sign in to comment.