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

Commit

Permalink
[#28] 가입기능 구현 (#38)
Browse files Browse the repository at this point in the history
* [#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 <chu7825@gmail.com>

* PR반영, 논의내용 반영
 - response는 json으로
 - crudRepository 활용
 - controller에 함께 aggregator 위치. (1:1 매칭)

* 코드정리, test fail 수정

Co-authored-by: Chanhyeong Cho <chu7825@gmail.com>
  • Loading branch information
youngvly and chanhyeong authored Nov 18, 2021
1 parent 6219617 commit 7b42fcb
Show file tree
Hide file tree
Showing 42 changed files with 978 additions and 25 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ project(":user-api") {
implementation("io.springfox:springfox-boot-starter:3.0.0")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-test")
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")

implementation("org.springframework.boot:spring-boot-starter-security")
runtimeOnly("com.h2database:h2")
runtimeOnly("mysql:mysql-connector-java")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sns.commons.utils

inline fun Boolean?.ifTrue(block: Boolean.() -> Unit): Boolean? {
if (this == true) {
block()
}
return this
}

inline fun Boolean?.ifFalse(block: Boolean.() -> Unit): Boolean? {
if (this == true) {
block()
}
return this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.sns.user.component.authcode.application

import com.sns.commons.utils.ifTrue
import com.sns.user.component.authcode.domain.AuthCode
import com.sns.user.component.authcode.domain.AuthCodeKey
import com.sns.user.component.authcode.domain.Purpose
import com.sns.user.component.authcode.repositories.AuthCodeRepository
import com.sns.user.component.user.domains.User
import com.sns.user.core.infrastructures.mail.MailService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class AuthCodeCommandService(
val authCodeRepository: AuthCodeRepository,
val mailService: MailService,
) {

@Transactional
fun create(user: User): AuthCode {
val authCode = AuthCode.createSignUp(user.id)
authCodeRepository.save(authCode)

mailService.sendSignUpAuthCodeMail(authCode.code, user.infoEmailAddress)
return authCode
}

@Transactional
fun verify(userId: String, purpose: Purpose, code: String): Boolean {
val authCodeKey = AuthCodeKey(purpose, userId)
val authCode: AuthCode? = authCodeRepository.findByAuthCodeKey(authCodeKey)
return authCode?.isCorrect(userId, code, purpose)
.ifTrue { authCodeRepository.delete(authCodeKey) } ?: false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.sns.user.component.authcode.domain

import kotlin.random.Random
import java.sql.ResultSet
import java.time.Instant
import javax.validation.constraints.NotBlank
import org.springframework.jdbc.core.RowMapper

data class AuthCode(
@NotBlank
val purpose: Purpose,
@NotBlank
val userId: String,
@NotBlank
val code: String = (1..CODE_LENGTH)
.map { Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString(""),
val createdAt: Instant = Instant.MIN
) {

fun isCorrect(userId: String, code: String, purpose: Purpose): Boolean =
(this.userId == userId) and (this.code == code) and (this.purpose == purpose)

companion object {
private const val CODE_LENGTH = 10;
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun createSignUp(userId: String) = AuthCode(purpose = Purpose.SIGN_UP, userId = userId)
val MAPPER: RowMapper<AuthCode> = AuthCodeRowMapper()
}
}

data class AuthCodeKey(
@NotBlank
val purpose: Purpose,
@NotBlank
val userId: String,
) {
fun toMap(): MutableMap<String, Any> = mutableMapOf(
"userId" to userId,
"purpose" to purpose.name,
)
}

// purpose enum 매핑이 안되서 수동으로 작성함. 확인필요.
class AuthCodeRowMapper : RowMapper<AuthCode> {
override fun mapRow(rs: ResultSet, rowNum: Int): AuthCode? {
return AuthCode(
purpose = Purpose.valueOf(rs.getString("purpose")),
userId = rs.getString("user_id"),
code = rs.getString("code"),
createdAt = Instant.ofEpochMilli(rs.getTimestamp("created_at").time),
)
}
}

enum class Purpose {
SIGN_UP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sns.user.component.authcode.repositories

import com.sns.user.component.authcode.domain.AuthCode
import com.sns.user.component.authcode.domain.AuthCodeKey

interface AuthCodeRepository {
fun save(authCode: AuthCode): AuthCode
fun findByAuthCodeKey(authCodeKey: AuthCodeKey): AuthCode?
fun delete(authCodeKey: AuthCodeKey)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.sns.user.component.authcode.repositories

import com.sns.user.component.authcode.domain.AuthCode
import com.sns.user.component.authcode.domain.AuthCodeKey
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

@Repository
class DefaultAuthCodeRepository(
val jdbcTemplate: NamedParameterJdbcTemplate,
) : AuthCodeRepository {

override fun findByAuthCodeKey(authCodeKey: AuthCodeKey): AuthCode? = jdbcTemplate.queryForObject(
"""
SELECT user_id,`code`,created_at,purpose
FROM auth_code
WHERE user_id = :userId AND purpose = :purpose
LIMIT 1
""".trimIndent(),
authCodeKey.toMap(), AuthCode.MAPPER,
)

@Transactional
override fun save(authCode: AuthCode): AuthCode {
jdbcTemplate.update(
"""
REPLACE INTO auth_code (user_id, `code`, created_at, purpose)
VALUES (:userId, :code, NOW(), :purpose)
""".trimIndent(),
mutableMapOf(
"userId" to authCode.userId,
"purpose" to authCode.purpose.name,
"code" to authCode.code,
),
)
return authCode
}

@Transactional
override fun delete(authCodeKey: AuthCodeKey) {
jdbcTemplate.update(
"""
DELETE FROM auth_code
WHERE user_id = :userId AND purpose = :purpose
LIMIT 1
""".trimIndent(),
authCodeKey.toMap(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.sns.user.component.user.application

import com.sns.commons.service.EventPublisher
import com.sns.user.component.user.domains.User
import com.sns.user.component.user.repositories.UserRepository
import com.sns.user.core.exceptions.AlreadyExistException
import com.sns.user.core.exceptions.NoAuthorityException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class UserCommandService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
private val eventPublisher: EventPublisher
) {

@Transactional
fun create(name: String, password: String, email: String): User {
userRepository.findById(email).ifPresent { throw AlreadyExistException() }

val user = User.create(email, passwordEncoder.encode(password), name, email) {
eventPublisher.publish(it)
}
userRepository.save(user)
return user
}

@Transactional
fun activate(userId: String) {
val user = userRepository.findByIdOrNull(userId) ?: throw NoAuthorityException()

user.activate() {
eventPublisher.publish(it)
}
userRepository.save(user)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ class UserQueryService(
private val userRepository: UserRepository
) {
fun getById(id: String): User? = userRepository.findByIdOrNull(id)

fun getByEmail(email: String): User? = userRepository.findByInfoEmailAddress(email).orElse(null)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.sns.user.component.user.domains

import com.sns.commons.DomainEvent
import com.sns.user.component.user.events.UserStatusChangedEvent
import com.sns.user.core.exceptions.AlreadyExistException
import java.sql.ResultSet
import java.time.Instant
import javax.validation.constraints.Max
import javax.validation.constraints.NotBlank
Expand All @@ -8,6 +12,7 @@ import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.Transient
import org.springframework.data.domain.Persistable
import org.springframework.jdbc.core.RowMapper

data class User(
@Id
Expand All @@ -32,27 +37,69 @@ data class User(

@LastModifiedDate
var updatedAt: Instant = Instant.MIN,

@NotBlank
var status: Status = Status.ACTIVATED
) : Persistable<String> {
@Transient
private var new: Boolean = false

override fun getId() = this.id
override fun isNew() = new

fun activate(publish: (DomainEvent) -> Unit = { _ -> }) {
status.checkAlready(Status.ACTIVATED)
status = Status.ACTIVATED
publish(UserStatusChangedEvent(this))
}

companion object {
val MAPPER: RowMapper<User> = UserRowMapper()

fun create(
id: String,
password: String,
name: String,
infoEmailAddress: String? = null
infoEmailAddress: String? = null,
publish: (DomainEvent) -> Unit = { _ -> }
): User {
// TODO validation
return User(
val user = User(
id = id,
password = password, // TODO encrypt
password = password,
name = name,
infoEmailAddress = infoEmailAddress ?: id,
status = Status.CREATED,
).apply { new = true }

publish(UserStatusChangedEvent(user))

return user
}
}
}

// purpose enum 매핑이 안되서 수동으로 작성함. 확인필요.
class UserRowMapper : RowMapper<User> {
override fun mapRow(rs: ResultSet, rowNum: Int): User? {
return User(
id = rs.getString("id"),
password = rs.getString("password"),
name = rs.getString("name"),
infoEmailAddress = rs.getString("info_email_address"),
status = Status.valueOf(rs.getString("status")),
createdAt = Instant.ofEpochMilli(rs.getTimestamp("created_at").time),
updatedAt = Instant.ofEpochMilli(rs.getTimestamp("updated_at").time),
)
}
}

enum class Status {
CREATED,
ACTIVATED;
// 비활 등등?

fun checkAlready(status: Status) {
if (status == this) throw AlreadyExistException()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sns.user.component.user.events

import com.sns.commons.DomainEvent
import com.sns.user.component.user.domains.User
import com.sns.user.core.config.IntegrationConfig

class UserStatusChangedEvent(val user: User) : DomainEvent {
override val eventId: String
get() = "$channel-$user.id-${System.currentTimeMillis()}"

override val channel = IntegrationConfig.Channels.USER_STATUS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sns.user.component.user.listeners

import com.sns.commons.annotation.CustomEventListener
import com.sns.user.component.authcode.application.AuthCodeCommandService
import com.sns.user.component.user.events.UserStatusChangedEvent

@CustomEventListener
class UserStatusListener(val authCodeCommandService: AuthCodeCommandService) {
// 인증 전, 기초 가입만 마친 상태
fun onCreated(createdEvent: UserStatusChangedEvent) {
val user = createdEvent.user
authCodeCommandService.create(user)
}

fun onActivated(activatedEvent: UserStatusChangedEvent) {
// TODO 타임라인생성, 프로필생성,,
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.sns.user.component.user.repositories

import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

@Repository
class DefaultUserRepository(
userCrudRepository: UserCrudRepository
userCrudRepository: UserCrudRepository,
private val jdbcTemplate: JdbcTemplate
) : UserRepository,
UserCrudRepository by userCrudRepository
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.sns.user.component.user.repositories

import com.sns.user.component.user.domains.User
import java.util.*
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository

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

import com.sns.user.component.user.domains.User
import java.util.*
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.NoRepositoryBean

@NoRepositoryBean
interface UserRepository : CrudRepository<User, String>
interface UserRepository : CrudRepository<User, String> {
fun findByInfoEmailAddress(email: String): Optional<User>
}
Loading

0 comments on commit 7b42fcb

Please sign in to comment.