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

Commit

Permalink
[#28] 가입기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
youngvly committed Nov 14, 2021
1 parent 90dead8 commit 4456124
Show file tree
Hide file tree
Showing 17 changed files with 270 additions and 22 deletions.
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
Expand Up @@ -13,7 +13,7 @@ import org.springframework.stereotype.Service
class AuthCodeCommand(
val authCodeRepository: AuthCodeRepository,
val mailService: MailService,
val userRepository: DefaultUserRepository
val userRepository: DefaultUserRepository,
) {
fun create(userId: String): AuthCode {
val user = userRepository.findByIdOrNull(userId) ?: throw NoAuthorityException()
Expand All @@ -27,9 +27,6 @@ class AuthCodeCommand(

fun verify(userId: String, purpose: Purpose, code: String): Boolean {
val authCode: AuthCode? = authCodeRepository.findByUserIdAndPurpose(userId, purpose)
return authCode?.isCorrect(userId, code, purpose)
.takeIf { it == true }.apply {
// TOOD update STATUS userRepository.save()
} ?: false
return authCode?.isCorrect(userId, code, purpose) ?: false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sns.user.component.user.application

import com.sns.user.component.user.domains.User
import com.sns.user.component.user.repositories.UserRepository
import com.sns.user.core.exceptions.NoAuthorityException
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service

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

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

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

user.activate() {
eventPublisher.publishEvent(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.findByInfoEmailAddressOrNull(email)
}
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.UserActivatedEvent
import com.sns.user.component.user.events.UserCreatedEvent
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,64 @@ 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 = Status.ACTIVATED
publish(UserActivatedEvent(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.ON_SIGN_UP,
).apply { new = true }

publish(UserCreatedEvent(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 {
ON_SIGN_UP,
ACTIVATED,
// 비활 등등?
}
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 UserActivatedEvent(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,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 UserCreatedEvent(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,19 @@
package com.sns.user.component.user.listeners

import com.sns.commons.annotation.CustomEventListener
import com.sns.user.component.authcode.application.AuthCodeCommand
import com.sns.user.component.user.events.UserActivatedEvent
import com.sns.user.component.user.events.UserCreatedEvent

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

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

import com.sns.user.component.user.domains.User
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
UserCrudRepository by userCrudRepository {

override fun findByInfoEmailAddressOrNull(email: String): User? = jdbcTemplate.queryForObject(
"""
SELECT `id`,`password`,`name`,info_email_address,created_at,updated_at,`status`
FROM `user`
WHERE info_email_address = ?
LIMIT 1
""".trimIndent(),
User.MAPPER, email,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.NoRepositoryBean

@NoRepositoryBean
interface UserRepository : CrudRepository<User, String>
interface UserRepository : CrudRepository<User, String> {
fun findByInfoEmailAddressOrNull(email: String): User?
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.sns.user.core.config
import com.sns.commons.config.IntegrationEventBaseConfig
import com.sns.user.component.test.dtos.LaughingEvent
import com.sns.user.component.test.listeners.EmotionListener
import com.sns.user.component.user.events.UserActivatedEvent
import com.sns.user.component.user.events.UserCreatedEvent
import com.sns.user.component.user.listeners.UserStatusListener
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
Expand All @@ -20,7 +23,19 @@ class IntegrationConfig {
}
}

@Bean
fun userStatusFlow(userStatusListener: UserStatusListener) = integrationFlow {
channel { publishSubscribe(Channels.USER_STATUS) }
handle<UserCreatedEvent> { event, _ ->
userStatusListener.onCreated(event)
}
handle<UserActivatedEvent> { event, _ ->
userStatusListener.onActivated(event)
}
}

object Channels {
const val EMOTION = "EMOTION_CHANNEL"
const val USER_STATUS = "USER_STATUS_CHANNEL"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.sns.user.endpoints.user

import com.sns.commons.utils.ifTrue
import com.sns.user.component.authcode.application.AuthCodeCommand
import com.sns.user.component.authcode.domain.Purpose
import com.sns.user.component.user.application.UserCommandService
import com.sns.user.component.user.application.UserQueryService
import com.sns.user.core.config.SwaggerTag
import com.sns.user.endpoints.user.requests.SignUpRequest
import io.swagger.v3.oas.annotations.media.Content
Expand All @@ -25,13 +28,17 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@Tag(name = SwaggerTag.SIGN_UP)
@RequestMapping("/api")
class SignUpController(val authCodeCommand: AuthCodeCommand) {
class SignUpController(
val authCodeCommand: AuthCodeCommand,
val userQueryService: UserQueryService,
val userCommandService: UserCommandService
) {

@ApiResponse(description = "회원 가입", responseCode = "202")
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/v1/sign-up")
fun signUp(@RequestBody request: SignUpRequest) {
// TODO 패스워드 유효성 검증
userCommandService.create(request.name, request.password, request.email)
}

@ApiResponse(
Expand All @@ -41,15 +48,14 @@ class SignUpController(val authCodeCommand: AuthCodeCommand) {
@ResponseStatus(HttpStatus.OK)
@GetMapping("/v1/sign-up/verifications/emails/{email}")
fun verifyEmail(@Email @PathVariable email: String): ResponseEntity<Boolean> {
// TODO email 중복 검사
return ResponseEntity.ok(false)
return (userQueryService.getByEmail(email) != null)
.let { ResponseEntity.ok(it) }
}

@ApiResponse(description = "가입 인증 코드 재발송", responseCode = "202")
@ResponseStatus(HttpStatus.CREATED)
@PutMapping("/v1/sign-up/verifications/auth-code/ids/{userId}")
fun createAuthenticationCode(@PathVariable userId: String) {

authCodeCommand.create(userId)
}

Expand All @@ -60,6 +66,10 @@ class SignUpController(val authCodeCommand: AuthCodeCommand) {
@ResponseStatus(HttpStatus.OK)
@PostMapping("/v1/sign-up/verifications/auth-code/ids/{userId}")
fun verifyAuthenticationCode(@PathVariable userId: String, @RequestBody code: String): ResponseEntity<Boolean> {
return ResponseEntity.ok(authCodeCommand.verify(userId, Purpose.SIGN_UP, code))
return authCodeCommand.verify(userId, Purpose.SIGN_UP, code)
.ifTrue { userCommandService.activate(userId) }
.let {
ResponseEntity.ok(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.sns.user.endpoints.user.requests

import javax.validation.constraints.Email
import javax.validation.constraints.Max
import javax.validation.constraints.NotEmpty
import org.hibernate.validator.constraints.Length
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

data class SignUpRequest(
@NotEmpty
@Length(max = 15)
@Max(15)
val name: String,

@NotEmpty
@Length(min = 8, max = 30)
@Size(min = 8, max = 30, message = "비밀번호는 8자 이상 30자 미만이어야 합니다.")
@Pattern(regexp = "(?=.*[A-z])(?=.*[0-9])", message = "비밀번호는 영문자와 숫자가 포함되어야 합니다.")
val password: String,

@NotEmpty
@Email
val email: String,
Expand Down
Loading

0 comments on commit 4456124

Please sign in to comment.