第4回「FirebaseAuthによるユーザ作成機能導入」@FirebaseAuth＋Nuxt.js＋Go(v1.11)＋GAE開発シリーズ

# お題
[前回の記事](https://qiita.com/sky0621/items/5103d5d87761c62ba041)でFirebaseAuthを使った単純なSignIn/Outを実装した。
今回は、SignUp（つまり、FirebaseAuth管理ユーザの新規登録）を実装する。
前回はFirebaseAuthのコンソール上で[事前にログイン用ユーザを作成](https://qiita.com/sky0621/items/5103d5d87761c62ba041#firebaseauthコンソールで事前のユーザ作成)したけど、例えばある種の管理画面などではログイン可能なユーザの作成機能を持たせたりするものなので。
　
SignIn/OutはJavaScriptのSDKで実装したのでSignUpも合わせようと思うところだけど、JavaScriptのSDKでSignUpを行うと**[作った途端にそのユーザで自動的にログインした状態になる](https://firebase.google.com/docs/auth/web/password-auth?hl=ja#create_a_password-based_account)**ので、これは使えない。
（つまり、ユーザ作成権限を持つ”ユーザA”でログインして”ユーザB”を作ると、その時点で（”ユーザA”でログインしていたのに）Firebase上は”ユーザB”でログインした状態に変わってしまう。）
なので、SignUpはサーバーサイド側で行う。今回はWebAPIとしてGoを使っているので、**[Go用のSDKでユーザ作成](https://firebase.google.com/docs/auth/admin/manage-users?hl=ja#create_a_user)**機能を実装する。

# FirebaseAuth＋Nuxt.js＋Go(v1.11)＋GAE開発シリーズIndex
- [第3回「FirebaseAuthによるログイン・ログアウト導入」](https://qiita.com/sky0621/items/5103d5d87761c62ba041)
- [第2回「FirebaseAuth導入前（ログインフォーム実装とバックエンドプロジェクトのガワ作成）」](https://qiita.com/sky0621/items/d885c52de0534560a10f)
- [第1回「FirebaseAuth導入前（フロントエンドプロジェクトのガワ作成）」](https://qiita.com/sky0621/items/583f45085cbd59fe946f)

# 前提
以下は他にいくらでもよい記事があるので省略。

- 開発環境の構築（GolangやらYarnやらのインストール等）
- Google Cloud Platform環境の取得（App Engine有効化）
- Firebase環境の取得
- Vue.jsやNuxt.jsのチュートリアル

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.1 LTS (Bionic Beaver)"
```

### # 依存モジュールのバージョン
`package.json`の内容より、以下の通り。

```
    "nuxt": "^2.3.4",
    "vuetify": "^1.3.14",
    "vuetify-loader": "^1.0.8",
    "@nuxtjs/axios": "^5.3.6"
```

### # Yarn
```
$ yarn -v
1.12.3
```

### # Golang
```
$ go version
go version go1.11.4 linux/amd64
```

# 実践

## ■ソース全量

今回作ったソースの全量は下記。
https://github.com/sky0621/Dotato-di-una-libreria/tree/faa9e6567718a7ed69847593315b950d0d7e5dca

## ■フロントエンドにユーザ作成フォームを実装

### ユーザ作成画面の見た目

こんな感じにしてみた。
[Vuetify](https://vuetifyjs.com/en/getting-started/quick-start)を使っているので、ほとんど手間をかけずともそれなりの画面が作れる。
あと、バリデーションは[Vuelidate](https://monterail.github.io/vuelidate/#getting-started)を使った。
ソースは後ほど。

![screenshot-localhost-3000-2019.02.10-16-28-28.png](https://qiita-image-store.s3.amazonaws.com/0/180599/be65c61d-0b4a-3051-f8c9-6c78d221fd85.png)

### 画面表示に関する部分のソース

#### `/frontend/pages/signup.vue`

pagesに「ユーザ作成画面」としてのテンプレートを書き、「ユーザ作成フォーム」の部分だけコンポーネントとして切り出し。
（今後、新規作成と変更とを分けて、フォーム部分を流用することを想定してみたけど、実際のところは、そのときになってみないと、この判断が正しかったかはわからない。。。）

```js:[frontend/pages/signup.vue]
<template>
  <v-container fluid>
    <v-layout
      align-start
      justify-start
      column
    >
      <v-flex>
        <v-label>ユーザー登録</v-label>
      </v-flex>
      <v-flex>
        <signup />
      </v-flex>
    </v-layout>
  </v-container>
</template>
```

#### `/frontend/components/signup.vue`

上記pagesの`signup.vue`で「`<signup />`」としていた箇所の中身に当たる。

```js:[frontend/components/signup.vue]
<template>
  <form>
    <v-text-field
      v-model="name"
      :error-messages="nameErrors"
      :counter="10"
      label="ニックネーム"
      required
      @input="$v.name.$touch()"
      @blur="$v.name.$touch()"
    />
    <v-text-field
      v-model="email"
      type="email"
      :error-messages="emailErrors"
      label="メールアドレス"
      required
      @input="$v.email.$touch()"
      @blur="$v.email.$touch()"
    />
    <v-text-field
      v-model="password"
      type="password"
      :error-messages="passwordErrors"
      label="パスワード"
      required
      @input="$v.password.$touch()"
      @blur="$v.password.$touch()"
    />

    <v-btn
      class="lime lighten-2"
      @click="submit"
    >
      登録
    </v-btn>
  </form>
</template>
```

### フォームのロジックに関する部分のソース

以降、フォーム内の３つの要素「ニックネーム」、「メールアドレス」、「パスワード」及び「登録」ボタンそれぞれについて、１要素ずつテンプレートとロジックを抜粋して説明。
抜粋しない完全版のソースは下記参照。
https://github.com/sky0621/Dotato-di-una-libreria/blob/faa9e6567718a7ed69847593315b950d0d7e5dca/frontend/components/signup.vue

#### ●「ニックネーム」テキストフィールド

```js:[frontend/components/signup.vue]
<template>
  <form>
    <v-text-field
      v-model="name"
      :error-messages="nameErrors"
      :counter="10"
      label="ニックネーム"
      required
      @input="$v.name.$touch()"
      @blur="$v.name.$touch()"
    />
  </form>
</template>

<script>
import { required, maxLength } from 'vuelidate/lib/validators'

export default {
  validations: {
    name: { required, maxLength: maxLength(10) },
  },

  data() {
    return {
      name: '',
    }
  },

  computed: {
    nameErrors() {
      const errors = []
      if (!this.$v.name.$dirty) return errors
      !this.$v.name.maxLength && errors.push('ニックネームは10文字以内にしてください')
      !this.$v.name.required && errors.push('ニックネームは必須です')
      return errors
    },
  },
}
</script>
```

テーマ外なのでバリデーションの具体的な仕組みについては解説なしで。↓参照。
https://monterail.github.io/vuelidate/#getting-started

ともあれ、上記のように実装すると以下のように入力状態に応じてチェックしてくれる。

![screenshot-localhost-3000-2019.02.11-14-37-15.png](https://qiita-image-store.s3.amazonaws.com/0/180599/661c8c57-2881-ed34-61fa-db871795b073.png)
![screenshot-localhost-3000-2019.02.11-14-37-50.png](https://qiita-image-store.s3.amazonaws.com/0/180599/abd31a03-24b3-6b14-343f-8a8303df5c1f.png)

#### ●「メールアドレス」テキストフィールド

```js:[frontend/components/signup.vue]
<template>
  <form>
    <v-text-field
      v-model="email"
      type="email"
      :error-messages="emailErrors"
      label="メールアドレス"
      required
      @input="$v.email.$touch()"
      @blur="$v.email.$touch()"
    />
  </form>
</template>

<script>
import { required, email } from 'vuelidate/lib/validators'

export default {
  validations: {
    email: { required, email },
  },

  data() {
    return {
      email: '',
    }
  },

  computed: {
    emailErrors() {
      const errors = []
      if (!this.$v.email.$dirty) return errors
      !this.$v.email.email && errors.push('適切なメールアドレスを入力してください')
      !this.$v.email.required && errors.push('メールアドレスは必須です')
      return errors
    },
  },
```

上記のように実装すると以下のように入力状態に応じてチェックしてくれる。

![screenshot-localhost-3000-2019.02.11-14-39-29.png](https://qiita-image-store.s3.amazonaws.com/0/180599/181067cc-8316-4b80-50a8-f2bfea1b3978.png)
![screenshot-localhost-3000-2019.02.11-14-39-52.png](https://qiita-image-store.s3.amazonaws.com/0/180599/b3a30069-d218-0327-780a-efbb9b45ff01.png)

#### ●「パスワード」テキストフィールド

```js:[frontend/components/signup.vue]
<template>
  <form>
    <v-text-field
      v-model="password"
      type="password"
      :error-messages="passwordErrors"
      label="パスワード"
      required
      @input="$v.password.$touch()"
      @blur="$v.password.$touch()"
    />
  </form>
</template>

<script>
import { required, minLength } from 'vuelidate/lib/validators'

export default {
  validations: {
    password: { required, minLength: minLength(8) }
  },

  data() {
    return {
      password: ''
    }
  },

  computed: {
    passwordErrors() {
      const errors = []
      if (!this.$v.password.$dirty) return errors
      !this.$v.password.minLength && errors.push('パスワードは8文字以上にしてください')
      !this.$v.password.required && errors.push('パスワードは必須です')
      return errors
    }
  },
}
</script>
```

上記のように実装すると以下のように入力状態に応じてチェックしてくれる。

![screenshot-localhost-3000-2019.02.11-15-08-40.png](https://qiita-image-store.s3.amazonaws.com/0/180599/6409ee48-6109-a791-7f3f-3fb210598a5a.png)
![screenshot-localhost-3000-2019.02.11-15-09-11.png](https://qiita-image-store.s3.amazonaws.com/0/180599/48b052d1-ca79-639b-8080-d3dcc5fa7951.png)

#### ●「登録」ボタン

`this.$v.$touch()`でバリデーションを発動させ、`if (this.$v.$invalid) return`により、チェックに引っかかったら処理中断。
あとは、バックエンド側のAPI（パス：`/users`）にフォームの中身をPOSTするだけ。

```js:[frontend/components/signup.vue]
<template>
  <form>
    <v-btn
      class="lime lighten-2"
      @click="submit"
    >
      登録
    </v-btn>
  </form>
</template>

<script>
import * as types from "~/store/types";

export default {
  data() {
    return {
      name: '',
      email: '',
      password: ''
    }
  },

  methods: {
    submit() {
      this.$v.$touch()
      if (this.$v.$invalid) return

      console.log('validate success')

      this.$axios
        .post(process.env.apiBaseUrl + '/users', {
          'name': this.name,
          'email': this.email,
          'password': this.password
        })
        .then((res) => {
          console.log(res)
          this.$store.dispatch(types.ACTIVATE_INFO_NOTIFICATION, {message: 'ユーザー「' + this.name + '」を登録しました。'});
          this.$router.push('/')
        })
        .catch((err) => {
          console.log(err)
          this.$store.dispatch(types.ACTIVATE_ERROR_NOTIFICATION, {message: err});
        })
    }
  }
}
</script>
```

#### ●「通知」スナックバー

ユーザ登録成功後は、スナックバー（メッセージ：「`ユーザー「○○」を登録しました。`」）を表示する。
ユーザ登録失敗時は、やはりスナックバー（メッセージ：サーバから帰ってきたエラーそのもの）を表示する。

```js:[frontend/components/signup.vue]
<script>
  methods: {
    submit() {
      this.$axios
        .post(process.env.apiBaseUrl + '/users', { 〜〜省略〜〜 })
        .then((res) => {
          this.$store.dispatch(types.ACTIVATE_INFO_NOTIFICATION, {message: 'ユーザー「' + this.name + '」を登録しました。'});
          this.$router.push('/')
        })
        .catch((err) => {
          this.$store.dispatch(types.ACTIVATE_ERROR_NOTIFICATION, {message: err});
        })
    }
  }
}
</script>
```

このスナックバーの表示内容はコンポーネントないしページをまたいで状態を共有するのでストアに定義。

```js:[frontend/store/index.js]
import * as types from './types'

export const strict = false

export const state = () => ({
  notification: {
    activate: false,
    message: '',
    color: ''
  }
})

export const getters = {
  notification: (state) => {
    return state.notification
  }
}

export const mutations = {
  setNotification(state, data) {
    const cdata = _.clone(data)
    state.notification = {
      activate: cdata.activate,
      message: cdata.message,
      color: cdata.color
    }
  }
}

export const actions = {
  // 通知を活性化する
  activateNotification({ commit }, data) {
    commit(types.SET_NOTIFICATION, { activate: true, message: data.message, color: data.color })
  },

  // INFO通知を活性化する
  activateInfoNotification({ commit }, data) {
    commit(types.SET_NOTIFICATION, { activate: true, message: data.message, color: 'green' })
  },

  // ERROR通知を活性化する
  activateErrorNotification({ commit }, data) {
    commit(types.SET_NOTIFICATION, { activate: true, message: data.message, color: 'red' })
  },

  // 通知を非活性化する
  deactivateNotification({ commit }) {
    commit(types.SET_NOTIFICATION, { activate: false, message: '', color: '' })
  }
}
```

スナックバー自体を表示するコンポーネントは下記。

```js:[frontend/components/notification.vue]
<template>
  <!-- 通知用スナックバー -->
  <v-snackbar
    v-model="snackbar"
    :color="color"
    :timeout="timeout"
  >
    {{ message }}
    <v-btn
      dark
      flat
      @click="closeSnackbar"
    >
      閉じる
    </v-btn>
  </v-snackbar>
</template>

<script>
import * as types from '~/store/types'

export default {
  data() {
    return {
      timeout: 5000,

      snackbar: false,
      message: '',
      color: ''
    }
  },

  computed: {
    notification() {
      return this.$store.getters[types.NOTIFICATION]
    }
  },

  watch: {
    notification(val) {
      this.apply(val)
    }
  },

  mounted() {
    const val = this.$store.getters[types.NOTIFICATION]
    this.apply(val)
  },

  methods: {
    closeSnackbar() {
      this.$store.dispatch(types.DEACTIVATE_NOTIFICATION)
    },

    apply(val) {
      this.message = val.message
      this.color = val.color
      this.snackbar = val.activate
      // TODO: スナックバーのタイムアウト発動タイミングをフックする方法が不明のため、苦肉の策で「同じ時間経過後にスナックバーを非活性化」する対応を仕込む
      if (this.snackbar) {
        console.log('setTimeout')
        setTimeout(this.closeSnackbar, this.timeout)
      }
    }
  }
}
</script>
```

これにより、ユーザー登録完了後は、トップページ（お知らせ画面）に遷移して、↓のようなスナックバーが表示される。

![screenshot-localhost-3000-2019.02.11-15-28-57.png](https://qiita-image-store.s3.amazonaws.com/0/180599/fe7adadf-3241-5e5c-4fd1-8455758b762c.png)

また、エラーが発生した場合は、ユーザー登録画面上で↓のようなスナックバーが表示される。
（※FirebaseAuth上に登録済みのメールアドレスで再度登録を試みた結果）

![screenshot-localhost-3000-2019.02.11-15-33-27.png](https://qiita-image-store.s3.amazonaws.com/0/180599/cb818b27-49ae-0e69-8592-5adbb9466346.png)


## ■バックエンドにユーザ作成APIを実装

バックエンド側の構造は「MVC+S」としている。
各リクエストは `controller` パッケージ下の機能別ソースで受け付けて、トランザクション境界を持つ `service` パッケージ下の機能別ソースを呼び出す。
`service` パッケージ下のソースは適宜、 `model` パッケージ下のソースを組み合わせて要件を実現する。
[前回](https://qiita.com/sky0621/items/5103d5d87761c62ba041)まででログイン後のトップページに表示する「お知らせ」機能は実装済み。
今回同じような構成で「ユーザ管理」機能を実装する。

### 実装後のパッケージ構成（MVC+S部分のみ抜粋）

```
├── controller
│   ├── apierror.go
│   ├── form
│   │   ├── form.go
│   │   └── user.go
│   ├── notice.go
│   ├── response
│   ├── router.go
│   └── user.go
├── main.go
├── model
│   ├── dto.go
│   ├── notice.go
│   └── user.go
├── service
│   ├── notice.go
│   └── user.go
```

### `backend/controller/router.go`

```go:[backend/main.go【抜粋】]
func main() {
	〜〜省略〜〜
	// https://echo.labstack.com/guide
	e := echo.New()
	〜〜省略〜〜

	// ルーティング設定の起点
	controller.Routing(e)

	appengine.Main()
}
```

```go:[backend/controller/router.go]
func Routing(e *echo.Echo) {
	〜〜省略〜〜
	http.Handle("/", e)

	// 個人の認証を要するWebAPI用のルート
	authGroup := e.Group("/api/v1")
	〜〜省略〜〜

	// 「ユーザ」機能のルーティング("/users")
	HandleUser(authGroup)
}
```

### `backend/controller/user.go`

```go:[backend/controller/user.go]
func HandleUser(g *echo.Group) {
	g.POST("/users", createUser)
}

func createUser(c echo.Context) error {
	ctx := middleware.GetCustomContext(c)
	user := &form.User{}
	if errUser := parse(c, user); errUser != nil {
		return c.JSON(http.StatusBadRequest, errorJSON(http.StatusBadRequest, errUser.Error()))
	}

	if err := service.NewUser(ctx, c.Request().Context()).CreateUser(user.ParseToDto()); err != nil {
		return c.JSON(http.StatusBadRequest, errorJSON(http.StatusBadRequest, err.Error()))
	}

	return c.JSON(http.StatusOK, "{}")
}
```

### `backend/service/user.go`

トランザクション境界を設け、最初にDBへのユーザー情報登録、成功したらFirebaseAuthのSDKを使ってFirebase上にユーザー作成という流れ。

```go:[backend/service/user.go]
〜〜インターフェース定義や構造体定義、New関数等は省略〜〜

func (s *userService) CreateUser(u *model.User) error {
	tx := s.db.Begin()
	defer func() {
		if tx != nil {
			db := tx.Commit()
			if err := db.Error; err != nil {
				s.lgr.Errorw("Transaction commit failed.", "error", err)
			}
		}
	}()

	u.ID = util.CreateUniqueID()
	err := model.NewUserDao(s.lgr, tx, s.firebaseApp).CreateUser(u)
	if err != nil {
		tx.Rollback()
		return err
	}

	fbAuth, err := s.firebaseApp.Auth(s.requestCtx)
	if err != nil {
		tx.Rollback()
		return err
	}
	fbUser := &auth.UserToCreate{}
	fbUser.Email(u.Mail)
	fbUser.Password(u.Password)
	_, err = fbAuth.CreateUser(s.requestCtx, fbUser)
	if err != nil {
		tx.Rollback()
		return err
	}

	return nil
}
```

## ■動作確認

登録画面にて

![screenshot-localhost-3000-2019.02.11-16-08-10.png](https://qiita-image-store.s3.amazonaws.com/0/180599/2abe4ac0-bbb9-982e-6b74-3d9f9432560d.png)

ユーザー情報を入力して「登録」ボタン押下

![screenshot-localhost-3000-2019.02.11-16-09-07.png](https://qiita-image-store.s3.amazonaws.com/0/180599/97dfb9e2-bf7a-8186-ac13-da3b446514e7.png)

トップページにて登録完了が通知される。

![screenshot-localhost-3000-2019.02.11-16-09-20.png](https://qiita-image-store.s3.amazonaws.com/0/180599/33fed6ae-4990-e1cf-756f-b4f298422ebc.png)

データベース上の登録を確認

```
mysql> select * from user where name='taro';
+----------------------------------+------+------------------+-------------+---------------------+-------------+---------------------+------------+
| id                               | name | mail             | create_user | created_at          | update_user | updated_at          | deleted_at |
+----------------------------------+------+------------------+-------------+---------------------+-------------+---------------------+------------+
| c0a5867c55064ff4b8fb944e89770e82 | taro | taro@example.com |             | 2019-02-11 16:09:10 |             | 2019-02-11 16:09:10 | NULL       |
+----------------------------------+------+------------------+-------------+---------------------+-------------+---------------------+------------+
1 row in set (0.00 sec)
```

FirebaseAuth上も登録を確認

![screenshot-console.firebase.google.com-2019-02-11-16-16-19-809.png](https://qiita-image-store.s3.amazonaws.com/0/180599/9358530e-63e1-0520-8d99-c0ae7332668f.png)


# まとめ

現在ログインしているユーザーとは別の新規ユーザーをFirebase上に登録することができた。
ユーザー管理系に触れたので、次回以降は、ユーザーの削除や、メールアドレス、パスワードの変更などを試していきたい。
が、その前に、
今の実装では、ログイン機能はあっても、ログインしているかどうかノーチェックで画面遷移もAPIコールもできてしまう。
これでは実用には耐えられないので、Firebaseログイン後、JWTがもらえるので、それをもとにログインチェック機能を実装しよう。