firebase-adminを使わず、Nuxt(SSR)でデータ取得

Nuxt
Gerd AltmannによるPixabayからの画像

目的

前回、firestoreのデータをサーバーサイドで取得するのにfirebase-adminを使っていました。
今回、firebase-adminを使わなくてもできるってことに今更気づいたのでまとめていきます。

※この記事は2020年5月ごろにはてなブログに投稿していた記事と同じです

開発環境

  • OS: Windows10 home
  • エディタ: Visual Studio Code
  • 言語: Typescript

ディレクトリ構成

sample/
 ├ @types/
 │ ├ js-cookie.d.ts
 │ └ jwt-decode.d.ts
 ├ middleware/
 │ ├ verifyLoggedIn.ts
 │ └ verifyNotLogInYet.ts
 ├ pages/
 │ ├ auth/
 │ │ └ signin.vue
 │ ├ membersOnly/
 │ │ └ index.vue
 │ └ index.vue
 ├ server/
 │ └ index.ts
 ├ store/
 │ ├ modules/
 │ │ └ user.ts
 │ └ index.ts
 ├ .env
 └ nuxt.config.js

※ 今回使用しているファイルのみ表示しています。

Nuxtのプロジェクトを作るときaxiosを使用するためcreate-nuxt-app を実行している時のオプションで選択してください。
もし選択するのを忘れた場合は以下のリンクで追加してください。

またその他cookie, jwt-decode, dotenvも別途この記事では使用しているため必要に応じてプロジェクト内にインストールしてください

ページ画面の作成

ここではlocalhostで開いたときの最初の画面、サインインする画面、アカウント持っている会員だけが開ける画面の三つを作成していきます。

<!-- pages/index.vue -->
<template>
 <section>
   <div>
     <nuxt-link to="/membersOnly">会員専用ページ</nuxt-link>
     <nuxt-link to="/auth/signin">サインイン</nuxt-link>
   </div>
 </section>
</template>

特に解説する部分はありません。

<!-- pages/auth/signin.vue -->
<template>
 <form @submit.prevent="signIn">
   <input id="usernameTxt" type="email" v-model="email" placeholder="メールアドレス">
   <input id="passwordTxt" type="password" v-model="password" placeholder="パスワード">
   <button type="submit">サインイン</button>
 </form>
</template>
<script lang="ts">
import Vue from 'vue'
import { mapActions } from 'vuex'
import axios from 'axios'

 export default Vue.extend({
   middleware: 'verifyLoggedIn', // 1
   data () {
     return {
       email: '',
       password: ''
     }
   },
   methods: {
     ...mapActions('modules/user', ['saveUserData']),
       signIn () {
         axios.get('/server/signin', {
           params: {
             email: this.email,
             password: this.password
           }
         }).then ((response) => {
           this.saveUserData(response.data) // 2
           this.$router.push('/membersOnly') // 3
         })
       }
    }
 })
 </script>
  • 1: もし既にサインインしている状態でもう一度サインインの画面を開こうとしても会員専用ページに飛ばすための部分です。
  • 2: 後にも解説しますが、サインインしたときに取得したユーザーのデータをstoreとcookieに保存します。

この処理によって画面をリロードしてもサインイン状態を保つことができます。

  • 3: データを保存し終えて無事サインインできたら会員専用ページに遷移させます。
<!-- pages/membersOnly/index.vue -->
<template>
 <section>
   会員専用ページ
   <div>
     <button @click="signOut">サインアウト</button>
   </div>
   <button @click="getUserData">usersコレクションのデータを取得する</button>
   <br>
   {{ viewData }}
 </section>
</template>
<script lang="ts">
 import Vue from 'vue'
 import { mapActions } from 'vuex'
 import axios from 'axios'

 export default Vue.extend({
   middleware: 'verifyNotLogInYet', // 4
   data () {
     return {
       viewData: {}
     }
   },
   methods: {
     ...mapActions('modules/user', ['removeUserData']),
     getUserData () {
     axios.get('/server/users') // 5
        .then((response) => {
          this.viewData = response.data
        })
   },
   async signOut () {
     await axios.get('/server/signout') // 6
     await this.removeUserData() // 7
     this.$router.push('/auth/signin')
   }
  }
})
</script>
  • 4: まだサインインしていないユーザが会員専用ページに行こうしようとしても強制的にサインインページに遷移させる部分です。
  • 5, 6: firestoreのデータ取得をserverMiddlewareを使ってapi処理として取得するのでここではaxiosを使って呼び出します。
  • 7: サインインするときに保存したデータを削除することでサインアウト後、会員専用ページに入れないようにしています。

middlewareの実装

次にmiddlewareの実装をしていきます。
serverMiddlewareではありません。middlewareです!
middlewareは公式によると特定のページまたはいくつかのページがレンダリングされる前に実行される関数を用意できる場所みたいです。

// middleware/verifyLoggedIn.ts
import { Context } from '@nuxt/types'

export default (context: Context) => {
  if (context.store.getters['modules/user/loggedIn']) { // 1
    return context.redirect('/membersOnly')
  }
}
  • 1: サインインする際にデータを保存していますが保存先の一つがstoreです。

storeのgettersから既にサインインしているかどうかを確認し、していたら専用ページに飛ばすようにしています。

// middleware/verifyNotLogInYet.ts
import { Context } from '@nuxt/types' 

export default (context: Context) => {
 if (!context.store.getters['modules/user/loggedIn']) { // 2
   return context.redirect('/auth/signin')
 } 
}
  • 2: 先ほどと逆でサインインしていなかったらサインイン画面に飛ばします。

serverMiddlewareを実装する

serverMiddlewareを実装していきます。
ここではサインイン、サインアウト、データの取得の三つの機能を用意します。

// server/index.ts
import { Request, Response } from 'express' 
import firebase from 'firebase/app' 
import 'firebase/auth' 
import 'firebase/firestore' 

require('dotenv').config({ path: './.env' }) // 1 

const config = {
 apiKey: process.env.API_KEY,
 authDomain: process.env.AUTH_DOMAIN,
 databaseURL: process.env.DATABASE_URL,
 projectId: process.env.PROJECT_ID,
 storageBucket: process.env.STORAGE_BUCKET,
 messagingSenderId: process.env.MESSAGING_SENDER_ID,
 appId: process.env.APP_ID,
 measurementId: process.env.MEASUREMENT_ID
}

const firebaseApp = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app() 
const auth = firebaseApp.auth() 
const firestore = firebaseApp.firestore() 
const express = require('express') 
const app = express() 

// サインイン 
~~~~~~~~~~ 処理を記載 ~~~~~~~~~~ 

// サインアウト 
~~~~~~~~~~ 処理を記載 ~~~~~~~~~~ 

// データ取得 
~~~~~~~~~~ 処理を記載 ~~~~~~~~~~ 

module.exports = { // 2
  path: '/server/',
  handler: app, 
}
  • 1:serverMiddlewareはnuxt.config.jsに定義している環境変数を読み込むことはできません。
    そのためserverMiddleware内にrequire(‘dotenv’).config({ path: ‘./.env’ })をして環境変数を使用します。
  • 2:http:~~/server/~を呼び出すことでserverMiddleware内の各apiを呼び出すことができます。

下の公式にNuxtでの”env“の扱い方が載っています。

// server/index.ts (サインイン)
// サインイン 
app.get('/signIn', (req: Request, res: Response) => {
  const email = req.query.email
  const password = req.query.password
  auth.signInWithEmailAndPassword(email, password)
  .then(async (response: any) => {
    const token = await response.user.getIdToken() // 3
    const info = {
      token: token,
      user: response.user
    }
    res.send(info) // 4
  })
})
  • 3:signInWithEmailAndPasswordで取得できるresponseの中にはuid登録したメールアドレス等が入っています。
    これをトークンにして呼び出し側に送り後にcookieに保存します。
  • 4: 呼び出し側に値を返しているのですが返ってきたresponse内のdataキーに格納されています。
// server/index.ts (サインアウト)
// サインアウト 
app.get('/signOut', async (req: Request, res: Response) => {
  await auth.signOut()
  res.end()
})

サインアウトのみしています。
データは返さないのでres.end()で終えています。

// server/index.ts (データ取得)
// データ取得 
app.get('/users', async (req: Request, res: Response) => {
  await firestore.collection('users').get()
  .then((query: any) => {
    const items: any[] = []
    query.forEach((doc: any) => {
      const userData = {
        id: doc.id,
        data: doc.data()
      }
      items.push(userData)
    })
    console.log(items)
    res.send(items)
  })
})

firebaseの公式ドキュメント通りにusersコレクション内のデータをまとめて取得しています。

ユーザデータをstore, cookieに保存

次にサインインしたときに取得したユーザのデータをstore, cookieに保存していきます。
storeに保存することでpagesやcomponentsをまたいだデータの共有等がやりやすくなります。
しかし、store内に保存したデータは画面をリロードしたりすると簡単に消えてしますためその対策としてcookieにトークンを保存しておきます。

// store/modules/user.ts
import Cookies from 'js-cookie' // 1 

export const state = () => ({ uid: null, email: null }) 

export const getters = {
  loggedIn (state: any) { // 2
    return !!state.uid
  }
}

export const actions = {
  async saveUserData ({ commit }: any, userInfo: any) {
    Cookies.set('userToken', userInfo.token) // 3
    commit('setUid', userInfo.user.uid)
    commit('setEmail', userInfo.user.email)
  },
  async removeUserData({ commit }: any) {
    Cookies.remove('userToken') // 4
    commit('setUid', null)
    commit('setEmail', null)
  },
} 

export const mutations = {
  setUid (state: any, uid: string) {
    state.uid = uid
  },
  setEmail (state: any, email: string) { 
    state.email = email
  },
}
  • 1: cookieに保存するためにjs-cookieを使っていますが、私の環境だとただimportしただけでは認識してくれませんでした。
    なので下のファイルも用意しました。
// @types/js-cookie.d.ts
declare module 'js-cookie'
  • 2: middlewareで用意した。サインインしているかしていないかで遷移するページを制御していましたが、ここを基準に処理しています。
  • 3, 4: 先ほども書きましたがstore内のデータは簡単に消えてしまうのでcookieに保存し、サインアウトするときは削除しています。
// store/index.ts
import jwtDecode from 'jwt-decode' // 5 

const cookieparser = require('cookie') 

export const actions = {
  nuxtServerInit ({ commit }: any, { req }: any) {
    if (!req.headers.cookie) return
    const parsedCookie = cookieparser.parse(req.headers.cookie)
    
    const userInfo = parsedCookie.userToken
    if (!userInfo) return

    const infomation = jwtDecode(userInfo)
    if (!infomation) return
    commit('modules/user/setUid', infomation.user_id)
    commit('modules/user/setEmail', infomation.email)
  }
}
  • 5:jwt-decodeの場合もimportしただけでは認識しなかったため下のファイルを用意しました。
// @types/jwt-decode.d.ts
declare module 'jwt-decode'

serverMiddlewareを追加する

// nuxt.config.js
modules: [
  '@nuxtjs/axios',
],
dotenv: {
  filename: '.env'
}, 
axios: {
}, 
serverMiddleware: [
  '~/server' // <= 追加する
],

上記のように追加します。
また、npm install axiosをする必要があるかもしれないのでインストールしておいてください。

最後に

今回は前回と違い、firebase-adminを使わずfirebaseモジュールのみでサインイン、データ取得をしてみました。
冗長な部分または、間違いがあるかもしれません。
発見しだい後々修正していくつもりです。
ここまで見て頂きありがとうございます。

タイトルとURLをコピーしました