0
1

More than 1 year has passed since last update.

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

Posted at

こんにちは。

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はフォーマットチェック
    • トークンは長さが8
  • ユーザーがアクティブならエラー
  • トークンの有効期限をどうやって判断しようか
    • 有効期限は30分
    • updated_atと現在の時刻を比べて判断しよう
  • ユーザーのstateをactiveに更新する処理が必要だ

これくらいですね。

まとめると

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

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

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に追加しました。
必要な場合はこちらのリンクをクリックしてください。

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

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