remove: 회원 / 인증 기능 적용 완료로 인한 기존 기능 제거

This commit is contained in:
이상진 2025-09-13 13:29:43 +09:00
parent f5192750c3
commit 40d687f7f2
40 changed files with 174 additions and 1025 deletions

View File

@ -1,10 +1,6 @@
package roomescape.admin.infrastructure.persistence package roomescape.admin.infrastructure.persistence
import jakarta.persistence.Entity import jakarta.persistence.*
import jakarta.persistence.EntityListeners
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import roomescape.common.entity.AuditingBaseEntity import roomescape.common.entity.AuditingBaseEntity

View File

@ -4,55 +4,111 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.LoginCheckResponse import roomescape.auth.web.LoginContext
import roomescape.auth.web.LoginRequest import roomescape.auth.web.LoginRequestV2
import roomescape.auth.web.LoginResponse import roomescape.auth.web.LoginSuccessResponse
import roomescape.member.implement.MemberFinder import roomescape.common.dto.CurrentUserContext
import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.common.dto.LoginCredentials
import roomescape.common.dto.PrincipalType
import roomescape.member.business.UserService
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
const val CLAIM_PERMISSION_KEY = "permission"
const val CLAIM_TYPE_KEY = "principal_type"
@Service @Service
class AuthService( class AuthService(
private val memberFinder: MemberFinder, private val adminService: AdminService,
private val jwtHandler: JwtHandler, private val userService: UserService,
private val loginHistoryService: LoginHistoryService,
private val jwtUtils: JwtUtils,
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun login(request: LoginRequest): LoginResponse { fun login(
val params = "email=${request.email}, password=${request.password}" request: LoginRequestV2,
log.debug { "[AuthService.login] 시작: $params" } context: LoginContext
): LoginSuccessResponse {
log.info { "[AuthService.login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" }
val member: MemberEntity = fetchOrThrow(AuthErrorCode.LOGIN_FAILED) { val (credentials, extraClaims) = getCredentials(request)
memberFinder.findByEmailAndPassword(request.email, request.password)
try {
verifyPasswordOrThrow(request, credentials)
val accessToken = jwtUtils.createToken(subject = credentials.id.toString(), claims = extraClaims)
loginHistoryService.createSuccessHistory(credentials.id, request.principalType, context)
return LoginSuccessResponse(accessToken).also {
log.info { "[AuthService.login] 로그인 완료: account=${request.account}, context=${context}" }
} }
val accessToken: String = jwtHandler.createToken(member.id!!)
return LoginResponse(accessToken) } catch (e: Exception) {
.also { log.info { "[AuthService.login] 완료: email=${request.email}, memberId=${member.id}" } } loginHistoryService.createFailureHistory(credentials.id, request.principalType, context)
when (e) {
is AuthException -> {
log.info { "[AuthService.login] 로그인 실패: account = ${request.account}" }
throw e
}
else -> {
log.warn { "[AuthService.login] 로그인 실패: message=${e.message} account = ${request.account}" }
throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR)
}
}
}
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun checkLogin(memberId: Long): LoginCheckResponse { fun findContextById(id: Long, type: PrincipalType): CurrentUserContext {
log.debug { "[AuthService.checkLogin] 시작: memberId=$memberId" } log.info { "[AuthService.checkLogin] 로그인 확인 시작: id=${id}, type=${type}" }
val member: MemberEntity = fetchOrThrow(AuthErrorCode.MEMBER_NOT_FOUND) { memberFinder.findById(memberId) } return when (type) {
PrincipalType.ADMIN -> {
return LoginCheckResponse(member.name, member.role.name) adminService.findContextById(id)
.also { log.info { "[AuthService.checkLogin] 완료: memberId=$memberId, role=${it.role}" } }
} }
private fun fetchOrThrow(errorCode: AuthErrorCode, block: () -> MemberEntity): MemberEntity { PrincipalType.USER -> {
try { userService.findContextById(id)
return block() }
} catch (e: Exception) { }.also {
throw AuthException(errorCode, e.message ?: errorCode.message) log.info { "[AuthService.checkLogin] 로그인 확인 완료: id=${id}, type=${type}" }
} }
} }
fun logout(memberId: Long) { private fun verifyPasswordOrThrow(
log.info { "[AuthService.logout] 로그아웃: memberId=$memberId" } request: LoginRequestV2,
credentials: LoginCredentials
) {
if (credentials.password != request.password) {
log.info { "[AuthService.login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" }
throw AuthException(AuthErrorCode.LOGIN_FAILED)
}
}
private fun getCredentials(request: LoginRequestV2): Pair<LoginCredentials, Map<String, Any>> {
val extraClaims: MutableMap<String, Any> = mutableMapOf()
val credentials: LoginCredentials = when (request.principalType) {
PrincipalType.ADMIN -> {
adminService.findCredentialsByAccount(request.account).also {
extraClaims.put(CLAIM_PERMISSION_KEY, it.permissionLevel)
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.ADMIN)
}
}
PrincipalType.USER -> {
userService.findCredentialsByAccount(request.account).also {
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.USER)
}
}
}
return credentials to extraClaims
} }
} }

View File

@ -1,114 +0,0 @@
package roomescape.auth.business
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.LoginContext
import roomescape.auth.web.LoginRequestV2
import roomescape.auth.web.LoginSuccessResponse
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.LoginCredentials
import roomescape.common.dto.PrincipalType
import roomescape.member.business.UserService
private val log: KLogger = KotlinLogging.logger {}
const val CLAIM_PERMISSION_KEY = "permission"
const val CLAIM_TYPE_KEY = "principal_type"
@Service
class AuthServiceV2(
private val adminService: AdminService,
private val userService: UserService,
private val loginHistoryService: LoginHistoryService,
private val jwtUtils: JwtUtils,
) {
@Transactional(readOnly = true)
fun login(
request: LoginRequestV2,
context: LoginContext
): LoginSuccessResponse {
log.info { "[AuthService.login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" }
val (credentials, extraClaims) = getCredentials(request)
try {
verifyPasswordOrThrow(request, credentials)
val accessToken = jwtUtils.createToken(subject = credentials.id.toString(), claims = extraClaims)
loginHistoryService.createSuccessHistory(credentials.id, request.principalType, context)
return LoginSuccessResponse(accessToken).also {
log.info { "[AuthService.login] 로그인 완료: account=${request.account}, context=${context}" }
}
} catch (e: Exception) {
loginHistoryService.createFailureHistory(credentials.id, request.principalType, context)
when (e) {
is AuthException -> {
log.info { "[AuthService.login] 로그인 실패: account = ${request.account}" }
throw e
}
else -> {
log.warn { "[AuthService.login] 로그인 실패: message=${e.message} account = ${request.account}" }
throw AuthException(AuthErrorCode.TEMPORARY_AUTH_ERROR)
}
}
}
}
@Transactional(readOnly = true)
fun findContextById(id: Long, type: PrincipalType): CurrentUserContext {
log.info { "[AuthService.checkLogin] 로그인 확인 시작: id=${id}, type=${type}" }
return when (type) {
PrincipalType.ADMIN -> {
adminService.findContextById(id)
}
PrincipalType.USER -> {
userService.findContextById(id)
}
}.also {
log.info { "[AuthService.checkLogin] 로그인 확인 완료: id=${id}, type=${type}" }
}
}
private fun verifyPasswordOrThrow(
request: LoginRequestV2,
credentials: LoginCredentials
) {
if (credentials.password != request.password) {
log.info { "[AuthService.login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" }
throw AuthException(AuthErrorCode.LOGIN_FAILED)
}
}
private fun getCredentials(request: LoginRequestV2): Pair<LoginCredentials, Map<String, Any>> {
val extraClaims: MutableMap<String, Any> = mutableMapOf()
val credentials: LoginCredentials = when (request.principalType) {
PrincipalType.ADMIN -> {
adminService.findCredentialsByAccount(request.account).also {
extraClaims.put(CLAIM_PERMISSION_KEY, it.permissionLevel)
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.ADMIN)
}
}
PrincipalType.USER -> {
userService.findCredentialsByAccount(request.account).also {
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.USER)
}
}
}
return credentials to extraClaims
}
}

View File

@ -1,46 +1,52 @@
package roomescape.auth.docs package roomescape.auth.docs
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import roomescape.auth.web.LoginCheckResponse import roomescape.auth.web.LoginRequestV2
import roomescape.auth.web.LoginRequest import roomescape.auth.web.LoginSuccessResponse
import roomescape.auth.web.LoginResponse import roomescape.auth.web.support.CurrentUser
import roomescape.auth.web.support.LoginRequired import roomescape.auth.web.support.Public
import roomescape.auth.web.support.MemberId import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다") @Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다")
interface AuthAPI { interface AuthAPI {
@Public
@Operation(summary = "로그인") @Operation(summary = "로그인")
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."), ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."),
) )
fun login( fun login(
@Valid @RequestBody loginRequest: LoginRequest @Valid @RequestBody loginRequest: LoginRequestV2,
): ResponseEntity<CommonApiResponse<LoginResponse>> servletRequest: HttpServletRequest
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>>
@Operation(summary = "로그인 상태 확인") @Operation(summary = "로그인 상태 확인")
@ApiResponses( @ApiResponses(
ApiResponse( ApiResponse(
responseCode = "200", responseCode = "200",
description = "로그인 상태이며, 로그인된 회원의 이름 / 권한을 반환합니다.", description = "입력된 ID / 결과(Boolean)을 반환합니다.",
useReturnTypeSchema = true useReturnTypeSchema = true
), ),
) )
fun checkLogin( fun checkLogin(
@MemberId @Parameter(hidden = true) memberId: Long @CurrentUser user: CurrentUserContext
): ResponseEntity<CommonApiResponse<LoginCheckResponse>> ): ResponseEntity<CommonApiResponse<CurrentUserContext>>
@LoginRequired
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"]) @Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다."), ApiResponse(responseCode = "200"),
) )
fun logout(@MemberId memberId: Long): ResponseEntity<CommonApiResponse<Unit>> fun logout(
@CurrentUser user: CurrentUserContext,
servletResponse: HttpServletResponse
): ResponseEntity<CommonApiResponse<Unit>>
} }

View File

@ -1,52 +0,0 @@
package roomescape.auth.docs
import io.swagger.v3.oas.annotations.Operation
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.RequestBody
import roomescape.auth.web.LoginRequestV2
import roomescape.auth.web.LoginSuccessResponse
import roomescape.auth.web.support.CurrentUser
import roomescape.auth.web.support.Public
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다")
interface AuthAPIV2 {
@Public
@Operation(summary = "로그인")
@ApiResponses(
ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."),
)
fun login(
@Valid @RequestBody loginRequest: LoginRequestV2,
servletRequest: HttpServletRequest
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>>
@Operation(summary = "로그인 상태 확인")
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "입력된 ID / 결과(Boolean)을 반환합니다.",
useReturnTypeSchema = true
),
)
fun checkLogin(
@CurrentUser user: CurrentUserContext
): ResponseEntity<CommonApiResponse<CurrentUserContext>>
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "200"),
)
fun logout(
@CurrentUser user: CurrentUserContext,
servletResponse: HttpServletResponse
): ResponseEntity<CommonApiResponse<Unit>>
}

View File

@ -1,64 +0,0 @@
package roomescape.auth.infrastructure.jwt
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import java.util.*
import javax.crypto.SecretKey
private val log: KLogger = KotlinLogging.logger {}
@Component
class JwtHandler(
@Value("\${security.jwt.token.secret-key}")
private val secretKeyString: String,
@Value("\${security.jwt.token.ttl-seconds}")
private val tokenTtlSeconds: Long
) {
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
fun createToken(memberId: Long): String {
log.debug { "[JwtHandler.createToken] 시작: memberId=$memberId" }
val date = Date()
val accessTokenExpiredAt = Date(date.time + (tokenTtlSeconds * 1_000))
return Jwts.builder()
.claim(MEMBER_ID_CLAIM_KEY, memberId)
.issuedAt(date)
.expiration(accessTokenExpiredAt)
.signWith(secretKey)
.compact()
.also { log.debug { "[JwtHandler.createToken] 완료. memberId=$memberId, token=$it" } }
}
fun getMemberIdFromToken(token: String?): Long {
try {
log.debug { "[JwtHandler.getMemberIdFromToken] 시작: token=$token" }
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.payload
.get(MEMBER_ID_CLAIM_KEY, Number::class.java)
.toLong()
.also { log.debug { "[JwtHandler.getMemberIdFromToken] 완료. memberId=$it, token=$token" } }
} catch (_: IllegalArgumentException) {
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
} catch (_: ExpiredJwtException) {
throw AuthException(AuthErrorCode.EXPIRED_TOKEN)
} catch (_: Exception) {
throw AuthException(AuthErrorCode.INVALID_TOKEN)
}
}
companion object {
private const val MEMBER_ID_CLAIM_KEY = "memberId"
}
}

View File

@ -1,44 +1,46 @@
package roomescape.auth.web package roomescape.auth.web
import io.swagger.v3.oas.annotations.Parameter import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.auth.business.AuthService import roomescape.auth.business.AuthService
import roomescape.auth.docs.AuthAPI import roomescape.auth.docs.AuthAPI
import roomescape.auth.web.support.MemberId import roomescape.auth.web.support.CurrentUser
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
@RestController @RestController
@RequestMapping("/auth")
class AuthController( class AuthController(
private val authService: AuthService private val authService: AuthService,
) : AuthAPI { ) : AuthAPI {
@PostMapping("/login") @PostMapping("/login")
override fun login( override fun login(
@Valid @RequestBody loginRequest: LoginRequest, loginRequest: LoginRequestV2,
): ResponseEntity<CommonApiResponse<LoginResponse>> { servletRequest: HttpServletRequest
val response: LoginResponse = authService.login(loginRequest) ): ResponseEntity<CommonApiResponse<LoginSuccessResponse>> {
val response = authService.login(request = loginRequest, context = servletRequest.toLoginContext())
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@GetMapping("/login/check") @GetMapping("/login/check")
override fun checkLogin( override fun checkLogin(
@MemberId @Parameter(hidden = true) memberId: Long @CurrentUser user: CurrentUserContext,
): ResponseEntity<CommonApiResponse<LoginCheckResponse>> { ): ResponseEntity<CommonApiResponse<CurrentUserContext>> {
val response: LoginCheckResponse = authService.checkLogin(memberId) return ResponseEntity.ok(CommonApiResponse(user))
return ResponseEntity.ok(CommonApiResponse(response))
} }
@PostMapping("/logout") @PostMapping("/logout")
override fun logout(@MemberId memberId: Long): ResponseEntity<CommonApiResponse<Unit>> { override fun logout(
authService.logout(memberId) @CurrentUser user: CurrentUserContext,
servletResponse: HttpServletResponse
return ResponseEntity.noContent().build() ): ResponseEntity<CommonApiResponse<Unit>> {
return ResponseEntity.ok().build()
} }
} }

View File

@ -1,46 +0,0 @@
package roomescape.auth.web
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import roomescape.auth.business.AuthServiceV2
import roomescape.auth.docs.AuthAPIV2
import roomescape.auth.web.support.CurrentUser
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
@RestController
@RequestMapping("/auth")
class AuthControllerV2(
private val authService: AuthServiceV2,
) : AuthAPIV2 {
@PostMapping("/login")
override fun login(
loginRequest: LoginRequestV2,
servletRequest: HttpServletRequest
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>> {
val response = authService.login(request = loginRequest, context = servletRequest.toLoginContext())
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/login/check")
override fun checkLogin(
@CurrentUser user: CurrentUserContext,
): ResponseEntity<CommonApiResponse<CurrentUserContext>> {
return ResponseEntity.ok(CommonApiResponse(user))
}
@PostMapping("/logout")
override fun logout(
@CurrentUser user: CurrentUserContext,
servletResponse: HttpServletResponse
): ResponseEntity<CommonApiResponse<Unit>> {
return ResponseEntity.ok().build()
}
}

View File

@ -1,24 +1,24 @@
package roomescape.auth.web package roomescape.auth.web
import io.swagger.v3.oas.annotations.media.Schema import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.constraints.Email import roomescape.common.dto.PrincipalType
import jakarta.validation.constraints.NotBlank
data class LoginResponse( data class LoginContext(
val ipAddress: String,
val userAgent: String,
)
fun HttpServletRequest.toLoginContext() = LoginContext(
ipAddress = this.remoteAddr,
userAgent = this.getHeader("User-Agent")
)
data class LoginRequestV2(
val account: String,
val password: String,
val principalType: PrincipalType
)
data class LoginSuccessResponse(
val accessToken: String val accessToken: String
) )
data class LoginCheckResponse(
@Schema(description = "로그인된 회원의 이름")
val name: String,
@Schema(description = "회원(MEMBER) / 관리자(ADMIN)")
val role: String,
)
data class LoginRequest(
@Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com")
val email: String,
@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
val password: String
)

View File

@ -1,24 +0,0 @@
package roomescape.auth.web
import jakarta.servlet.http.HttpServletRequest
import roomescape.common.dto.PrincipalType
data class LoginContext(
val ipAddress: String,
val userAgent: String,
)
fun HttpServletRequest.toLoginContext() = LoginContext(
ipAddress = this.remoteAddr,
userAgent = this.getHeader("User-Agent")
)
data class LoginRequestV2(
val account: String,
val password: String,
val principalType: PrincipalType
)
data class LoginSuccessResponse(
val accessToken: String
)

View File

@ -2,18 +2,6 @@ package roomescape.auth.web.support
import roomescape.admin.infrastructure.persistence.Privilege import roomescape.admin.infrastructure.persistence.Privilege
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Admin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LoginRequired
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class MemberId
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class AdminOnly( annotation class AdminOnly(

View File

@ -1,63 +0,0 @@
package roomescape.auth.web.support
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.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.member.implement.MemberFinder
import roomescape.member.infrastructure.persistence.MemberEntity
private val log: KLogger = KotlinLogging.logger {}
const val MDC_MEMBER_ID_KEY: String = "member_id"
@Component
class AuthInterceptor(
private val memberFinder: MemberFinder,
private val jwtHandler: JwtHandler
) : HandlerInterceptor {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
if (handler !is HandlerMethod) {
return true
}
val loginRequired = handler.getMethodAnnotation(LoginRequired::class.java)
val admin = handler.getMethodAnnotation(Admin::class.java)
if (loginRequired == null && admin == null) {
return true
}
val accessToken: String? = request.accessToken()
log.info { "[AuthInterceptor] 인증 시작. accessToken=${accessToken}" }
val member: MemberEntity = findMember(accessToken)
if (admin != null && !member.isAdmin()) {
log.info { "[AuthInterceptor] 관리자 인증 실패. memberId=${member.id}, role=${member.role}" }
throw AuthException(AuthErrorCode.ACCESS_DENIED)
}
MDC.put(MDC_MEMBER_ID_KEY, "${member.id}")
log.info { "[AuthInterceptor] 인증 완료. memberId=${member.id}, role=${member.role}" }
return true
}
private fun findMember(accessToken: String?): MemberEntity {
try {
val memberId = jwtHandler.getMemberIdFromToken(accessToken)
return memberFinder.findById(memberId)
.also { MDC.put(MDC_MEMBER_ID_KEY, "$memberId") }
} catch (e: Exception) {
log.info { "[AuthInterceptor] 회원 조회 실패. accessToken = $accessToken" }
val errorCode = AuthErrorCode.MEMBER_NOT_FOUND
throw AuthException(errorCode, e.message ?: errorCode.message)
}
}
}

View File

@ -1,46 +0,0 @@
package roomescape.auth.web.support
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.MDC
import org.springframework.core.MethodParameter
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtHandler
private val log: KLogger = KotlinLogging.logger {}
@Component
class MemberIdResolver(
private val jwtHandler: JwtHandler
) : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(MemberId::class.java)
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any {
val request: HttpServletRequest = webRequest.nativeRequest as HttpServletRequest
val token: String? = request.accessToken()
try {
return jwtHandler.getMemberIdFromToken(token)
.also { MDC.put("member_id", "$it") }
} catch (e: Exception) {
log.info { "[MemberIdResolver] 회원 조회 실패. message=${e.message}" }
val errorCode = AuthErrorCode.MEMBER_NOT_FOUND
throw AuthException(errorCode, e.message ?: errorCode.message)
}
}
}

View File

@ -7,7 +7,7 @@ import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.auth.business.AuthServiceV2 import roomescape.auth.business.AuthService
import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.support.Authenticated import roomescape.auth.web.support.Authenticated
import roomescape.auth.web.support.accessToken import roomescape.auth.web.support.accessToken
@ -17,7 +17,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Component @Component
class AuthenticatedInterceptor( class AuthenticatedInterceptor(
private val jwtUtils: JwtUtils, private val jwtUtils: JwtUtils,
private val authService: AuthServiceV2 private val authService: AuthService
) : HandlerInterceptor { ) : HandlerInterceptor {
override fun preHandle( override fun preHandle(

View File

@ -9,7 +9,7 @@ import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.method.support.ModelAndViewContainer
import roomescape.auth.business.AuthServiceV2 import roomescape.auth.business.AuthService
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.infrastructure.jwt.JwtUtils
@ -21,7 +21,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Component @Component
class CurrentUserContextResolver( class CurrentUserContextResolver(
private val jwtUtils: JwtUtils, private val jwtUtils: JwtUtils,
private val authService: AuthServiceV2 private val authService: AuthService
) : HandlerMethodArgumentResolver { ) : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean { override fun supportsParameter(parameter: MethodParameter): Boolean {

View File

@ -4,8 +4,6 @@ import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import roomescape.auth.web.support.AuthInterceptor
import roomescape.auth.web.support.MemberIdResolver
import roomescape.auth.web.support.interceptors.AdminInterceptor import roomescape.auth.web.support.interceptors.AdminInterceptor
import roomescape.auth.web.support.interceptors.AuthenticatedInterceptor import roomescape.auth.web.support.interceptors.AuthenticatedInterceptor
import roomescape.auth.web.support.interceptors.UserInterceptor import roomescape.auth.web.support.interceptors.UserInterceptor
@ -13,8 +11,6 @@ import roomescape.auth.web.support.resolver.CurrentUserContextResolver
@Configuration @Configuration
class WebMvcConfig( class WebMvcConfig(
private val memberIdResolver: MemberIdResolver,
private val authInterceptor: AuthInterceptor,
private val adminInterceptor: AdminInterceptor, private val adminInterceptor: AdminInterceptor,
private val userInterceptor: UserInterceptor, private val userInterceptor: UserInterceptor,
private val authenticatedInterceptor: AuthenticatedInterceptor, private val authenticatedInterceptor: AuthenticatedInterceptor,
@ -22,12 +18,10 @@ class WebMvcConfig(
) : WebMvcConfigurer { ) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) { override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(memberIdResolver)
resolvers.add(currentUserContextResolver) resolvers.add(currentUserContextResolver)
} }
override fun addInterceptors(registry: InterceptorRegistry) { override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(authInterceptor)
registry.addInterceptor(adminInterceptor) registry.addInterceptor(adminInterceptor)
registry.addInterceptor(userInterceptor) registry.addInterceptor(userInterceptor)
registry.addInterceptor(authenticatedInterceptor) registry.addInterceptor(authenticatedInterceptor)

View File

@ -1,7 +1,9 @@
package roomescape.common.entity package roomescape.common.entity
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.domain.Persistable import org.springframework.data.domain.Persistable
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
@ -10,28 +12,24 @@ import kotlin.jvm.Transient
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener::class) @EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity( abstract class AuditingBaseEntity(
id: Long,
) : PersistableBaseEntity(id) {
@Column(updatable = false) @Column(updatable = false)
@CreatedDate @CreatedDate
var createdAt: LocalDateTime? = null, lateinit var createdAt: LocalDateTime
@Column(updatable = false)
@CreatedBy
var createdBy: Long = 0L
@Column
@LastModifiedDate @LastModifiedDate
var lastModifiedAt: LocalDateTime? = null, lateinit var updatedAt: LocalDateTime
) : Persistable<Long> {
@Transient
private var isNewEntity: Boolean = true
@PostLoad
@PostPersist
fun markNotNew() {
isNewEntity = false
}
override fun isNew(): Boolean = isNewEntity
abstract override fun getId(): Long?
@Column
@LastModifiedBy
var updatedBy: Long = 0L
} }
@MappedSuperclass @MappedSuperclass
@ -43,12 +41,13 @@ abstract class PersistableBaseEntity(
@Transient @Transient
private var isNewEntity: Boolean = true private var isNewEntity: Boolean = true
) : Persistable<Long> { ) : Persistable<Long> {
@PostLoad @PostLoad
@PostPersist @PrePersist
fun markNotNew() { fun markNotNew() {
isNewEntity = false isNewEntity = false
} }
override fun isNew(): Boolean = isNewEntity
override fun getId(): Long = _id override fun getId(): Long = _id
override fun isNew(): Boolean = isNewEntity
} }

View File

@ -1,53 +0,0 @@
package roomescape.common.entity
import jakarta.persistence.*
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.domain.Persistable
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime
import kotlin.jvm.Transient
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AuditingBaseEntity(
id: Long,
) : BaseEntityV2(id) {
@Column(updatable = false)
@CreatedDate
lateinit var createdAt: LocalDateTime
@Column(updatable = false)
@CreatedBy
var createdBy: Long = 0L
@Column
@LastModifiedDate
lateinit var updatedAt: LocalDateTime
@Column
@LastModifiedBy
var updatedBy: Long = 0L
}
@MappedSuperclass
abstract class BaseEntityV2(
@Id
@Column(name = "id")
private val _id: Long,
@Transient
private var isNewEntity: Boolean = true
) : Persistable<Long> {
@PostLoad
@PrePersist
fun markNotNew() {
isNewEntity = false
}
override fun getId(): Long = _id
override fun isNew(): Boolean = isNewEntity
}

View File

@ -1,46 +0,0 @@
package roomescape.member.business
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.member.implement.MemberFinder
import roomescape.member.implement.MemberWriter
import roomescape.member.infrastructure.persistence.Role
import roomescape.member.web.*
private val log = KotlinLogging.logger {}
@Service
class MemberService(
private val memberWriter: MemberWriter,
private val memberFinder: MemberFinder,
) {
@Transactional(readOnly = true)
fun findMembers(): MemberRetrieveListResponse {
log.debug { "[MemberService.findMembers] 시작" }
return memberFinder.findAll()
.toRetrieveListResponse()
.also { log.info { "[MemberService.findMembers] 완료. ${it.members.size}명 반환" } }
}
@Transactional(readOnly = true)
fun findSummaryById(id: Long): MemberSummaryRetrieveResponse {
log.debug { "[MemberService.findSummaryById] 시작" }
return memberFinder.findById(id)
.toSummaryRetrieveResponse()
.also {
log.info { "[MemberService.findSummaryById] 완료. memberId=${id}, email=${it.email}" }
}
}
@Transactional
fun createMember(request: SignupRequest): SignupResponse {
log.debug { "[MemberService.createMember] 시작" }
return memberWriter.create(request.name, request.email, request.password, Role.MEMBER)
.toSignupResponse()
.also { log.info { "[MemberService.create] 완료: email=${request.email} memberId=${it.id}" } }
}
}

View File

@ -1,37 +0,0 @@
package roomescape.member.docs
import io.swagger.v3.oas.annotations.Operation
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 org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody
import roomescape.auth.web.support.Admin
import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.web.MemberRetrieveListResponse
import roomescape.member.web.SignupRequest
import roomescape.member.web.SignupResponse
@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.")
interface MemberAPI {
@Admin
@Operation(summary = "모든 회원 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "성공",
useReturnTypeSchema = true
)
)
fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>>
@Operation(summary = "회원 가입")
@ApiResponses(
ApiResponse(
responseCode = "201",
description = "성공",
useReturnTypeSchema = true
)
)
fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>>
}

View File

@ -1,13 +0,0 @@
package roomescape.member.exception
import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode
enum class MemberErrorCode(
override val httpStatus: HttpStatus,
override val errorCode: String,
override val message: String
) : ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.")
}

View File

@ -1,8 +0,0 @@
package roomescape.member.exception
import roomescape.common.exception.RoomescapeException
class MemberException(
override val errorCode: MemberErrorCode,
override val message: String = errorCode.message
) : RoomescapeException(errorCode, message)

View File

@ -1,47 +0,0 @@
package roomescape.member.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
import roomescape.member.exception.MemberErrorCode
import roomescape.member.exception.MemberException
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository
private val log: KLogger = KotlinLogging.logger {}
@Component
class MemberFinder(
private val memberRepository: MemberRepository
) {
fun findAll(): List<MemberEntity> {
log.debug { "[MemberFinder.findAll] 회원 조회 시작" }
return memberRepository.findAll()
.also { log.debug { "[MemberFinder.findAll] 회원 ${it.size}명 조회 완료" } }
}
fun findById(id: Long): MemberEntity {
log.debug { "[MemberFinder.findById] 조회 시작: memberId=$id" }
return memberRepository.findByIdOrNull(id)
?.also { log.debug { "[MemberFinder.findById] 조회 완료: memberId=$id, email=${it.email}" } }
?: run {
log.info { "[MemberFinder.findById] 조회 실패: id=$id" }
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
}
}
fun findByEmailAndPassword(email: String, password: String): MemberEntity {
log.debug { "[MemberFinder.findByEmailAndPassword] 조회 시작: email=$email, password=$password" }
return memberRepository.findByEmailAndPassword(email, password)
?.also { log.debug { "[MemberFinder.findByEmailAndPassword] 조회 완료: email=${email}, memberId=${it.id}" } }
?: run {
log.info { "[MemberFinder.findByEmailAndPassword] 조회 실패: email=${email}, password=${password}" }
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
}
}
}

View File

@ -1,26 +0,0 @@
package roomescape.member.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.member.exception.MemberErrorCode
import roomescape.member.exception.MemberException
import roomescape.member.infrastructure.persistence.MemberRepository
private val log: KLogger = KotlinLogging.logger {}
@Component
class MemberValidator(
private val memberRepository: MemberRepository
) {
fun validateCanSignup(email: String) {
log.debug { "[MemberValidator.validateCanSignup] 시작: email=$email" }
if (memberRepository.existsByEmail(email)) {
log.info { "[MemberValidator.validateCanSignup] 중복 이메일: email=$email" }
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
}
log.debug { "[MemberValidator.validateCanSignup] 완료: email=$email" }
}
}

View File

@ -1,35 +0,0 @@
package roomescape.member.implement
import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.common.config.next
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.infrastructure.persistence.Role
private val log: KLogger = KotlinLogging.logger {}
@Component
class MemberWriter(
private val tsidFactory: TsidFactory,
private val memberValidator: MemberValidator,
private val memberRepository: MemberRepository
) {
fun create(name: String, email: String, password: String, role: Role): MemberEntity {
log.debug { "[MemberWriter.create] 시작: email=$email" }
memberValidator.validateCanSignup(email)
val member = MemberEntity(
_id = tsidFactory.next(),
name = name,
email = email,
password = password,
role = role
)
return memberRepository.save(member)
.also { log.debug { "[MemberWriter.create] 완료: email=$email, memberId=${it.id}" } }
}
}

View File

@ -1,34 +0,0 @@
package roomescape.member.infrastructure.persistence
import jakarta.persistence.*
import roomescape.common.entity.BaseEntity
@Entity
@Table(name = "members")
class MemberEntity(
@Id
@Column(name = "member_id")
private var _id: Long?,
@Column(name = "name", nullable = false)
var name: String,
@Column(name = "email", nullable = false)
var email: String,
@Column(name = "password", nullable = false)
var password: String,
@Column(name = "role", nullable = false, length = 20)
@Enumerated(value = EnumType.STRING)
var role: Role
) : BaseEntity() {
override fun getId(): Long? = _id
fun isAdmin(): Boolean = role == Role.ADMIN
}
enum class Role {
MEMBER,
ADMIN,
}

View File

@ -1,9 +0,0 @@
package roomescape.member.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository
interface MemberRepository : JpaRepository<MemberEntity, Long> {
fun existsByEmail(email: String): Boolean
fun findByEmailAndPassword(email: String, password: String): MemberEntity?
}

View File

@ -1,31 +0,0 @@
package roomescape.member.web
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.business.MemberService
import roomescape.member.docs.MemberAPI
import java.net.URI
@RestController
class MemberController(
private val memberService: MemberService
) : MemberAPI {
@PostMapping("/members")
override fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>> {
val response: SignupResponse = memberService.createMember(request)
return ResponseEntity.created(URI.create("/members/${response.id}"))
.body(CommonApiResponse(response))
}
@GetMapping("/members")
override fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> {
val response: MemberRetrieveListResponse = memberService.findMembers()
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -1,56 +0,0 @@
package roomescape.member.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.Role
fun MemberEntity.toRetrieveResponse(): MemberRetrieveResponse = MemberRetrieveResponse(
id = id!!,
name = name
)
data class MemberRetrieveResponse(
@Schema(description = "회원 식별자")
val id: Long,
@Schema(description = "회원 이름")
val name: String
)
fun List<MemberEntity>.toRetrieveListResponse(): MemberRetrieveListResponse = MemberRetrieveListResponse(
members = this.map { it.toRetrieveResponse() }
)
data class MemberRetrieveListResponse(
val members: List<MemberRetrieveResponse>
)
data class SignupRequest(
val email: String,
val password: String,
val name: String
)
data class SignupResponse(
val id: Long,
val name: String,
)
fun MemberEntity.toSignupResponse(): SignupResponse = SignupResponse(
id = this.id!!,
name = this.name
)
data class MemberSummaryRetrieveResponse(
val id: Long,
val name: String,
val email: String,
val role: Role
)
fun MemberEntity.toSummaryRetrieveResponse() = MemberSummaryRetrieveResponse(
id = this.id!!,
name = this.name,
email = this.email,
role = this.role
)

View File

@ -1,6 +1,5 @@
package roomescape.payment.web package roomescape.payment.web
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
@ -8,7 +7,6 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.auth.web.support.CurrentUser import roomescape.auth.web.support.CurrentUser
import roomescape.auth.web.support.MemberId
import roomescape.common.dto.CurrentUserContext import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.payment.business.PaymentService import roomescape.payment.business.PaymentService

View File

@ -4,7 +4,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.EnumType import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated import jakarta.persistence.Enumerated
import jakarta.persistence.Table import jakarta.persistence.Table
import roomescape.common.entity.BaseEntityV2 import roomescape.common.entity.PersistableBaseEntity
import java.time.LocalDateTime import java.time.LocalDateTime
@Entity @Entity
@ -19,8 +19,7 @@ class CanceledReservationEntity(
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
val status: CanceledReservationStatus, val status: CanceledReservationStatus,
) : PersistableBaseEntity(id)
) : BaseEntityV2(id)
enum class CanceledReservationStatus { enum class CanceledReservationStatus {
PROCESSING, FAILED, COMPLETED PROCESSING, FAILED, COMPLETED

View File

@ -9,7 +9,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService import roomescape.admin.business.AdminService
import roomescape.common.config.next import roomescape.common.config.next
import roomescape.member.business.MemberService
import roomescape.schedule.exception.ScheduleErrorCode import roomescape.schedule.exception.ScheduleErrorCode
import roomescape.schedule.infrastructure.persistence.ScheduleEntity import roomescape.schedule.infrastructure.persistence.ScheduleEntity
import roomescape.schedule.infrastructure.persistence.ScheduleRepository import roomescape.schedule.infrastructure.persistence.ScheduleRepository

View File

@ -4,16 +4,13 @@ import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
import jakarta.validation.Valid import jakarta.validation.Valid
import org.aspectj.internal.lang.annotation.ajcPrivileged
import org.springframework.format.annotation.DateTimeFormat import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import roomescape.admin.infrastructure.persistence.Privilege import roomescape.admin.infrastructure.persistence.Privilege
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.AdminOnly import roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.LoginRequired
import roomescape.auth.web.support.UserOnly import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.schedule.web.* import roomescape.schedule.web.*

View File

@ -8,7 +8,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService import roomescape.admin.business.AdminService
import roomescape.common.config.next import roomescape.common.config.next
import roomescape.member.business.MemberService
import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.exception.ThemeException import roomescape.theme.exception.ThemeException
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity

View File

@ -9,9 +9,7 @@ import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import roomescape.admin.infrastructure.persistence.Privilege import roomescape.admin.infrastructure.persistence.Privilege
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.AdminOnly import roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.LoginRequired
import roomescape.auth.web.support.UserOnly import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.web.* import roomescape.theme.web.*

View File

@ -8,16 +8,6 @@ create table if not exists region (
dong_name varchar(20) not null dong_name varchar(20) not null
); );
create table if not exists members (
member_id bigint primary key,
email varchar(255) not null,
name varchar(255) not null,
password varchar(255) not null,
role varchar(20) not null,
created_at timestamp,
last_modified_at timestamp
);
create table if not exists users( create table if not exists users(
id bigint primary key, id bigint primary key,
name varchar(50) not null, name varchar(50) not null,

View File

@ -18,11 +18,7 @@ import roomescape.payment.infrastructure.persistence.*
import roomescape.payment.web.PaymentConfirmRequest import roomescape.payment.web.PaymentConfirmRequest
import roomescape.payment.web.PaymentCreateResponse import roomescape.payment.web.PaymentCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.util.FunSpecSpringbootTest import roomescape.util.*
import roomescape.util.INVALID_PK
import roomescape.util.PaymentFixture
import roomescape.util.runExceptionTest
import roomescape.util.runTest
class PaymentAPITest( class PaymentAPITest(
@MockkBean @MockkBean

View File

@ -4,8 +4,6 @@ import com.github.f4b6a3.tsid.TsidFactory
import roomescape.admin.infrastructure.persistence.AdminEntity import roomescape.admin.infrastructure.persistence.AdminEntity
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import roomescape.common.config.next import roomescape.common.config.next
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.Role
import roomescape.member.infrastructure.persistence.UserEntity import roomescape.member.infrastructure.persistence.UserEntity
import roomescape.member.infrastructure.persistence.UserStatus import roomescape.member.infrastructure.persistence.UserStatus
import roomescape.member.web.MIN_PASSWORD_LENGTH import roomescape.member.web.MIN_PASSWORD_LENGTH
@ -25,24 +23,6 @@ import java.time.OffsetDateTime
const val INVALID_PK: Long = 9999L const val INVALID_PK: Long = 9999L
val tsidFactory = TsidFactory(0) val tsidFactory = TsidFactory(0)
object MemberFixture {
val admin: MemberEntity = MemberEntity(
_id = 9304,
name = "ADMIN",
email = "admin@example.com",
password = "adminPassword",
role = Role.ADMIN
)
val user: MemberEntity = MemberEntity(
_id = 9305,
name = "USER",
email = "user@example.com",
password = "userPassword",
role = Role.MEMBER
)
}
object AdminFixture { object AdminFixture {
val default: AdminEntity = create() val default: AdminEntity = create()

View File

@ -14,7 +14,6 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ActiveProfiles
import roomescape.admin.infrastructure.persistence.AdminRepository import roomescape.admin.infrastructure.persistence.AdminRepository
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.infrastructure.persistence.UserRepository import roomescape.member.infrastructure.persistence.UserRepository
import roomescape.payment.business.PaymentWriter import roomescape.payment.business.PaymentWriter
import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository
@ -32,9 +31,6 @@ object KotestConfig : AbstractProjectConfig() {
abstract class FunSpecSpringbootTest : FunSpec({ abstract class FunSpecSpringbootTest : FunSpec({
extension(DatabaseCleanerExtension()) extension(DatabaseCleanerExtension())
}) { }) {
@Autowired
private lateinit var memberRepository: MemberRepository
@Autowired @Autowired
private lateinit var userRepository: UserRepository private lateinit var userRepository: UserRepository
@ -51,7 +47,7 @@ abstract class FunSpecSpringbootTest : FunSpec({
override suspend fun beforeSpec(spec: Spec) { override suspend fun beforeSpec(spec: Spec) {
RestAssured.port = port RestAssured.port = port
authUtil = AuthUtil(memberRepository, userRepository, adminRepository) authUtil = AuthUtil(userRepository, adminRepository)
} }
} }

View File

@ -14,57 +14,17 @@ import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import roomescape.admin.infrastructure.persistence.AdminEntity import roomescape.admin.infrastructure.persistence.AdminEntity
import roomescape.admin.infrastructure.persistence.AdminRepository import roomescape.admin.infrastructure.persistence.AdminRepository
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginRequestV2 import roomescape.auth.web.LoginRequestV2
import roomescape.common.config.next
import roomescape.common.dto.PrincipalType import roomescape.common.dto.PrincipalType
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
import roomescape.member.infrastructure.persistence.* import roomescape.member.infrastructure.persistence.UserEntity
import roomescape.member.infrastructure.persistence.UserRepository
import roomescape.member.web.UserCreateRequest import roomescape.member.web.UserCreateRequest
class AuthUtil( class AuthUtil(
private val memberRepository: MemberRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val adminRepository: AdminRepository private val adminRepository: AdminRepository
) { ) {
fun login(email: String, password: String, role: Role = Role.MEMBER): String {
if (!memberRepository.existsByEmail(email)) {
memberRepository.save(
MemberEntity(
_id = tsidFactory.next(),
email = email,
password = password,
name = email.split("@").first(),
role = role
)
)
}
return Given {
contentType(MediaType.APPLICATION_JSON_VALUE)
body(LoginRequest(email, password))
} When {
post("/login")
} Then {
statusCode(200)
} Extract {
path("data.accessToken")
}
}
fun loginAsAdmin(): String {
return login(MemberFixture.admin.email, MemberFixture.admin.password, Role.ADMIN)
}
fun loginAsUser(): String {
return login(MemberFixture.user.email, MemberFixture.user.password)
}
fun getUser(): MemberEntity = memberRepository.findByEmailAndPassword(
MemberFixture.user.email,
MemberFixture.user.password
) ?: throw AssertionError("Unexpected Exception Occurred.")
fun createAdmin(admin: AdminEntity): AdminEntity { fun createAdmin(admin: AdminEntity): AdminEntity {
return adminRepository.save(admin) return adminRepository.save(admin)
} }