Nuxt TypeScript（Composition API）でFirebase Authentication を使用する。

こちらの開発を続けています。

https://qiita.com/JQinglong/items/e35294f7a260c5a26ae8

https://qiita.com/JQinglong/items/3f04e4050739f85bfc12

次は、Googleログインでの認証に対応したいと思います。

これに関しては、大変ありがたい記事があり、過去のアプリ作成でも参考にさせていただいています。以下、参考サイトと記載します。

https://qiita.com/0622okakyo/items/19438a44f55d12274e27

ただ、この記事は、メールアドレスとパスワードによるログインのみなので、Google認証（ついでにFaecebookも）に対応したいと思いました。

## Firebaseでプロジェクト作成

まずは、Firebaseとしての準備です。  
既存のプロジェクトに追加というのもできるが、新規で名前を作りました。

とりあえずはアナリティクスは無効で良いかと。 

Authentication＞始める
ログインプロバイダ
＜Google＞
プロジェクトの公開名、これは変えられます。

今回だと、  
project-1093232205030→project-jqrally

プロジェクトのサポートメールは自分のメールアドレス。

＜メール / パスワード＞
有効
メールリンクは無効

＜承認済みドメイン＞
開発中ですが、すでにAzure Static Web Appsにデプロイ7手いるので、そちらのURL。
gentle-sand-0b8386900.azurestaticapps.net

＜1 つのメールアドレスにつき 1 つのアカウント＞
本当はやると良いと思いますが、テスト用にはやらないでおきます。

けど、同一アドレスで複数ログインはできなくなっている気はします。


## FirebaseUI にチャレンジ、しようと思いましたが断念

FirebaseUI、見た目もカッコ良いし、過去のアプリでも使ったことがあるので、今回も使いたいと思いました。
が、結論としては利用はできませんでした。

参考サイト、および、そこからさらに参照しているサイト、

https://zenn.dev/okakyo/articles/f39d5c2c328b6d

の記載にあるように、Composition API を利用する場合、状態管理はVuexではなく、provide, injectの利用が良さそうということで、それを前提にします。

その際、injectは、setup() の中でしか使えないという制約があります。

FirebaseUI では、callbacks として、signInSuccessWithAuthResult という関数を定義しておいて、そこで authResult を受け取る、という形になっていますが（と思いましたが、今、もしかしたらこれ使わなくてもよかったのかなと訝しんでもいます）、これを、setup の中に書いておいても、

```
inject() can only be used inside setup() or functional components.
```

のエラーになってしまうのです。

ということで、自分で、「Sign in with Google」ボタンを作ることにします。

## 実装

何も飾っていないのでダサいですが、GoogleとFacebookのボタンを表示するコンポーネントです。

```ts:SinginButtons.vue
<template>
  <div>
    <v-card>
      <v-card-text>
        <v-btn
          color="blue-grey"
          class="ma-2 white--text"
          @click="login(0)"
        >
          Sign in with Google
          <v-icon
            right
            dark
          >
            mdi-google
          </v-icon>
        </v-btn>
      </v-card-text>
      <v-card-text>
        <v-btn
          color="blue-grey"
          class="ma-2 white--text"
          @click="login(1)"
        >
          Sign in with Facebook
          <v-icon
            right
            dark
          >
            mdi-facebook
          </v-icon>
        </v-btn>
      </v-card-text>
    </v-card>
  </div>
</template>

<script lang="ts">
import {
  defineComponent
} from '@nuxtjs/composition-api'

import { injectGlobalState } from '@/compositions/states/user'
import { googleProvider, facebookProvider } from '~/plugins/firebase.config'
import { SigninWithOAuth } from '~/compositions/firebase/auth'

export default defineComponent({
  name: 'SinginButtons',
  props: {
  },
  setup (_, { root }) {
    const userState = injectGlobalState()

    const provider = [googleProvider, facebookProvider]

    const login = async (value: number) => {
      try {
        const currentUser = await SigninWithOAuth(provider[value])
        if (currentUser.status === 'ok') {
          const userInfo = currentUser.data.user
          // eslint-disable-next-line no-console
          console.log('loginAuth', userState)

          userState.setUserState({
            uid: userInfo ? userInfo.uid : '',
            email: userInfo && userInfo.email ? userInfo.email : '',
            displayName: userInfo && userInfo.displayName ? userInfo.displayName : '',
            photoURL: userInfo && userInfo.photoURL ? userInfo.photoURL : ''
          })
        } else {
          alert('Authログイン失敗')
        }
        root.$router.push('/')
      } catch (e) {
        alert(e)
      }
    }

    return {
      login
    }
  }
})
</script>
```

参考サイトだと、

```ts
const currentUser = await SigninWithBasic(emailValue, passwordValue)
```

としている部分を、

```ts
const currentUser = await SigninWithOAuth(provider[value])
```

としています。メールアドレス、パスワードの代わりに、各認証サイトのプロバイダー情報を渡す形です。

providerとして何を渡すのかは、

https://firebase.google.com/docs/auth/web/google-signin?authuser=0

に記載のあるように、

```js
new firebase.auth.GoogleAuthProvider();
```

というレベルで、難しい物ではありません。
ただし、使う前に、各サービスでの設定を行なって、APIキーなどを取得してFirebaseに設定しておく必要があります。

今回は、参考サイトに従って作った、プラグインの中に定義しました。

```ts:firebase.config.ts

// ・・・

const firebase = Firebase.apps.length ? Firebase.app() : Firebase.initializeApp(config)
export const auth = firebase.auth()

const googleProvider = new Firebase.auth.GoogleAuthProvider()
const facebookProvider = new Firebase.auth.FacebookAuthProvider()
export { googleProvider }
export { facebookProvider }
```

SigninWithOAuth の本体は、参考サイトではutilsとして作っている、auth.ts の中で書いています。

```ts:auth.ts
export const SigninWithOAuth = async (provider: any):Promise<SuccessResType|FailureResType> => {
  try {
    const loginUser = await auth.signInWithPopup(provider)
    // eslint-disable-next-line no-console
    console.error(loginUser)
    return {
      status: 'ok',
      data: loginUser
    }
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error(e)
    return {
      status: 'failed',
      message: e.message
    }
  }
}
```

ログインページにコンポーネントを貼り付け。
参考サイトのメールアドレス・パウワードログインコンポーネントも貼り付けています。

```ts:pages/login.vue
<template>
  <div>
    <v-card>
      <v-card-title class="headline">
        Login
      </v-card-title>
      <v-card-text>
        <singin-buttons />
      </v-card-text>
      <v-card-text>
        <firebase-auth />
      </v-card-text>
    </v-card>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import SinginButtons from '~/components/firebase/SinginButtons.vue'
import FirebaseAuth from '~/components/firebase/FirebaseAuth.vue'

export default defineComponent({
  components: { SinginButtons, FirebaseAuth }
})
</script>

<style>

</style>

```

最後に、レイアウトで、認証情報取得。余計なところがありますが、気に入っているレイアウトなので、ついでに紹介。

```ts:layouts/default.vue

<template>
  <v-app>
    <v-navigation-drawer v-model="drawer" app clipped>
      <v-list dense>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
          exact
          dense
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
      <v-divider />
      <v-list dense>
        <v-list-item
          v-for="(item, i) in adminitems"
          :key="i"
          :to="item.to"
          router
          exact
          height="32px"
        >
          <v-list-item-action class="my-1">
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-app-bar app clipped-left color="green accent-1" dense>
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-btn icon :to="'/'">
        <v-icon class="mx-4" large>
          mdi-postage-stamp
        </v-icon>
      </v-btn>
      <v-toolbar-title class="mr-12 align-center">
        <span class="title">Let's スタンプラリー（絶賛開発中）</span>
      </v-toolbar-title>

      <v-spacer />
      <v-btn icon>
        <v-icon>mdi-twitter</v-icon>
      </v-btn>
      <v-btn icon>
        <v-icon>mdi-facebook</v-icon>
      </v-btn>
      <v-btn icon>
        <v-icon>mdi-instagram</v-icon>
      </v-btn>

      <v-menu
        left
        bottom
      >
        <template #activator="{ on, attrs }">
          <v-btn
            icon
            v-bind="attrs"
            v-on="on"
          >
            <v-avatar
              v-if="stateGlobal.user && stateGlobal.user.value.photoURL"
              size="36"
            >
              <img :src="stateGlobal.user.value.photoURL" :alt="stateGlobal.user.value.photoURL">
            </v-avatar>
            <v-icon
              v-else
            >
              mdi-account
            </v-icon>
          </v-btn>
        </template>
        <v-list>
          <v-list-item
            v-if="stateGlobal.user.value.uid"
            :to="'/settings'"
          >
            <v-list-item-avatar>
              <v-icon>mdi-cog</v-icon>
            </v-list-item-avatar>
            <v-list-item-title>設定</v-list-item-title>
          </v-list-item>
          <v-list-item
            v-if="!stateGlobal.user.value.uid"
            :to="'/login'"
          >
            <v-list-item-avatar>
              <v-icon>mdi-login</v-icon>
            </v-list-item-avatar>
            <v-list-item-title>Login</v-list-item-title>
          </v-list-item>
          <v-list-item
            v-if="stateGlobal.user.value.uid"
            @click="signOut"
          >
            <v-list-item-avatar>
              <v-icon>mdi-logout</v-icon>
            </v-list-item-avatar>
            <v-list-item-title>Logout</v-list-item-title>
          </v-list-item>
        </v-list>
      </v-menu>

      <template #extension>
        <v-tabs
          background-color="white"
          align-with-title
        >
          <!-- >v-model="tab" は削除している-->
          <v-tabs-slider color="blue" />
          <v-tab
            v-for="item in tabitems"
            :key="item.title"
            :to="item.to"
          >
            {{ item.title }}
          </v-tab>
        </v-tabs>
      </template>
    </v-app-bar>

    <v-main>
      <v-container fluid>
        <nuxt />
      </v-container>
    </v-main>

    <v-footer
      :absolute="!fixed"
      app
    >
      <span>&copy; {{ new Date().getFullYear() }} JQ</span>
      {{ stateGlobal.user }}
    </v-footer>
  </v-app>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from '@vue/composition-api'
import { provideGlobalState, injectGlobalState } from '@/compositions/states/user'
import { auth } from '~/plugins/firebase.config'
import { SignOut } from '~/compositions/firebase/auth'

type Item = {
  icon: string
  title: string
  to: string
}

export default defineComponent({
  name: 'DefaultLayout',

  setup (_, { root }) {
    const clipped = ref(false)
    const drawer = ref(false)
    const fixed = ref(false)

    const items = ref<Item[]>([
      {
        icon: 'mdi-home',
        title: 'ホーム',
        to: '/'
      },
      // {
      //   icon: 'mdi-apps',
      //   title: 'memo',
      //   to: '/memo'
      // },
      {
        icon: 'mdi-forum',
        title: 'メニュー１',
        to: '/menu1'
      },
      {
        icon: 'mdi-tooltip-account',
        title: 'メニュー２',
        to: '/menu2'
      },
      {
        icon: 'mdi-bookmark',
        title: 'メニュー３',
        to: '/menu3'
      }
    ])
    const adminitems = ref<Item[]>([
      {
        icon: 'mdi-pencil',
        title: 'Memo',
        to: '/memo'
      }
    ])

    const tabitems = ref<Item[]>([
      {
        icon: 'mdi-pencil',
        title: 'Memo',
        to: '/memo'
      },
      {
        icon: 'mdi-forum',
        title: 'メニュー１',
        to: '/menu1'
      }
    ])
    provideGlobalState()
    const stateGlobal = injectGlobalState()

    onMounted(() => {
      // const publicPath = ["/signin","/signup"]
      if (stateGlobal.user.value.uid === '') {
        auth.onAuthStateChanged((user: any) => {
          if (user) {
            stateGlobal.setUserState({
              uid: user ? user.uid : '',
              email: user && user.email ? user.email : '',
              displayName: user && user.displayName ? user.displayName : '',
              photoURL: user && user.photoURL ? user.photoURL : ''
            })
          } else {
            if (root.$route.path.indexOf('/admin') === 0) {
              root.$router.push('/login')
            }
            ;
          }
        })
      }
    })

    const signOut = () => {
      SignOut()
    }

    return {
      clipped,
      drawer,
      fixed,
      items,
      tabitems,
      adminitems,
      stateGlobal,
      signOut
    }
  }
})
</script>

<style scoped>
.v-list-item__action {
  margin-top: 4px;
  margin-bottom: 8px;
}
</style>

```

stateGlobal経由で、認証情報を利用する形です。

各コンポーネントでも、同様に取得できます。

```ts:
<template>
  <div>
省略
  </div>
</template>

<script lang="ts">
import { reactive, toRefs, useFetch, defineComponent } from '@nuxtjs/composition-api'
import { useEventSpot } from '@/compositions'
import { injectGlobalState } from '@/compositions/states/user'

export default defineComponent({
  name: 'StampBook',
  props: {
    eventId: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const state = reactive({
      search: '',
      filter: {},
      sortDesc: false
    })
    const { state: eventSpotState, getList } = useEventSpot()

    const fetchData = async () => {
      await getList({ event: props.eventId })
    }

    const { fetchState } = useFetch(() => fetchData())

    const gState = injectGlobalState()
    // eslint-disable-next-line no-console
    console.log('StampBook gState', gState.user)

    return {
      ...toRefs(state),
      eventSpotState,
      fetchState
    }
  }
})
</script>

```

これも色々書いてますが、injectGlobalState　の部分だけ見ると、簡単に取得できることが分かります。

今回も、ここに至るまでには回り道しながら苦労していますが、先人の知恵に助けられて進めています。