【Go】ログイン機能でウェブアプリを作ってみる（7）

こんにちは。

Part 7は本登録 /auth/register/completeについてです。

# 今回の目標
- 本登録を実装する！

では、仮登録時と同じようにざっと確認していきましょう。

# 全体の流れの確認

- 仮登録 /auth/register/initial
    - クライアントからemail, passwordを受け取る
    - email宛に本人確認トークンを送信する
- 本登録 /auth/register/complete
    - クライアントからemailと本人確認トークンを受け取る
    - ユーザーの本登録を行う
- ログイン /auth/login
    - クライアントからemail, passwordを受け取る
    - 認証トークンとしてJWTを返す
- ユーザー情報の取得 /restricted/user/me
    - クライアントからJWTを受け取る
    - ユーザー情報を返す

では、本登録にはどんな機能が必要になるでしょうか。

# 必要な機能を考えよう！

- emailとトークンを受け取り、バリデーション
    - emailはフォーマットチェック
    - トークンは長さが８
- ユーザーがアクティブならエラー
- トークンの有効期限をどうやって判断しようか
    - 有効期限は30分
    - updated_atと現在の時刻を比べて判断しよう
- ユーザーのstateをactiveに更新する処理が必要だ

これくらいですね。

まとめると

| パッケージ | 役割 | 機能 |
|:-----------|:------------|:------------|
| Repository       | DBとのやりとり        | ・emailからユーザーを取得する<br/>・ユーザーのstateをactiveに更新する         |
| Usecase     | 本登録処理を行う	      | ・ユーザーがアクティブならエラー<br/>・トークンが一致するか確認<br/>・updated_atと現在の時刻の差が30分を超えていたらエラー       |
| Handler       | リクエストボディの取得レスポンスの作成        | ・リクエストボディの取得<br/>・リクエストボディの検証<br/>・emailのフォーマット検証<br/>・トークンの長さは８<br/>・レスポンスの作成         |

こんな感じで実装していきます！頑張りましょう！

## Repository

repository/user_repository.go
```
type IUserRepository interface {
	PreRegister(ctx context.Context, u *entity.User) error
	GetByEmail(ctx context.Context, email string) (*entity.User, error)
	Delete(ctx context.Context, id entity.UserID) error
+	Activate(ctx context.Context, u *entity.User) error
}
```
repository/user_repository.go
```
// ユーザーのstateをactivateに更新する
func (r *userRepository) Activate(ctx context.Context, u *entity.User) error {
	u.UpdatedAt = time.Now()
	u.State = entity.UserActive

	query := `UPDATE user SET state = :state, updated_at = :updated_at WHERE email = :email`
	if _, err := r.db.NamedExecContext(ctx, query, u); err != nil {
		return fmt.Errorf("failed to exec update: %v", err)
	}
	return nil
}
```

## Usecase

usecase/user_usecase.go
```
type IUserUsecase interface {
	PreRegister(ctx context.Context, email, pw string) (*entity.User, error)
+	Activate(ctx context.Context, email, token string) error
}
```

usecase/user_usecase.go
```
// ~~~

func (uu *userUsecase) Activate(ctx context.Context, email, token string) error {
	// emailをもとにDBからユーザーを取得する。
	u, err := uu.ur.GetByEmail(ctx, email)
	if err != nil {
		return err
	}

	// すでにユーザーがアクティブの場合、エラーを返す
	if u.IsActive() {
		return errors.New("user already active")
	}

	// トークンが一致しなければエラーをかえす
	if token != u.ActivateToken {
		return errors.New("invalid token")
	}

	// トークンが作成されて30分以上ならエラーをかえす
	if u.UpdatedAt.Add(30*time.Minute).Compare(time.Now()) != +1 {
		return errors.New("token expired")
	}

	if err := uu.ur.Activate(ctx, u); err != nil {
		return err
	}
	return nil
}
```

### Usecaseの解説
```
// トークンが作成されて30分以上ならエラーをかえす
if u.UpdatedAt.Add(30*time.Minute).Compare(time.Now()) != -1 {
	return errors.New("token expired")
}
```

- 時間の比較がちょっとわかりずらいので解説
- トークン作成時間（u.UpdatedAt）が6:00だとする。
- これに30分を加えると6:30
- 現在時刻が6:20なら、6:30 > 6:20 なのでCompareは +1 をかえす
- 現在時刻が6:40なら、6:30 < 6:40 なのでCompareは -1 をかえす
- トークンの有効期限は30分と考えているので、（トークン作成時刻＋30分）＞ 現在時刻　の場合のみ登録処理をする

## Handler

handler/user_handler.go
```
type IUserHandler interface {
	PreRegister(c echo.Context) error
+	Activate(c echo.Context) error
}
```

handler/user_handler.go
```
func (h *userHandler) Activate(c echo.Context) error {
	rb := struct {
		Email string `json:"email" validate:"required,email"`
		Token string `json:"token" validate:"required,len=8"`
	}{}
	if err := c.Bind(&rb); err != nil {
		return err
	}
	if err := c.Validate(rb); err != nil {
		return err
	}

	ctx := c.Request().Context()

	if err := h.uu.Activate(ctx, rb.Email, rb.Token); err != nil {
		return err
	}

	return c.JSON(http.StatusOK, echo.Map{
		"message": "activate ok",
	})
}
```

## routerへの登録
router.go
```
func NewRouter(db *sqlx.DB, mailer mail.IMailer) *echo.Echo {
	// ~~

	a := e.Group("/api/auth")
	a.POST("/register/initial", uh.PreRegister)
+	a.POST("/register/complete", uh.Activate)

	return e
}
```

これで本登録処理ができるようになりました！

# 確認

というわけで本当にうまくいくか確認していきましょう。

```
$ docker compose up -d
```
まずはユーザーを仮登録します。
```
$ curl -XPOST localhost:8000/api/auth/register/initial \
	-H 'Content-Type: application/json; charset=UTF-8' \
	-d '{"email": "test-user-1@example.xyz", "password": "foobar"}'
```

```
{"message":"ok"}
```

localhost:8025を確認してトークンを取得しましょう
そのトークンを使って

```
$ curl -XPOST localhost:8000/api/auth/register/complete \
	-H 'Content-Type: application/json; charset=UTF-8' \
	-d '{"email": "test-user-1@example.xyz", "token": "lSRslXKs"}'
```

```
{"message":"activate ok"}
```

実際にユーザーがactiveになっているかも確認しておきましょう。

```
$ docker compose exec db mysql -u login-user -plogin-pass login-db

mysql> SELECT * FROM user\G;
*************************** 1. row ***************************
            id: 100004
         email: test-user-1@example.xyz
      password: $2a$10$/sCYvLIEB5J3q1dgg6/Uo.xagFyr3PLye8euRH9c7KBjEk1b1smIO
          salt: y6bTEvUxviaiY5z8tnEvm30rHWKkvm
         state: active
activate_token: lSRslXKs
    updated_at: 2023-07-03 08:56:56.760694
    created_at: 2023-07-03 08:56:05.656498
1 row in set (0.01 sec)

ERROR: 
No query specified
```

ちゃんとstateがactiveになってます。

最後に、test-user-1@example.xyzが仮登録できないことも確認しておきましょう。
```
$ curl -XPOST localhost:8000/api/auth/register/initial \
	-H 'Content-Type: application/json; charset=UTF-8' \
	-d '{"email": "test-user-1@example.xyz", "password": "foobar"}'
```

```
{"message":"user already active"}
```

ちゃんとエラーが返ってきました！

# まとめ

今回やったこと

- 本登録処理を実装した

今日は以上です。
ありがとうございました。

また、今のディレクトリはこんな感じです。

```
  ├── .air.toml
  ├── _tools
  │   └── mysql
  │       ├── conf.d
  │       │   └── my.cnf
  │       └── init.d
  │           └── init.sql
  ├── Dockerfile
  ├── db
  │   └── db.go
  ├── docker-compose.yml
  ├── entity
  │   └── user.go
  ├── error_handler.go
  ├── go.mod
  ├── go.sum
+ ├── handler
+ │   └── user_handler.go
  ├── mail
  │   └── mailer.go
  ├── main.go
+ ├── router.go
+ ├── validator.go
+ ├── repository
+ │   └── user_repository.go
+ └── usecase
+     └── user_usecase.go
```

今日作成したアプリをgithubに追加しました。
必要な場合は[こちらのリンク](https://github.com/pinpointdev90/login-example-go-part7)をクリックしてください。

次のPart 8は「JWTについて説明してみる」です。
よろしくお願いいたします。