Firebase(Cloud functions+Authentication+Firestore)とnuxt.jsで完結するユーザ管理画面を作る。(firebase側)

#前書き
　FirebaseのクライアントSDKは、アカウントを新規作成すると自動的に作成したアカウントにログインする仕様になっています。そのため管理者がアカウントを発行してユーザーに配る形態でサービスを運用したいという場合はAdminSDKを使用する必要があります。またカスタムクレームなどを使用したい場合も同じくAdminSDKが必要です。
　今回はAdminSDKの機能を使いつつ、ウェブ上からFirebaseのコンソール以外でユーザー管理を行えるようにするまでの流れを備忘録代わりにまとめておきます。
#やる事
自前の管理画面からユーザー管理(新規作成・一覧表示・更新・削除)ができるようにする。
#省略すること
- Firebaseの基本的な知識
- nuxt.jsの基本的な知識

#構成
当然全てFirebase

- Firestore
- Cloud functions
- Authentication
- Hosting

#処理の流れ
Firestoreに保存した情報とAuth側の認証情報をCloud functionsで同期させます。
## ユーザー作成
![雑な図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425022/9f97f4db-5024-3ceb-d39d-3f876705b7c2.png)
Firestoreにユーザー情報を作成すると、それを元にCloud functionsでAdminSDKのcreateUser()を使ってユーザーを作成し、ユーザーIDをFirestoreに書き込むという流れです。
UIDが書き込まれるとユーザー更新関数でカスタムクレームやプロフィールを更新します。
##ユーザー更新
![雑な図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425022/53177bbc-4e0c-0c73-8148-9091f4d2d10a.png)
ユーザー作成の後半部分と同じです。

#バックエンド
##1: Firestore
Firestoreのドキュメントはこのようになっています。
![Firestoreのコンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425022/061ad039-1c6b-790d-cf35-cbd8fd2ee96b.png)

- uid: auth側で発行されるUID (初期値null)
- name:ユーザー名
- email:メールアドレス
- password:パスワード(アカウント作成後nullに更新)
- admin:管理者権限(カスタムクレームの設定に使用)
- disabled: authの無効化設定(trueにするとログインできなくなる。)
- created_date:作成日時
- has_error: エラー発生フラグ(これがtrueの場合はユーザ更新関数を何もせずに終了させる)

このドキュメントを操作するとそれに応じてAuthの情報も変更されるようにします。

##2: Cloud functions
Admin SDKの機能をフルに使う場合は認証情報が必要なので注意。

```javascript:index.js
const functions = require('firebase-functions')
const admin = require('firebase-admin')
// // https://firebase.google.com/docs/functions/write-firebase-functions

const serviceAccount = require('./opabinia.json')

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'ebikaniuni'
})

// 認証情報作成関数
exports.createNewAccount = functions.firestore
  .document('account/{docId}')
  .onCreate((snapshot, context) => {
    const docId = context.params.docId
    const data = snapshot.data()
    const uid = data.uid
    // uidがnullでない場合は認証情報がすでに存在する。
    if (uid) {
      console.log('すでに認証情報が存在します。uid:', uid
      )
      return 0
    }

    const newAccount = {
      email: data.email,
      password: data.password
    }

    admin.auth().createUser(newAccount).then((user) => {
      admin.firestore().collection('account').doc(docId).update({ uid: user.uid, password: null }) 
      console.log('認証情報を作成 uid:', user.uid)
    }).catch((err) => {
      admin.firestore().collection('account').doc(docId).delete()
      console.log('認証情報の作成に失敗', err)
    })
    return 0
  })

// 認証情報更新関数
exports.updateAccount = functions.firestore
  .document('account/{docId}')
  .onUpdate((snapshot, context) => {
    const docId = context.params.docId
    const data = snapshot.after.data()
    const has_error = data.has_error
    if (has_error || !data.uid) {
      console.log('情報が不正なドキュメントです。docId:', docId)
      return 0
    }
    const uid = data.uid
    const newProfile = {
      displayName: data.name,
      email: data.email,
      disabled: data.disabled
    }
    const newClaim = {
      admin: data.admin, 
      documentId: docId // firestore側のドキュメントID。あまりいい使い方では無い気がする。
    }
    admin.auth().updateUser(uid, newProfile).then(() => {
      console.log('アカウント情報を更新 uid:', uid)
    }).catch((err) => {
      console.log('アカウント情報の更新に失敗uid:', uid, err)
      admin.firestore().collection('account').doc(docId).update({ has_error: true })
    })

    admin.auth().setCustomUserClaims(uid, newClaim).then(() => {
      console.log('権限設定を更新 uid:', uid)
    }).catch((err) => {
      console.log('権限設定の更新に失敗uid:', uid, err)
      admin.firestore().collection('account').doc(docId).update({ has_error: true })
    })
    return 0
  })

// 認証情報削除関数
exports.deleteAuthData = functions.firestore
  .document('account/{docId}')
  .onDelete((snapshot, context) => {
    const docId = context.params.docId
    const data = snapshot.data()
    const uid = data.uid
    if (!uid) {
      console.log('すでに認証情報が削除されています。docId:', docId)
      return 0
    }
    admin.auth().deleteUser(uid).then(() => {
      console.log('認証情報を削除しました。uid:', uid)
    }).catch((err) => {
      console.log('認証情報の削除に失敗しました。 docId: ', docId)
    })
    return 0
  })
```


##3: Authentication
メールアドレスとパスワード認証を有効化。以上
![認証方法.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425022/da12cbba-379f-169d-e072-24d1f9e19460.png)


#注意事項
##onUpdateトリガーの無限ループ
Cloud functionのonUpdateトリガーは該当するドキュメントに更新があれば必ず発動します。そのため関数内で同じドキュメントを更新すると無限に関数が動き続ける場合があるので注意。私は最終更新時刻の更新で何度か無限ループに陥った事があります。
　今回はOncreateトリガーで発動する関数ではドキュメントの更新を行わず、またエラーが発生した場合はフラグを立てて、trueなら何もせずに終了するようにしています。

#セキュリティ
Firestoreのセキュリティルールをきっちり設定しましょう。ユーザー一覧が必要な時は別途名前とIDのみの読み取り専用コレクションを用意するとよいでしょう。

#参考資料
[Firebase で公開するウェブサイトに「管理者機能」を付ける](https://lnly.hatenablog.com/entry/2019/01/25/012744)