generated from pricelees/issue-pr-template
[#44] 매장 기능 도입 #45
@ -6,14 +6,10 @@ import io.jsonwebtoken.Claims
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.auth.business.CLAIM_TYPE_KEY
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.common.dto.MDC_PRINCIPAL_ID_KEY
|
||||
import roomescape.common.dto.PrincipalType
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@ -47,21 +43,6 @@ class JwtUtils(
|
||||
}
|
||||
}
|
||||
|
||||
fun extractIdAndType(token: String?): Pair<Long, PrincipalType> {
|
||||
val id: Long = extractSubject(token)
|
||||
.also { MDC.put(MDC_PRINCIPAL_ID_KEY, it) }
|
||||
.toLong()
|
||||
|
||||
val type: PrincipalType = extractClaim(token, CLAIM_TYPE_KEY)
|
||||
?.let { PrincipalType.valueOf(it) }
|
||||
?: run {
|
||||
log.info { "[JwtUtils.extractIdAndType] 회원 타입 조회 실패. id=$id" }
|
||||
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)
|
||||
}
|
||||
|
||||
return id to type
|
||||
}
|
||||
|
||||
fun extractSubject(token: String?): String {
|
||||
if (token.isNullOrBlank()) {
|
||||
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package roomescape.auth.web.support
|
||||
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class AdminOnly(
|
||||
val type: AdminType,
|
||||
val privilege: Privilege
|
||||
)
|
||||
|
||||
|
||||
@ -4,17 +4,21 @@ import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import roomescape.auth.business.CLAIM_ADMIN_TYPE_KEY
|
||||
import roomescape.auth.business.CLAIM_PERMISSION_KEY
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import roomescape.auth.web.support.AdminOnly
|
||||
import roomescape.auth.web.support.accessToken
|
||||
import roomescape.common.dto.PrincipalType
|
||||
import roomescape.common.dto.MDC_PRINCIPAL_ID_KEY
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -30,32 +34,67 @@ class AdminInterceptor(
|
||||
if (handler !is HandlerMethod) {
|
||||
return true
|
||||
}
|
||||
|
||||
val annotation: AdminOnly = handler.getMethodAnnotation(AdminOnly::class.java) ?: return true
|
||||
|
||||
val token: String? = request.accessToken()
|
||||
val (id, type) = jwtUtils.extractIdAndType(token)
|
||||
|
||||
val permission: AdminPermissionLevel = jwtUtils.extractClaim(token, key = CLAIM_PERMISSION_KEY)
|
||||
?.let {
|
||||
AdminPermissionLevel.valueOf(it)
|
||||
}
|
||||
?: run {
|
||||
if (type != PrincipalType.ADMIN) {
|
||||
log.warn { "[AdminInterceptor] 회원의 관리자 API 접근: id=${id}" }
|
||||
throw AuthException(AuthErrorCode.ACCESS_DENIED)
|
||||
}
|
||||
log.warn { "[AdminInterceptor] 토큰에서 이용자 권한이 조회되지 않음: id=${id}" }
|
||||
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)
|
||||
}
|
||||
try {
|
||||
run {
|
||||
val id: String = jwtUtils.extractSubject(token).also { MDC.put(MDC_PRINCIPAL_ID_KEY, it) }
|
||||
val type: AdminType = validateTypeAndGet(token, annotation.type)
|
||||
val permission: AdminPermissionLevel = validatePermissionAndGet(token, annotation.privilege)
|
||||
|
||||
if (!permission.hasPrivilege(annotation.privilege)) {
|
||||
log.warn { "[AdminInterceptor] 관리자 권한 부족: required=${annotation.privilege} / current=${permission}" }
|
||||
throw AuthException(AuthErrorCode.ACCESS_DENIED)
|
||||
log.info { "[AdminInterceptor] 인증 완료. adminId=$id, type=${type}, permission=${permission}" }
|
||||
}
|
||||
|
||||
log.info { "[AdminInterceptor] 인증 완료. adminId=$id, permission=${permission}" }
|
||||
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
log.warn { "[AdminInterceptor] 예상치 못한 예외: message=${e.message}" }
|
||||
throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateTypeAndGet(token: String?, requiredType: AdminType): AdminType {
|
||||
val typeClaim: String? = jwtUtils.extractClaim(token, key = CLAIM_ADMIN_TYPE_KEY)
|
||||
|
||||
if (typeClaim == null) {
|
||||
log.warn { "[AdminInterceptor] 관리자 타입 조회 실패: token=${token}" }
|
||||
throw AuthException(AuthErrorCode.INVALID_TOKEN)
|
||||
}
|
||||
|
||||
val type = try {
|
||||
AdminType.valueOf(typeClaim)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
log.warn { "[AdminInterceptor] 관리자 타입 변환 실패: token=${token}, typeClaim=${typeClaim}" }
|
||||
throw AuthException(AuthErrorCode.INVALID_TOKEN)
|
||||
}
|
||||
|
||||
if (type != AdminType.HQ && type != requiredType) {
|
||||
log.warn { "[AdminInterceptor] 관리자 권한 부족: requiredType=${requiredType} / current=${type}" }
|
||||
throw AuthException(AuthErrorCode.ACCESS_DENIED)
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
private fun validatePermissionAndGet(token: String?, requiredPrivilege: Privilege): AdminPermissionLevel {
|
||||
val permissionClaim: String? = jwtUtils.extractClaim(token, key = CLAIM_PERMISSION_KEY)
|
||||
|
||||
if (permissionClaim == null) {
|
||||
log.warn { "[AdminInterceptor] 관리자 권한 조회 실패: token=${token}" }
|
||||
throw AuthException(AuthErrorCode.INVALID_TOKEN)
|
||||
}
|
||||
|
||||
val permission = try {
|
||||
AdminPermissionLevel.valueOf(permissionClaim)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
log.warn { "[AdminInterceptor] 관리자 권한 변환 실패: token=${token}, permissionClaim=${permissionClaim}" }
|
||||
throw AuthException(AuthErrorCode.INVALID_TOKEN)
|
||||
}
|
||||
|
||||
if (!permission.hasPrivilege(requiredPrivilege)) {
|
||||
log.warn { "[AdminInterceptor] 관리자 권한 부족: required=${requiredPrivilege} / current=${permission.privileges}" }
|
||||
throw AuthException(AuthErrorCode.ACCESS_DENIED)
|
||||
}
|
||||
|
||||
return permission
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
@ -12,7 +13,7 @@ import roomescape.auth.exception.AuthException
|
||||
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||
import roomescape.auth.web.support.UserOnly
|
||||
import roomescape.auth.web.support.accessToken
|
||||
import roomescape.common.dto.PrincipalType
|
||||
import roomescape.common.dto.MDC_PRINCIPAL_ID_KEY
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -29,16 +30,17 @@ class UserInterceptor(
|
||||
if ((handler !is HandlerMethod) || (handler.getMethodAnnotation(UserOnly::class.java) == null)) {
|
||||
return true
|
||||
}
|
||||
|
||||
val token: String? = request.accessToken()
|
||||
val (id, type) = jwtUtils.extractIdAndType(token)
|
||||
|
||||
if (type != PrincipalType.USER) {
|
||||
log.warn { "[UserInterceptor] 관리자의 회원 API 접근: id=${id}" }
|
||||
throw AuthException(AuthErrorCode.ACCESS_DENIED)
|
||||
try {
|
||||
jwtUtils.extractSubject(token).also {
|
||||
MDC.put(MDC_PRINCIPAL_ID_KEY, it)
|
||||
log.info { "[UserInterceptor] 인증 완료. userId=$it" }
|
||||
}
|
||||
|
||||
log.info { "[UserInterceptor] 인증 완료. userId=$id" }
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
log.warn { "[UserInterceptor] 예상치 못한 예외: message=${e.message}" }
|
||||
throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import roomescape.auth.web.support.AdminOnly
|
||||
import roomescape.auth.web.support.Public
|
||||
@ -53,21 +54,21 @@ interface ScheduleAPI {
|
||||
@PathVariable("id") id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.READ_DETAIL)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.READ_DETAIL)
|
||||
@Operation(summary = "일정 상세 조회", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "감사 정보를 포함하여 일정 상세 조회", useReturnTypeSchema = true))
|
||||
fun findScheduleDetail(
|
||||
@PathVariable("id") id: Long
|
||||
): ResponseEntity<CommonApiResponse<ScheduleDetailResponse>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.CREATE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.CREATE)
|
||||
@Operation(summary = "일정 생성", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun createSchedule(
|
||||
@Valid @RequestBody request: ScheduleCreateRequest
|
||||
): ResponseEntity<CommonApiResponse<ScheduleCreateResponse>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.UPDATE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.UPDATE)
|
||||
@Operation(summary = "일정 수정", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun updateSchedule(
|
||||
@ -75,7 +76,7 @@ interface ScheduleAPI {
|
||||
@Valid @RequestBody request: ScheduleUpdateRequest
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.DELETE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.DELETE)
|
||||
@Operation(summary = "일정 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
||||
fun deleteSchedule(
|
||||
|
||||
@ -8,6 +8,7 @@ import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import roomescape.admin.infrastructure.persistence.AdminType
|
||||
import roomescape.admin.infrastructure.persistence.Privilege
|
||||
import roomescape.auth.web.support.AdminOnly
|
||||
import roomescape.auth.web.support.Public
|
||||
@ -16,28 +17,27 @@ import roomescape.theme.web.*
|
||||
|
||||
@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.")
|
||||
interface ThemeAPIV2 {
|
||||
|
||||
@AdminOnly(privilege = Privilege.READ_SUMMARY)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.READ_SUMMARY)
|
||||
@Operation(summary = "모든 테마 조회", description = "관리자 페이지에서 요약된 테마 목록을 조회합니다.", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.READ_DETAIL)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.READ_DETAIL)
|
||||
@Operation(summary = "테마 상세 조회", description = "해당 테마의 상세 정보를 조회합니다.", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findAdminThemeDetail(@PathVariable("id") id: Long): ResponseEntity<CommonApiResponse<AdminThemeDetailResponse>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.CREATE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.CREATE)
|
||||
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun createTheme(@Valid @RequestBody themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.DELETE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.DELETE)
|
||||
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
||||
fun deleteTheme(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@AdminOnly(privilege = Privilege.UPDATE)
|
||||
@AdminOnly(type = AdminType.STORE, privilege = Privilege.UPDATE)
|
||||
@Operation(summary = "테마 수정", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun updateTheme(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user