Nuxt.js と Firebase Authentication で認証回りにで実装するテンプレートを構築する

この記事は、[Jamstack Advent Calendar](https://qiita.com/advent-calendar/2020/jamstack) 11日目の記事になります。
今回は、NuxtJS で、Firebase Authentication を利用した時に最もよく使用する実装方法についてまとめました。


なお、今回実装したサンプルは 以下の GitHub リポジトリに保存しています。
<a href="https://github.com/okakyo/firebase-auth-sample"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/okakyo/firebase-auth-sample.png" width="460px"></a>

## 環境構築

アプリの環境構築は次の通りに行いました。

```
npx create-nuxt-app firebase-auth-sample
```

今回は次のような状態で設定しました。

- Project name : **TypeScript**
- Package manager: **npm**
- UI framework: **None**
- Nuxt.js modules: **Axios, Progresive Web App, Content** (今回は不要)
- Linting modules: **ESLint, Prettier, Lint Staged files, Stylelint**
- Testing Framework: **Jest**
- Rendering mode: **Single Page App**
- Developmenyt target: **Static**
- Development tools: **jsconfig,.json**
- Continuous integration: **Github Actions**

![2020-12-03 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271708/97e3b169-47ec-cc69-2eef-966db1e9d732.png)

今回は、Vue composition API を利用して、Firebase を使用しての認証回りについて実装します。作成後に、このディレクトリにして、

```
npm i --save firebase @vue/composition-api
```
を実行してライブラリをインストールします。

Vue compositino API での設定は、`/plugins/composition.ts` にて、

```/plugins/composition.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

```
として、`nuxt.config.ts` にて

```nuxt.config.ts
export.default{
  ...
  plugins: ['~/plugins/composition'],
}
```
を追加してください。

## Firebase Authentication について

Firebase Authentication は、Firebase のライブラリの一つとして提供される認証ライブラリです。様々な認証ライブラリと異なり、Basic 認証だけでなく、Google やTwitter などのOauth 認証やSMS, メールでの2段階認証も用意されています。このライブラリは、Node.js だけでなく、Python やGolang, Ruby などの他言語も利用できます。

この記事では、NuxtJS で Firebase Authentication で個人的によく利用するテンプレートについて書きました。

それまでに至るまでの初期設定の部分は今回は省略して、実装のために設定したコードを掲載します。

```plugins/firebase.config.ts
import  Firebase from "firebase/app";
import "firebase/auth";

// config について一部省略。
const config = {
    apiKey: "xxxxxxx",
    authDomain: "xxxxxxxxxxx",
    databaseURL: "xxxxxxxxx",
    projectId: "xxxxxxxxxxxxxxxxxxx",
    storageBucket: "xxxxxxxxxxxxxxxx",
    messagingSenderId: "xxxxxxxxxxxx",
    appId: "xxxxxxxxxxxxxxxxxxxxx",
    measurementId: "xxxxxxxxxxxxxxxx"
}
const firebase = Firebase.apps.length? Firebase.app():Firebase.initializeApp(config);
export const auth = firebase.auth();

```

## Provide, Inject について
今回、グローバルステートの管理には**Provide, Inject** を使用しました。それらの特徴や実装の仕方については次の記事を参考にしてください。

<a class="embedly-card" href="https://qiita.com/0622okakyo/items/9d3d264f0417a1cb4923">Vue Composition API でprovide , inject を用いたデータの状態を管理する方法 - Qiita</a><script async src="//cdn.embedly.com/widgets/platform.js" charset="UTF-8"></script>

このState を扱ったコードは次の通りです。

```utils/states/user.ts
import {provide,inject, InjectionKey, reactive, toRefs} from "@vue/composition-api";

export type UserType  = {
    id?:string,
    name:string,
    email:string,
    thumbnail:string,
}

export type ErrorType = {
    state:number
    message:string
}
export type GlobalStateType = {
    user:UserType,
    error?:ErrorType
}

export const useGlobalState = ()=>{
    const globalState =reactive<GlobalStateType>({
        user:{
            id:"",
            name:"",
            email:"",
            thumbnail:""
        }
    });
    const setUserState = (state: UserType)=>{
        globalState.user.id=state.id;
        globalState.user.email=state.email;
        globalState.user.thumbnail=state.thumbnail;
        globalState.user.name=state.name;
    }
    const cleanUserState = () =>{
        globalState.user={
            id:"",
            name:"",
            email:"",
            thumbnail:""
        }
    }
    return {
        ...toRefs(globalState),
        setUserState,
        cleanUserState
    }
}

type StateType = ReturnType<typeof useGlobalState>

export const GlobalStateKey:InjectionKey<StateType> =Symbol("GlobalState");
export const ErrorStateKey: InjectionKey<ErrorType> = Symbol("ErrorState");

export const provideGlobalState= ()=>{
    provide(GlobalStateKey,useGlobalState());
};

export const injectGlobalState =()=>{
    const state = inject(GlobalStateKey);
    if(!state){
        throw Error("Unable to install User State");
    }
    return state;
}

```

## Firebase Authentication 周りで使用したコードについて

今回 Firebase 周りで使用したコードについては次の通りです。

```utils/firebase/auth.ts
import {auth} from "@/plugins/firebase.config";
import Firebase from "firebase";

type SuccessResType ={
    status: "ok",
    data: Firebase.auth.UserCredential,
}

type FailureResType ={
    status: "failed",
    message: string,

}

export const SigninWithBasic = async (email:string, password:string):Promise<SuccessResType|FailureResType>=>{
    try {
        const loginUser = await auth.signInWithEmailAndPassword(email,password);
        return {
            status:"ok",
            data: loginUser
        }

    } catch(e){
        console.error(e);
        return {
            status:"failed",
            message:e.message
        }

    }
}

export const SignUpWithBasic = async (email:string, password: string):Promise<SuccessResType|FailureResType>=>{
    try {
        console.log(email,password)
        const createdUser = await auth.createUserWithEmailAndPassword(email,password);
        console.log("作成された情報について",createdUser);
        return {
            status:"ok",
            data : createdUser
        }
    } catch(e){
        console.error(e);
        return {
            status: 'failed',
            message: e.message
        }
    }
}

export const SignOut = async ():Promise<void|FailureResType> =>{
    try {
        await auth.signOut();
    } catch(e){
        console.error(e);
        return {
            status:"failed",
            message:e.message
        }
    }
}
``` 

## 新規登録のページ

URL は、http://localhost:3000/signup です。

デザインは次のようになっています。

![2020-12-11 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271708/47a387ec-3e69-d381-514b-b3f0e6e251a7.png)

コードは次の通りです、

```login.vue
<template>
  <div class="container">
    <div class="card">
      <h3>メール</h3>
      <p class="my-input">
        <input v-model="email" placeholder="" type="email" />
      </p>
      <h3>パスワード</h3>
      <p class="my-input">
        <input v-model="password" type="password" />
      </p>
      <h3>パスワード（確認用）</h3>
      <p class="my-input">
        <input v-model="passwordVerify" type="password" />
      </p>
       <nuxt-link to="/signin">ログインページに戻る</nuxt-link>
      <p class="my-input">
        <button @click="signUpBasic()">アカウント登録</button>
      </p>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'
import { SignUpWithBasic } from '~/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'

export default defineComponent({
  name: 'SignUpPage',
  setup(props: any, { root }) {
    // TODO: この部分を reactive で実装したほうがいいかもしれない
    const email = ref('')
    const password = ref('')
    const passwordVerify = ref('')

    const userState = injectGlobalState()

    const signUpBasic = () => {
      if (password.value !== passwordVerify.value) {
        alert('パスワードが違います。')
      } else {
        const emailValue = email.value
        const passwordValue = password.value
        SignUpWithBasic(emailValue, passwordValue)
          .then((createdUser) => {
            console.log(createdUser)
            if (createdUser.status === 'ok') {
              const userInfo = createdUser.data.user
              userState.setUserState({
                id: userInfo ? userInfo.uid : '',
                email: userInfo && userInfo.email ? userInfo.email : '',
                name:
                  userInfo && userInfo.displayName ? userInfo.displayName : '',
                thumbnail:
                  userInfo && userInfo.photoURL ? userInfo.photoURL : '',
              })
              root.$router.push('/');
            } else {
              alert('アカウント登録失敗')
            }
          })
          .catch(() => {
            alert('エラーが発生しました')
          })
      }
    }
    return {
      email,
      password,
      passwordVerify,
      signUpBasic,
    }
  },
})
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

```
## ログインページの実装
URL は、http://localhost:3000/signin です。

画面は次のように表示されていると思います。

![2020-12-11 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271708/0d21ff6d-5c57-0770-c5a8-01438ec8771b.png)


コードは次の通りです。

```pages/login.vue
<template>
  <div class="container">
    <div class="card">
      <h3>メール</h3>
      <p class="my-input">
        <input v-model="email" placeholder="" type="email" />
      </p>
      <h3>パスワード</h3>
      <p class="my-input">
        <input v-model="password" type="password" />
      </p>
      <nuxt-link to="/signup">アカウントの登録はこちら</nuxt-link>
      <p class="my-input">
        <button @click="loginBasic()">ログイン</button>
      </p>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'
import { SigninWithBasic } from '@/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'
export default defineComponent({
  name: 'SigninPage',
  setup(props: any, { root }) {
    // local States
    const email = ref('')
    const password = ref('')

    // Global States
    const userState = injectGlobalState()
    console.log(userState.user)
    // method Functions
    const loginBasic = async () => {
      try {
        const emailValue = email.value
        const passwordValue = password.value
        const currentUser = await SigninWithBasic(emailValue, passwordValue)
        if (currentUser.status === 'ok') {
          const userInfo = currentUser.data.user
          console.log(userState)
          userState.setUserState({
            id: userInfo ? userInfo.uid : '',
            email: userInfo && userInfo.email ? userInfo.email : '',
            name: userInfo && userInfo.displayName ? userInfo.displayName : '',
            thumbnail: userInfo && userInfo.photoURL ? userInfo.photoURL : '',
          })
        }  else {
          alert('ログイン失敗')
        }
        root.$router.push('/')
      } catch (e) {
        alert(e)
      }
    }
    return {
      email,
      password,
      loginBasic,
    }
  },
})
</script>
<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.my-input {
  margin-top: 0.5rem;
  margin-bottom: 0.5rem;
}
</style>

```
## ログインしているかどうかを知る場合

`onMounted()`関数を利用して、ログイン状況を調べている間のローディング画面を設定します。`middleware` で認証の判定を行うこともできますが, Vue composition API での実装がうまくいかなかったので、ログインしているかどうかの判定は`layouts/default.vue` に実装しました。


```layouts/default.vue
<script lang="ts">
import { defineComponent, onMounted } from '@vue/composition-api'
import { provideGlobalState, injectGlobalState } from '@/utils/states/user'
import { auth } from '~/plugins/firebase.config'
export default defineComponent({
  name: 'DefaultLayout',

  setup(props: any, { root }) {
    provideGlobalState()
    const state = injectGlobalState()

    onMounted(() => {
      const publicPath = ["/signin","/signup"]
      if (state.user.value.id === '') {
        auth.onAuthStateChanged((user) => {

          if (user) {
            state.setUserState({
              id: user ? user.uid : '',
              email: user && user.email ? user.email : '',
              name: user && user.displayName ? user.displayName : '',
              thumbnail: user && user.photoURL ? user.photoURL : '',
            })
          } else {
            root.$router.push('/signin')
          }
        })
      }
    })
  },
})
</script>
```

## トップページについて

トップページに、ログアウトするためのボタンを実装しました。
![2020-12-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271708/622d4dbf-d87d-39c2-12cc-521302af58ca.png)


コードは次の通りです。

```pages/index.vue
<template>
  <div class="container">
    <div>
      <Logo />
      <h1 class="title">firebase-auth-sample</h1>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          rel="noopener noreferrer"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          rel="noopener noreferrer"
          class="button--grey"
        >
          GitHub
        </a>
        <!--以下を追加-->
        <button
          target="_blank"
          rel="noopener noreferrer"
          class="button--grey"
          @click="signout"
        >
          Signout
        </button>
      <!---->
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { SignOut } from '~/utils/firebase/auth'
import { injectGlobalState } from '~/utils/states/user'
export default defineComponent({
  name: 'IndexPage',
  setup(props: any, { root }) {
    const state = injectGlobalState()
    const signout = () => {
      SignOut()
        .then(() => {
          state.cleanUserState()
        })
        .catch(() => {
          alert('ログアウトに失敗しました。')
        })
    }
    return {
      signout,
    }
  },
})
</script>
<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

```

## 最後に

この記事では、Firebase Authentication を利用した認証機能をNuxt.js で行いましたが、この実装法は、Vue や React などの様々なフレームワークでも適用することは可能です。その際はぜひこの記事を参考にしてください。

## 参考文献
- https://composition-api.vuejs.org/
- https://dev.to/aaronksaunders/using-vue-composition-api-with-firebase-1oib