From 639fa42e266ab3866ecb4af89c26a194eb0c5380 Mon Sep 17 00:00:00 2001 From: "toshinori.chiba" Date: Sun, 12 Mar 2023 12:17:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?= =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E5=A4=89=E6=9B=B4API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create/CreateDomainModelRequest.scala | 4 -- .../edit/email/EditUserEmailController.scala | 39 +++++++++++++++++++ .../edit/email/EditUserEmailRequest.scala | 20 ++++++++++ .../api/user/json/UserResponse.scala | 4 +- .../edit/email/ChangeEmailInteractor.scala | 27 +++++++++++++ .../dev/tchiba/auth/core/user/User.scala | 23 ++++++++++- .../email/unique/UserEmailUniqueChecker.scala | 37 ++++++++++++++++++ .../infra/core/user/JdbcUserRepository.scala | 2 +- .../unique/JdbcUserEmailUniqueChecker.scala | 18 +++++++++ .../dev/tchiba/auth/module/AuthModule.scala | 7 ++++ .../user/edit/email/ChangeEmailInput.scala | 7 ++++ .../user/edit/email/ChangeEmailOutput.scala | 16 ++++++++ .../user/edit/email/ChangeEmailUseCase.scala | 8 ++++ conf/routes | 2 + 14 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 app/interfaces/api/user/edit/email/EditUserEmailController.scala create mode 100644 app/interfaces/api/user/edit/email/EditUserEmailRequest.scala create mode 100644 auth-application/src/main/scala/dev/tchiba/auth/application/interactors/user/edit/email/ChangeEmailInteractor.scala create mode 100644 auth-core/src/main/scala/dev/tchiba/auth/core/user/specs/email/unique/UserEmailUniqueChecker.scala create mode 100644 auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/specs/email/unique/JdbcUserEmailUniqueChecker.scala create mode 100644 auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala create mode 100644 auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailOutput.scala create mode 100644 auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailUseCase.scala diff --git a/app/interfaces/api/domainmodel/create/CreateDomainModelRequest.scala b/app/interfaces/api/domainmodel/create/CreateDomainModelRequest.scala index e1361b4d..27c49f81 100644 --- a/app/interfaces/api/domainmodel/create/CreateDomainModelRequest.scala +++ b/app/interfaces/api/domainmodel/create/CreateDomainModelRequest.scala @@ -5,11 +5,7 @@ import dev.tchiba.sdmt.core.boundedContext.BoundedContextId import dev.tchiba.sdmt.core.domainmodel.{EnglishName, Knowledge, UbiquitousName} import dev.tchiba.sdmt.usecase.domainmodel.create.CreateDomainModelInput import interfaces.json.request.play.{PlayJsonRequest, PlayJsonRequestCompanion} -import interfaces.json.{JsonRequest, JsonValidator} import play.api.libs.json.{Json, OFormat} -import play.api.mvc.{BodyParser, PlayBodyParsers} - -import scala.concurrent.ExecutionContext case class CreateDomainModelRequest( ubiquitousName: String, diff --git a/app/interfaces/api/user/edit/email/EditUserEmailController.scala b/app/interfaces/api/user/edit/email/EditUserEmailController.scala new file mode 100644 index 00000000..0ff7076b --- /dev/null +++ b/app/interfaces/api/user/edit/email/EditUserEmailController.scala @@ -0,0 +1,39 @@ +package interfaces.api.user.edit.email + +import dev.tchiba.auth.usecase.user.edit.email.{ChangeEmailOutput, ChangeEmailUseCase} +import interfaces.api.SdmtApiController +import interfaces.api.user.json.UserResponse +import interfaces.security.UserAction +import play.api.mvc.{Action, ControllerComponents} + +import javax.inject.Inject +import scala.concurrent.ExecutionContext + +class EditUserEmailController @Inject() ( + cc: ControllerComponents, + userAction: UserAction, + changeEmailUseCase: ChangeEmailUseCase +)(implicit ec: ExecutionContext) + extends SdmtApiController(cc) { + + def action(): Action[EditUserEmailRequest] = userAction(EditUserEmailRequest.validateJson) { implicit request => + val input = request.body.get(request.user.id) + changeEmailUseCase.handle(input) match { + case ChangeEmailOutput.Success(user) => + val response = UserResponse(user) + Ok(response.json) + case ChangeEmailOutput.NotFoundUser(userId) => + notFound( + code = "sdmt.user.edit.email.notFoundUser", + message = "not found target user.", + params = "userId" -> userId.value + ) + case ChangeEmailOutput.EmailIsNotUnique(email) => + badRequest( + code = "sdmt.user.edit.email.isNotUnique", + message = "input email address is not unique.", + params = "email" -> email.value + ) + } + } +} diff --git a/app/interfaces/api/user/edit/email/EditUserEmailRequest.scala b/app/interfaces/api/user/edit/email/EditUserEmailRequest.scala new file mode 100644 index 00000000..cf4dc408 --- /dev/null +++ b/app/interfaces/api/user/edit/email/EditUserEmailRequest.scala @@ -0,0 +1,20 @@ +package interfaces.api.user.edit.email + +import cats.data.ValidatedNel +import dev.tchiba.auth.core.user.UserId +import dev.tchiba.auth.usecase.user.edit.email.ChangeEmailInput +import dev.tchiba.sub.email.EmailAddress +import interfaces.json.request.play.{PlayJsonRequest, PlayJsonRequestCompanion} +import play.api.libs.json.{Json, OFormat} + +case class EditUserEmailRequest(email: String) extends PlayJsonRequest { + + override type VM = UserId => ChangeEmailInput + + override def validateParameters: ValidatedNel[(String, String), VM] = + EmailAddress.validate(email).toValidated("email").map(e => ChangeEmailInput(_, e)) +} + +object EditUserEmailRequest extends PlayJsonRequestCompanion[EditUserEmailRequest] { + implicit val jsonFormat: OFormat[EditUserEmailRequest] = Json.format[EditUserEmailRequest] +} diff --git a/app/interfaces/api/user/json/UserResponse.scala b/app/interfaces/api/user/json/UserResponse.scala index d6f12f70..97c7ba9a 100644 --- a/app/interfaces/api/user/json/UserResponse.scala +++ b/app/interfaces/api/user/json/UserResponse.scala @@ -7,14 +7,14 @@ final class UserResponse(private val user: User) { import UserResponse._ - private val response = Response(user.id.string, user.name) + private val response = Response(user.id.string, user.name, user.email.value, user.avatarUrl.map(_.value)) def json: JsValue = Json.toJson(response) } object UserResponse { - private case class Response(id: String, name: Option[String]) + private case class Response(id: String, name: Option[String], email: String, avatarUrl: Option[String]) implicit private val jsonFormat: OFormat[Response] = Json.format[Response] diff --git a/auth-application/src/main/scala/dev/tchiba/auth/application/interactors/user/edit/email/ChangeEmailInteractor.scala b/auth-application/src/main/scala/dev/tchiba/auth/application/interactors/user/edit/email/ChangeEmailInteractor.scala new file mode 100644 index 00000000..542f0239 --- /dev/null +++ b/auth-application/src/main/scala/dev/tchiba/auth/application/interactors/user/edit/email/ChangeEmailInteractor.scala @@ -0,0 +1,27 @@ +package dev.tchiba.auth.application.interactors.user.edit.email + +import dev.tchiba.auth.core.user.UserRepository +import dev.tchiba.auth.core.user.specs.email.unique.UserEmailUniqueChecker +import dev.tchiba.auth.usecase.user.edit.email.{ChangeEmailInput, ChangeEmailOutput, ChangeEmailUseCase} + +import javax.inject.Inject + +class ChangeEmailInteractor @Inject() ( + userEmailUniqueChecker: UserEmailUniqueChecker, + userRepository: UserRepository +) extends ChangeEmailUseCase { + + import ChangeEmailOutput._ + override def handle(input: ChangeEmailInput): ChangeEmailOutput = { + val result = for { + user <- userRepository.findById(input.userId).toRight(NotFoundUser(input.userId)) + emailIsUniqueMessage <- userEmailUniqueChecker.check(input.email).left.map { _ => EmailIsNotUnique(input.email) } + } yield { + val emailUpdatedUser = user.changeEmail(emailIsUniqueMessage) + userRepository.update(emailUpdatedUser) + Success(emailUpdatedUser) + } + + result.unwrap + } +} diff --git a/auth-core/src/main/scala/dev/tchiba/auth/core/user/User.scala b/auth-core/src/main/scala/dev/tchiba/auth/core/user/User.scala index 1bcfe9fe..0ef245d1 100644 --- a/auth-core/src/main/scala/dev/tchiba/auth/core/user/User.scala +++ b/auth-core/src/main/scala/dev/tchiba/auth/core/user/User.scala @@ -1,14 +1,17 @@ package dev.tchiba.auth.core.user import dev.tchiba.arch.ddd.{Aggregate, Entity} +import dev.tchiba.auth.core.user.specs.email.unique.UserEmailUniqueChecker.EmailIsUnique import dev.tchiba.sub.email.EmailAddress import dev.tchiba.sub.url.Url /** * ユーザー * - * @param id ユーザーID - * @param name 氏名 + * @param id + * ユーザーID + * @param name + * 氏名 */ final class User private ( val id: UserId, @@ -17,9 +20,25 @@ final class User private ( val avatarUrl: Option[Url] ) extends Entity[UserId] with Aggregate { + + /** + * メールアドレスを変更する + * @param message + * 変更しようとしているメールアドレスがユニークであることを示すメッセージ + */ + def changeEmail(message: EmailIsUnique): User = copy(email = message.email) + override def canEqual(that: Any): Boolean = that.isInstanceOf[User] override def toString = s"User(id=${id.value}, name=$name, email=${email.value}, avatarUrl=${avatarUrl.map(_.value)})" + + private def copy( + id: UserId = this.id, + name: Option[String] = this.name, + email: EmailAddress = this.email, + avatarUrl: Option[Url] = this.avatarUrl + ) = + new User(id, name, email, avatarUrl) } object User { diff --git a/auth-core/src/main/scala/dev/tchiba/auth/core/user/specs/email/unique/UserEmailUniqueChecker.scala b/auth-core/src/main/scala/dev/tchiba/auth/core/user/specs/email/unique/UserEmailUniqueChecker.scala new file mode 100644 index 00000000..f4ec6ae2 --- /dev/null +++ b/auth-core/src/main/scala/dev/tchiba/auth/core/user/specs/email/unique/UserEmailUniqueChecker.scala @@ -0,0 +1,37 @@ +package dev.tchiba.auth.core.user.specs.email.unique + +import dev.tchiba.arch.ddd.DomainService +import dev.tchiba.sub.email.EmailAddress + +trait UserEmailUniqueChecker extends DomainService { + import UserEmailUniqueChecker._ + + /** + * メールアドレスがユニークなものであるか調べる + * + * @param email + * 調べるメールアドレス + * @return + * 引数のメールアドレスがまだDBに同じものが存在せず、ユニークである時、[[Right]]で[[EmailIsUnique]]メッセージを返す + */ + def check(email: EmailAddress): Either[Because, EmailIsUnique] = + Either.cond(isNotExist(email), new EmailIsUnique(email), EmailAlreadyExist) + + /** + * 引数のメールアドレスを持つユーザーがまだDB上に存在しない + * @param email + * メールアドレス + * @return + * 存在する時: `false`, 存在しない時: `true` + */ + protected def isNotExist(email: EmailAddress): Boolean +} + +object UserEmailUniqueChecker { + + final class EmailIsUnique private[unique] (val email: EmailAddress) + + sealed trait Because + + case object EmailAlreadyExist extends Because +} diff --git a/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/JdbcUserRepository.scala b/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/JdbcUserRepository.scala index d2bbf6a6..892f5554 100644 --- a/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/JdbcUserRepository.scala +++ b/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/JdbcUserRepository.scala @@ -38,7 +38,7 @@ final class JdbcUserRepository extends UserRepository with UsersTranslator { ) .where .eq(c.userId, user.id.string) - } + }.update().apply() } override def delete(user: User): Unit = DB localTx { implicit session => diff --git a/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/specs/email/unique/JdbcUserEmailUniqueChecker.scala b/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/specs/email/unique/JdbcUserEmailUniqueChecker.scala new file mode 100644 index 00000000..afbe711e --- /dev/null +++ b/auth-infra/src/main/scala/dev/tchiba/auth/infra/core/user/specs/email/unique/JdbcUserEmailUniqueChecker.scala @@ -0,0 +1,18 @@ +package dev.tchiba.auth.infra.core.user.specs.email.unique + +import dev.tchiba.auth.core.user.specs.email.unique.UserEmailUniqueChecker +import dev.tchiba.sdmt.infra.scalikejdbc.Users +import dev.tchiba.sub.email.EmailAddress +import scalikejdbc._ +class JdbcUserEmailUniqueChecker extends UserEmailUniqueChecker { + + private val u = Users.u + override protected def isNotExist(email: EmailAddress): Boolean = DB readOnly { implicit session => + withSQL { + select(sqls.count) + .from(Users.as(u)) + .where + .eq(u.emailAddress, email.value) + }.map(rs => rs.int(1)).single().apply().get == 0 + } +} diff --git a/auth-module/src/main/scala/dev/tchiba/auth/module/AuthModule.scala b/auth-module/src/main/scala/dev/tchiba/auth/module/AuthModule.scala index 93e81673..d6295556 100644 --- a/auth-module/src/main/scala/dev/tchiba/auth/module/AuthModule.scala +++ b/auth-module/src/main/scala/dev/tchiba/auth/module/AuthModule.scala @@ -4,16 +4,20 @@ import com.google.inject.AbstractModule import dev.tchiba.auth.application.interactors.signIn.SignInInteractor import dev.tchiba.auth.application.interactors.signOut.SignOutInteractor import dev.tchiba.auth.application.interactors.signUp.SignUpInteractor +import dev.tchiba.auth.application.interactors.user.edit.email.ChangeEmailInteractor import dev.tchiba.auth.application.interactors.user.verify.VerifyUserInteractor import dev.tchiba.auth.application.services.AuthInfoUserLinker import dev.tchiba.auth.core.accessToken.AccessTokenService import dev.tchiba.auth.core.authInfo.AuthInfoRepository +import dev.tchiba.auth.core.user.specs.email.unique.UserEmailUniqueChecker import dev.tchiba.auth.infra.application.services.JdbcAuthInfoUserLinker import dev.tchiba.auth.infra.core.accessToken.MemcachedAccessTokenService import dev.tchiba.auth.infra.core.authInfo.JdbcAuthInfoRepository +import dev.tchiba.auth.infra.core.user.specs.email.unique.JdbcUserEmailUniqueChecker import dev.tchiba.auth.usecase.signIn.SignInUseCase import dev.tchiba.auth.usecase.signOut.SignOutUseCase import dev.tchiba.auth.usecase.signUp.SignUpUseCase +import dev.tchiba.auth.usecase.user.edit.email.ChangeEmailUseCase import dev.tchiba.auth.usecase.user.verify.VerifyUserUseCase import net.codingwell.scalaguice.ScalaModule @@ -23,9 +27,12 @@ class AuthModule extends AbstractModule with ScalaModule { bind[SignInUseCase].to[SignInInteractor] bind[SignOutUseCase].to[SignOutInteractor] bind[VerifyUserUseCase].to[VerifyUserInteractor] + bind[ChangeEmailUseCase].to[ChangeEmailInteractor] bind[AuthInfoRepository].to[JdbcAuthInfoRepository] + bind[UserEmailUniqueChecker].to[JdbcUserEmailUniqueChecker] + bind[AccessTokenService].to[MemcachedAccessTokenService] bind[AuthInfoUserLinker].to[JdbcAuthInfoUserLinker] } diff --git a/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala new file mode 100644 index 00000000..c7dbf73e --- /dev/null +++ b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala @@ -0,0 +1,7 @@ +package dev.tchiba.auth.usecase.user.edit.email + +import dev.tchiba.arch.usecase.Input +import dev.tchiba.auth.core.user.UserId +import dev.tchiba.sub.email.EmailAddress + +case class ChangeEmailInput(userId: UserId, email: EmailAddress) extends Input[ChangeEmailOutput] diff --git a/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailOutput.scala b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailOutput.scala new file mode 100644 index 00000000..ad4fec90 --- /dev/null +++ b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailOutput.scala @@ -0,0 +1,16 @@ +package dev.tchiba.auth.usecase.user.edit.email + +import dev.tchiba.arch.usecase.Output +import dev.tchiba.auth.core.user.{User, UserId} +import dev.tchiba.sub.email.EmailAddress + +sealed abstract class ChangeEmailOutput extends Output + +object ChangeEmailOutput { + + final case class Success(user: User) extends ChangeEmailOutput + + final case class NotFoundUser(userId: UserId) extends ChangeEmailOutput + + final case class EmailIsNotUnique(email: EmailAddress) extends ChangeEmailOutput +} diff --git a/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailUseCase.scala b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailUseCase.scala new file mode 100644 index 00000000..9970e195 --- /dev/null +++ b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailUseCase.scala @@ -0,0 +1,8 @@ +package dev.tchiba.auth.usecase.user.edit.email + +import dev.tchiba.arch.usecase.UseCase + +/** + * ユーザーのメールアドレス変更ユースケース + */ +abstract class ChangeEmailUseCase extends UseCase[ChangeEmailInput, ChangeEmailOutput] diff --git a/conf/routes b/conf/routes index 76d61938..c96cb3b6 100644 --- a/conf/routes +++ b/conf/routes @@ -8,6 +8,8 @@ POST /api/sign-out GET /api/users interfaces.api.user.UserApiController.allUsers() GET /api/users/:userId interfaces.api.user.UserApiController.findUser(userId: String) +PUT /api/users/self/edit-email interfaces.api.user.edit.email.EditUserEmailController.action() + GET /api/bounded-contexts interfaces.api.boundedContext.list.ListBoundedContextsApiController.action() POST /api/bounded-contexts interfaces.api.boundedContext.create.CreateBoundedContextApiController.action() GET /api/bounded-contexts/:idOrAlias interfaces.api.boundedContext.find.FindBoundedContextApiController.action(idOrAlias: String) From ee6d8abc6b36dc547cd51fb0ea3c2238d0718ca5 Mon Sep 17 00:00:00 2001 From: "toshinori.chiba" Date: Fri, 14 Apr 2023 23:34:17 +0900 Subject: [PATCH 2/2] add comment --- .../usecase/user/edit/email/ChangeEmailInput.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala index c7dbf73e..69ac4054 100644 --- a/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala +++ b/auth-usecase/src/main/scala/dev/tchiba/auth/usecase/user/edit/email/ChangeEmailInput.scala @@ -4,4 +4,12 @@ import dev.tchiba.arch.usecase.Input import dev.tchiba.auth.core.user.UserId import dev.tchiba.sub.email.EmailAddress -case class ChangeEmailInput(userId: UserId, email: EmailAddress) extends Input[ChangeEmailOutput] +/** + * メールアドレス変更ユースケースの入力 + * + * @param userId + * 変更対象のユーザーID + * @param email + * 置き換えるメールアドレス + */ +final case class ChangeEmailInput(userId: UserId, email: EmailAddress) extends Input[ChangeEmailOutput]