0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ES自動生成AIを作った ~コード解説編~ (SignUp)

Last updated at Posted at 2024-08-21

今回は以下の拡張機能を作成したときのことについて書いていく。
ES自動生成AIを作った

以下の機能がどの様に動いているのかについて、それぞれコードを追いながら解説していきたいと思う。(解説が少し長くなってしまったので、SignIn以降は別の記事に書こうと思う)

構成は以下の様になっている。

フロントエンド
frontend   (モジュールやビルドパッケージについては省略)
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── README.md
├── src
│   ├── components
│   │   ├── Profile.tsx
│   │   └── ProfileForm.tsx
│   ├── contents
│   │   └── index.tsx
│   ├── popup
│   │   ├── index.tsx
│   │   └── routes
│   │       ├── App.tsx
│   │       ├── checkEmail.tsx
│   │       ├── genAnswer.tsx
│   │       ├── generating.tsx
│   │       ├── home.tsx
│   │       ├── logOut.tsx
│   │       ├── openProfileForm.tsx
│   │       ├── signIn.tsx
│   │       └── signUp.tsx
│   └── tabs
│       ├── profile.html
│       └── profile.tsx
├── style.css
├── tailwind.config.js
└── tsconfig.json
バックエンド
backend
├── controller
│   ├── auth_controller.go
│   ├── controllerUtils
│   │   └── controllerUtils.go
│   ├── generate_controller.go
│   └── user_controller.go
├── db
│   └── db.go
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── swagger.yml
├── go.mod
├── go.sum
├── handler
│   └── handler.go
├── infrastructure
│   └── infrastructure.go
├── main.go
├── middleware
│   ├── auth
│   │   └── auth.go
│   └── cors
│       └── cors.go
├── migrate
│   └── migrate.go
├── model
│   ├── generate.go
│   └── user.go
├── README.md
├── repository
│   ├── auth_repository.go
│   ├── generate_usecase.go
│   └── user_repository.go
├── start.sh
├── terraform
│   ├── alb.tf
│   ├── cognito.tf
│   ├── docker-compose.yml
│   ├── ec2.tf
│   ├── provider.tf
│   ├── rds.tf
│   ├── route53.tf
│   ├── variables.tf
│   └── vpc.tf
├── usecase
│   ├── auth_usecase.go
│   ├── generate_usecase.go
│   ├── usecaseUtils
│   │   └── usecaseUtils.go
│   └── user_usecase.go
└── validator
    └── validator.go

SignUp (登録)

まずはフロントから見ていく。
拡張機能のボタンが押されるとhome.tsxが描画される。ここでは以下のように、現在がSignInしている状態かどうかを判別し、それぞれの状態に基づいて表示画面を判断している。

home.tsx
async function fetchData(
  loginState: string | undefined,
  setLoginState: (loginState: string) => void
) {
  try {
    const response = await fetch(api_endpoint + "/auth/login", {
      method: "POST",
      credentials: "include"
    })

    if (response.ok) {
      setLoginState("logged-in")
    } else {
      if (
        typeof loginState === "undefined" ||
        loginState === "not-logged-in" ||
        loginState === "logged-in"
      ) {
        setLoginState("not-logged-in")
      }
    }
  } catch (error) {
    console.error("Fetch error:", error)
  }
}

function IndexPopup() {
  const navigate = useNavigate()

  const [loginState, setLoginState] = useStorage<string>("loginState")

  useEffect(() => {
    fetchData(loginState, setLoginState)
  }, [])

  if (loginState === "not-logged-in") {
      //ログインしていない人用の画面(SignUpボタンやSignInボタンなど)
  } else if (loginState === "logged-in") {
      //ログインしている人用の画面(回答生成ボタンや経歴入力ボタンなど)
  }
}

そして、バックエンドのmain.goから順にファイルを参照し、SignInしているかどうかの確認をしていく。
main.goではDBの初期化を行いそれに伴って各種リポジトリーやユースケース、ミドルウェアなどを初期化してhandler.goというルーティングを行うところに渡す。

main.go
package main

import (
	"es-app/controller"
	"es-app/db"
	"es-app/infrastructure"
	"es-app/middleware/auth"
	"es-app/repository"
	"es-app/handler"
	"es-app/usecase"
)

func main() {
	db := db.NewDB()
	authRepository := repository.NewAuthRepository(db)
	userRepository := repository.NewUserRepository(db)
	generateRepository := repository.NewGenerateRepository()
	infrastructure := infrastructure.NewInfrastructure()
	authUsecase := usecase.NewAuthUsecase(authRepository, infrastructure)
	userUsecase := usecase.NewUserUsecase(userRepository)
	generateUsecase := usecase.NewGenerateUsecase(generateRepository, userRepository)
	authController := controller.NewAuthController(authUsecase)
	userController := controller.NewUserController(userUsecase)
	generateController := controller.NewGenerateController(generateUsecase)
	authMiddleware := auth.NewAuthMiddleware(infrastructure)
	e := handler.NewRouter(authController, authMiddleware, userController, generateController)
	e.Logger.Fatal(e.Start(":8080"))
}
handler.go
package handler

import (
	"es-app/controller"
	"es-app/middleware/auth"
	"es-app/middleware/cors"
	"es-app/validator"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/labstack/gommon/log"
)

func NewRouter(
	ac controller.IAuthController,
	am auth.IAuthMiddleware,
	uc controller.IUserController,
	gc controller.IGenerateController,
) *echo.Echo {
	e := echo.New()
	e.Validator = validator.NewValidator()
	e.Use(middleware.Logger())
	e.Logger.SetLevel(log.INFO)

	authGroup := e.Group("/auth")
	{
		authGroup.Use(cors.SetupAuthCORS())
		authGroup.POST("/signup", ac.SignUp)
		authGroup.POST("/checkEmail", ac.CheckEmail)
		authGroup.POST("/resendEmail", ac.ResendEmail)
		authGroup.POST("/login", ac.Login)
		authGroup.POST("/logout", ac.LogOut)
	}
	appGroup := e.Group("/app")
	{
		appGroup.Use(cors.SetupUserCORS())
		appGroup.Use(am.JwtMiddleware())
		userGroup := appGroup.Group("/profile")
		{
			userGroup.GET("/getProfile", uc.GetProfile)
			userGroup.PATCH("/updateProfile", uc.UpdateProfile)
		}
		generateGroup := appGroup.Group("/generate")
		{
			generateGroup.POST("/generateAnswers", gc.GenerateAnswers)
		}
	}
	return e
}

ここではEchoフレームワークを利用してインスタンスを初期化する。その後認証関連とログイン後に使用できるロジックとでエンドポイントをそれぞれグループ化して分けて、対応するエンドポイントに処理が流される。今回はフロントでaugh/loginを呼び出しているので、そこの処理を見ていく。

handler.goより、controller層auth_controller.goの中にある、Loginという以下の関数で確認していく。

Login
func (ac *authController) Login(c echo.Context) error {
	loginUser := model.LoginUser{}
	accessToken, err := c.Cookie("accessToken")
	if err == nil {
		res, err := ac.authUsecase.AccessToken(c, accessToken.Value)
		if err == nil {
			c.Logger().Debug("🟡 Use access token")
			return c.JSON(http.StatusOK, res)
		}
	}

	refreshToken, err := c.Cookie("refreshToken")
	if err == nil {
		cookieRes, userRes, err := ac.authUsecase.RefreshToken(c, refreshToken.Value)
		if err == nil {
			c.Logger().Debug("🟡 Use refresh token")
			controllerUtils.SetLoginCookie(c, cookieRes.IDToken, cookieRes.AccessToken, cookieRes.RefreshToken)
			return c.JSON(http.StatusOK, userRes)
		}
	}

	if err := c.Bind(&loginUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if err := c.Validate(loginUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	loginRes, err := ac.authUsecase.LogIn(c, loginUser)
	if err != nil {
		return echo.NewHTTPError(http.StatusForbidden, err.Error())
	}

	c.Logger().Debug("🟡 Use username and password")

	controllerUtils.SetLoginCookie(c, loginRes.IDToken, loginRes.AccessToken, loginRes.RefreshToken)

	return c.JSON(http.StatusOK, loginUser)
}

ここではまず、Access Tokenが有効かどうかを確認する。もし有効ならstatus OKを返し、ログインしていることになる。次にもしAccess Tokenが無効、もしくは無かった場合はRefresh Tokenを確認する。もし有効な場合はRefresh Tokenを使用して新しいAccess Tokenを作成し、もし成功したら status OKを返却する。そして、これらの両方がうまくいかなかった場合、例えば初回ログインなど(次の項で詳しく書く)、ユーザー名とパスワードでログインし、成功した場合はトークンが発行され、新しいトークンをCookieにセットして、status OKを返却する。そして、もし成功していない場合は途中の

	if err := c.Bind(&loginUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if err := c.Validate(loginUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	loginRes, err := ac.authUsecase.LogIn(c, loginUser)
	if err != nil {
		return echo.NewHTTPError(http.StatusForbidden, err.Error())
	}

ここの部分Bad RequestなりForbiddenなりが返却され、ログインログインが出来ていない事を知らせる。今回は以下の様にPOSTメソッドだが、bodyに何も入っていないのでBadRequestが返却される。(Login関数でこれを行うのではなくて、CheckStateの様な別関数に分けてもいい気もする)

const response = await fetch(api_endpoint + "/auth/login", {
      method: "POST",
      credentials: "include"
    })

そして、返却された値によりフロントで表示する画面が変わり、今回はログインしていない人用の画面が表示される。
そしてSignUpボタンが押されることでやっとSignUpの工程に進むことができる。

SignUpのボタンが押されるおsignUp.tsxというファイルを読み込む。ここではパスワードの監視(大文字、小文字、記号、数字を含んでいるかや8文字以上かどうか)やその入力されたパスワードによって文字色を変えたりなどをしているが、今回は主要な部分のみ解説していくので、それらは省き、入力されたユーザーネームやパスワード、メールアドレスのチェックなどをどの様に行なっているかについて見ていく。

signUp.tsxではusernamepasswordEmailを入力してもらい、まずそれらのうちusernameEmailに被りがなければAWSのCognitoにデータを登録し、Cookieにもユーザー名などをセットする。その後、Emailの確認として、確認コードをを送って、それを確認し、やっとSignUpが完了する。
では、実際のコードと共に流れを追っていく。
まずsignUp.tsxで以下の様にdata情報をPOSTする。

signUp.tsx
const onSubmit = async (data) => {
    console.log("SignUp form submitted")
    const response = await fetch(api_endpoint + "/auth/signup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(data)
    })
    if (response.ok) {
      console.log("SignUp successful")
      setLoginState("checkEmail")
      navigate("/checkEmail")
    }else if (response.status === 409) {
        // メールやユーザーネームの重複
    }else if (response.status === 500) {
        // サーバーエラー
    }else {
        // サインアップに失敗
    }

ここでdataとはusername email passwordを含むオブジェクトである。
以下の様にregister関数を使用して定義している。

data
  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      className="flex flex-col space-y-1.5 w-60 h-auto items-center mb-2 mt-2">
      <input
        {...register("username", {
          required: "ユーザー名は必須です",
          pattern: {
            value: /^[\x20-\x7E]+$/,
            message: "使用可能文字は半角英数字・記号のみです"
          },
          maxLength: {
            value: 20,
            message: "ユーザー名は20文字以下にしてください"
          }
        })}
        type="text"
        placeholder="Username"
        className="border border-gray-300 rounded-md px-4 py-1 w-5/6"
      />
      {errors.username && typeof errors.username.message === "string" && (
        <span className="text-red-500 text-xs">{errors.username.message}</span>
      )}

      <input
        {...register("email", {
          required: "メールアドレスは必須です",
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: "正しいメールアドレスを入力してください"
          }
        })}
        type="text"
        placeholder="Email"
        className="border border-gray-300 rounded-md px-4 py-1 w-5/6"
      />
      {errors.email && typeof errors.email.message === "string" && (
        <span className="text-red-500 text-xs">{errors.email.message}</span>
      )}

      <input
        {...register("password", {
          required: "パスワードは必須です",
          pattern: {
            value:
              /^(?=.*\d)(?=.*[\^$*.[\]{}()?"!@#%&\/\\,><':;|_~`=+\-])(?=.*[a-z])(?=.*[A-Z])[\x20-\x7E]{8,}$/,
            message: "パスワードが不正です"
          }
        })}
        type="password"
        placeholder="Password"
        className="border border-gray-300 rounded-md px-4 py-1 w-5/6"
      />

そして、handler.goでルーティング処理を行い、auth_controller.goのSignUpにアクセスされる。main.goはアプリケーションのエントリーポイントであり起動時に一度だけ実行される。そして依存性注入を利用して、初期化されたコンポーネントをcontroller.goに渡し、controller.goでルーティング処理を行うことで、次にアクセスするときはmain.goを介さずにルーティング処理部分にアクセスできるため、毎回初期化されるという事を防げる。

そしてauth_controller.goSignUp関数では以下の様にemailusernameが使用されていないかを確認してCognitoに登録される。

auth_controller.go
func (ac *authController) SignUp(c echo.Context) error {
	signUpUser := model.SignUpUser{}
	if err := c.Bind(&signUpUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if err := c.Validate(signUpUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	// メールアドレスが既に登録されているか確認
	isAlreadyRegisteredEmail, err := ac.authUsecase.IsAlreadyRegisteredEmail(c, signUpUser.Email)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}
	if isAlreadyRegisteredEmail {
		return echo.NewHTTPError(http.StatusConflict, "メールアドレスが既に登録されています")
	}

	userRes, err := ac.authUsecase.SignUp(c, signUpUser)
	if err != nil {
		// ユーザー名が既に登録されているか確認
		if httpError, ok := err.(*echo.HTTPError); ok && httpError.Code == http.StatusConflict {
			return echo.NewHTTPError(http.StatusConflict, "ユーザー名が既に登録されています")
		}
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

	controllerUtils.SetSignupCookie(c, "username", signUpUser.Username, 10*time.Minute)

	return c.JSON(http.StatusCreated, userRes)
}

まずsignUpUserを初期化する。(リクエストデータをバインドするため)
そして、以下のようにリクエストボディに入っているデータをsignUpUserという構造体にバインド(HTTPリクエストのデータを構造体に変換すること)する。

リクエストボディをバインド
if err := c.Bind(&signUpUser); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

補足 ~バインドとマッピングの違い~

  • バインドはHTTPリクエストのBodyを構造体に変換するプロセス
  • マッピングはあるデータ構造のフィールドから別のデータ構造のフィールドに結びつけるプロセス
    バインドはリクエストのデータを処理する際に使用され、マッピングはデータの変換や転送などに使用される

次にバインドされたデータ構造が正しい形をしているか確認する。

SingUpUser
type SignUpUser struct {
	Username  string    `json:"username" validate:"required"`
	Email     string    `json:"email" validate:"required,email"`
	Password  string    `json:"password" validate:"required"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

SinUpUser構造体は上記の様になっている。ここのUsername, Email, Passwordの3つに対して、空でないかどうかとEmailに対しては正しいメールアドレスの形式になっているかをValidateで確認する。(requiredフィールドは必須である事を示す)

その後IsAlreadyRegisterdEmail関数でメールアドレスに被りがないかを確認する。
まずusecase層IsAlreadyRegisterdEmail関数に飛ぶ。

auth_usecase.go
func (u *authUsecase) IsAlreadyRegisteredEmail(c echo.Context, email string) (bool, error) {
	email, err := u.authRepo.FindByEmail(c, email)
	if err != nil {
		return false, err
	}
	return email != "", nil
}

ここでは単にrepository層FindByEmail関数でDBにアクセスして返却されたものにエラーがないか確認しているだけである。usecase層repository層を分けることでテストが容易になり、依存関係も減り、再利用性も高まる。ついでにそれぞれの層が何をしているのかについてまとめておく。

  • controller層:
    ユーザーからのリクエストを受け取り、リクエストのパラメータを検証し、正しいusecase層を呼び出す
  • usecase層:
    ビジネスロジックを実装する層。アプリケーションのユースケース、具体的な処理や流れを記載する。controller層からのリクエストに対して適切な処理を行う。
  • repository層:
    データアクセスを担当する層。データベースにアクセスし、データの取得や作成などを行う。
  • infrastructure層:
    アプリケーション外部とのやり取りを行う。外部APIの呼び出しなど、データアクセス以外の外部リソースとのやりとりを担当する。

では本題のFindByEmail関数について見ていく。

auth_repository.go
type authRepository struct {
	db *gorm.DB
}

func (r *authRepository) FindByEmail(c echo.Context, email string) (string, error) {
	var user model.User
	result := r.db.WithContext(c.Request().Context()).Select("email").Where("email = ?", email).First(&user)
	if result.Error != nil {
		if result.Error == gorm.ErrRecordNotFound {
			return "", nil
		}
		return "", result.Error
	}
	return email, nil
}

ここではGormを使用して、受け取ったemailが存在するかどうかを確認し、存在しない場合は空文字を返却し、存在する場合はそのemailをそのまま返している。Gormとは、Go言語用のORMライブラリである。ORM(Object-Relational Mapping)とはデータベースのテーブルをオブジェクトとして扱うことができるというもの。

ORMのメリット
https://baapuro.com/Django/eight/
これらによってもしメールアドレスがすでに登録されていた場合はそのメールアドレスが返され、auth_usecase.goのISAlreadyREgisterdEmail関数contorller.gotrueが返却されフロントでそれを判別する。

これでメールアドレスの重複は出来た。

次にユーザー名の確認をして、登録を行う。今回は別関数を作ってユーザー名の確認をするのではなく、Cognitoでユーザー名が被ったらそもそも登録できずにエラーが返される様になっているので、そこを使って処理した。

auth_controller.go
userRes, err := ac.authUsecase.SignUp(c, signUpUser)
	if err != nil {
		// ユーザー名が既に登録されているか確認
		if httpError, ok := err.(*echo.HTTPError); ok && httpError.Code == http.StatusConflict {
			return echo.NewHTTPError(http.StatusConflict, "ユーザー名が既に登録されています")
		}
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

これでusecase層SignUp関数を呼び出して登録可能かどうかを判別していく。

auth_usecase.go
func (au *authUsecase) SignUp(c echo.Context, signUpUser model.SignUpUser) (model.User, error) {
	user, err := au.infrastructure.SignUp(c, signUpUser)
	if err != nil {
		return model.User{}, err
	}

	if err := au.authRepo.CreateUser(c, user); err != nil {
		return model.User{}, err
	}

	return user, nil
}

ここではinfrastructure層SinUp関数で外部API、Cognitoにアクセスして登録を行う。infrastructure層が何をしているかは先ほど述べたとおりだ。

infrastructure.go
func (i *infrastructure) SignUp(c echo.Context, signUpUser model.SignUpUser) (model.User, error) {
	svc, err := i.CreateCognitoClient(c)
	if err != nil {
		return model.User{}, err
	}

	signUpInput := &cognitoidentityprovider.SignUpInput{
		ClientId: aws.String(i.clientID),
		Username: aws.String(signUpUser.Username),
		Password: aws.String(signUpUser.Password),
		UserAttributes: []types.AttributeType{
			{
				Name:  aws.String("email"),
				Value: aws.String(signUpUser.Email),
			},
		},
	}

	signUpOutput, err := svc.SignUp(c.Request().Context(), signUpInput)
	if err != nil {
		var ae *types.UsernameExistsException
		if errors.As(err, &ae) {
			return model.User{}, echo.NewHTTPError(http.StatusConflict, "ユーザーネームが既に存在します")
		}
		return model.User{}, err
	}

	userID := *signUpOutput.UserSub
	createdAt := time.Now()

	user := model.User{
		UserID:    userID,
		Username:  signUpUser.Username,
		Email:     signUpUser.Email,
		CreatedAt: createdAt,
		UpdatedAt: createdAt,
	}

	return user, nil
}

ここでは、まずCognitoへのサインアップの入力データを作成する。(SignUpInput)
それをCognitoSignUp関数へ渡して登録する。その際Passwordはハッシュ化されて登録されるのでこちらでハッシュ化してから渡す必要はない。

ユーザー名の重複チェック
signUpOutput, err := svc.SignUp(c.Request().Context(), signUpInput)
	if err != nil {
		var ae *types.UsernameExistsException
		if errors.As(err, &ae) {
			return model.User{}, echo.NewHTTPError(http.StatusConflict, "ユーザーネームが既に存在します")
		}
		return model.User{}, err
	}

もしCognitoの登録の際にユーザーネームに被りがあったら、上記のようにerrUsernameExistsExceptionというエラーが返ってきてユーザー名の重複を知らせてくれる。

そしてもし正しく登録できた場合は、UserIDメールアドレスなどの情報を、呼び出し元のauth_usecase.goSignUp関数で受け取り、それをauth_controller.goSignUp関数に返却する。もし、重複があった場合はそれをフロント側にStatus Codeで伝え、適切なメッセージを出す。特に問題なくCognitoに登録できていた場合はユーザー名をCookieに保存する。これはこの後のメールアドレスを認証するときにCognitousernameverification codeの両方を送らなきゃいけないためである。登録したすぐ直後にメールアドレスの認証を行うのだが、わざわざもう一度usernameを打たせるのは面倒なのでCookieにセットした。

これでやっとCognitoへの登録が完了する。

Check Email (メールアドレスの確認)

次にメールアドレスのチェクを行う。この操作を持ってSignUpが完了する。メールアドレスのチェックとは、Cognitoに登録した際のメールアドレスにVerificationCodeという6桁の数字を送り、それを入力してもらって正しい確認し、もし正しければメールアドレスを含めたすべての登録が完了するという手順である。

SignUp.tsx
const onSubmit = async (data) => {
    console.log("SignUp form submitted")
    const response = await fetch(api_endpoint + "/auth/signup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(data)
    })
    if (response.ok) {
      console.log("SignUp successful")
      setLoginState("checkEmail")
      navigate("/checkEmail")
    } else //エラーだった場合

先ほどのSignUpが上手くいった場合は/checkEmailという画面に自動的に遷移する。

checkEmail.tsx
...

const onSubmit = async (data) => {
    console.log("Check Email form submitted")
    const response = await fetch(api_endpoint + "/auth/checkEmail", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ verificationCode: data.verificationCode })
    })
    if (response.ok) {
      console.log("Check Email successful")
      navigate("/signin")
    } else {
      console.error("Check Email failed")
      alert("認証コードが違います")
    }
  }

...

  onSubmit={handleSubmit(onSubmit)}
      <input
        type="text"
        placeholder="VerificationCode"
        {...register("verificationCode", {
          required: "コードを入力してください",
          minLength: { value: 6, message: "正しいコードを入力してください" },
          maxLength: { value: 6, message: "正しいコードを入力してください" }
        })}
        
...

このように6文字でのみ認証コードを受け取る様にしている。

それをonsubmitでバックエンドにPOSTリクエストを飛ばし、正しいコードかどうかを確認する。

そして再びhandler.goにより、controller層auth_controller.goに流されるので、その後の流れを順を追ってみていく。

auth_controller.go
func (ac *authController) CheckEmail(c echo.Context) error {
	checkStruct := model.CheckEmail{}
	if err := c.Bind(&checkStruct); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if err := c.Validate(checkStruct); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	usernameCookie, err := c.Cookie("username")
	if err != nil {
		return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
	}
	checkStruct.Username = usernameCookie.Value

	checkRes, err := ac.authUsecase.CheckEmail(c, checkStruct)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

	return c.JSON(http.StatusOK, checkRes)
}

ここでも先ほどと同じように構造体にバインドして、それが正しいかどうかをValidate関数で確かめる

model / user.go
type CheckEmail struct {
	Username         string `json:"username"`
	VerificationCode string `json:"verificationCode" validate:"required"`
}

今回はVerificationCodeを確かめている。
今回はフロントで6文字かどうか確かめているのにわざわざValidate関数で確認する必要ある?と思うかもしれないが、実際のところブラウザ上での確認はユーザーが開発者ツールを使えば簡単にスキップできてしまうし、コードの一貫性や整合性を持たせるために毎回きちんとValidationを行なっている。

そしてValidationが終わったらCookieからusernameを取得し(先ほどSignUpの際に仕込んだ)、それらの二つをusecase層CheckEmail関数で本当に正しいかどうか確かめる。

usecase層では以下のようにinfrastructure層に促すだけである。CognitoはDBではなく外部アプリケーションなのでinfrastructure層に記載。(認証、認可に特化したAWSのサービス)

auth_usecase.go
func (au *authUsecase) CheckEmail(c echo.Context, checkEmail model.CheckEmail) (bool, error) {
	res, err := au.infrastructure.CheckEmail(c, checkEmail)
	if err != nil {
		return false, err
	}

	return res, nil
}

実際のチェックはinfrastructure.goで行っている。

infrastructure.go
func (i *infrastructure) CheckEmail(c echo.Context, checkEmail model.CheckEmail) (bool, error) {
	svc, err := i.CreateCognitoClient(c)
	if err != nil {
		return false, err
	}

	confirmSignUpInput := &cognitoidentityprovider.ConfirmSignUpInput{
		ClientId:         aws.String(i.clientID),
		Username:         aws.String(checkEmail.Username),
		ConfirmationCode: aws.String(checkEmail.VerificationCode),
	}

	_, err = svc.ConfirmSignUp(c.Request().Context(), confirmSignUpInput)
	if err != nil {
		return false, err
	}

	return true, nil
}

と言っても、内容は難しくなく、Cognitoに必要な情報(clientIDはどのアプリケーションからのリクエストか判別するために必要らしい)を渡すだけだ。aws.Stringは文字列をポインタにしている。(AWS SDKの多くのAPIはポインタ型を要求するため覚えておくと良い)

これでもし正しければ、trueが返却され、フロントエンドに200 Okが返却され、メールアドレスのチェックが完了し、サインインの画面へ自動で遷移する。

Resend Email (メールアドレスにVerificationコードを再送)

ついでに、Resend Emailを少しだけ触れておく。

infrastructure.go
func (i *infrastructure) ResendEmail(c echo.Context, resendEmail model.ResendEmail) (bool, error) {
   svc, err := i.CreateCognitoClient(c)
   if err != nil {
   	return false, err
   }

   resendInput := &cognitoidentityprovider.ResendConfirmationCodeInput{
   	ClientId: aws.String(i.clientID),
   	Username: aws.String(resendEmail.Username),
   }

   _, err = svc.ResendConfirmationCode(c.Request().Context(), resendInput)
   if err != nil {
   	return false, err
   }

   return true, nil
}

まぁ見ての通りなのだが、受け取ったusernameに紐付いているemailにCognitoが自動的にメールを再送してくれるというだけだ。

終わりに

これにて登録の全ての手順が終了した。難しくてわからない、というところがないようになるべく丁寧に解説したつもりだ。内容自体はそこまで難しくないが、クリーンアーキテクチャに沿っていて層が分かれていてコード上では少し追いずらい、などがあるかもしれない。しかし、それぞれの層が何をしているのかについてきちんと理解すれば、少しは読みやすくなり、保守性、運用性が高いことが見て取れるだろう。
次回以降でSignInや回答生成などの別機能について解説していく。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?