Skip to content

review #3

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

Open
wants to merge 42 commits into
base: single-module-review
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7c8b671
Feature: 엔티티 생성
yoonseon12 Jul 4, 2024
ebe188c
Feature: 회원가입 구현
yoonseon12 Jul 4, 2024
d373c35
Chore: @Colume 명시
yoonseon12 Jul 6, 2024
581f43f
Feature: 공통 성공응답 구현
yoonseon12 Jul 6, 2024
5dcc2b7
Feature: 예외처리 구현
yoonseon12 Jul 6, 2024
aa4695e
Test: 회원가입 유효성 검증 테스트 구현
yoonseon12 Jul 6, 2024
5930d8d
Feature: BaseEntity 생성
yoonseon12 Jul 6, 2024
09badc9
Bug: 회원가입 권한 누락 오류 수정
yoonseon12 Jul 6, 2024
3d4b2a7
Feature: API 버저닝 적용
yoonseon12 Jul 9, 2024
105a7a3
Feature: @Validated 및 예외처리 적용
yoonseon12 Jul 9, 2024
0a0fc18
Feature: 커스텀 검증 어노테이션 적용
yoonseon12 Jul 9, 2024
e4d6ea3
Feature: messages.properties 추출
yoonseon12 Jul 9, 2024
015b38a
Test: 회원가입 유효성 검증 컨트롤러 테스트 추가
yoonseon12 Jul 9, 2024
55fa97a
Chore: messages.properties 인코딩
yoonseon12 Jul 9, 2024
9110374
Chore: messages.properties 인코딩
yoonseon12 Jul 9, 2024
22c5113
Feature: 시큐리티 및 JWT 설
yoonseon12 Jul 10, 2024
097efac
Chore: JPARepository 구현계층으로 이동
yoonseon12 Jul 10, 2024
7049c03
Refactor: Repository 어댑터 패턴 적용
yoonseon12 Jul 12, 2024
e832310
Fix: 누락된 filterChain.doFilter 적용
yoonseon12 Jul 12, 2024
970a3d1
Chore: 세미콜론 제거
yoonseon12 Jul 12, 2024
45104f5
Feature: 회원 로그인 구현
yoonseon12 Jul 13, 2024
e377932
Test: AutoConfigureMockMvc 제거 및 SecurityConfig 설정으로 변경
yoonseon12 Jul 13, 2024
2c30790
Feature: 회원가입 메일전송 구현
yoonseon12 Jul 13, 2024
a922357
Refactor: 회원가입 메일전송 Async 이벤트 리스너로 변경
yoonseon12 Jul 14, 2024
c34e847
Dose: Readme 작성
yoonseon12 Jul 14, 2024
4dedf1c
Fix: 잘못된 메서드 체이닝 변경 parseClaimsJwt -> parseClaimsJws
yoonseon12 Jul 16, 2024
0038202
Feature: 회원정보 조회 구현
yoonseon12 Jul 16, 2024
2a87268
Docs: http 파일명 변경
yoonseon12 Jul 16, 2024
4e96334
Feature: 본인확인 유효성 검사 AOP로 분리
yoonseon12 Jul 16, 2024
78e789e
Test: OnlyOwnerAllowedAspectTest
yoonseon12 Jul 16, 2024
9be7658
Chore: add mock bean memberInfoService
yoonseon12 Jul 16, 2024
d773d6e
Refactor: 회원정보 조회 fetch join 적용
yoonseon12 Jul 16, 2024
bac3d1b
Test: 응용계층 Member 도메인 Test 추가
yoonseon12 Jul 16, 2024
fee98d4
Test: 응용계층 Member 도메인 Test 추가
yoonseon12 Jul 16, 2024
b80cfb5
Test: 도메인계층 Member 도메인 Test 추가
yoonseon12 Jul 16, 2024
987842d
Merge branch 'main' of https://github.com/yoonseon12/kotlin-api-server
yoonseon12 Jul 16, 2024
2b423d3
Feature: 비밀번호 초기화 구현
yoonseon12 Jul 17, 2024
27b724c
Chore: @ValidEmail 어노테이션 추가
yoonseon12 Jul 17, 2024
b3633df
Refactor: 도메인_DTO -> UI_DTO 의존성 역전
yoonseon12 Jul 17, 2024
6b5375f
Test: MemberResetService.resetPassword 단위 테스트 추가
yoonseon12 Jul 17, 2024
e382b90
Refactor: add findByIdOrElseNull
yoonseon12 Aug 3, 2024
0f3ebc4
Docs: add DS_Store in gitignore
yoonseon12 Aug 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ out/

### Kotlin ###
.kotlin

### mac OS ###
**/.DS_Store

/src/main/resources/application-mail.yml
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# kotlin-api-server

### 🔥 개요

- [코틀린 스터디](https://github.com/brdm-study/atomic-kotlin-study)와 개인 학습 내용을 기반으로
[java-api-server](https://github.com/yoonseon12/java-api-server) 프로젝트를 코틀린으로 마이그레이션 했습니다.

### 🔥 프로젝트 구현 내용 포스팅

- 💡 [@TransactionEventListener의 phase 옵션을 주의해서 사용하자](https://yoonseon.tistory.com/157)
- 💡 [스프링+코틀린에서 @Valid이 동작하지 않는 경우](https://yoonseon.tistory.com/156)
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,27 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'

// kotlin
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
implementation 'org.jetbrains.kotlin:kotlin-reflect'

// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// H2
runtimeOnly 'com.h2database:h2'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
4 changes: 4 additions & 0 deletions http/get_memberInfo.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GET localhost:8080/api/members/1
x-api-version: v1
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ5b29uc2VvbjNAZ21haWwuY29tIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTcyMTEyMzQ4MH0.3-MFaAiWwAgICiTFxej4E-BvBNjoAhVD_8s6igLl-6U1jLcxEJwv-YE0v3hmTlbegQeOcfH8WHnFMp9vvr-UKA
7 changes: 7 additions & 0 deletions http/post_resetPassword.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POST localhost:8080/api/members/reset-password
x-api-version: v1
Content-Type: application/json

{
"email": "[email protected]"
}
8 changes: 8 additions & 0 deletions http/post_signin.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
POST localhost:8080/api/members/signin
x-api-version: v1
Content-Type: application/json

{
"email": "[email protected]",
"password": "aa1234@@@@"
}
9 changes: 9 additions & 0 deletions http/post_signup.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
POST localhost:8080/api/members
x-api-version: v1
Content-Type: application/json

{
"nickname": "Kevin3",
"email": "[email protected]",
"password": "aa1234@@@@"
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.study.kotlinapiserver

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication
@ConfigurationPropertiesScan
class KotlinApiServerApplication

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.study.kotlinapiserver.api.domain.member.application

import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class MemberInfoService(

private val memberDomainService: MemberDomainService,

) {

@Transactional(readOnly = true)
fun getMemberInfo(id: Long): MemberInfoResponse {
return memberDomainService.getInfo(id)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.study.kotlinapiserver.api.domain.member.application

import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
import io.study.kotlinapiserver.api.domain.member.domain.event.ResetPasswordEvent
import io.study.kotlinapiserver.web.base.event.Events
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.security.SecureRandom
import java.util.*

@Service
@Transactional(readOnly = true)
class MemberResetService(

private val memberDomainService: MemberDomainService,

) {
companion object {
private const val VALID_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
private const val SPECIAL_CHARACTERS = "!@#$%^&*()\\-_=+"
private const val MAX_SPECIAL_CHARACTER = 3;
private const val MIN_LENGTH = 8;
private const val MAX_LENGTH = 16;
}

@Transactional
fun resetPassword(request: MemberResetPasswordRequest) {
val tempPassword = createTempPassword()

memberDomainService.resetPassword(MemberResetPasswordRequest(request.email, tempPassword))

/** 비밀번호 변경 메일 전송 **/
publishResetPasswordSuccessEmailEvent(tempPassword, request.email)
}

private fun createTempPassword(): String {
val random = SecureRandom()
val passwordLength = MIN_LENGTH + random.nextInt(MAX_LENGTH - MIN_LENGTH + 1)
val password = StringBuilder(passwordLength)

val specialCharacterLength = addSpecialCharacters(password)
addValidCharacters(specialCharacterLength, passwordLength, password)

return shufflePassword(password)
}

private fun publishResetPasswordSuccessEmailEvent(
password: String,
registeredEmail: String,
) {
Events.raise(ResetPasswordEvent.of(password, registeredEmail))
}

private fun shufflePassword(password: StringBuilder): String {
val map = password.toList()
Collections.shuffle(map)
return map.joinToString("")
}

private fun addSpecialCharacters(password: StringBuilder): Int {
val random = SecureRandom()
val specialCharacterLength = random.nextInt(MAX_SPECIAL_CHARACTER) + 1

for (i in 0 until specialCharacterLength) {
password.append(SPECIAL_CHARACTERS[random.nextInt(SPECIAL_CHARACTERS.length)])
}

return specialCharacterLength
}

private fun addValidCharacters(
specialCharacterLength: Int,
passwordLength: Int,
password: StringBuilder
) {
val random = SecureRandom()
for (i in specialCharacterLength until passwordLength) {
val index = random.nextInt(VALID_CHARACTERS.length)
password.append(VALID_CHARACTERS[index])
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.study.kotlinapiserver.api.domain.member.application

import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSigninRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSigninResponse
import io.study.kotlinapiserver.web.jwt.JwtProvider
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class MemberSigninService(
private val jwtProvider: JwtProvider,
private val authenticationManagerBuilder: AuthenticationManagerBuilder,
) {

fun signin(request: MemberSigninRequest): MemberSigninResponse {
val authenticationToken = UsernamePasswordAuthenticationToken(request.email, request.password)
val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
val token = jwtProvider.createToken(authentication)

return MemberSigninResponse.of(token)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.study.kotlinapiserver.api.domain.member.application

import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse
import io.study.kotlinapiserver.api.domain.member.domain.event.SignupEvent
import io.study.kotlinapiserver.web.base.event.Events
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class MemberSignupService(
private val memberDomainService: MemberDomainService,
) {

@Transactional
fun signup(command: MemberSignupRequest): MemberSignupResponse {
/** 회원가입 **/
val savedInfo = memberDomainService.register(command)

/** 이메일 전송 **/
publishSignupSuccessEmailEvent(savedInfo.email)

return savedInfo
}

private fun publishSignupSuccessEmailEvent(registeredEmail: String) {
Events.raise(SignupEvent.of(registeredEmail))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.study.kotlinapiserver.api.domain.member.domain

import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse

interface MemberDomainService {

fun register(request: MemberSignupRequest): MemberSignupResponse

fun getInfo(id: Long): MemberInfoResponse

fun resetPassword(request: MemberResetPasswordRequest)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.study.kotlinapiserver.api.domain.member.domain

import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse
import io.study.kotlinapiserver.api.domain.member.domain.entity.Member
import io.study.kotlinapiserver.api.domain.member.domain.repository.MemberQueryRepository
import io.study.kotlinapiserver.api.domain.member.domain.repository.MemberRepository
import io.study.kotlinapiserver.api.domain.member.domain.validation.MemberValidator
import io.study.kotlinapiserver.web.exception.ApiException
import io.study.kotlinapiserver.web.exception.error.MemberErrorCode
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class MemberDomainServiceImpl(

private val memberRepository: MemberRepository,
private val memberQueryRepository: MemberQueryRepository,
private val memberValidator: MemberValidator,
private val passwordEncoder: PasswordEncoder,

) : MemberDomainService {

override fun register(request: MemberSignupRequest): MemberSignupResponse {
memberValidator.signinValidate(request)
val initBasicMember = Member.createBasicMember(request.email, request.nickname, encodePassword(request.password))
val savedMember = memberRepository.save(initBasicMember)

return MemberSignupResponse(savedMember.email)
}

override fun getInfo(id: Long): MemberInfoResponse {
val findMember = memberQueryRepository.findByIdWithAuthorities(id)
?: throw ApiException(MemberErrorCode.NOT_FOUND_MEMBER)

return MemberInfoResponse(
email = findMember.email,
nickname = findMember.nickname,
roles = findMember.authorities.map { it.authority }
)
}

override fun resetPassword(request: MemberResetPasswordRequest) {
val findMember = memberQueryRepository.findByEmail(request.email)
?: throw ApiException(MemberErrorCode.NOT_FOUND_MEMBER)

findMember.changePassword(encodePassword(request.tempPassword!!))
}

private fun encodePassword(password: String): String {
return passwordEncoder.encode(password)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.study.kotlinapiserver.api.domain.member.domain.dto.request

data class MemberResetPasswordRequest(
val email: String,
val tempPassword: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.study.kotlinapiserver.api.domain.member.domain.dto.request

import io.study.kotlinapiserver.api.domain.member.ui.dto.request.PostMemberSigninRequest

data class MemberSigninRequest(
val email: String,
val password: String,
) {
companion object {
fun of(request: PostMemberSigninRequest): MemberSigninRequest {
return MemberSigninRequest(
email = request.email,
password = request.password,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.study.kotlinapiserver.api.domain.member.domain.dto.request

import io.study.kotlinapiserver.api.domain.member.ui.dto.request.PostMemberSignupRequest

data class MemberSignupRequest(
val nickname: String,
val email: String,
val password: String,
) {
companion object {
fun of(request: PostMemberSignupRequest): MemberSignupRequest {
return MemberSignupRequest(
nickname = request.nickname,
email = request.email,
password = request.password,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.study.kotlinapiserver.api.domain.member.domain.dto.response

import io.study.kotlinapiserver.api.domain.member.domain.entity.authority.AuthorityType

data class MemberInfoResponse(
val email: String,
val nickname: String,
val roles: List<AuthorityType>
)
Loading