今回は以下の拡張機能を作成したときのことについて書いていく。
ES自動生成AIを作った
以下の機能がどの様に動いているのかについて、それぞれコードを追いながら解説していきたいと思う。(解説が少し長くなってしまったので、SignIn以降は別の記事に書こうと思う)
- SignUp(メールアドレスなどの認証も含む登録) (今回の記事)
- SignIn(ログイン)
- 経歴入力(自己PR、経験、今まで作った作品
- 回答生成(埋め込み含む)
- SignOut(ログアウト)
構成は以下の様になっている。
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している状態かどうかを判別し、それぞれの状態に基づいて表示画面を判断している。
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
というルーティングを行うところに渡す。
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"))
}
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
という以下の関数で確認していく。
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
ではusername
、password
、Email
を入力してもらい、まずそれらのうちusername
とEmail
に被りがなければAWSのCognitoにデータを登録し、Cookieにもユーザー名などをセットする。その後、Email
の確認として、確認コードをを送って、それを確認し、やっとSignUpが完了する。
では、実際のコードと共に流れを追っていく。
まずsignUp.tsx
で以下の様にdata
情報をPOST
する。
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関数
を使用して定義している。
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.go
のSignUp関数
では以下の様にemail
とusername
が使用されていないかを確認してCognito
に登録される。
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を構造体に変換するプロセス
- マッピングはあるデータ構造のフィールドから別のデータ構造のフィールドに結びつけるプロセス
バインドはリクエストのデータを処理する際に使用され、マッピングはデータの変換や転送などに使用される
次にバインドされたデータ構造が正しい形をしているか確認する。
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関数
に飛ぶ。
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関数
について見ていく。
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.go
にtrue
が返却されフロントでそれを判別する。
これでメールアドレスの重複は出来た。
次にユーザー名の確認をして、登録を行う。今回は別関数を作ってユーザー名の確認をするのではなく、Cognito
でユーザー名が被ったらそもそも登録できずにエラーが返される様になっているので、そこを使って処理した。
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関数
を呼び出して登録可能かどうかを判別していく。
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層
が何をしているかは先ほど述べたとおりだ。
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)
それをCognito
のSignUp関数
へ渡して登録する。その際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
の登録の際にユーザーネームに被りがあったら、上記のようにerr
にUsernameExistsException
というエラーが返ってきてユーザー名の重複を知らせてくれる。
そしてもし正しく登録できた場合は、UserID
やメールアドレス
などの情報を、呼び出し元のauth_usecase.go
のSignUp関数
で受け取り、それをauth_controller.go
のSignUp関数
に返却する。もし、重複があった場合はそれをフロント側にStatus Code
で伝え、適切なメッセージを出す。特に問題なくCognito
に登録できていた場合はユーザー名
をCookieに保存する。これはこの後のメールアドレスを認証するときにCognito
にusername
とverification code
の両方を送らなきゃいけないためである。登録したすぐ直後にメールアドレスの認証を行うのだが、わざわざもう一度username
を打たせるのは面倒なのでCookieにセットした。
これでやっとCognito
への登録が完了する。
Check Email (メールアドレスの確認)
次にメールアドレスのチェクを行う。この操作を持ってSignUpが完了する。メールアドレスのチェックとは、Cognito
に登録した際のメールアドレスにVerificationCode
という6桁の数字を送り、それを入力してもらって正しい確認し、もし正しければメールアドレスを含めたすべての登録が完了するという手順である。
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
という画面に自動的に遷移する。
...
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
に流されるので、その後の流れを順を追ってみていく。
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関数
で確かめる
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のサービス)
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
で行っている。
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を少しだけ触れておく。
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や回答生成などの別機能について解説していく。