Skip to content

Commit 4bd4261

Browse files
committed
review
1 parent b3022e5 commit 4bd4261

File tree

87 files changed

+2315
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+2315
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ out/
3838

3939
### Kotlin ###
4040
.kotlin
41+
42+
### mac OS ###
43+
**/.DS_Store
44+
45+
/src/main/resources/application-mail.yml

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# kotlin-api-server
2+
3+
### 🔥 개요
4+
5+
- [코틀린 스터디](https://github.com/brdm-study/atomic-kotlin-study)와 개인 학습 내용을 기반으로
6+
[java-api-server](https://github.com/yoonseon12/java-api-server) 프로젝트를 코틀린으로 마이그레이션 했습니다.
7+
8+
### 🔥 프로젝트 구현 내용 포스팅
9+
10+
- 💡 [@TransactionEventListener의 phase 옵션을 주의해서 사용하자](https://yoonseon.tistory.com/157)
11+
- 💡 [스프링+코틀린에서 @Valid이 동작하지 않는 경우](https://yoonseon.tistory.com/156)

build.gradle

+16
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,27 @@ repositories {
2222
dependencies {
2323
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2424
implementation 'org.springframework.boot:spring-boot-starter-web'
25+
implementation 'org.springframework.boot:spring-boot-starter-validation'
26+
implementation 'org.springframework.boot:spring-boot-starter-security'
27+
implementation 'org.springframework.boot:spring-boot-starter-mail'
28+
29+
// kotlin
2530
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
2631
implementation 'org.jetbrains.kotlin:kotlin-reflect'
32+
33+
// jwt
34+
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
35+
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
36+
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
37+
38+
// H2
2739
runtimeOnly 'com.h2database:h2'
40+
41+
// test
2842
testImplementation 'org.springframework.boot:spring-boot-starter-test'
43+
testImplementation 'org.springframework.security:spring-security-test'
2944
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
45+
testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
3046
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
3147
}
3248

http/get_memberInfo.http

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
GET localhost:8080/api/members/1
2+
x-api-version: v1
3+
Content-Type: application/json
4+
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ5b29uc2VvbjNAZ21haWwuY29tIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTcyMTEyMzQ4MH0.3-MFaAiWwAgICiTFxej4E-BvBNjoAhVD_8s6igLl-6U1jLcxEJwv-YE0v3hmTlbegQeOcfH8WHnFMp9vvr-UKA

http/post_resetPassword.http

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
POST localhost:8080/api/members/reset-password
2+
x-api-version: v1
3+
Content-Type: application/json
4+
5+
{
6+
"email": "[email protected]"
7+
}

http/post_signin.http

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
POST localhost:8080/api/members/signin
2+
x-api-version: v1
3+
Content-Type: application/json
4+
5+
{
6+
"email": "[email protected]",
7+
"password": "testtest1@@@@"
8+
}

http/post_signup.http

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
POST localhost:8080/api/members
2+
x-api-version: v1
3+
Content-Type: application/json
4+
5+
{
6+
"nickname": "Kevin3",
7+
"email": "[email protected]",
8+
"password": "testtest1@@@@"
9+
}

src/main/kotlin/io/study/kotlinapiserver/KotlinApiServerApplication.kt

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.study.kotlinapiserver
22

33
import org.springframework.boot.autoconfigure.SpringBootApplication
4+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
45
import org.springframework.boot.runApplication
56

67
@SpringBootApplication
8+
@ConfigurationPropertiesScan
79
class KotlinApiServerApplication
810

911
fun main(args: Array<String>) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.study.kotlinapiserver.api.domain.member.application
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
5+
import org.springframework.stereotype.Service
6+
import org.springframework.transaction.annotation.Transactional
7+
8+
@Service
9+
@Transactional
10+
class MemberInfoService(
11+
12+
private val memberDomainService: MemberDomainService,
13+
14+
) {
15+
16+
@Transactional(readOnly = true)
17+
fun getMemberInfo(id: Long): MemberInfoResponse {
18+
return memberDomainService.getInfo(id)
19+
}
20+
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package io.study.kotlinapiserver.api.domain.member.application
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
5+
import io.study.kotlinapiserver.api.domain.member.domain.event.ResetPasswordEvent
6+
import io.study.kotlinapiserver.web.base.event.Events
7+
import org.springframework.stereotype.Service
8+
import org.springframework.transaction.annotation.Transactional
9+
import java.security.SecureRandom
10+
import java.util.*
11+
12+
@Service
13+
@Transactional(readOnly = true)
14+
class MemberResetService(
15+
16+
private val memberDomainService: MemberDomainService,
17+
18+
) {
19+
companion object {
20+
private const val VALID_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
21+
private const val SPECIAL_CHARACTERS = "!@#$%^&*()\\-_=+"
22+
private const val MAX_SPECIAL_CHARACTER = 3;
23+
private const val MIN_LENGTH = 8;
24+
private const val MAX_LENGTH = 16;
25+
}
26+
27+
@Transactional
28+
fun resetPassword(request: MemberResetPasswordRequest) {
29+
val tempPassword = createTempPassword()
30+
31+
memberDomainService.resetPassword(MemberResetPasswordRequest(request.email, tempPassword))
32+
33+
/** 비밀번호 변경 메일 전송 **/
34+
publishResetPasswordSuccessEmailEvent(tempPassword, request.email)
35+
}
36+
37+
private fun createTempPassword(): String {
38+
val random = SecureRandom()
39+
val passwordLength = MIN_LENGTH + random.nextInt(MAX_LENGTH - MIN_LENGTH + 1)
40+
val password = StringBuilder(passwordLength)
41+
42+
val specialCharacterLength = addSpecialCharacters(password)
43+
addValidCharacters(specialCharacterLength, passwordLength, password)
44+
45+
return shufflePassword(password)
46+
}
47+
48+
private fun publishResetPasswordSuccessEmailEvent(
49+
password: String,
50+
registeredEmail: String,
51+
) {
52+
Events.raise(ResetPasswordEvent.of(password, registeredEmail))
53+
}
54+
55+
private fun shufflePassword(password: StringBuilder): String {
56+
val map = password.toList()
57+
Collections.shuffle(map)
58+
return map.joinToString("")
59+
}
60+
61+
private fun addSpecialCharacters(password: StringBuilder): Int {
62+
val random = SecureRandom()
63+
val specialCharacterLength = random.nextInt(MAX_SPECIAL_CHARACTER) + 1
64+
65+
for (i in 0 until specialCharacterLength) {
66+
password.append(SPECIAL_CHARACTERS[random.nextInt(SPECIAL_CHARACTERS.length)])
67+
}
68+
69+
return specialCharacterLength
70+
}
71+
72+
private fun addValidCharacters(
73+
specialCharacterLength: Int,
74+
passwordLength: Int,
75+
password: StringBuilder
76+
) {
77+
val random = SecureRandom()
78+
for (i in specialCharacterLength until passwordLength) {
79+
val index = random.nextInt(VALID_CHARACTERS.length)
80+
password.append(VALID_CHARACTERS[index])
81+
}
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.study.kotlinapiserver.api.domain.member.application
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSigninRequest
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSigninResponse
5+
import io.study.kotlinapiserver.web.jwt.JwtProvider
6+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
7+
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
8+
import org.springframework.stereotype.Service
9+
import org.springframework.transaction.annotation.Transactional
10+
11+
@Service
12+
@Transactional(readOnly = true)
13+
class MemberSigninService(
14+
private val jwtProvider: JwtProvider,
15+
private val authenticationManagerBuilder: AuthenticationManagerBuilder,
16+
) {
17+
18+
fun signin(request: MemberSigninRequest): MemberSigninResponse {
19+
val authenticationToken = UsernamePasswordAuthenticationToken(request.email, request.password)
20+
val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
21+
val token = jwtProvider.createToken(authentication)
22+
23+
return MemberSigninResponse.of(token)
24+
}
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.study.kotlinapiserver.api.domain.member.application
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.MemberDomainService
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
5+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse
6+
import io.study.kotlinapiserver.api.domain.member.domain.event.SignupEvent
7+
import io.study.kotlinapiserver.web.base.event.Events
8+
import org.springframework.stereotype.Service
9+
import org.springframework.transaction.annotation.Transactional
10+
11+
@Service
12+
@Transactional(readOnly = true)
13+
class MemberSignupService(
14+
private val memberDomainService: MemberDomainService,
15+
) {
16+
17+
@Transactional
18+
fun signup(command: MemberSignupRequest): MemberSignupResponse {
19+
/** 회원가입 **/
20+
val savedInfo = memberDomainService.register(command)
21+
22+
/** 이메일 전송 **/
23+
publishSignupSuccessEmailEvent(savedInfo.email)
24+
25+
return savedInfo
26+
}
27+
28+
private fun publishSignupSuccessEmailEvent(registeredEmail: String) {
29+
Events.raise(SignupEvent.of(registeredEmail))
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
5+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
6+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse
7+
8+
interface MemberDomainService {
9+
10+
fun register(request: MemberSignupRequest): MemberSignupResponse
11+
12+
fun getInfo(id: Long): MemberInfoResponse
13+
14+
fun resetPassword(request: MemberResetPasswordRequest)
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberResetPasswordRequest
4+
import io.study.kotlinapiserver.api.domain.member.domain.dto.request.MemberSignupRequest
5+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberInfoResponse
6+
import io.study.kotlinapiserver.api.domain.member.domain.dto.response.MemberSignupResponse
7+
import io.study.kotlinapiserver.api.domain.member.domain.entity.Member
8+
import io.study.kotlinapiserver.api.domain.member.domain.repository.MemberQueryRepository
9+
import io.study.kotlinapiserver.api.domain.member.domain.repository.MemberRepository
10+
import io.study.kotlinapiserver.api.domain.member.domain.validation.MemberValidator
11+
import io.study.kotlinapiserver.web.exception.ApiException
12+
import io.study.kotlinapiserver.web.exception.error.MemberErrorCode
13+
import org.springframework.security.crypto.password.PasswordEncoder
14+
import org.springframework.stereotype.Component
15+
16+
@Component
17+
class MemberDomainServiceImpl(
18+
19+
private val memberRepository: MemberRepository,
20+
private val memberQueryRepository: MemberQueryRepository,
21+
private val memberValidator: MemberValidator,
22+
private val passwordEncoder: PasswordEncoder,
23+
24+
) : MemberDomainService {
25+
26+
override fun register(request: MemberSignupRequest): MemberSignupResponse {
27+
memberValidator.signinValidate(request)
28+
val initBasicMember = Member.createBasicMember(request.email, request.nickname, encodePassword(request.password))
29+
val savedMember = memberRepository.save(initBasicMember)
30+
31+
return MemberSignupResponse(savedMember.email)
32+
}
33+
34+
override fun getInfo(id: Long): MemberInfoResponse {
35+
val findMember = memberQueryRepository.findByIdWithAuthorities(id)
36+
?: throw ApiException(MemberErrorCode.NOT_FOUND_MEMBER)
37+
38+
return MemberInfoResponse(
39+
email = findMember.email,
40+
nickname = findMember.nickname,
41+
roles = findMember.authorities.map { it.authority }
42+
)
43+
}
44+
45+
override fun resetPassword(request: MemberResetPasswordRequest) {
46+
val findMember = memberQueryRepository.findByEmail(request.email)
47+
?: throw ApiException(MemberErrorCode.NOT_FOUND_MEMBER)
48+
49+
findMember.changePassword(encodePassword(request.tempPassword!!))
50+
}
51+
52+
private fun encodePassword(password: String): String {
53+
return passwordEncoder.encode(password)
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain.dto.request
2+
3+
data class MemberResetPasswordRequest(
4+
val email: String,
5+
val tempPassword: String? = null,
6+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain.dto.request
2+
3+
import io.study.kotlinapiserver.api.domain.member.ui.dto.request.PostMemberSigninRequest
4+
5+
data class MemberSigninRequest(
6+
val email: String,
7+
val password: String,
8+
) {
9+
companion object {
10+
fun of(request: PostMemberSigninRequest): MemberSigninRequest {
11+
return MemberSigninRequest(
12+
email = request.email,
13+
password = request.password,
14+
)
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain.dto.request
2+
3+
import io.study.kotlinapiserver.api.domain.member.ui.dto.request.PostMemberSignupRequest
4+
5+
data class MemberSignupRequest(
6+
val nickname: String,
7+
val email: String,
8+
val password: String,
9+
) {
10+
companion object {
11+
fun of(request: PostMemberSignupRequest): MemberSignupRequest {
12+
return MemberSignupRequest(
13+
nickname = request.nickname,
14+
email = request.email,
15+
password = request.password,
16+
)
17+
}
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.study.kotlinapiserver.api.domain.member.domain.dto.response
2+
3+
import io.study.kotlinapiserver.api.domain.member.domain.entity.authority.AuthorityType
4+
5+
data class MemberInfoResponse(
6+
val email: String,
7+
val nickname: String,
8+
val roles: List<AuthorityType>
9+
)

0 commit comments

Comments
 (0)