[#34] 회원 / 인증 도메인 재정의 #43

Merged
pricelees merged 73 commits from refactor/#34 into main 2025-09-13 10:13:45 +00:00
40 changed files with 174 additions and 1025 deletions
Showing only changes of commit 40d687f7f2 - Show all commits

View File

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

View File

@ -4,55 +4,111 @@ 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.JwtHandler
import roomescape.auth.web.LoginCheckResponse
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginResponse
import roomescape.member.implement.MemberFinder
import roomescape.member.infrastructure.persistence.MemberEntity
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 AuthService(
private val memberFinder: MemberFinder,
private val jwtHandler: JwtHandler,
private val adminService: AdminService,
private val userService: UserService,
private val loginHistoryService: LoginHistoryService,
private val jwtUtils: JwtUtils,
) {
@Transactional(readOnly = true)
fun login(request: LoginRequest): LoginResponse {
val params = "email=${request.email}, password=${request.password}"
log.debug { "[AuthService.login] 시작: $params" }
fun login(
request: LoginRequestV2,
context: LoginContext
): LoginSuccessResponse {
log.info { "[AuthService.login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" }
val member: MemberEntity = fetchOrThrow(AuthErrorCode.LOGIN_FAILED) {
memberFinder.findByEmailAndPassword(request.email, request.password)
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}" }
}
val accessToken: String = jwtHandler.createToken(member.id!!)
return LoginResponse(accessToken)
.also { log.info { "[AuthService.login] 완료: email=${request.email}, memberId=${member.id}" } }
} 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 checkLogin(memberId: Long): LoginCheckResponse {
log.debug { "[AuthService.checkLogin] 시작: memberId=$memberId" }
fun findContextById(id: Long, type: PrincipalType): CurrentUserContext {
log.info { "[AuthService.checkLogin] 로그인 확인 시작: id=${id}, type=${type}" }
val member: MemberEntity = fetchOrThrow(AuthErrorCode.MEMBER_NOT_FOUND) { memberFinder.findById(memberId) }
return LoginCheckResponse(member.name, member.role.name)
.also { log.info { "[AuthService.checkLogin] 완료: memberId=$memberId, role=${it.role}" } }
return when (type) {
PrincipalType.ADMIN -> {
adminService.findContextById(id)
}
private fun fetchOrThrow(errorCode: AuthErrorCode, block: () -> MemberEntity): MemberEntity {
try {
return block()
} catch (e: Exception) {
throw AuthException(errorCode, e.message ?: errorCode.message)
PrincipalType.USER -> {
userService.findContextById(id)
}
}.also {
log.info { "[AuthService.checkLogin] 로그인 확인 완료: id=${id}, type=${type}" }
}
}
fun logout(memberId: Long) {
log.info { "[AuthService.logout] 로그아웃: memberId=$memberId" }
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,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
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.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.LoginCheckResponse
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginResponse
import roomescape.auth.web.support.LoginRequired
import roomescape.auth.web.support.MemberId
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 AuthAPI {
@Public
@Operation(summary = "로그인")
@ApiResponses(
ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."),
)
fun login(
@Valid @RequestBody loginRequest: LoginRequest
): ResponseEntity<CommonApiResponse<LoginResponse>>
@Valid @RequestBody loginRequest: LoginRequestV2,
servletRequest: HttpServletRequest
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>>
@Operation(summary = "로그인 상태 확인")
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "로그인 상태이며, 로그인된 회원의 이름 / 권한을 반환합니다.",
description = "입력된 ID / 결과(Boolean)을 반환합니다.",
useReturnTypeSchema = true
),
)
fun checkLogin(
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<LoginCheckResponse>>
@CurrentUser user: CurrentUserContext
): ResponseEntity<CommonApiResponse<CurrentUserContext>>
@LoginRequired
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
@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
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import roomescape.auth.business.AuthService
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
@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AuthService
private val authService: AuthService,
) : AuthAPI {
@PostMapping("/login")
override fun login(
@Valid @RequestBody loginRequest: LoginRequest,
): ResponseEntity<CommonApiResponse<LoginResponse>> {
val response: LoginResponse = authService.login(loginRequest)
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(
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<LoginCheckResponse>> {
val response: LoginCheckResponse = authService.checkLogin(memberId)
return ResponseEntity.ok(CommonApiResponse(response))
@CurrentUser user: CurrentUserContext,
): ResponseEntity<CommonApiResponse<CurrentUserContext>> {
return ResponseEntity.ok(CommonApiResponse(user))
}
@PostMapping("/logout")
override fun logout(@MemberId memberId: Long): ResponseEntity<CommonApiResponse<Unit>> {
authService.logout(memberId)
return ResponseEntity.noContent().build()
override fun logout(
@CurrentUser user: CurrentUserContext,
servletResponse: HttpServletResponse
): 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
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.servlet.http.HttpServletRequest
import roomescape.common.dto.PrincipalType
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
)
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
@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)
@Retention(AnnotationRetention.RUNTIME)
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.web.method.HandlerMethod
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.web.support.Authenticated
import roomescape.auth.web.support.accessToken
@ -17,7 +17,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Component
class AuthenticatedInterceptor(
private val jwtUtils: JwtUtils,
private val authService: AuthServiceV2
private val authService: AuthService
) : HandlerInterceptor {
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.method.support.HandlerMethodArgumentResolver
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.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils
@ -21,7 +21,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Component
class CurrentUserContextResolver(
private val jwtUtils: JwtUtils,
private val authService: AuthServiceV2
private val authService: AuthService
) : HandlerMethodArgumentResolver {
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.servlet.config.annotation.InterceptorRegistry
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.AuthenticatedInterceptor
import roomescape.auth.web.support.interceptors.UserInterceptor
@ -13,8 +11,6 @@ import roomescape.auth.web.support.resolver.CurrentUserContextResolver
@Configuration
class WebMvcConfig(
private val memberIdResolver: MemberIdResolver,
private val authInterceptor: AuthInterceptor,
private val adminInterceptor: AdminInterceptor,
private val userInterceptor: UserInterceptor,
private val authenticatedInterceptor: AuthenticatedInterceptor,
@ -22,12 +18,10 @@ class WebMvcConfig(
) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(memberIdResolver)
resolvers.add(currentUserContextResolver)
}
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(authInterceptor)
registry.addInterceptor(adminInterceptor)
registry.addInterceptor(userInterceptor)
registry.addInterceptor(authenticatedInterceptor)

View File

@ -1,7 +1,9 @@
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
@ -10,28 +12,24 @@ import kotlin.jvm.Transient
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity(
abstract class AuditingBaseEntity(
id: Long,
) : PersistableBaseEntity(id) {
@Column(updatable = false)
@CreatedDate
var createdAt: LocalDateTime? = null,
lateinit var createdAt: LocalDateTime
@Column(updatable = false)
@CreatedBy
var createdBy: Long = 0L
@Column
@LastModifiedDate
var lastModifiedAt: LocalDateTime? = null,
) : Persistable<Long> {
@Transient
private var isNewEntity: Boolean = true
@PostLoad
@PostPersist
fun markNotNew() {
isNewEntity = false
}
override fun isNew(): Boolean = isNewEntity
abstract override fun getId(): Long?
lateinit var updatedAt: LocalDateTime
@Column
@LastModifiedBy
var updatedBy: Long = 0L
}
@MappedSuperclass
@ -43,12 +41,13 @@ abstract class PersistableBaseEntity(
@Transient
private var isNewEntity: Boolean = true
) : Persistable<Long> {
@PostLoad
@PostPersist
@PrePersist
fun markNotNew() {
isNewEntity = false
}
override fun isNew(): Boolean = isNewEntity
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
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
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.RestController
import roomescape.auth.web.support.CurrentUser
import roomescape.auth.web.support.MemberId
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.response.CommonApiResponse
import roomescape.payment.business.PaymentService

View File

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

View File

@ -9,7 +9,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.common.config.next
import roomescape.member.business.MemberService
import roomescape.schedule.exception.ScheduleErrorCode
import roomescape.schedule.infrastructure.persistence.ScheduleEntity
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.ApiResponses
import jakarta.validation.Valid
import org.aspectj.internal.lang.annotation.ajcPrivileged
import org.springframework.format.annotation.DateTimeFormat
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.Privilege
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.LoginRequired
import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.response.CommonApiResponse
import roomescape.schedule.web.*

View File

@ -8,7 +8,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.common.config.next
import roomescape.member.business.MemberService
import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.exception.ThemeException
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.RequestBody
import roomescape.admin.infrastructure.persistence.Privilege
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.LoginRequired
import roomescape.auth.web.support.UserOnly
import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.web.*

View File

@ -8,16 +8,6 @@ create table if not exists region (
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(
id bigint primary key,
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.PaymentCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.util.FunSpecSpringbootTest
import roomescape.util.INVALID_PK
import roomescape.util.PaymentFixture
import roomescape.util.runExceptionTest
import roomescape.util.runTest
import roomescape.util.*
class PaymentAPITest(
@MockkBean

View File

@ -4,8 +4,6 @@ import com.github.f4b6a3.tsid.TsidFactory
import roomescape.admin.infrastructure.persistence.AdminEntity
import roomescape.admin.infrastructure.persistence.AdminPermissionLevel
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.UserStatus
import roomescape.member.web.MIN_PASSWORD_LENGTH
@ -25,24 +23,6 @@ import java.time.OffsetDateTime
const val INVALID_PK: Long = 9999L
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 {
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.test.context.ActiveProfiles
import roomescape.admin.infrastructure.persistence.AdminRepository
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.infrastructure.persistence.UserRepository
import roomescape.payment.business.PaymentWriter
import roomescape.payment.infrastructure.persistence.PaymentRepository
@ -32,9 +31,6 @@ object KotestConfig : AbstractProjectConfig() {
abstract class FunSpecSpringbootTest : FunSpec({
extension(DatabaseCleanerExtension())
}) {
@Autowired
private lateinit var memberRepository: MemberRepository
@Autowired
private lateinit var userRepository: UserRepository
@ -51,7 +47,7 @@ abstract class FunSpecSpringbootTest : FunSpec({
override suspend fun beforeSpec(spec: Spec) {
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 roomescape.admin.infrastructure.persistence.AdminEntity
import roomescape.admin.infrastructure.persistence.AdminRepository
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.LoginRequestV2
import roomescape.common.config.next
import roomescape.common.dto.PrincipalType
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
class AuthUtil(
private val memberRepository: MemberRepository,
private val userRepository: UserRepository,
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 {
return adminRepository.save(admin)
}