0
1

ES自動生成AIを作ってた ~コード解説編~ (SignIn & SignOut)

Last updated at Posted at 2024-08-28

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

以下の機能がどの様に動いているのかについて、それぞれコードを追いながら解説していきたいと思う。

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

フロントエンド
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

SignIn (ログイン)

まずはフロントから見ていく。
home.tsxというファイルでサインインというボタンを押すとsignIn.tsxというファイルにナビゲートされる。このファイルではバックエンドのサインイン用のAPIに入力してもらったユーザーネームとパスワードを投げて、サインインできるかどうかを判断するというものである。

signIn.tsx
import React from "react"
import { useForm } from "react-hook-form"
import { useNavigate } from "react-router-dom"

import { useStorage } from "@plasmohq/storage/hook"

import { api_endpoint } from "../../contents/index"
import openProfileForm from "./openProfileForm"

const SignIn = () => {
 const navigate = useNavigate()
 const [loginState, setLoginState] = useStorage<string>("loginState")
 const {
   register,
   handleSubmit,
   formState: { errors }
 } = useForm()

 const onSubmit = async (data) => {
   console.log("SignIn form submitted")
   const response = await fetch(api_endpoint + "/auth/login", {
     method: "POST",
     headers: {
       "Content-Type": "application/json"
     },
     body: JSON.stringify(data)
   })

   if (response.ok) {
     console.log("SignIn successful")
     setLoginState("logged-in")
     openProfileForm()
   } else {
     console.error("Sign in failed")
     alert("ユーザーネームまたはパスワードが違います")
   }
 }

 return (
   <form
     onSubmit={handleSubmit(onSubmit)}
     className="flex flex-col space-y-1.5 w-40 items-center mb-2 mt-2">
     <input
       type="text"
       placeholder="Username"
       {...register("username", {
         required: "ユーザー名は必須です"
       })}
       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
       type="password"
       placeholder="Password"
       {...register("password", {
         required: "パスワードは必須です"
       })}
       className="border border-gray-300 rounded-md px-4 py-1 w-5/6"
     />
     {errors.password && typeof errors.password.message === "string" && (
       <span className="text-red-500 text-xs">{errors.password.message}</span>
     )}

     <div className="flex justify-center space-x-4">
       <button
         type="submit"
         className="bg-blue-500 text-white rounded-md px-3.5 py-2 hover:bg-blue-700">
         Sign In
       </button>
       <button
         onClick={() => {
           setLoginState("not-logged-in")
           navigate("/")
         }}
         className="bg-gray-500 text-white rounded-md px-3 py-2 hover:bg-gray-700">
         Back
       </button>
     </div>
   </form>
 )
}

export default SignIn

注意点としては以下のところでパスワードが間違っているのか、ユーザーネームが間違っているのかを知らせてはいけないということだ。どちらかがわかるとブルートフォース攻撃などされてしまう可能性があるので、ユーザーネームかパスワードのどちらかが間違っていても、指定しないことに注意しよう。

APIからのレスポンスの処理
if (response.ok) {
      console.log("SignIn successful")
      setLoginState("logged-in")
      openProfileForm()
    } else {
      console.error("Sign in failed")
      alert("ユーザーネームまたはパスワードが違います")//ここでどちらの間違いかに言及しない様にしよう
    }

もしサインインに成功したらcookieにログインしていることを書き込む。こうすることでログイン状態を維持して、様々なログイン後のアクションを確認なしでしようすることができる。

そして次にバックエンドのルーティング処理が行われる

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
}

そしてcontroller層のLoginという関数に飛ばされる。各層が何をしているのかなどはSignUpの記事を見てもらえればわかると思う。

前回の記事のSignUpの際にもLoginしているかどうかを確認するためにこの関数について解説したので今回は簡単にだけ説明する。この層より下のusecase層などは今回は省略する。

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)
}

ここでは、まずアクセストークンをクッキーから取得し、それが有効ならそれを使用してサインインを済ませる。
もし有効でなく、レフレッシュトークンが有効ならばそれを使用して新しいアクセストークンをクッキーにセットしてログインを完了する。
これらのどちらも有効でない場合はパスワードとユーザーネームを利用してログインをし、アクセストークンとリフレッシュトークンをクッキーにセットする。

これらのクッキーのセットを行うことで次回以降ユーザーネームなどを入力することなく別の画面に飛んだりしても再ログインせずに拡張機能を同じように使用することができる

これでサインインが完了した。
前回にサインアップの記事でサインインの処理のところも詳しく説明していたので、とても簡単に終わってしまった。

SignOut (ログアウト)

フロントのhome.tsxでログアウトというボタンを押すとlogOut.tsxというファイルに飛ぶ。

logOut.tsx
import React from "react"
import { useNavigate } from "react-router-dom"

import { useStorage } from "@plasmohq/storage/hook"

import { api_endpoint } from "../../contents"

function LogOut() {
  const navigate = useNavigate()
  const [_, setLoginState] = useStorage<string>("loginState")

  const handleLogout = async () => {
    try {
      const response = await fetch(api_endpoint + "/auth/logout", {
        method: "POST",
        credentials: "include"
      })
      if (response.ok) {
        await setLoginState("not-logged-in")
        navigate("/")
      } else {
        console.error("Logout failed")
      }
    } catch (error) {
      console.error("An error occurred during logout", error)
    }
  }

  return (
    <button
      className="block mx-auto bg-gray-500 hover:bg-gray-700 text-white rounded-md w-24 h-8 p-2 mt-1 mb-4"
      onClick={handleLogout}>
      ログアウト
    </button>
  )
}

export default LogOut

この関数内では特に難しいことはしておらず、signIn.tsxのファイルと同様にログアウト用のAPIを叩いているだけである。ログアウトはクッキーを消すだけなので何かdataを送る必要はない。ここで少しだけなぜPOSTメソッドを使用したのかについて記す

  • なぜPOSTメソッドを使用するのか
    • 状態変更の意図を伝える:
      REST APIの設計原則の様なものがある。これはWeb上での通信を効率的かつ安全に行うためのガイドラインだ。参考記事1 参考記事2
      これらを簡単に読めばわかるのだが、一般的に、リソースの作成やサーバーの状態変更などの操作を行う時にはPOSTメソッドを使用することが一般的である。

そしてAPIに送られた処理を同様にルーティング処理を行い、auth_controller.goに処理が送られる

auth_controller.go
func (ac *authController) LogOut(c echo.Context) error {
	accessToken, err := c.Cookie("accessToken")
	if err != nil {
		return echo.NewHTTPError(http.StatusUnauthorized, "Access token not found")
	}

	// Cognitoからのサインアウト
	err = ac.authUsecase.LogOut(c, accessToken.Value)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

	// クライアント側でクッキーを削除するためのセット
	controllerUtils.ClearLoginCookie(c)

	c.Logger().Debug("🟡 Logged out")

	return c.NoContent(http.StatusOK)
}

ここではCognitoからサインアウトを行い、クッキーからもアクセストークンやリフレッシュトークン、IDトークンなどを削除(何もセットしない)を行う。以下のようにMaxAgeを-1(過去)にすることでブラウザ側でそのクッキーの中身を削除してくれる。また、Cognitoからサインアウトすることで、サーバー側でユーザーのセッションを無効にし、アクセストークンやリフレッシュトークンが不正に使用されるリスクを減らす。

controllerUtils.go(controller層)
func ClearLoginCookie(c echo.Context) {
	c.SetCookie(&http.Cookie{
		Name:     "accessToken",
		Value:    "",
		MaxAge:  -1,
		HttpOnly: true,
		Secure:  false,
		SameSite: http.SameSiteStrictMode,
		Path:     "/",
	})
	c.SetCookie(&http.Cookie{
		Name:     "refreshToken",
		Value:    "",
		MaxAge:  -1,
		HttpOnly: true,
		Secure:  false,
		SameSite: http.SameSiteStrictMode,
		Path:     "/",
	})
	c.SetCookie(&http.Cookie{
		Name:     "idToken",
		Value:    "",
		MaxAge:  -1,
		HttpOnly: true,
		Secure:  false,
		SameSite: http.SameSiteStrictMode,
		Path:    "/",
	})
}

これにてサインアウトの処理も完了した。

最後に

前回の記事でサインインの処理を詳しく書いていたため割と簡潔な記事になった。サインアウトではCognitoでもサインアウトすることで作成したトークンが全て無効になり、悪用(e.g.不正アクセス)なども防ぐことができる。

0
1
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
1