📝

Firestoreで簡易的なログインを実装する

2021/03/29に公開

FirebaseのFirestoreを使用してログイン機能を実装します。

・ログイン機能をつけたいが、何らかの理由でAuthenticationを使えない場合
(メアドが使えない、googleアカウントを持っていないなど)

実装について

・Nuxt.js(v2.14.5)、Node.js(v13.14.0)、firebase(v8.2.6)を使用
・Firestoreでログイン情報を管理する
・firebaseの情報は/pluginsに記載する
・vuex-persistedstateを使用し、localstrageでログイン情報を保存することでリロードも対応する
・1回目ログインで新規登録、2回目以降のログインではデータベース(以下"DB"と記載)、またはlocalstrageよりデータを取得する
・middlewareを使用し、ログイン情報がない場合は"/"にリダイレクトさせる(情報がある場合は"/page01"にリダイレクト)
※Nuxt.js、Firestoreの連携や設定は省略

コード

page/index.vue
<template>
  <div class="container">
    <div class="container__wrap">
      <div class="container__info">
        <div class="container__info--block">
          <div>
            ユーザID<br />
            <p class="container__info--text">(半角英数字)</p>
          </div>
          <input type="text" v-model="user.number" class="container__info--number" placeholder="000000" oncopy="return false" onpaste="return false" @keyup="inputCheck" />
        </div>
        <div class="container__info--block">
          <div>
            ユーザID(確認用)<br />
            <p class="container__info--text">(半角英数字)</p>
          </div>
          <input type="text" class="container__info--confirm" placeholder="000000" oncopy="return false" onpaste="return false" @keyup="inputCheck" />
        </div>
        <div class="container__info--block">氏名<input type="text" v-model="user.name" class="container__info--name" placeholder="鈴木 太郎" @keyup="inputCheck" /></div>
      </div>

      <button type="submit" @click="login" disabled class="container__wrap--start">ログイン</button>
    </div>
  </div>
</template>

<script>
import firebase from '@/plugins/firebase';
const db = firebase.firestore();
const usersCollection = db.collection('users');

export default {
  data() {
    return {
      user: {
        name: '',
        number: '',
      },
    };
  },
  methods: {
    inputCheck() {
      //ユーザIDと名前の空欄チャック
      const loginButton = document.querySelector('.container__wrap--start');
      const inputValueNumber = document.querySelector('.container__info--number').value;
      const inputValueName = document.querySelector('.container__info--name').value;
      const inputValueConfirm = document.querySelector('.container__info--confirm').value;

      if (inputValueNumber == '' || inputValueName == '' || inputValueNumber.match(/[^A-Za-z0-9]+/) || inputValueConfirm !== inputValueNumber) {
        loginButton.disabled = true;
      } else {
        loginButton.disabled = false;
      }
    },
    login() {
      const inputUser = this.user.name;
      const inputNmber = this.user.number;

      //storeに登録情報を送信する
      this.$store.dispatch('setInfo', {
        number: inputNmber,
        name: inputUser,
      });

      usersCollection
        .where('number', '==', inputNmber)
        .get()
        .then((querySnapshot) => {
          if (querySnapshot.empty) {
            //新規ユーザーの場合
            console.log('new-user');
            usersCollection.add({
              name: inputUser,
              number: inputNmber,
              startTime: firebase.firestore.FieldValue.serverTimestamp(), //初回ログイン時間
            });
          } else {
            //既にユーザーが存在する場合
            console.log('exist-user');
            querySnapshot.forEach((doc) => {
              usersCollection.doc(doc.id).set(
                {
                  continueTime: firebase.firestore.FieldValue.serverTimestamp(), //途中参加時間
                },
                { merge: true }
              );
            });
          }
        })
        .catch(function (error) {
          console.log('Error getting documents: ', error);
        });
      this.$router.push('/page01/');
    },
  },
};
</script>
plugins/localStorage.js
import createPersistedState from 'vuex-persistedstate';

export default ({ store }) => {
  window.onNuxtReady(() => {
    createPersistedState({})(store);
  });
};
store/index.js
export const state = () => ({
  userInfo: {
    number: '',
    name: '',
  },
});

export const mutations = {
  setInfo(state, payload) {
    state.userInfo.number = payload.number;
    state.userInfo.name = payload.name;
  },
};

export const actions = {
  setInfo({ commit }, payload) {
    commit('setInfo', {
      number: payload.number,
      name: payload.name,
    });
  },
};

export const getters = {
  userInfo: (state) => {
    return state.userInfo;
  },
};
middleware/authenticated.js
export default function ({ store, redirect }) {
  // ユーザーが認証されていない場合
  if (!store.state.userInfo.number) {
    return redirect('/');
  }else{
    return redirect('/page01');
  }
}

説明

page/index.vue

inputタグに情報が入力されているか、ユーザIDが正しいか等をチェック
入力情報がOKならログインボタンが押せるようになる

page/index.vue
    inputCheck() {
      //ユーザIDと名前の空欄チャック
      const loginButton = document.querySelector('.container__wrap--start');
      const inputValueNumber = document.querySelector('.container__info--number').value;
      const inputValueName = document.querySelector('.container__info--name').value;
      const inputValueConfirm = document.querySelector('.container__info--confirm').value;

      if (inputValueNumber == '' || inputValueName == '' || inputValueNumber.match(/[^A-Za-z0-9]+/) || inputValueConfirm !== inputValueNumber) {
        loginButton.disabled = true;
      } else {
        loginButton.disabled = false;
      }
    },

page/index.vue

storeに入力情報を登録する

page/index.vue
      const inputUser = this.user.name;
      const inputNmber = this.user.number;

      //storeに登録情報を送信する
      this.$store.dispatch('setInfo', {
        number: inputNmber,
        name: inputUser,
      });

page/index.vue

inputNmberでDBの情報を検索し、情報がない場合は新規登録する

page/index.vue
      usersCollection
        .where('number', '==', inputNmber)
        .get()
        .then((querySnapshot) => {
          if (querySnapshot.empty) {
            //新規ユーザーの場合
            console.log('new-user');
            usersCollection.add({
              name: inputUser,
              number: inputNmber,
              startTime: firebase.firestore.FieldValue.serverTimestamp(), //初回ログイン時間
            });
          }

page/index.vue

inputNmberでDBの情報を検索し、情報がある場合は途中参加時間のみ追加する
"{ merge: true }"とすることで存在する情報はそのままに、途中参加時間のみ追加できる

page/index.vue
 else {
            //既にユーザーが存在する場合
            console.log('exist-user');
            querySnapshot.forEach((doc) => {
              usersCollection.doc(doc.id).set(
                {
                  continueTime: firebase.firestore.FieldValue.serverTimestamp(), //途中参加時間
                },
                { merge: true }
              );
            });
          }

page/index.vue

上記どちらかの工程が済んだ後、/page01に飛ぶ

page/index.vue
this.$router.push('/page01/');

vuex-persistedstateの使用について

このままコピペでもOK

plugins/localStorage.js
import createPersistedState from 'vuex-persistedstate';

export default ({ store }) => {
  window.onNuxtReady(() => {
    createPersistedState({})(store);
  });
};

store/index.jsについて

ここにログイン情報(ユーザID:number、氏名:name)を保管する記載をしている

store/index.js
export const state = () => ({
  userInfo: {
    number: '',
    name: '',
  },
});

export const mutations = {
  setInfo(state, payload) {
    state.userInfo.number = payload.number;
    state.userInfo.name = payload.name;
  },
};

export const actions = {
  setInfo({ commit }, payload) {
    commit('setInfo', {
      number: payload.number,
      name: payload.name,
    });
  },
};

export const getters = {
  userInfo: (state) => {
    return state.userInfo;
  },
};

middleware/authenticated.jsについて

store/index.jsの情報を元に、どこにリダイレクトするかを決めている

middleware/authenticated.js
export default function ({ store, redirect }) {
  // ユーザーが認証されていない場合
  if (!store.state.userInfo.number) {
    return redirect('/');
  }else{
    return redirect('/page01');
  }
}

middlewareについて

middlewareについての読み込みはnuxt.config.jsに記載してもOK(全ページに適用する)、
各ページごとに設定したい場合はそれぞれに記載でもOK

nuxt.config.js
 //全ページに適用する場合
export default {
        :
  router: {
    middleware: 'authenticated'
  }
        :
}
// 各ページごとに設定したい場合
<script>
import firebase from '@/plugins/firebase';
const db = firebase.firestore();
        :
export default {
        :
  mounted() {
        :
  },
  methods: {
        :
  },
  middleware: 'authenticated',
};
</script>

以上となります。

最後に

初めてFirebaseを使用して作成したので、もっと良い記述方法あれば教えてください。
良ければ記事をLike、Twitterのフォローをお願いします。

Discussion