diff --git a/data/src/main/kotlin/com/kw/data/domain/member/Member.kt b/data/src/main/kotlin/com/kw/data/domain/member/Member.kt index 06a0dab..a166760 100644 --- a/data/src/main/kotlin/com/kw/data/domain/member/Member.kt +++ b/data/src/main/kotlin/com/kw/data/domain/member/Member.kt @@ -2,9 +2,11 @@ package com.kw.data.domain.member import com.kw.data.domain.Base import jakarta.persistence.* +import org.hibernate.annotations.SQLRestriction import java.time.LocalDateTime @Entity +@SQLRestriction("deleted_at is null") class Member(email: String) : Base() { @Id @Column(name = "id", nullable = false, updatable = false) @@ -13,6 +15,7 @@ class Member(email: String) : Base() { @Column(name = "nickname") var nickname: String? = null + protected set @Column(name = "email", nullable = false, updatable = false) val email: String = email @@ -31,6 +34,14 @@ class Member(email: String) : Base() { var memberRoles : MutableList = mutableListOf(MemberRoleType.ROLE_USER) + fun updateMemberNickname(nickname: String) { + this.nickname = nickname + } + + fun withdrawMember() { + this.deletedAt = LocalDateTime.now() + } + enum class MemberRoleType { ROLE_USER, ROLE_ADMIN diff --git a/data/src/main/kotlin/com/kw/data/domain/member/repository/MemberRepository.kt b/data/src/main/kotlin/com/kw/data/domain/member/repository/MemberRepository.kt index adabf75..d5cdbfa 100644 --- a/data/src/main/kotlin/com/kw/data/domain/member/repository/MemberRepository.kt +++ b/data/src/main/kotlin/com/kw/data/domain/member/repository/MemberRepository.kt @@ -4,5 +4,7 @@ import com.kw.data.domain.member.Member import org.springframework.data.jpa.repository.JpaRepository interface MemberRepository : JpaRepository { - fun findMemberByEmail(email : String) : Member? + fun findMemberByEmail(email : String): Member? + + fun existsByNickname(nickname: String): Boolean } diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt index 9d702b0..b37cb1c 100644 --- a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/oauth/OAuth2SuccessHandler.kt @@ -2,6 +2,7 @@ package com.kw.infrasecurity.oauth import com.kw.data.domain.member.Member import com.kw.data.domain.member.repository.MemberRepository +import com.kw.infraredis.repository.RedisRefreshTokenRepository import com.kw.infrasecurity.jwt.JwtTokenProvider import com.kw.infrasecurity.util.HttpResponseUtil import jakarta.servlet.http.HttpServletRequest @@ -16,7 +17,8 @@ import org.springframework.transaction.annotation.Transactional @Component @Transactional class OAuth2SuccessHandler(private val jwtTokenProvider: JwtTokenProvider, - private val memberRepository: MemberRepository) : AuthenticationSuccessHandler { + private val memberRepository: MemberRepository, + private val redisRefreshTokenRepository: RedisRefreshTokenRepository) : AuthenticationSuccessHandler { override fun onAuthenticationSuccess( request: HttpServletRequest?, @@ -46,7 +48,7 @@ class OAuth2SuccessHandler(private val jwtTokenProvider: JwtTokenProvider, val accessToken = jwtTokenProvider.generateAccessToken(oAuth2UserDetails) val refreshToken = jwtTokenProvider.generateRefreshToken() - // TODO 리프레시 토큰 레디스 저장 + redisRefreshTokenRepository.save(refreshToken = refreshToken, memberId = member.id!!) HttpResponseUtil.writeResponse(response!!, accessToken, refreshToken) } diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt new file mode 100644 index 0000000..661bfee --- /dev/null +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMember.kt @@ -0,0 +1,6 @@ +package com.kw.infrasecurity.resolver + +@Retention(value = AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class AuthToMember { +} diff --git a/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt new file mode 100644 index 0000000..5f937e6 --- /dev/null +++ b/infra/infra-security/src/main/kotlin/com/kw/infrasecurity/resolver/AuthToMemberArgumentResolver.kt @@ -0,0 +1,45 @@ +package com.kw.infrasecurity.resolver + +import com.kw.data.domain.member.Member +import com.kw.data.domain.member.repository.MemberRepository +import com.kw.infrasecurity.oauth.OAuth2UserDetails +import org.springframework.core.MethodParameter +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +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 + +@Component +@Transactional +class AuthToMemberArgumentResolver(val memberRepository: MemberRepository): HandlerMethodArgumentResolver { + override fun supportsParameter(parameter: MethodParameter): Boolean { + val hasParameterAnnotation = parameter.hasParameterAnnotation(AuthToMember::class.java) + val hasLongParameterType = parameter.parameterType.isAssignableFrom(Member::class.java) + return hasParameterAnnotation && hasLongParameterType + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Any? { + val authentication = SecurityContextHolder.getContext().authentication ?: throw IllegalArgumentException("접근이 거부되었습니다.") + val userDetails = authentication.principal as OAuth2UserDetails + val member = memberRepository.findByIdOrNull(userDetails.id) ?: throw IllegalArgumentException("존재하지 않는 회원입니다.") + + isMemberWithdraw(member) + + return member + } + + private fun isMemberWithdraw(member: Member) { + if (member.deletedAt != null) { + throw IllegalArgumentException("탈퇴한 회원입니다.") + } + } +} diff --git a/server/api/src/main/kotlin/com/kw/api/common/dto/response/ApiResponse.kt b/server/api/src/main/kotlin/com/kw/api/common/dto/response/ApiResponse.kt index a17f50d..a90cb67 100644 --- a/server/api/src/main/kotlin/com/kw/api/common/dto/response/ApiResponse.kt +++ b/server/api/src/main/kotlin/com/kw/api/common/dto/response/ApiResponse.kt @@ -1,6 +1,6 @@ package com.kw.api.common.dto.response -data class ApiResponse(val message: String, val data: T) { +data class ApiResponse(val message: String, val data: T?) { companion object { fun ok(result: T): ApiResponse { return ApiResponse("ok", result) @@ -9,5 +9,9 @@ data class ApiResponse(val message: String, val data: T) { fun created(result: T): ApiResponse { return ApiResponse("created", result) } + + fun noContent(): ApiResponse { + return ApiResponse("noContent", null) + } } } diff --git a/server/api/src/main/kotlin/com/kw/api/common/exception/ApiErrorCode.kt b/server/api/src/main/kotlin/com/kw/api/common/exception/ApiErrorCode.kt index b307759..6990f1b 100644 --- a/server/api/src/main/kotlin/com/kw/api/common/exception/ApiErrorCode.kt +++ b/server/api/src/main/kotlin/com/kw/api/common/exception/ApiErrorCode.kt @@ -21,6 +21,8 @@ enum class ApiErrorCode( // member NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "NOT_FOUND_MEMBER", "존재하지 않는 회원입니다."), + NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "NICKNAME_ALREADY_EXISTS", "이미 존재하는 닉네임입니다."), + MEMBER_WITHDRAWN(HttpStatus.BAD_REQUEST, "MEMBER_WITHDRAWN", "탈퇴한 회원입니다."), // auth REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "REFRESH_TOKEN_EXPIRED", "리프레시 토큰이 만료되었습니다. 다시 로그인 해주세요."), @@ -28,7 +30,6 @@ enum class ApiErrorCode( ACCESS_TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "401/0003", "올바르지 않은 토큰입니다."), ACCESS_DENIED(HttpStatus.FORBIDDEN, "403/0001", "접근이 거부되었습니다."), - // common BAD_REQUEST(HttpStatus.BAD_REQUEST, "BAD_REQUEST", "잘못된 요청입니다."), diff --git a/server/api/src/main/kotlin/com/kw/api/config/WebMvcConfig.kt b/server/api/src/main/kotlin/com/kw/api/config/WebMvcConfig.kt index 4e1cbe8..8eda68d 100644 --- a/server/api/src/main/kotlin/com/kw/api/config/WebMvcConfig.kt +++ b/server/api/src/main/kotlin/com/kw/api/config/WebMvcConfig.kt @@ -1,12 +1,17 @@ package com.kw.api.config +import com.kw.infrasecurity.resolver.AuthToMemberArgumentResolver import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration -class WebMvcConfig : WebMvcConfigurer { +class WebMvcConfig(val authToMemberArgumentResolver: AuthToMemberArgumentResolver) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(authToMemberArgumentResolver) + } override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") .allowedOriginPatterns("/**") diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt index 3bff06f..9f866c3 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/controller/MemberController.kt @@ -3,23 +3,38 @@ package com.kw.api.domain.member.controller import com.kw.api.common.dto.response.ApiResponse import com.kw.api.domain.member.dto.response.MemberInfoResponse import com.kw.api.domain.member.service.MemberService -import com.kw.infrasecurity.oauth.OAuth2UserDetails +import com.kw.data.domain.member.Member +import com.kw.infrasecurity.resolver.AuthToMember import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* @Tag(name = "회원", description = "회원 관련 API 입니다.") @RestController -@RequestMapping("/api/v1/member") +@RequestMapping("/api/v1/members") class MemberController(private val memberService: MemberService) { @Operation(summary = "로그인된 사용자의 정보를 가져옵니다.") @GetMapping("/me") - fun getUserInfo(@AuthenticationPrincipal userDetails : OAuth2UserDetails?) : ApiResponse { - val response = memberService.getMemberInfo(userDetails) + fun getUserInfo(@AuthToMember member: Member): ApiResponse { + val response = memberService.getMemberInfo(member) return ApiResponse.ok(response) } + + @Operation(summary = "사용자의 닉네임을 변경합니다.") + @PutMapping("/me/nickname") + fun updateMemberNickname(@AuthToMember member: Member, + @RequestParam nickname: String): ApiResponse { + val response = memberService.updateMemberNickname(member, nickname) + return ApiResponse.ok(response) + } + + @Operation(summary = "회원탈퇴합니다.") + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/me") + fun withdrawMember(@AuthToMember member: Member): ApiResponse { + val response = memberService.withdrawMember(member) + return ApiResponse.noContent() + } } diff --git a/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt b/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt index 488b5ad..f9d2ef5 100644 --- a/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt +++ b/server/api/src/main/kotlin/com/kw/api/domain/member/service/MemberService.kt @@ -3,16 +3,31 @@ package com.kw.api.domain.member.service import com.kw.api.common.exception.ApiErrorCode import com.kw.api.common.exception.ApiException import com.kw.api.domain.member.dto.response.MemberInfoResponse +import com.kw.data.domain.member.Member import com.kw.data.domain.member.repository.MemberRepository -import com.kw.infrasecurity.oauth.OAuth2UserDetails import org.springframework.stereotype.Service @Service class MemberService(val memberRepository: MemberRepository) { - fun getMemberInfo(userDetails: OAuth2UserDetails?) : MemberInfoResponse{ - val email = userDetails?.email ?: throw ApiException(ApiErrorCode.ACCESS_DENIED) - val member = memberRepository.findMemberByEmail(email) ?: throw ApiException(ApiErrorCode.NOT_FOUND_MEMBER) + fun getMemberInfo(member: Member): MemberInfoResponse{ return MemberInfoResponse.from(member) } + + fun updateMemberNickname(member: Member, nickname: String): MemberInfoResponse { + isNicknameUnique(nickname) + member.updateMemberNickname(nickname) + return MemberInfoResponse.from(member) + } + + fun withdrawMember(member: Member) { + member.withdrawMember() + } + + private fun isNicknameUnique(nickname: String) { + if(!memberRepository.existsByNickname(nickname)){ + throw ApiException(ApiErrorCode.NICKNAME_ALREADY_EXISTS) + } + + } }