Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 회원관련 기능들 추가 #34

Merged
merged 12 commits into from
Feb 28, 2024
11 changes: 11 additions & 0 deletions data/src/main/kotlin/com/kw/data/domain/member/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -31,6 +34,14 @@ class Member(email: String) : Base() {

var memberRoles : MutableList<MemberRoleType> = mutableListOf(MemberRoleType.ROLE_USER)

fun updateMemberNickname(nickname: String) {
this.nickname = nickname
}

fun withdrawMember() {
this.deletedAt = LocalDateTime.now()
}

enum class MemberRoleType {
ROLE_USER,
ROLE_ADMIN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import com.kw.data.domain.member.Member
import org.springframework.data.jpa.repository.JpaRepository

interface MemberRepository : JpaRepository<Member, Long> {
fun findMemberByEmail(email : String) : Member?
fun findMemberByEmail(email : String): Member?

fun existsByNickname(nickname: String): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?,
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kw.infrasecurity.resolver

@Retention(value = AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class AuthToMember {
}
Original file line number Diff line number Diff line change
@@ -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 RuntimeException("forbidden")
val userDetails = authentication.principal as OAuth2UserDetails
val member = memberRepository.findByIdOrNull(userDetails.id) ?: throw RuntimeException("존재하지 않는 회원")

isMemberWithdraw(member)

return member
}

private fun isMemberWithdraw(member: Member) {
if (member.deletedAt != null) {
throw RuntimeException("이미 탈퇴한 회원")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.kw.api.common.dto.response

data class ApiResponse<T>(val message: String, val data: T) {
data class ApiResponse<T>(val message: String, val data: T?) {
companion object {
fun <T> ok(result: T): ApiResponse<T> {
return ApiResponse("ok", result)
Expand All @@ -9,5 +9,9 @@ data class ApiResponse<T>(val message: String, val data: T) {
fun <T> created(result: T): ApiResponse<T> {
return ApiResponse("created", result)
}

fun <T> noContent(): ApiResponse<T> {
return ApiResponse("noContent", null)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ enum class ApiErrorCode(

// member
NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "NOT_FOUND_MEMBER", "존재하지 않는 회원입니다."),
NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "NICKNAME_ALREADY_EXISTS", "이미 존재하는 닉네임입니다."),

// auth
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "REFRESH_TOKEN_EXPIRED", "리프레시 토큰이 만료되었습니다. 다시 로그인 해주세요."),
ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED,"401/0002", "어세스 토큰이 만료되었으니 재발급 해주세요"),
ACCESS_TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "401/0003", "올바르지 않은 토큰입니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "403/0001", "접근이 거부되었습니다."),


// common
BAD_REQUEST(HttpStatus.BAD_REQUEST, "BAD_REQUEST", "잘못된 요청입니다."),

Expand Down
7 changes: 6 additions & 1 deletion server/api/src/main/kotlin/com/kw/api/config/WebMvcConfig.kt
Original file line number Diff line number Diff line change
@@ -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<HandlerMethodArgumentResolver?>) {
resolvers.add(authToMemberArgumentResolver)
}
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOriginPatterns("/**")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MemberInfoResponse> {
val response = memberService.getMemberInfo(userDetails)
fun getUserInfo(@AuthToMember member: Member): ApiResponse<MemberInfoResponse> {
val response = memberService.getMemberInfo(member)
return ApiResponse.ok(response)
}

@Operation(summary = "사용자의 닉네임을 변경합니다.")
@PutMapping("/me/nickname")
fun updateMemberNickname(@AuthToMember member: Member,
@RequestParam nickname: String): ApiResponse<MemberInfoResponse> {
val response = memberService.updateMemberNickname(member, nickname)
return ApiResponse.ok(response)
}

@Operation(summary = "회원탈퇴합니다.")
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/me")
fun withdrawMember(@AuthToMember member: Member): ApiResponse<Nothing> {
val response = memberService.withdrawMember(member)
return ApiResponse.noContent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
}
Loading