Firebase Authentication + FirebaseUI + Vuetify でメール認証でログインする

# やること

- [Vue.js + Firebase を使って爆速でユーザ認証を実装する](https://qiita.com/sin_tanaka/items/ea149a33bd9e4b388241)
- [Vue.js+Firebase Authenticationで認証画面を爆速実装！](https://www.isoroot.jp/blog/2451/)
- [Vue.js (Vuex, vue-router, vuetify) とFirebaseで始めるユーザー管理](https://qiita.com/kwashi/items/0bc56d79f7d198396a3f)
- [Vuetify ＋ Firebase UI で簡単にログイン機能を追加とナビゲーションメニュー](https://www.typea.info/blog/index.php/2019/05/07/vuetify_firebase_ui/)

これらの記事を足して 3 で割ったような感じ。

- Vue.js (vue2) + Vuetify を使う
- Firebase Authentication でメールアドレス認証をする
- Firebase UI を使ってログインフォームを表示する
- `v-app-bar` を画面上部に出したまま `router-view` でメイン画面だけを切り替える。
- ログインしてないときは `/` に強制的にリダイレクトする 

firebase は v8 です。

# 用意

## インストールとバージョン

vue cli, firebase-tools 関連のインストール、firebase 自体の準備は下記など参照のこと。

- [Firebase + Vue.js を WSL2 (Windows10) + Ubuntu 20.04LTS でやる](https://qiita.com/mml/items/862fdab2809fd9b1226a)

この記事で試したときの、各コマンドのバージョンは以下の通り。

```
$ npm -v
7.21.0
$ yarn -v
1.22.11
$ node -v
v14.17.4
$ firebase --version
9.16.6
$ vue --version
@vue/cli 4.5.13
```

vue cli は 3 以上でないと `create` コマンドは使用できないぽい。

firebase はプロジェクトをひとつ作成し、web アプリケーションをひとつ作成しておく。

## Firebase Authentication の準備と apiKey の取得

下記を参照のこと。

- [Firebase の web アプリにメールアドレスでユーザ登録をさせて確認メールを送る](https://qiita.com/mml/items/5e325bb19ba532ca56b7)

プロジェクトの Authentication の Sign-In Method のタブで、メール／アカウントの項目を有効にしておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b270023c-21bb-6d5a-449b-523909b7fd8d.png)


## プロジェクトの作成

vue + vuetify プロジェクトを作成。vue2 を使います。

```
$ vue create firebase-auth-sample
..
 Please pick a preset: Default ([Vue 2] babel, eslint)
...
$ cd firebase-auth-sample
$ vue add vuetify
 WARN  There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Yes
...
? Choose a preset: Default (recommended)
...
$
```

firebase の初期化。すでに作成済みのプロジェクトを使用。

```
$ firebase init hosting
Please select an option: Use an existing project
? Select a default Firebase project for this directory: fir-hider-sample-2021-01 (yourprojectname)
i  Using project fir-hider-sample-2021-01 (yourprojectname)
? What do you want to use as your public directory? dist
? Configure as a single-page app (rewrite all urls to /index.html)? No
? Set up automatic builds and deploys with GitHub? No
✔  Wrote dist/404.html
✔  Wrote dist/index.html
```

`public` の代わりに `dist` を指定する。あとはデフォルトで OK。

firebase. vue-router, mdi-font を yarn で追加しておく。 mdi-font は、アイコン類を使わなければ不要。

```
$ yarn add firebase@^8.10.0
$ yarn add firebaseui@^5.0.0
$ yarn add vue-router@^3.2.0
$ yarn add @mdi/font
```

日本語版の firebaseui-jp を使う場合はそれも追加する。firebase は 9.0.0 だと import でエラーが出たりしたので、バージョンを指定してます。

vue-router も最新のものだと vue2 でうまく動かない可能性があるので、バージョンを指定します。

## 動作確認

```
$ yarn serve
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://127.0.0.1:8080/
```

ブラウザで `http://localhost:8000` にアクセスする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/c8199db6-21fb-54e5-565f-07f4f1eab535.png)

こんな感じの画面になればOK。

firebase コマンドでも試しておく。

```
$ yarn build
$ firebase serve
i  hosting: Serving hosting files from: dist
✔  hosting: Local server: http://localhost:5000
```

ブラウザで `http://localhost:5000` にアクセスして、同じ画面が出たらOK。

# コードを書く

ファイル構成はこんなかんじで。

```
$ tree src
src
├── App.vue
├── components
│   ├── HomeView.vue
│   ├── LoginView.vue
│   └── LogoutView.vue
├── main.js
├── plugins
│   └── vuetify.js
└── routes
    └── index.js
```

どこかので時点で ESLint の仕様の変更？か jsconfig の出力内容の変更か何かがあったようで、vue ファイルはひとつの単語では許してもらえなくなったぽい（たとえば Home.vue というファイル名は弾かれる）。そのため、ファイル名を二つの単語にしてます。

## main.js

firebase の apiKey 関連のコードはここに書いておく。firebaseConfig の部分は、環境変数を読む形にする方法もあり。

```main.js
import Vue from 'vue';
import vuetify from './plugins/vuetify';

import '@mdi/font/css/materialdesignicons.css';     // mdi-xxx のために必要
import router from './routes/index';                // router 

import App from './App.vue';

import firebase from 'firebase/app'; 
// 以下に apiKey のコードを貼る
var firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "yourprojectname.firebaseapp.com",
  projectId: "yourprojectname",
  storageBucket: "yourprojectname.appspot.com",
  messagingSenderId: "yyyyyyyyyyyyyyy",
  appId: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

Vue.config.productionTip = false

new Vue({
  vuetify,
  router,  // router を使う
  render: h => h(App)
}).$mount('#app')
```

## App.vue

ページ上部に固定のバーを置いて、メイン画面は router で切り替える。デフォルトでは Home を表示する。

`firebase.auth().onAuthStateChanged()` は、認証の状態に変更が起こると呼ばれるので、そこでサインイン、サインアウトの表示を切り替える。

```App.vue
<template>
  <v-app>
    <v-app-bar
      app
      color="primary"
      dark
    >

      <v-toolbar-title class="text-h6">
        <span>Auth Sample</span>
      </v-toolbar-title>

      <v-spacer></v-spacer>

      <v-btn icon to="/">
          <v-icon>mdi-home</v-icon>
      </v-btn>

      <v-btn v-if="isLogin==false"
          outlined
          to="/login"
      >
        Sign In
      </v-btn>
      <v-btn v-else
          outlined
          to="/logout"
      >
          {{ loginUser.displayName }}
      </v-btn>

    </v-app-bar>

    <v-main>
      <router-view/>
    </v-main>
  </v-app>
</template>

<script>
import firebase from 'firebase/app'
export default {
    name: 'App',
    data: () => ({
        isLogin: false,
        loginUser: null,
    }),
    mounted(){
        const tmp = this;
        firebase.auth().onAuthStateChanged(user => {
            console.log("onAuthStateChanged", user);

            // ログイン状態の取得
            if ( user != null ) {
                tmp.isLogin = true;
                tmp.loginUser = user;
            }else{
                tmp.isLogin = false;
                tmp.loginUser = null;
            }
        });
    },
}
</script>
```

## routes/index.js

`vue-router` を使って、パスに対応する表示内容を対応付ける。

```routes/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from   '../components/HomeView';
import Login from  '../components/LoginView';
import Logout from '../components/LogoutView';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
    {
      path: '/logout',
      name: 'logout',
      component: Logout
    }
  ]
});
```

## components/LoginView.vue

ログイン画面を表示する。Sign-In の UI の部分は `firebase-ui-auth` をそのまま使用。

未登録のメールアドレスを入れると、自動的に登録モードになる。kiyaku と policy はコードを書いてないので、クリックすると 404 エラーになる。

```components/Login.vue
<template>
  <v-container>
    <h1>ログイン</h1>
    <div id="firebaseui-auth-container"></div>
  </v-container>
</template>

<script>
import firebase from 'firebase/app';
/*
// 上のでダメな場合は下をためす
import * as firebase from 'firebase/app'
*/

import "firebaseui/dist/firebaseui.css";
// require("firebaseui/dist/firebaseui.css"); // 上がだめならこれ
import "firebase/auth";
import * as firebaseui from 'firebaseui';
// import firebaseui from 'firebaseui' // 上がダメならこれ


export default {
    name: "Login",
    mounted(){
        let user = firebase.auth().currentUser;
        console.log ( user );

          var uiConfig = {
          signInSuccessUrl: '/', // ログインに成功したときに飛ぶページ
          signInOptions: [
            {
              provider: firebase.auth.EmailAuthProvider.PROVIDER_ID 
              //メール認証のみ使用する。google 認証などを使う場合はさらに羅列する。
            }
          ],

          tosUrl: '/kiyaku',           // 規約とかを書いたページ
          privacyPolicyUrl: '/policy'  // プライバシーポリシーを書いたページ
        };
  
        // firebase auth ui を使用
        let ui = new firebaseui.auth.AuthUI(firebase.auth());
        ui.start('#firebaseui-auth-container', uiConfig);
        // <div id="firebaseui-auth-container"></div> の部分にUIを展開する
    },
}
</script>
```

## components/LogoutView.vue

ボタンを押すと確認なくログアウトする。

```components/Logout.vue
<template>
  <v-container>
    <h1>ログアウト</h1>
    <v-btn
        outlined
        @click="signOut"
    >
        Sign Out
    </v-btn>
  </v-container>
</template>
<script>
import firebase from 'firebase/app';
export default {
    methods: {
        signOut: function () {
            firebase.auth().signOut().then(() => {
                // ログアウトに成功した場合は / に強制遷移する
                this.$router.push('/');
            });
        }
    }
}
</script>
```

## components/HomeView.vue

デフォルト `/` 画面。

```components/Home.vue
<template>
  <v-container>
    <h1>ホーム画面</h1>
  </v-container>
</template>
```

# 実行

```
$ yarn build
$ firebase serve
i  hosting: Serving hosting files from: dist
✔  hosting: Local server: http://localhost:5000
```

これで `http://localhost:5000` をブラウザで開くと、下記画面が出るはず。出ない場合はエラーメッセージやブラウザのコンソールを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b68f1901-14df-101f-8644-e001e22f2eab.png)

右上の Sign In を押すと次の画面になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/742f750d-8b76-2630-c865-beed27e90f6f.png)

メールアドレスを入力する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/aac4365c-8584-a9e7-bb6a-97860e7db807.png)

未登録のアドレスを入れると、自動的に登録モードになる。名前とパスワードを入れると登録されて、ログイン状態になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/0816f5ee-6027-639b-716b-294a64689638.png)

ログインできると、右上の表示が登録した名前になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/6e62ab46-7bca-8bc2-32fb-1cf981b6df41.png)

ログインしている状態で、右上の名前のところを押すと、ログアウト画面になる。ここで Sign out を押すと、ログアウトしてホーム画面に戻る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/b68f1901-14df-101f-8644-e001e22f2eab.png)

## ログイン状態のチェック

`firebase.auth().currentUser` を取得して、null でなければログインしている。これを使って、ログイン時とそうでないときの表示を切り替えればOK（多分）。

メールアドレスが確認済みかどうかは `firebase.auth().emailVerified` で取得できる。

このへんは下記参照のこと

- [Firebase でユーザーを管理する](https://firebase.google.com/docs/auth/web/manage-users?hl=ja)

# おわり

分かってしまえば簡単（有限）だけど、わからないと無限にわからない「フレームワーク問題」ってどうにかなりませんかね。