【Go+Gin+gorm】クリーンアーキテクチャでログイン機能を実装したAPI

## 保険
この記事はGoを勉強して１ヶ月くらいの奴が書いてます。
また、クリーンアーキテクチャについてわかったつもりのやつが書いてます。
ご了承ください。

## 機能
- ユーザー登録
- ログイン
- ログアウト

## クリーンアーキテクチャ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2932655/5e2201cd-77e3-3072-628f-208c1fbf6683.png)


## ログイン処理の流れ
理解を簡単にするために、ユーザーがログインする際の実際の処理の流れについて考えてみます。
1. ユーザーがエンドポイント```/login```にPOSTリクエスト
1. ルーターが```/login```に対応するコントローラーを呼ぶ
1. コントローラーがリクエストボディのjsonを構造体に変換してユースケースに渡す
1. ユースケースが渡された構造体の```Email```を引数にリポジトリを呼び出す
1. リポジトリがDBにアクセスし、```Email```が一致するレコードをSELECTし、構造体に変換してユースケースに渡す
1. ユースケースが```Email```と```Password```が一致するか判定し、一致したらトークンを発行
1. コントローラーがトークンをjsonに変換してレスポンスを返す

## 各レイヤーの役割
### router
- ```POST /signup```でコントローラーの```Signup```関数を呼ぶ
- ```POST /login```でコントローラーの```Login```関数を呼ぶ
- ```POST /logout```でコントローラーの```Logout```関数を呼ぶ

### controller
- ```Signup```：リクエストのbodyのjsonを構造体に変換してユースケースの```Signup```関数に渡し、結果をjsonで返す
- ```Login```：リクエストのbodyのjsonを構造体に変換してユースケースの```Login```関数に渡し、ログインに成功した場合はクッキーにログイン情報を保存
- ```Logout```：クッキーのログイン情報を削除

### usecase
- ```Signup```：User構造体のパスワードをハッシュ化し、リポジトリの```CreateUser```に渡す
- ```Login```：```Email```フィールドを引数としてリポジトリの```GetByEmail```を呼び出し、```Email```と```Password```を照合し、一致したらトークンを生成してコントローラーに返す

### repository
- ```CreateUser```：DBにユーザー情報をINSERTする
- ```GetByEmail```：```Email```フィールドが一致するレコードをSELECTし構造体に変換してユースケースに返す

## request / response
今回実装するAPIのリクエスト・レスポンスの例です。

### ```POST /signup```
```json:example request
{
    "email": "hogehoge1@example.com"
    "password": "hoge1"
}
```

```json:example response
{
    "ID": 1
    "email": "hogehoge1@example.com"
}
```

### ```POST /login```
```json:example request
{
    "email": "hogehoge1@example.com"
    "password": "hoge1"
}
```

```json:example response
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDExMjc5MTgsInVzZXJfaWQiOjF9.Tp5tUkrLCNhxJnDc7Z0a4UfqCcsF22jEqbheeRn82E0"
}
```

## 実装

### entity
ここでは```User```構造体を定義します。主キーの```ID```と、登録に必要な```Email```と```Password```です。

```go:./entity/user.go
package entity

type User struct {
	ID       uint   `json:"id" gorm:"primaryKey"`
	Email    string `json:"email" gorm:"unique"`
	Password string `json:"password"`
}
```

```Email```には重複がないようにunique制約をつけています。
ここで、一番内側のentity層に```gorm```の情報が書かれているのは厳密には良くないのではと思いましたが、どうなんでしょう。

### main
```go:main.go
package main

import (
	"gin-login/controller"
	"gin-login/db"
	"gin-login/repository"
	"gin-login/router"
	"gin-login/usecase"
)

func main() {
	db := db.NewDB()
	userRepository := repository.NewUserRepository(db)
	userUsecase := usecase.NewUserUsecase(userRepository)
	userController := controller.NewUserController(userUsecase)
	r := router.NewRouter(userController)
	r.Run()
}
```

### router
特にいうことはないです。
```go:./router/router.go
package router

import (
	"gin-login/controller"

	"github.com/gin-gonic/gin"
)

func NewRouter(uc controller.UserController) *gin.Engine {
	r := gin.Default()

	r.POST("/signup", uc.Signup)
	r.POST("/login", uc.Login)
	r.POST("/logout", uc.Logout)

	return r
}
```

### controller
- ```Signup```
- ```Login```
- ```Logout```

の3つのメソッドを持つインターフェースを定義し、実装しています。
ログイン情報はCookieに保存しています。

```go:./controller/user_controller.go
package controller

import (
	"gin-login/entity"
	"gin-login/usecase"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

type UserController interface {
	Signup(c *gin.Context)
	Login(c *gin.Context)
	Logout(c *gin.Context)
}

type userController struct {
	uu usecase.UserUsecase
}

func NewUserController(uu usecase.UserUsecase) UserController {
	return &userController{uu}
}

func (uc *userController) Signup(c *gin.Context) {
	user := entity.User{}
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, err)
	}

	err := uc.uu.Signup(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
	}
	c.JSON(http.StatusOK, nil)
}

func (uc *userController) Login(c *gin.Context) {
	user := entity.User{}
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, err)
	}

	tokenString, err := uc.uu.Login(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
	}

	cookie := &http.Cookie{
		Name:     "token",
		Value:    tokenString,
		Expires:  time.Now().Add(1 * time.Hour),
		Path:     "/",
		Domain:   "localhost",
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteNoneMode,
	}
	http.SetCookie(c.Writer, cookie)

	c.JSON(http.StatusOK, tokenString)
}

func (uc *userController) Logout(c *gin.Context) {
	cookie := &http.Cookie{
		Name:     "token",
		Value:    "",
		Expires:  time.Now(),
		Path:     "/",
		Domain:   "localhost",
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteNoneMode,
	}

	http.SetCookie(c.Writer, cookie)
	c.JSON(http.StatusOK, nil)
}

```

### usecase
- ```Signup```
- ```Login```

の2つのメソッドを持つインターフェースを定義し、実装しています。
ログインに成功した場合はトークンを生成します。

```go:./usecase/user_usecase.go
package usecase

import (
	"gin-login/entity"
	"gin-login/repository"
	"os"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
)

type UserUsecase interface {
	Signup(user entity.User) error
	Login(user entity.User) (string, error)
}

type userUsecase struct {
	ur repository.UserRepository
}

func NewUserUsecase(ur repository.UserRepository) UserUsecase {
	return &userUsecase{ur}
}

func (uu *userUsecase) Signup(user entity.User) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
	if err != nil {
		return err
	}
	newUser := entity.User{Email: user.Email, Password: string(hash)}
	if err := uu.ur.CreateUser(&newUser); err != nil {
		return err
	}
	return nil
}

func (uu *userUsecase) Login(user entity.User) (string, error) {
	storedUser := entity.User{}
	if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
		return "", err
	}
	err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
	if err != nil {
		return "", err
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": storedUser.ID,
		"exp":     time.Now().Add(time.Hour * 12).Unix(),
	})
	tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

```

### repository
リポジトリは、DBへの読み書きを行います。
2つのメソッド

- ```GetUserByEmail```
- ```CreateUser```

を持つインターフェースを定義し、それらを実装していきます。

```go:./repository/user_repository.go
package repository

import (
	"gin-login/entity"

	"gorm.io/gorm"
)

type UserRepository interface {
	GetUserByEmail(user *entity.User, email string) error
	CreateUser(user *entity.User) error
}

type userRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
	return &userRepository{db}
}

func (ur *userRepository) GetUserByEmail(user *entity.User, email string) error {
	if err := ur.db.Where("email=?", email).First(user).Error; err != nil {
		return err
	}
	return nil
}

func (ur *userRepository) CreateUser(user *entity.User) error {
	if err := ur.db.Create(user).Error; err != nil {
		return err
	}
	return nil
}
```

- ```GetUserByEmail(user *entity.User, email string) error```

空のUser構造体へのポインタ```user```とstring型の```email```を受け取り、合致するレコードを構造体に変換して```user```へ格納します。Gormを使っているので構造体への変換は勝手にやってくれます。エラーが起きた場合はerrorをそのまま返します。

-   ```CreateUser(user *entity.User) error```

ユーザーから入力された```Email```と```Password```を持つUser構造体のポインタを受け取り、DBへINSERTします。エラーが起きた場合はerrorをそのまま返します。