Skip to content
This repository was archived by the owner on Nov 21, 2023. It is now read-only.

Commit 7b42fcb

Browse files
youngvlychanhyeong
andauthored
[#28] 가입기능 구현 (#38)
* [#28] security 적용 (SecurityConfig.kt) , api mock 생성 * [#28] 가입 인증코드 기능구현 * [#28] 가입기능 구현 * [#28] authCode 복합키로 설정 - 복합키 미지원 확인 spring-projects/spring-data-relational#574 * [#28] spring security에서 swagger관련 제외 * [#28] 이벤트 통일 , exception 응답 설정, 트랜잭션 커밋안되는 이슈 수정 * [#28] 이전 상태 체크 추가 * Update user-api/src/main/kotlin/com/sns/user/core/config/SwaggerTag.kt Co-authored-by: Chanhyeong Cho <[email protected]> * PR반영, 논의내용 반영 - response는 json으로 - crudRepository 활용 - controller에 함께 aggregator 위치. (1:1 매칭) * 코드정리, test fail 수정 Co-authored-by: Chanhyeong Cho <[email protected]>
1 parent 6219617 commit 7b42fcb

File tree

42 files changed

+978
-25
lines changed

Some content is hidden

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

42 files changed

+978
-25
lines changed

build.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ project(":user-api") {
6262
implementation("io.springfox:springfox-boot-starter:3.0.0")
6363
implementation("org.springframework.boot:spring-boot-starter-security")
6464
implementation("org.springframework.security:spring-security-test")
65+
implementation("org.springframework.boot:spring-boot-starter-mail")
66+
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
67+
68+
implementation("org.springframework.boot:spring-boot-starter-security")
6569
runtimeOnly("com.h2database:h2")
6670
runtimeOnly("mysql:mysql-connector-java")
6771
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.sns.commons.utils
2+
3+
inline fun Boolean?.ifTrue(block: Boolean.() -> Unit): Boolean? {
4+
if (this == true) {
5+
block()
6+
}
7+
return this
8+
}
9+
10+
inline fun Boolean?.ifFalse(block: Boolean.() -> Unit): Boolean? {
11+
if (this == true) {
12+
block()
13+
}
14+
return this
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.sns.user.component.authcode.application
2+
3+
import com.sns.commons.utils.ifTrue
4+
import com.sns.user.component.authcode.domain.AuthCode
5+
import com.sns.user.component.authcode.domain.AuthCodeKey
6+
import com.sns.user.component.authcode.domain.Purpose
7+
import com.sns.user.component.authcode.repositories.AuthCodeRepository
8+
import com.sns.user.component.user.domains.User
9+
import com.sns.user.core.infrastructures.mail.MailService
10+
import org.springframework.stereotype.Service
11+
import org.springframework.transaction.annotation.Transactional
12+
13+
@Service
14+
class AuthCodeCommandService(
15+
val authCodeRepository: AuthCodeRepository,
16+
val mailService: MailService,
17+
) {
18+
19+
@Transactional
20+
fun create(user: User): AuthCode {
21+
val authCode = AuthCode.createSignUp(user.id)
22+
authCodeRepository.save(authCode)
23+
24+
mailService.sendSignUpAuthCodeMail(authCode.code, user.infoEmailAddress)
25+
return authCode
26+
}
27+
28+
@Transactional
29+
fun verify(userId: String, purpose: Purpose, code: String): Boolean {
30+
val authCodeKey = AuthCodeKey(purpose, userId)
31+
val authCode: AuthCode? = authCodeRepository.findByAuthCodeKey(authCodeKey)
32+
return authCode?.isCorrect(userId, code, purpose)
33+
.ifTrue { authCodeRepository.delete(authCodeKey) } ?: false
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.sns.user.component.authcode.domain
2+
3+
import kotlin.random.Random
4+
import java.sql.ResultSet
5+
import java.time.Instant
6+
import javax.validation.constraints.NotBlank
7+
import org.springframework.jdbc.core.RowMapper
8+
9+
data class AuthCode(
10+
@NotBlank
11+
val purpose: Purpose,
12+
@NotBlank
13+
val userId: String,
14+
@NotBlank
15+
val code: String = (1..CODE_LENGTH)
16+
.map { Random.nextInt(0, charPool.size) }
17+
.map(charPool::get)
18+
.joinToString(""),
19+
val createdAt: Instant = Instant.MIN
20+
) {
21+
22+
fun isCorrect(userId: String, code: String, purpose: Purpose): Boolean =
23+
(this.userId == userId) and (this.code == code) and (this.purpose == purpose)
24+
25+
companion object {
26+
private const val CODE_LENGTH = 10;
27+
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
28+
fun createSignUp(userId: String) = AuthCode(purpose = Purpose.SIGN_UP, userId = userId)
29+
val MAPPER: RowMapper<AuthCode> = AuthCodeRowMapper()
30+
}
31+
}
32+
33+
data class AuthCodeKey(
34+
@NotBlank
35+
val purpose: Purpose,
36+
@NotBlank
37+
val userId: String,
38+
) {
39+
fun toMap(): MutableMap<String, Any> = mutableMapOf(
40+
"userId" to userId,
41+
"purpose" to purpose.name,
42+
)
43+
}
44+
45+
// purpose enum 매핑이 안되서 수동으로 작성함. 확인필요.
46+
class AuthCodeRowMapper : RowMapper<AuthCode> {
47+
override fun mapRow(rs: ResultSet, rowNum: Int): AuthCode? {
48+
return AuthCode(
49+
purpose = Purpose.valueOf(rs.getString("purpose")),
50+
userId = rs.getString("user_id"),
51+
code = rs.getString("code"),
52+
createdAt = Instant.ofEpochMilli(rs.getTimestamp("created_at").time),
53+
)
54+
}
55+
}
56+
57+
enum class Purpose {
58+
SIGN_UP
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.sns.user.component.authcode.repositories
2+
3+
import com.sns.user.component.authcode.domain.AuthCode
4+
import com.sns.user.component.authcode.domain.AuthCodeKey
5+
6+
interface AuthCodeRepository {
7+
fun save(authCode: AuthCode): AuthCode
8+
fun findByAuthCodeKey(authCodeKey: AuthCodeKey): AuthCode?
9+
fun delete(authCodeKey: AuthCodeKey)
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.sns.user.component.authcode.repositories
2+
3+
import com.sns.user.component.authcode.domain.AuthCode
4+
import com.sns.user.component.authcode.domain.AuthCodeKey
5+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
6+
import org.springframework.stereotype.Repository
7+
import org.springframework.transaction.annotation.Transactional
8+
9+
@Repository
10+
class DefaultAuthCodeRepository(
11+
val jdbcTemplate: NamedParameterJdbcTemplate,
12+
) : AuthCodeRepository {
13+
14+
override fun findByAuthCodeKey(authCodeKey: AuthCodeKey): AuthCode? = jdbcTemplate.queryForObject(
15+
"""
16+
SELECT user_id,`code`,created_at,purpose
17+
FROM auth_code
18+
WHERE user_id = :userId AND purpose = :purpose
19+
LIMIT 1
20+
""".trimIndent(),
21+
authCodeKey.toMap(), AuthCode.MAPPER,
22+
)
23+
24+
@Transactional
25+
override fun save(authCode: AuthCode): AuthCode {
26+
jdbcTemplate.update(
27+
"""
28+
REPLACE INTO auth_code (user_id, `code`, created_at, purpose)
29+
VALUES (:userId, :code, NOW(), :purpose)
30+
""".trimIndent(),
31+
mutableMapOf(
32+
"userId" to authCode.userId,
33+
"purpose" to authCode.purpose.name,
34+
"code" to authCode.code,
35+
),
36+
)
37+
return authCode
38+
}
39+
40+
@Transactional
41+
override fun delete(authCodeKey: AuthCodeKey) {
42+
jdbcTemplate.update(
43+
"""
44+
DELETE FROM auth_code
45+
WHERE user_id = :userId AND purpose = :purpose
46+
LIMIT 1
47+
""".trimIndent(),
48+
authCodeKey.toMap(),
49+
)
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.sns.user.component.user.application
2+
3+
import com.sns.commons.service.EventPublisher
4+
import com.sns.user.component.user.domains.User
5+
import com.sns.user.component.user.repositories.UserRepository
6+
import com.sns.user.core.exceptions.AlreadyExistException
7+
import com.sns.user.core.exceptions.NoAuthorityException
8+
import org.springframework.data.repository.findByIdOrNull
9+
import org.springframework.security.crypto.password.PasswordEncoder
10+
import org.springframework.stereotype.Service
11+
import org.springframework.transaction.annotation.Transactional
12+
13+
@Service
14+
class UserCommandService(
15+
private val userRepository: UserRepository,
16+
private val passwordEncoder: PasswordEncoder,
17+
private val eventPublisher: EventPublisher
18+
) {
19+
20+
@Transactional
21+
fun create(name: String, password: String, email: String): User {
22+
userRepository.findById(email).ifPresent { throw AlreadyExistException() }
23+
24+
val user = User.create(email, passwordEncoder.encode(password), name, email) {
25+
eventPublisher.publish(it)
26+
}
27+
userRepository.save(user)
28+
return user
29+
}
30+
31+
@Transactional
32+
fun activate(userId: String) {
33+
val user = userRepository.findByIdOrNull(userId) ?: throw NoAuthorityException()
34+
35+
user.activate() {
36+
eventPublisher.publish(it)
37+
}
38+
userRepository.save(user)
39+
}
40+
}

user-api/src/main/kotlin/com/sns/user/component/user/application/UserQueryService.kt

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ class UserQueryService(
1010
private val userRepository: UserRepository
1111
) {
1212
fun getById(id: String): User? = userRepository.findByIdOrNull(id)
13+
14+
fun getByEmail(email: String): User? = userRepository.findByInfoEmailAddress(email).orElse(null)
1315
}

user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.sns.user.component.user.domains
22

3+
import com.sns.commons.DomainEvent
4+
import com.sns.user.component.user.events.UserStatusChangedEvent
5+
import com.sns.user.core.exceptions.AlreadyExistException
6+
import java.sql.ResultSet
37
import java.time.Instant
48
import javax.validation.constraints.Max
59
import javax.validation.constraints.NotBlank
@@ -8,6 +12,7 @@ import org.springframework.data.annotation.Id
812
import org.springframework.data.annotation.LastModifiedDate
913
import org.springframework.data.annotation.Transient
1014
import org.springframework.data.domain.Persistable
15+
import org.springframework.jdbc.core.RowMapper
1116

1217
data class User(
1318
@Id
@@ -32,27 +37,69 @@ data class User(
3237

3338
@LastModifiedDate
3439
var updatedAt: Instant = Instant.MIN,
40+
41+
@NotBlank
42+
var status: Status = Status.ACTIVATED
3543
) : Persistable<String> {
3644
@Transient
3745
private var new: Boolean = false
3846

3947
override fun getId() = this.id
4048
override fun isNew() = new
4149

50+
fun activate(publish: (DomainEvent) -> Unit = { _ -> }) {
51+
status.checkAlready(Status.ACTIVATED)
52+
status = Status.ACTIVATED
53+
publish(UserStatusChangedEvent(this))
54+
}
55+
4256
companion object {
57+
val MAPPER: RowMapper<User> = UserRowMapper()
58+
4359
fun create(
4460
id: String,
4561
password: String,
4662
name: String,
47-
infoEmailAddress: String? = null
63+
infoEmailAddress: String? = null,
64+
publish: (DomainEvent) -> Unit = { _ -> }
4865
): User {
4966
// TODO validation
50-
return User(
67+
val user = User(
5168
id = id,
52-
password = password, // TODO encrypt
69+
password = password,
5370
name = name,
5471
infoEmailAddress = infoEmailAddress ?: id,
72+
status = Status.CREATED,
5573
).apply { new = true }
74+
75+
publish(UserStatusChangedEvent(user))
76+
77+
return user
5678
}
5779
}
5880
}
81+
82+
// purpose enum 매핑이 안되서 수동으로 작성함. 확인필요.
83+
class UserRowMapper : RowMapper<User> {
84+
override fun mapRow(rs: ResultSet, rowNum: Int): User? {
85+
return User(
86+
id = rs.getString("id"),
87+
password = rs.getString("password"),
88+
name = rs.getString("name"),
89+
infoEmailAddress = rs.getString("info_email_address"),
90+
status = Status.valueOf(rs.getString("status")),
91+
createdAt = Instant.ofEpochMilli(rs.getTimestamp("created_at").time),
92+
updatedAt = Instant.ofEpochMilli(rs.getTimestamp("updated_at").time),
93+
)
94+
}
95+
}
96+
97+
enum class Status {
98+
CREATED,
99+
ACTIVATED;
100+
// 비활 등등?
101+
102+
fun checkAlready(status: Status) {
103+
if (status == this) throw AlreadyExistException()
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.sns.user.component.user.events
2+
3+
import com.sns.commons.DomainEvent
4+
import com.sns.user.component.user.domains.User
5+
import com.sns.user.core.config.IntegrationConfig
6+
7+
class UserStatusChangedEvent(val user: User) : DomainEvent {
8+
override val eventId: String
9+
get() = "$channel-$user.id-${System.currentTimeMillis()}"
10+
11+
override val channel = IntegrationConfig.Channels.USER_STATUS
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.sns.user.component.user.listeners
2+
3+
import com.sns.commons.annotation.CustomEventListener
4+
import com.sns.user.component.authcode.application.AuthCodeCommandService
5+
import com.sns.user.component.user.events.UserStatusChangedEvent
6+
7+
@CustomEventListener
8+
class UserStatusListener(val authCodeCommandService: AuthCodeCommandService) {
9+
// 인증 전, 기초 가입만 마친 상태
10+
fun onCreated(createdEvent: UserStatusChangedEvent) {
11+
val user = createdEvent.user
12+
authCodeCommandService.create(user)
13+
}
14+
15+
fun onActivated(activatedEvent: UserStatusChangedEvent) {
16+
// TODO 타임라인생성, 프로필생성,,
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.sns.user.component.user.repositories
22

3+
import org.springframework.jdbc.core.JdbcTemplate
34
import org.springframework.stereotype.Repository
45

56
@Repository
67
class DefaultUserRepository(
7-
userCrudRepository: UserCrudRepository
8+
userCrudRepository: UserCrudRepository,
9+
private val jdbcTemplate: JdbcTemplate
810
) : UserRepository,
911
UserCrudRepository by userCrudRepository
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.sns.user.component.user.repositories
22

33
import com.sns.user.component.user.domains.User
4+
import java.util.*
45
import org.springframework.data.repository.CrudRepository
56
import org.springframework.stereotype.Repository
67

78
@Repository
8-
interface UserCrudRepository : CrudRepository<User, String>
9+
interface UserCrudRepository : CrudRepository<User, String> {
10+
fun findByInfoEmailAddress(email: String): Optional<User>
11+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.sns.user.component.user.repositories
22

33
import com.sns.user.component.user.domains.User
4+
import java.util.*
45
import org.springframework.data.repository.CrudRepository
56
import org.springframework.data.repository.NoRepositoryBean
67

78
@NoRepositoryBean
8-
interface UserRepository : CrudRepository<User, String>
9+
interface UserRepository : CrudRepository<User, String> {
10+
fun findByInfoEmailAddress(email: String): Optional<User>
11+
}

0 commit comments

Comments
 (0)