generated from pricelees/issue-pr-template
[#34] 회원 / 인증 도메인 재정의 #43
@ -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
|
||||
|
||||
|
||||
@ -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}" }
|
||||
}
|
||||
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
val accessToken: String = jwtHandler.createToken(member.id!!)
|
||||
|
||||
return LoginResponse(accessToken)
|
||||
.also { log.info { "[AuthService.login] 완료: email=${request.email}, memberId=${member.id}" } }
|
||||
}
|
||||
|
||||
@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 when (type) {
|
||||
PrincipalType.ADMIN -> {
|
||||
adminService.findContextById(id)
|
||||
}
|
||||
|
||||
return LoginCheckResponse(member.name, member.role.name)
|
||||
.also { log.info { "[AuthService.checkLogin] 완료: memberId=$memberId, role=${it.role}" } }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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>>
|
||||
}
|
||||
|
||||
@ -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>>
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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}" } }
|
||||
}
|
||||
}
|
||||
@ -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>>
|
||||
}
|
||||
@ -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", "이미 가입된 이메일이에요.")
|
||||
}
|
||||
@ -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)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" }
|
||||
}
|
||||
}
|
||||
@ -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}" } }
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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?
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.*
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.*
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user