2
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?

ToDoアプリの作成(User編)

Last updated at Posted at 2024-10-29

はじめに

今回は,前回に続きユーザーの新規作成、ログイン、ログアウト機能を実装していきます。

※動作確認にPgAdminとPostmanを使います。

リポジトリの実装

User関係のリポジトリインターフェースを実装していきます。
このコードは、ユーザー情報を扱うためのリポジトリインターフェース IUserRepository と、そのインターフェースを実装する具体的なリポジトリ構造体 userRepository を定義しています。
NewUserRepository 関数は、userRepository 構造体のインスタンスを作成するために使用されます。
repository/user_repository.go
package repository

import (
	"go-rest-api/model"

	"gorm.io/gorm"
)

type IUserRepository interface {
    //このメソッドは、emailアドレスで指定されたユーザーの情報を取得します。
    //ユーザー情報をmodel.Userのポインタに格納します。
	GetUserByEmail(user *model.User, email string) error
    //このメソッドは、新しいユーザーを作成します。
    //user *model.Userは新規に作成するユーザーの情報を格納するmodel.User構造体のポインタです。
	CreateUser(user *model.User) error
}
type userRepository struct {
    //このフィールドは、データベース接続 (`gorm.DB` オブジェクト) を保持します。
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) IUserRepository {
    //新しい `userRepository` インスタンスを作成し、データベース接続を `db` フィールドに設定して返します。
	return &userRepository{db}
}

func (ur *userRepository) GetUserByEmail(user *model.User, email string) error {
    //gorm.DB の Where、First、Error メソッドを使って、メールアドレスでユーザーを取得します。
	if err := ur.db.Where("email=?", email).First(user).Error; err != nil {
		return err
	}
	return nil
}


func (ur *userRepository) CreateUser(user *model.User) error {
    //gorm.DB の Create メソッドを使って、新しいユーザーを作成します。
	if err := ur.db.Create(user).Error; err != nil {
		return err
	}
	return nil
}

ユースケースの実装

次はユースケースを作成していきます。
下記のコードはユーザー登録(サインアップ)とログイン処理を行うユースケースを実装したものです。
usecase/user_usecase.go
package usecase

import (
	"go-rest-api/model"
	"go-rest-api/repository"
	"os"
	"time"
    //go mod tidyコマンドを実行する
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
)

//IUserUsecase: ユーザー関連のユースケースが実装すべきメソッドを定義しています。
type IUserUsecase interface {
    //ユーザー登録処理を実行するメソッドです。
	SignUp(user model.User) (model.UserResponse, error)
    //ユーザーログイン処理を実行するメソッドです。
	Login(user model.User) (string, error)
}

//ユーザーリポジトリへの依存関係を保持するフィールドです。
type userUsecase struct {
	ur repository.IUserRepository
}

//userUsecase 構造体のインスタンスを生成する関数です。
func NewUserUsecase(ur repository.IUserRepository) IUserUsecase {
	return &userUsecase{ur}
}

//ユーザー登録処理を実行するメソッドです。
func (uu *userUsecase) SignUp(user model.User) (model.UserResponse, error) {
    //パスワードをハッシュ化して、データベースに保存します。
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
    //ハッシュ化処理中にエラーが発生した場合、エラーを返します。
	if err != nil {
		return model.UserResponse{}, err
	}
    //ハッシュ化されたパスワードとメールアドレスを使って、新しいユーザーの情報を格納する model.User 構造体を作成します。
	newUser := model.User{Email: user.Email, Password: string(hash)}
    //ユーザーリポジトリの CreateUser メソッドを呼び出して、新しいユーザーを作成します。
	if err := uu.ur.CreateUser(&newUser); err != nil {
		return model.UserResponse{}, err
	}
    //登録されたユーザーのIDとメールアドレスを使って、レスポンス構造体 model.UserResponse を作成します。
	resUser := model.UserResponse{
		ID:    newUser.ID,
		Email: newUser.Email,
	}
    //登録されたユーザーの情報を含む model.UserResponse とエラーオブジェクト nil を返します。
	return resUser, nil
}

//ログイン処理を実行するメソッドです。
func (uu *userUsecase) Login(user model.User) (string, error) {
    //データベースから取得したユーザー情報を格納する model.User 構造体を空で作成します。
	storedUser := model.User{}
    //ユーザーリポジトリの GetUserByEmail メソッドを呼び出して、メールアドレスでユーザーを取得します。
	if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
		return "", err
	}
    //bcrypt パッケージの CompareHashAndPassword 関数を使って、入力されたパスワードとデータベースに保存されたハッシュ化されたパスワードを比較します。
    //storedUser.Password は、データベースに保存されたハッシュ化されたパスワードです。
    //user.Password は、ユーザーがログイン時に入力したプレーンテキストのパスワードです。
	err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
	if err != nil {
		return "", err
	}
    //jwt パッケージの NewWithClaims 関数を使って、JWT トークンを作成します。
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": storedUser.ID,
        //jwtトークンの有効期限を12時間に設定
		"exp":     time.Now().Add(time.Hour * 12).Unix(),
	})
    //NewWithClaimsの返り値に対してSignedStringというメソッドを実行することによってjwtトークンを生成する
	tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

コントローラの実装

下記のコードは、ユーザーのサインアップ、ログイン、ログアウト処理を行うためのコントローラを定義しています。
controller/user_controller.go
package controller

import (
	"go-rest-api/model"
	"go-rest-api/usecase"
	"net/http"
	"os"
	"time"
    //go mod tidyコマンドを実行してパッケージをダウンロードしておく
	"github.com/labstack/echo/v4"
)

//ユーザーコントローラが実装すべきメソッドを定義するインターフェースです。
type IUserController interface {
    //ユーザー登録処理を実行するメソッドです。
	SignUp(c echo.Context) error
    //ユーザーログイン処理を実行するメソッドです。
	Login(c echo.Context) error
    //ユーザーログアウト処理を実行するメソッドです。
	LogOut(c echo.Context) error
}

type userController struct {
    //ユーザーユースケースへの依存関係を保持するフィールドです。
	uu usecase.IUserUsecase
}

    //ユーザーユースケースオブジェクトを受け取り、userController構造体のインスタンスを生成して返します。
func NewUserController(uu usecase.IUserUsecase) IUserController {
	return &userController{uu}
}

//ユーザー登録処理を実行するメソッドの実装です。
func (uc *userController) SignUp(c echo.Context) error {
    //ユーザーの情報を格納する model.User 構造体を空で作成します。
	user := model.User{}
    //リクエストからユーザー情報を取得する処理です。
    //リクエストからユーザー情報を取得して、user 構造体にバインドして格納します。
    //バインド処理中にエラーが発生した場合、エラーを返します。
    //エラーが発生した場合、HTTPステータスコード 400 (Bad Request) を返します。
	if err := c.Bind(&user); err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}
    //ユーザーユースケースの SignUp メソッドを呼び出して、ユーザー登録処理を実行します。
    //uu フィールドは、ユーザーユースケースへのポインタです。
    //SignUp メソッドは、ユーザー登録処理を実行します。
	userRes, err := uc.uu.SignUp(user)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
    //ユーザー登録処理が成功した場合、userRes 変数には、ユーザーの情報を格納する 
    //ユーザー登録処理が成功した場合、HTTPステータスコード 201 (Created) と、ユーザーの情報を含む model.UserResponse オブジェクトを JSON形式で返します。
    //model.UserResponse 構造体が格納されます。
	return c.JSON(http.StatusCreated, userRes)
}

//ログイン処理を実行するメソッドの実装です。
func (uc *userController) Login(c echo.Context) error {
    //ユーザーの情報を格納する model.User 構造体を空で作成します。
	user := model.User{}
    //リクエストからユーザー情報を取得する処理です。
    //c.Bind(&user) は、リクエストからユーザー情報を取得して、user 構造体にバインドします。
    //if err != nil { ... } は、バインド処理中にエラーが発生した場合、エラーを返します。
    //エラーが発生した場合、HTTPステータスコード 400 (Bad Request) を返します。
	if err := c.Bind(&user); err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}
    //ユーザーユースケースの Login メソッドを呼び出して、ユーザーログイン処理を実行します。
    //ログイン処理が成功した場合、tokenString 変数には、JWTトークンが格納されます。
	tokenString, err := uc.uu.Login(user)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
    //新しいクッキーを作成します。
	cookie := new(http.Cookie)
    //クッキーの名前を "token" に設定します。
	cookie.Name = "token"
    //クッキーの値を、生成されたJWTトークンに設定します。
	cookie.Value = tokenString
    //クッキーの有効期限を24時間後に設定します。
	cookie.Expires = time.Now().Add(24 * time.Hour)
    //クッキーのパスを "/" に設定します。
	cookie.Path = "/"
    //クッキーのドメインを、環境変数 API_DOMAIN から取得した値に設定します。
	cookie.Domain = os.Getenv("API_DOMAIN")
    //クッキーをHTTPのみで使用できるように設定します。
	cookie.HttpOnly = true
    // クッキーを SameSiteNoneMode に設定します。
    //http.SameSiteNoneMode は、クッキーがすべてのサイトからアクセスできることを示します。これは、CSRF攻撃に対する保護が弱いため、注意が必要です。
	cookie.SameSite = http.SameSiteNoneMode
    //クッキーをレスポンスに設定して、クライアントに送信します。
	c.SetCookie(cookie)
    //HTTPステータスコード 200 (OK) を返します。
	return c.NoContent(http.StatusOK)
}

//ログアウト処理を実行するメソッドの実装です。
func (uc *userController) LogOut(c echo.Context) error {
    //新しいクッキーを作成します。
	cookie := new(http.Cookie)
    //クッキーの名前を "token" に設定します。
	cookie.Name = "token"
    //クッキーの値を空文字列に設定します。
	cookie.Value = ""
    //クッキーの有効期限を現在時刻に設定します。これにより、クッキーはすぐに期限切れになり、削除されます。
	cookie.Expires = time.Now()
    //クッキーのパスを "/" に設定します。これは、クッキーがアプリケーションのすべてのページで使用できることを示します。
	cookie.Path = "/"
    //クッキーのドメインを、環境変数 API_DOMAIN から取得した値に設定します。
	cookie.Domain = os.Getenv("API_DOMAIN")
    //クッキーをHTTPのみで使用できるように設定します。これにより、JavaScriptからクッキーにアクセスできないように、セキュリティを強化します。
	cookie.HttpOnly = true
    //クッキーを SameSiteNoneMode に設定します。これは、クッキーがすべてのサイトからアクセスできることを示します。
	cookie.SameSite = http.SameSiteNoneMode
    //クッキーをレスポンスに設定して、クライアントに送信します。これにより、クライアントは、このクッキーを次回のアクセス時にサーバーに送信します。
	c.SetCookie(cookie)
    //HTTPステータスコード 200 (OK) を返します。
	return c.NoContent(http.StatusOK)
}

routerの実装

このコードは、NewRouter 関数を呼び出すことで、REST APIのルーティング設定を行います。
NewRouter 関数は、ユーザーコントローラーのインスタンス uc を引数として受け取ります。
この関数は、Echoフレームワークのインスタンスを作成し、signup, login, logout エンドポイントへのリクエストをそれぞれユーザーコントローラーの SignUp, Login, LogOut メソッドにマッピングします。
router/router.go
package router

import (
	"go-rest-api/controller"
     //go mod tidyコマンドを実行してパッケージをダウンロードしておく
	"github.com/labstack/echo/v4"
)

func NewRouter(uc controller.IUserController) *echo.Echo {
    //Echoフレームワークの新しいインスタンスを作成します。
	e := echo.New()
    // `POST` メソッドで `/signup` エンドポイントにアクセスしたときに、`uc.SignUp` 関数を呼び出すように設定します。
	e.POST("/signup", uc.SignUp)
    //`POST` メソッドで `/login` エンドポイントにアクセスしたときに、`uc.Login` 関数を呼び出すように設定します。
	e.POST("/login", uc.Login)
    //`POST` メソッドで `/logout` エンドポイントにアクセスしたときに、`uc.LogOut` 関数を呼び出すように設定します。
	e.POST("/logout", uc.LogOut)
    //設定されたルーティング設定を持つ Echoフレームワークのインスタンスを返します。
	return e
}

mainの実装

このコードは、データベース接続、ユーザーリポジトリ、ユーザーユースケース、ユーザーコントローラー、ルーティング設定などのコンポーネントを構築し、REST APIサーバーを起動します。
main.go
package main

import (
	"go-rest-api/controller"
	"go-rest-api/db"
	"go-rest-api/repository"
	"go-rest-api/router"
	"go-rest-api/usecase"
)

func main() {
    //`db` パッケージの `NewDB` 関数を呼び出して、データベースとの接続を作成します。
	db := db.NewDB()
    //`repository` パッケージの `NewUserRepository` 関数を呼び出して、ユーザーリポジトリの新しいインスタンスを作成します。
	userRepository := repository.NewUserRepository(db)
    //`usecase` パッケージの `NewUserUsecase` 関数を呼び出して、ユーザーユースケースの新しいインスタンスを作成します。
	userUsecase := usecase.NewUserUsecase(userRepository)
    //controller パッケージの NewUserController 関数を呼び出して、ユーザーコントローラの新しいインスタンスを作成します。
	userController := controller.NewUserController(userUsecase)
    //`router` パッケージの `NewRouter` 関数を呼び出して、REST APIのルーティング設定を行います。
	e := router.NewRouter(userController)
    //Echoフレームワークの `Start` メソッドを呼び出して、ポート 8080 でREST APIサーバーを起動します。
	e.Logger.Fatal(e.Start(":8080"))
}

プログラムを起動

下記のコマンドでプログラムを起動していきます。

GO_ENV=dev go run main.go

起動できました!
スクリーンショット 2024-10-28 18.42.58.png

次はPostmanを使って動作確認します。
http://localhost:8080/signupのエンドポイントを入力します。
メソッドはPOSTを選択し、Bodyのところはrawを選択し、JSONを選択します。
emailとpasswordを入力しSendボタンを押下します。
成功しますと201 Createdのステータス、生成されたユーザーのidとemailが返ってきます。

スクリーンショット 2024-10-28 18.47.28.png

PgAdminを起動していきます。
Dockerで立ち上げてるPostgressのデータベースの中に作成したユーザー情報が登録されています。(usersテーブルを見ています)
パスワードはハッシュ化されたパスワードが登録されています。

スクリーンショット 2024-10-28 18.58.20.png

エンドポイントをhttp://localhost:8080/loginに変更してSendボタンを押下します。
200 OKのステータスが返ってきて、CookiesタブにCookieが設定されます。

スクリーンショット 2024-10-28 19.02.56.png

エンドポイントをhttp://localhost:8080/logoutに変更してSendボタンを押下します。
200 OKのステータスが返ってきて、Cookieが削除されます。

スクリーンショット 2024-10-28 19.15.33.png

まとめ

今回はユーザーの新規追加、ログイン、ログアウト機能を実装していきました。
次回はタスク関係の処理を実装していきます。
2
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
2
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?