こんにちは。
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について説明してみる」です。
よろしくお願いいたします。