くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt3でFirebase Auth v9を使ってみる

Nuxt3&Firebase Auth v9を試してみたときの備忘録。
Nuxt2&Firebase Auth v8とは結構違うので、びっくりする(*´ω`*)

まずはインストール

$ npm i firebase

プラグインでFirebaseの初期化

Firebaseの初期化はプラグインでおこなう。
.clientサフィックスで、クライアントのみになるので便利

// ~/plugins/firebase.client.ts
import { initializeApp } from 'firebase/app'
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  const firebaseConfig = {
    apiKey: config.FB_API_KEY,
    authDomain: config.FB_AUTH_DOMAIN,
    projectId: config.FB_PROJECT_ID,
    storageBucket: config.FB_STORAGE_BUCKET,
    messagingSenderId: config.FB_MESSAGING_SENDER_ID,
    appId: config.FB_APP_ID,
    measurementId: config.FB_MEASUREMENT_ID,
  }
  initializeApp(firebaseConfig)
})

環境変数を利用するので、.envも用意する。

# .env
# Firebase Config
FB_API_KEY=XXXXXXXXXXXXXX
FB_AUTH_DOMAIN=xxxx.firebaseapp.com
FB_PROJECT_ID=xxxx
FB_STORAGE_BUCKET=xxxx.appspot.com
FB_MESSAGING_SENDER_ID=123456789012
FB_APP_ID=XXXXXXXXXXXXXX
FB_MEASUREMENT_ID=G-XXXXXXXXXX

useRuntimeConfig()で取得できるように、nuxt.config.tsにも追加。
クライアント側で取得できるようruntimeConfig.public配下に設定。

// ~/nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      FB_API_KEY: process.env.FB_API_KEY || "",
      FB_AUTH_DOMAIN: process.env.FB_AUTH_DOMAIN || "",
      FB_PROJECT_ID: process.env.FB_PROJECT_ID || "",
      FB_STORAGE_BUCKET: process.env.FB_STORAGE_BUCKET || "",
      FB_MESSAGING_SENDER_ID: process.env.FB_MESSAGING_SENDER_ID || "",
      FB_APP_ID: process.env.FB_APP_ID || "",
      FB_MEASUREMENT_ID: process.env.FB_MEASUREMENT_ID || "",
    },
  },
})

認証関係のComposable

vueuseの処理を参考にuseAuth.tsを用意する。Google認証の例。 - @vueuse/firebase | VueUse

// ~/composables/useAuth.ts
import type { Auth, User } from "firebase/auth";
import {
  getAuth,
  onAuthStateChanged,
  signInWithPopup,
  signOut,
  GoogleAuthProvider,
} from "firebase/auth";
import { computed, ref } from "vue";

export function useAuth(auth: Auth = getAuth()) {
  // ********************************************************
  // * data
  // ********************************************************
  const user = ref<User | null>(auth.currentUser);
  const isAuthed = computed(() => !!user.value);

  // idTokenが変化したら更新する
  auth.onIdTokenChanged((authUser) => (user.value = authUser));

  // ********************************************************
  // * methods
  // ********************************************************
  // 認証状態チェック
  async function checkAuthState() {
    try {
      const _user = await _checkAuthState(auth);
      user.value = _user;
    } catch (error) {
      user.value = null;
    }
  }
  
  // ログイン
  async function login() {
    try {
      const provider = new GoogleAuthProvider();
      await signInWithPopup(auth, provider);
    } catch (error) {
      throw error;
    }
  }

  // ログアウト
  async function logout() {
    try {
      await signOut(auth);
      user.value = null;
    } catch (error) {
      throw error;
    }
  }

  return { isAuthed, user, checkAuthState, login, logout };
}


// ********************************************************
// * utils
// ********************************************************
// onAuthStateChangedのPromise版Util
async function _checkAuthState(auth: Auth) {
  return new Promise<User | null>((resolve, reject) => {
    // client only
    if (process.server) return resolve(null);
    onAuthStateChanged(
      auth,
      (user) => resolve(user || null),
      (error) => reject(error)
    );
  });
}

認証チェック

認証必須のページを設定できるように、middlewareを用意。
isAuthedもuserもRefなので注意(.valueが必要)。

// ~/middleware/auth.ts
export default defineNuxtRouteMiddleware(async () => {
  if (!process.server) {
    const { isAuthed, checkAuthState } = useAuth();
    await checkAuthState();
    if (!isAuthed.value) {
      return await navigateTo("/login", { replace: true });
    }
  }
});

pages配下では、<script setup>内のdefinePageMeta()で、
利用するmiddlewareを指定。

<script setup>
definePageMeta({
  middleware: ['auth']
})
</script>

ログインページ用に、ログイン済みならトップページに移動する処理があってもよさそう。

// ~/middleware/auth-login.ts
export default defineNuxtRouteMiddleware(async () => {
  if (!process.server) {
    const { isAuthed, checkAuthState } = useAuth();
    await checkAuthState();
    if (isAuthed.value) {
      return await navigateTo("/", { replace: true });
    }
  }
});

ログイン後に元のページに戻る

未認証時に~/middleware/auth.tsで遷移したあと、
元のページに戻るのは、useRoute().redirectedFrom()を使うとよいっぽい

// ログイン後に元のページへ戻る
const to = useRoute().redirectedFrom?.fullPath || '/'
navigateTo(to, { redirectCode: 302 })

おまけ: エミュレータを利用する

エミュレータを利用する場合は、connectAuthEmulator()などを使う。
環境変数(USE_EMULATOR)を用意しておいて、切り替えれるようにするのも便利

// ~/plugins/firebase.client.ts
import { initializeApp } from "firebase/app";
import { connectAuthEmulator, getAuth } from "firebase/auth";
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";
import { connectFunctionsEmulator, getFunctions } from "firebase/functions";

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  const firebaseConfig = {
    // ...
  }
  const app = initializeApp(firebaseConfig);
  
  // setup emulators
  if (config.USE_EMULATOR === "true") {
    connectAuthEmulator(getAuth(app), "http://localhost:9099");
    connectFirestoreEmulator(getFirestore(app), "localhost", 8080);

    const functions = getFunctions(app, "asia-northeast1");
    connectFunctionsEmulator(functions, "localhost", 5001);
  }
})

以上!! 変わってるところも多いけど、なれるとだいぶ楽っぽい(*´ω`*)

参考にしたサイト様