VueでGoogleでログインしているユーザーのみにFirestoreのCRUD権限を与える方法

今日はVue3のフレームワークを使ってGCP(Google Cloud Platform)のAuthenticationの機能からGoogleでログインしているユーザーを認知し、ログインしているユーザーのみにFirestoreのデータベースのCRUD権限を与える方法を紹介します。

まず初めにこの記事を読んで理解しておきましょう。

完成したコードはGitHubから確認してください。

https://github.com/DanNakatoshi/Vue-userLogin-Firebase-CRUD

今日の目的

VueとFirebaseを使ってGoogleでログインしているユーザーにデータベースの書き込み権限を与える(記事の投稿など)

プロジェクトのセットアップ

では上記の記事をもとにしてプロジェクトを作成します。

  • FirebaseのGoogleプロバイダーによる認証APIの取得
  • Vueのプロジェクトの作成
  • (必要に応じて)VuetifyなどのUIライブラリのインストール

本日は、Googleの認証システムに集中するので詳しいセットアップやGoogleクラウドの説明は省きます。

では下記のようなコードをApp.vueに記載しましょう。

<template>
  <v-app id="inspire">
    <v-app-bar
      class="px-3"
      color="white"
      flat
      density="compact"
    >
      <v-avatar
        color="grey-darken-1"
        size="32"
      ></v-avatar>

      <v-spacer></v-spacer>

      <v-tabs
        centered
        color="grey-darken-2"
      >
        <v-tab>
          <router-link to="/">ホーム</router-link>
        </v-tab>
        <v-tab v-if="isLoggedIn">
          <router-link to="/profile">プロフィール</router-link>
        </v-tab>
      </v-tabs>
      <v-spacer></v-spacer>

      <v-btn
        v-if="!isLoggedIn"
        @click="signInWithGoogle"
      >
        サインイン
      </v-btn>
      <v-btn
        v-if="isLoggedIn"
        @click="handleSignOut"
      >
        サインアウト
      </v-btn>
    </v-app-bar>

    <v-main class="bg-grey-lighten-3">
      <v-container>
        <v-row>
          <v-col
            cols="12"
            sm="3"
          >
            <v-sheet
              rounded="lg"
              min-height="268"
            >
              <div class="d-flex justify-center">
                <p
                  v-if="displayName"
                  class="pa-3"
                >
                  {{ displayName }}さん
                </p>
              </div>
            </v-sheet>
          </v-col>

          <v-col
            cols="12"
            sm="9"
          >
            <v-sheet
              min-height="70vh"
              rounded="lg"
            >
              <div class="pa-1">
                <router-view />
              </div>
            </v-sheet>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
  <!-- <router-view signInWithGoogle="signInWithGoogle" /> -->
</template>

<script setup>
import { onMounted, ref } from 'vue';
import {
  getAuth,
  onAuthStateChanged,
  signOut,
  GoogleAuthProvider,
  signInWithPopup,
} from 'firebase/auth';
import { useRouter } from 'vue-router';

const router = useRouter();
const isLoggedIn = ref(false);
const displayName = ref('ゲスト');

let auth;
onMounted(() => {
  auth = getAuth();
  onAuthStateChanged(auth, (user) => {
    if (user) {
      isLoggedIn.value = true;
      displayName.value = user.displayName;
      console.log(user.uid);
    } else {
      isLoggedIn.value = false;
      displayName.value = 'ゲスト';
    }
  });
});

function handleSignOut() {
  signOut(auth)
    .then(() => {
      console.log('サインアウトしました');
      router.push('/');
    })
    .catch((error) => {
      console.log(error);
    });
}

function signInWithGoogle() {
  const provider = new GoogleAuthProvider();
  signInWithPopup(auth, provider)
    .then((result) => {
      displayName.value = result.user.displayName;
      router.push('/profile');
    })
    .catch((error) => {});
}
</script>

テンプレート(Template)セクション

  • <v-app>:Vuetifyのv-appコンポーネントで、アプリケーション全体のコンテナを定義します。
  • <v-app-bar>:アプリケーションの上部に表示されるナビゲーションバーです。ユーザーがログインしているかどうかによって表示内容が変わります。
  • <v-avatar>:アバター(ユーザーのアイコン)を表示します。
  • <v-tabs>:タブを表示し、ルーターリンクを含んでいます。
  • <v-btn>:ボタンを表示し、ログイン状態によって表示内容が変わります。
  • <v-main>:アプリケーションのメインコンテンツエリアを定義します。
  • <v-container>:コンテンツを格納するコンテナです。
  • <v-row>:行を作成し、グリッドシステムを使用して列を配置します。
  • <v-col>:列を作成し、グリッドシステムを使用してコンテンツを配置します。

スクリプト(Script)セクション

  • import:Vue.jsとFirebaseの関連するメソッドやモジュールをインポートしています。
  • onMounted:Vue 3のフック関数であり、コンポーネントがマウントされた後に実行されるコードを定義します。
  • ref:Vue 3のリアクティブ参照を作成します。
  • getAuth:Firebase AuthenticationのgetAuth関数を使用して、認証オブジェクトを取得します。
  • onAuthStateChanged:認証状態の変更を監視し、ユーザーのログイン状態に応じて表示内容を変更します。
  • signOut:ユーザーをログアウトさせるためにFirebase AuthenticationのsignOut関数を使用します。
  • GoogleAuthProvider:Googleの認証プロバイダーオブジェクトを作成します。
  • signInWithPopup:Googleのポップアップウィンドウを使用してユーザーをログインさせるためにFirebase AuthenticationのsignInWithPopup関数を使用します。
  • displayName:ユーザーの表示名をリアクティブな変数として定義します。
  • handleSignOut:ログアウト処理を行う関数です。ログアウトが成功した場合、コンソールにメッセージを表示し、ホームページにリダイレクトします。
  • signInWithGoogle:Googleアカウントでログインするための関数です。Googleのポップアップウィンドウを開き、ログインが成功した場合に表示名を更新し、プロフィールページにリダイレクトします。

このコードは、Vue.jsとFirebaseを組み合わせて、ユーザーのログイン状態に基づいて表示内容を切り替えるシンプルなアプリケーションの一部です。ユーザーがログインしている場合、表示名やプロフィールへのリンクが表示され、ログアウトボタンが表示されます。ログアウト状態では、ログインボタンが表示されます。

では、main.jsはこのようになります。

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { initializeApp } from 'firebase/app';
import './style.css'

// import { getAuth } from 'firebase/auth';

// Vuetify
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
const vuetify = createVuetify({
  components,
  directives,
})



const firebaseConfig = {
  apiKey: "AIzaSyC4e_0LxspH9Eoi92hEbyZCCxxxxkuTw",
  authDomain: "tour-386117.firebaseapp.com",
  projectId: "tour-386117",
  storageBucket: "tour-386117.appspot.com",
  messagingSenderId: "638542335894",
  appId: "1:638542335894:web:b10c3f97432352bc5e17b7",
  measurementId: "G-71QMF1RPK1"
};

initializeApp(firebaseConfig);
// const initFirebase = initializeApp(firebaseConfig);

const app = createApp(App);

app.use(router);
app.use(vuetify);
app.mount('#app');


このコードは、Vue.jsを使用して作成されたアプリケーションのエントリーポイントです。以下では、コードの主な機能と各セクションについて説明します。

  1. モジュールのインポート:
    • createApp:Vueアプリケーションを作成するための関数です。
    • App:アプリケーションのルートコンポーネントです。
    • router:Vue Routerの設定が含まれたルーターオブジェクトです。
    • initializeApp:Firebaseアプリケーションを初期化するための関数です。
    • createVuetify:Vuetifyのインスタンスを作成するための関数です。
    • components:Vuetifyのコンポーネントの一覧です。
    • directives:Vuetifyのディレクティブの一覧です。
  2. Firebaseの設定:
    • firebaseConfig:Firebaseプロジェクトの構成情報が含まれています。APIキー、認証ドメイン、プロジェクトIDなどが指定されています。
    • initializeApp(firebaseConfig):Firebaseアプリケーションを初期化します。Firebaseプロジェクトとの接続を確立します。
  3. Vueアプリケーションの作成とマウント:
    • app:Vueアプリケーションのインスタンスを作成します。
    • app.use(router):Vue Routerをアプリケーションに登録します。
    • app.use(vuetify):Vuetifyをアプリケーションに登録します。
    • app.mount('#app'):アプリケーションを指定した要素(#app)にマウントします。

※APIキーは環境変数からインポートするようにして他人とシェアはしないように!

では、vue-routerをインストールして、index.jsをこのように書きます。

import { createRouter, createWebHistory } from 'vue-router';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('../views/Home.vue') },
    {
      path: '/profile',
      component: () => import('../views/Profile.vue'),
      meta: { requiresAuth: true },
    },
  ],
});

const getCurrentUser = () => {
  return new Promise((resolve, reject) => {
    const removeListener = onAuthStateChanged(
      getAuth(),
      // Callback function
      (user) => {
        removeListener();
        resolve(user);
      },
      reject
    );
  });
};

router.beforeEach(async (to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth)) {
    if (await getCurrentUser()) {
      next();
    } else {
      next('/');
    }
  } else {
    next();
  }
});

export default router;

このコードは、Vue Routerを使用してルーティングを設定し、Firebase Authenticationを使用して認証状態を監視するためのルーターファイルです。以下では、コードの主な機能と各セクションについて説明します。

  1. モジュールのインポート:
    • createRouter:Vue Routerのインスタンスを作成するための関数です。
    • createWebHistory:ブラウザの履歴モードを使用してルーター履歴を作成するための関数です。
    • getAuth:Firebase Authenticationの認証オブジェクトを取得するための関数です。
    • onAuthStateChanged:Firebase Authenticationの認証状態の変更を監視するための関数です。
  2. ルーターの設定:
    • createRouter関数を使用してルーターのインスタンスを作成します。
    • createWebHistory関数を使用して、ブラウザの履歴モードを設定します。
    • routesオプションには、ルートの定義が含まれています。pathプロパティにはパス、componentプロパティにはコンポーネントが指定されています。
    • metaオプションは、ルートに関連するメタデータを指定します。ここでは、requiresAuthメタフィールドがtrueの場合、認証が必要なルートであることを示しています。
  3. getCurrentUser関数:
    • getCurrentUser関数は、認証状態が変更されるたびに呼び出され、現在のユーザーを解決するPromiseを返します。onAuthStateChanged関数を使用して、認証状態の変更を監視し、変更があった場合には解決されるPromiseを返します。
  4. ルーターナビゲーションガード:
    • router.beforeEachメソッドは、ルートナビゲーションが行われる前に実行されるコールバック関数を定義します。
    • ルートがrequiresAuthメタフィールドを持っている場合、現在のユーザーを取得し、ユーザーが存在する場合は次の処理に進みます。存在しない場合は、ルート'/'にリダイレクトします。
  5. ルーターのエクスポート:
    • export default routerステートメントにより、作成したルーターのインスタンスが他のファイルで使用できるようになります。

このコードでは、Vue Routerを使用してアプリケーションのルーティングを設定し、Firebase Authenticationを使用して認証状態を監視し、必要な場合には適切なページにリダイレクトするルーターナビゲーションガードを実装しています。

ではnpm run devのコマンドでログインとログアウトができることと、ユーザー名の取得ができることを確認しましょう!

ここまでできたら、次にFirestoreのデータベースを作成して、デフォルトで表示させるデータとユーザーのプロフィールページを作成してみます。状態管理はPiniaで行うとコードもスッキリするのでまずはPiniaをインストールしましょう。

詳しいPiniaの使い方はこちらの記事を参照して下さい。

ではpinia.jsなど適当なファイルを作成してPiniaを定義していきます。

import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useDataStore = defineStore('data', () => {
  const userData = ref(null);

  return {
    userData,
  };
});

App.vueではこのようにインポートしてPiniaにユーザーのデータを保管します。

下記の様にdataStoreからPiniaにユーザーのデータを保管させます。

※必要に応じてこの記事を参照してください。

import { useDataStore } from "@/stores/pinia";

const dataStore = useDataStore();

onMounted(() => {
  auth = getAuth();
  onAuthStateChanged(auth, (user) => {
    if (user) {
      isLoggedIn.value = true;
      displayName.value = user.displayName;
      dataStore.userData = user;
      // console.log(user.uid);
    } else {
      isLoggedIn.value = false;
      displayName.value = 'ゲスト';
      dataStore.userData = null
    }
  });
});

次にこちらのGitHunのリポを参考にFirestoreのデータベースに接続して初期のデータをロードできるようにしましょう。

例としてpost(記事)コレクションを作成してGoogleのユーザーに付属するuidの情報を割り当てます。これでどのユーザーが記事を作成したか、またuidが一致するユーザーのみ記事の編集や削除が行えるようにします。

まずはfirestoreをmain.jsでインポートしてイニシャライズします。必要な部分だけ追加してください。

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
  apiKey: 'AIzaSyCJCPiLJI_cxxxxLt-kkm8Pe_9bVG8',
  authDomain: 'js-hiroba.firebaseapp.com',
  projectId: 'js-hiroba',
  storageBucket: 'js-hiroba.appspot.com',
  messagingSenderId: '221494043726',
  appId: '1:221494043726:web:ea25e09f171d3769984f1f',
  measurementId: 'G-XTRC17KCB5',
};
const fireApp = initializeApp(firebaseConfig);
const db = getFirestore(fireApp);
export { db };

次にApp.vueに下記の様なコードを追加します。

import { collection, getDocs } from 'firebase/firestore';


const posts = ref();
onMounted(
  
  // Load Initial Data
  (async () => {
    const querySnapshot = await getDocs(collection(db, 'post'));
    let fbPosts = [];
    querySnapshot.forEach((doc) => {
      const post= {
        id: doc.id,
        title: doc.data().title,
        content: doc.data().content,
      };
      console.log(`${doc.id} => ${doc.data()}`);
      fbPosts.push(post);
      console.log(fbPosts);
    });

    posts.value = fbPosts;
  })()

});

postsはVue.jsのリアクティブなデータ(ref)です。refはVue.jsでデータを監視し、変更を追跡するために使用されます。

onMountedはVue.jsのライフサイクルフックの1つであり、コンポーネントがマウントされた後に実行される関数です。つまり、コンポーネントが表示された後に実行される処理を指定するために使用されます。

このコードでは、コンポーネントがマウントされた後に非同期関数が実行されます。非同期関数は即時関数(() => {})として定義されています。

即時関数内部では、FirestoreのgetDocsメソッドを使用して、’post’というコレクション内のドキュメントを取得します。取得したドキュメントはquerySnapshotというオブジェクトに格納されます。

querySnapshotオブジェクトは、取得したドキュメントのスナップショットであり、forEachメソッドを使用して各ドキュメントに対してループ処理を行います。

ループ内部では、各ドキュメントのデータを取得し、postオブジェクトとして作成します。postオブジェクトには、ドキュメントのID、タイトル、および完了フラグの情報が含まれます。

また、console.logを使用してドキュメントのIDとデータをコンソールに表示し、fbPosts配列にtodoオブジェクトを追加します。その後、fbPosts配列もコンソールに表示されます。

最後に、posts.valuefbPosts配列を代入します。これにより、Vue.jsのリアクティブなデータであるpostsが更新され、コンポーネント内でこれらのデータを使用できるようになります。

では、実際にFirestoreにデータを作成してみましょう。

ではHome.vueに下記のようなコードを書きPiniaに保管したFirestoreのデータベースを読み込ませます。

<template>
  <div v-for="post in dataStore.posts" :key="post.id" class="ma-2">
    <v-card
      width="400"
      :title="post.title"
      :subtitle="post.userName"
      :text="post.content"
    ></v-card>
  </div>


</template>

<script setup>
import { useDataStore } from "@/stores/pinia";

const dataStore = useDataStore();

</script>

テンプレート部分では、v-forディレクティブを使用して、dataStore.posts内の各要素に対して反復処理を行います。dataStore.postsは、データストアから取得した投稿データの配列を指します。

v-forディレクティブ内では、postという名前の変数を定義し、dataStore.posts内の各要素を参照します。:keyディレクティブは、各要素の一意な識別子であるpost.idを指定します。これにより、Vue.jsが要素を効率的に追跡し、リレンダーの最適化を行うことができます。

v-cardコンポーネントは、Vue Materialのカードコンポーネントであり、データストア内の各投稿に対して表示されます。:title:subtitle:textなどのプロパティには、投稿のタイトル、ユーザー名、コンテンツなどのデータがバインドされます。

これでランディングページにForestoreのデータが表示されるようになりました。

プロフィールページで記事の投稿

ではProfile.vueに行きログインしているユーザーが記事を投稿する機能を作成します。

Profile.vue

<template>
  <v-sheet
    width="300"
    class="mx-auto"
  >
    <v-form @submit.prevent>
      <v-text-field
        v-model="postForm.title"
        label="タイトル"
      ></v-text-field>
      <v-text-field
        v-model="postForm.content"
        label="コンテンツ"
      ></v-text-field>
      <v-btn
        type="submit"
        block
        class="mt-2"
        @click="addPost"
        >Submit</v-btn
      >
    </v-form>
  </v-sheet>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { useDataStore } from '@/stores/pinia';
import { collection, addDoc } from 'firebase/firestore';
import { db } from '@/main.js';

const dataStore = useDataStore();

const postInitial = {
  title: '',
  content: '',
};

const postForm = reactive({
  ...postInitial,
});

// フォームをリセットする関数
function resetForm() {
  Object.assign(postForm, postInitial);
}

function addPost() {
  addDoc(collection(db, 'post'), {
    title: postForm.title,
    content: postForm.content,
    uid: dataStore.userData.uid,
    userName: dataStore.userData.displayName,
  });
  resetForm()
}
</script>

テンプレート部分では、v-sheetコンポーネントを使用して、フォームの外側を囲むシートを作成しています。

フォーム部分では、v-formコンポーネントを使用してフォームを作成しています。

v-text-fieldコンポーネントを使用して、タイトルとコンテンツの入力フィールドを作成しています。v-modelディレクティブを使用して、入力された値をpostFormオブジェクトの該当するプロパティにバインドしています。

v-btnコンポーネントは、送信ボタンを表示するために使用されています。type="submit"によって、ボタンがフォームの送信トリガーとして機能するようになります。@clickイベントリスナーは、ボタンがクリックされたときにaddPost関数を呼び出します。

useDataStore関数を使用して、データストアフックをインポートし、dataStoreという変数に割り当てています。これにより、データストアのデータにアクセスできます。

refreactiveを使用して、Vue.jsのリアクティブなデータを作成しています。postFormreactiveを使用してオブジェクトをリアクティブにするため、postInitialオブジェクトを展開して初期値として設定しています。

resetForm関数は、フォームをリセットするための関数です。postFormオブジェクトにpostInitialオブジェクトのプロパティを割り当てることで、フォームの値を初期状態に戻します。

addPost関数は、投稿を追加するための関数です。addDoc関数を使用してFirestoreのpostコレクションに新しいドキュメントを追加します。postFormオブジェクトの値を使用して、投稿のタイトル、コンテンツ、ユーザーID、ユーザー名などのプロパティを設定します。addDocの実行後には、resetForm関数が呼び出され、フォームがリセットされます。

以上がこのコードの概要です。ユーザーはフォームを使用してタイトルとコンテンツを入力し、送信ボタンをクリックすることで投稿を追加できます。投稿はFirebaseのFirestoreのpostコレクションに保存されます。

このように記事が投稿され、さらにFirebaseのリアルタイムDBでブラウザをリロードすることなくブラウザのデータを更新することができました。

同じ要領で、DBの削除と編集も行う事ができます。

それぞれのドキュメント(データ)はGoogleユーザーのuidなどで紐づけしておきましょう!

全体的にコードが煩雑になってしまいましたが、firebaseのコードは別途pluginsなどのディレクトリを作成して別で読みこみさせるようにしましょう。

今日はこれくらいで。

お疲れ様です。