本文書ではSupabaseとVue3のComposition APIと状態管理ライブラリのPiniaを利用した場合のメールアドレスによる認証方法について説明を行っています。メールアドレスによる認証だけではなくGoogle, GitHubなどのプロバイダーによるソーシャルログインによる認証機能も簡単に利用することができます。

SupabaseはオープンソースでFirebase Alternativeと言われているクラウドベースのサービスでデータベースにはリレーショナルデータベースのPostgresを利用しています。データベースのみを提供するサービスではなく認証機能やストレージ機能も備えています。サービスを開始してまだ時間が経過していないので今後も機能の追加が行われていきます。

Supabaseのサインアップ

Supabaseへのサインアップの方法やテーブルに挿入したデータの取得方法などについては先日公開した下記の記述で説明しています。

Vueプロジェクトの作成

npm init vueコマンドを実行してVue3のプロジェクトの作成を行います。サインアップ、サインインなど複数のページにまたがるアプリケーションなのでVue Routerの機能を選択します。また冒頭で説明したい通り状態管理にPiniaを利用するのでPiniaを選択しています。プロジェクト名はvue3_supabase_piniaとしていますが任意の名前をつけてください。npm init vueコマンドで作成したVueプロジェクトはViteを利用しています。


 % npm init vue@latest         

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue3_supabase_pinia
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/mac/Desktop/vue3_supabase_pinia...

Done. Now run:

  cd vue3_supabase_pinia
  npm install
  npm run dev

Vue.js用の状態管理ライブラリであるPiniaについては下記の記事で公開しています。

Supabaseへの接続設定

作成されるプロジェクトフォルダvue3_supabase_piniaに移動してnpm installコマンドを実行します。


 % cd vue3_supabase_pinia
 % npm install

Supabaseを利用するためにsupabase-jsライブラリのインストールを行います。


 % npm install @supabase/supabase-js

インストールが完了したら.envファイルをプロジェクトフォルダの直下に作成して環境変数の設定を行います。

環境変数にはURLとキーの設定を行います。APIのキーについては左側のサイドメニューにあるSettingからAPIをクリックすることで確認することができます。

APIキーの確認
APIキーの確認

anonにあるキーをコピーして.envファイルのVITE_SUPABASE_ANON_KEYに設定しURLをVITE_SUPABASE_URLに設定します。


VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

srcフォルダ直下にsupabase.jsファイルを作成して以下の初期設定を記述します。.envファイルに記述した環境変数はViteではimport.meta.env.環境変数名でアクセスすることができます。


import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

認証設定

Supabaseはデフォルトの設定のままでメールアドレスとパスワードによる認証機能 (Email Auth)を利用することができます。Email Auth以外にもMagic Links、Social providers(Apple, Facebook, Googleなど)やPhone loginsの機能を使った認証も利用することができます。

認証の各種設定はSupabaseの管理画面から行うことができます本文書ではメールアドレスとパスワードによるEmail Authを利用するので下記の設定(デフォルト設定)で行います。Enable email confirmationを有効にしている場合はサインアップした後に入力したメールアドレス向けにConfirmationメールが送信されます。Confirmationメールに含まれるリンクをクリックすることでサインアップが完了してサインインができるようになります。

認証の設定
認証の設定

認証に利用するメソッド

インストールしたsupabase.jsに含まれるメソッドを利用します。本文書の認証設定の中で利用するメソッドは以下の通りです。

  • signUp・・・新規ユーザを作成
  • signIn・・・登録済みのユーザでログイン
  • signOut・・・ユーザのログアウト、ブラウザのセッションからログインしたユーザ情報を削除
  • user・・・ログインしていればユーザ情報を取得
  • onAuthStateChange・・・認証イベントが発生した場合に通知を受け取る。ログインとログアウトに利用。

本文書でも上記のメソッドの説明を行っていますが各メソッドの詳細を確認したい場合はドキュメントで確認することができます。

メソッドをどのように利用するか先に簡単な例を使って説明を行っておきます。signUpであればsignUpメソッドの引数のオブジェクトにemailとpasswordを指定することで Supabaseにユーザを作成することができます。


  const { user, session, error } = await supabase.auth.signUp({
    email: 'john@test.com,
    password: 'password',
  });

利用するメソッドの中で最も重要なOnAuthStateChangeメソッドの中の処理はサインイン、サインアウトなどイベントが発生すると実行されます。サインインを行うとeventにSIGNED_IN、サインアウトを行うとeventにSIGNED_OUTが入るためそれぞれのイベントに応じた処理を実行することが可能になります。


supabase.auth.onAuthStateChange((event, session) => {
  if (event == 'SIGNED_IN') サインイン後の処理
  if (event == 'SIGNED_OUT') サインアウト後の処理
});

Piniaの設定

アプリケーション全体でログインしたユーザ情報を共有するためにVue.js向けの状態管理ライブラリPiniaを利用します。Piniaで設定するStoreにユーザ情報が保存されているかどうかでユーザがログインしているかどうかを判断します。

Pinia機能はプロジェクト作成時にインストールされているのでstoresフォルダにはデフォルトではcounter.jsファイルが保存されてます。counter.jsファイルを削除してauth.jsファイルを作成して以下のStoreの定義を記述します。共有する情報としてstateにuserを定義してデフォルト値はnullに設定しています。gettersはComputedプロパティのように利用することができisLoggedInを定義しています。このisLoggeInによって戻される値によってユーザがログインしているかどうかを判断します。actionsではstateで定義したuserの値を操作することができ、setUserメソッドでは引数から渡されるuser情報をuserに設定します。clearUserメソッドではuserの値をnullにします。サインインした場合はsetUser, サインアウトした場合にはclearUserを利用します。


import { defineStore } from 'pinia';
export const useAuthStore = defineStore({
  id: 'auth',
  state: () => ({
    user: null,
  }),
  getters: {
    isLoggedIn: (state) => state.user,
  },
  actions: {
    setUser(user) {
      this.user = user;
    },
    clearUser() {
      this.user = null;
    },
  },
});

App.vueファイルの設定

srcフォルダの直下にあるApp.vueファイルはすべてのコンポーネントファイルの元になる親コンポーネントです。scriptタグの中ではonAuthStateChangeメソッドを使ってサインインとサインアウトのイベントを監視します。SIGNED_INイベントの場合はPiniaで設定したsetUserメソッドの引数にsessionに含まれるuserを設定します。SIGNED_OUTイベントの場合はログアウトが行われるのでclearUserメソッドによりuserの値をnullに設定しています。


<script setup>
import { supabase } from './supabase';
import { useAuthStore } from './stores/auth';
const auth = useAuthStore();

auth.setUser(supabase.auth.user());

supabase.auth.onAuthStateChange((event, session) => {
  if (event == 'SIGNED_IN') auth.setUser(session.user);
  if (event == 'SIGNED_OUT') auth.clearUser();
});
</script>

App.vueファイルのtemplateタグではVue Routerを利用しているのでこの後ルーティングを設定するページコンポーネントの中身が表示できるようにrouter-viewタグを設定しています。


<template>
  <router-view></router-view>
</template>

ここまでの設定でnpm run devコマンドを実行してlocalhost:3000にアクセスすると以下の画面が表示されます。

現在の設定でのlocalhost:3000の画面
現在の設定でのlocalhost:3000の画面

表示されているのはviewsフォルダにあるHomeView.vueファイルの内容です。

ルーティングの設定

デフォルトから存在するHomeView.vue以外に4つのページを作成します。

  • SignIn.vue・・・サインインを行うためのページ
  • SignUp.vue・・・サインアップを行うためのページ
  • DashBoard.vue・・・サインインを行った後に表示するページ。サインインしたユーザのみ閲覧ができる
  • EmailConfirmation.vue・・・サインアップ後に表示されるページ。設定で”Enable email confirmation”を有効にしている場合はConfirmationメールに含まれるリンクをクリックするまでサインインすることができないのでこのページを表示させます。

viewフォルダに上記の4つのファイルを追加します。デフォルトではAboutView.vueファイルが存在しますが今回は利用しないので削除します。個々のファイルの中身については後ほど設定していくのでtemplateタグの中にはh1タグでファイルの名前のみ記述しておきます。下記はDashBoard.vueファイルの例です。他の3つのファイルについても同様に記述してください。


<template>
  <h1>DashBoard</h1>
</template>

ファイルの作成が完了したらrouterフォルダにあるindex.jsファイルに追加した4つのファイルのルーティングを追加します。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/signup',
      name: 'signup',
      component: () => import('../views/SignUp.vue'),
    },
    {
      path: '/signin',
      name: 'signin',
      component: () => import('../views/SignIn.vue'),
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      component: () => import('../views/DashBoard.vue'),
    },
    {
      path: '/email_confirmation',
      name: 'emailConfirmation',
      component: () => import('../views/EmailConfirmation.vue'),
    },
  ],
});

export default router;

ルーティングを設定後に/dashboardにアクセスするとブラウザ上にDashBoardの文字列を確認することができます。

DashBoardページの表示
DashBoardページの表示

/signupにアクセスするとSignUp, /signinにアクセスするとSignInが表示されます。

HomeView.vueファイルの設定

Piniaで定義したgettersのisLoggedInを利用することで/(ルート)にアクセスした時に表示するリンクの表示・非表示を制御することができます。


<script setup>
import { useAuthStore } from '../stores/auth';
const auth = useAuthStore();
</script>

<template>
  <ul>
    <li v-if="auth.isLoggedIn">
      <router-link to="/dashboard">Dashboard</router-link>
    </li>
    <li v-if="!auth.isLoggedIn">
      <router-link to="/signup">SignUp</router-link>
    </li>
    <li v-if="!auth.isLoggedIn">
      <router-link to="/signin">SignIn</router-link>
    </li>
  </ul>
  <h1>Supabase + Vue3 + Pinia</h1>
</template>

デフォルトではPiniaで定義したuserはnullなのでisLoggedInの値はnullが戻されます。v-ifディレクティブで表示・非表示を制御しているのでSignUpとSignInのリンクのみ表示されます。DashBoardは分岐により非表示になるため表示されません。

Piniaのgettersを利用して表示・非表示を制御
Piniaのgettersを利用して表示・非表示を制御

SignUp、SignInのリンクをクリックするとページを移動することができます。

SignUpの設定

Supabaseへのサインアップ(ユーザ作成)を行うためにSignUp.vueファイルの設定を行います。メールアドレスとパスワードが入力できるようにフォームを追加します。入力した値を保持するためにref関数を利用しています。入力した値のバリデーションなどは行っていません。SignUpボタンをクリックするとhandleSubmit関数が実行されます。


<script setup>
import { ref } from 'vue';

const email = ref('');
const password = ref('');
const handleSubmit = async () => {
  console.log('Email:', email.value);
  console.log('Password:', password.value);
};
</script>

<template>
  <div>
    <h1>SignUp</h1>
    <form @submit.prevent="handleSubmit">
      <div>
        <label for="email">Email: </label>
        <input type="email" placeholder="email" v-model="email" />
      </div>
      <div>
        <label for="password">Password: </label>
        <input type="password" placeholder="password" v-model="password" />
      </div>
      <div>
        <button>SignUp</button>
      </div>
    </form>
  </div>
</template>

ブラウザで確認するとサインアップフォームを確認することができます。EmailとPasswordに入力してSignUpボタンをクリックするとブラウザのデベロッパーツールのコンソールに入力したメールアドレスとパスワードが表示されます。

サインアップフォームの表示
サインアップフォームの表示

handleSubmit関数の中で入力したメールアドレスとパスワードの取得ができるようになったのでsupabase.authのsignUpメソッドを利用してサインアップ処理を追加します。

try,catch構文を利用してエラーが発生した場合にalert関数でエラーメッセージを表示させるように設定を行っています。サインアップの処理が成功した場合にルーティングに設定したemailConfirmationにリダイレクトするように設定を行っています。その際に入力したメールアドレスを渡しています。


import { ref } from 'vue';
import { supabase } from '../supabase';
import { useRouter } from 'vue-router';

const router = useRouter();

const email = ref('');
const password = ref('');
const handleSubmit = async () => {
  try {
    const { error, user } = await supabase.auth.signUp({
      email: email.value,
      password: password.value,
    });
    if (error) throw error;
    if (user)
      router.push({
        name: 'emailConfirmation',
        query: { email: email.value },
      });
  } catch (error) {
    alert(error.message);
  }
};

Supabaseの設定で”Enable email confirmation”を有効にしている場合はConfirmationメールに含まれるリンクをクリックするまでサインインすることができません。emailConfirmationページを作成してメールの確認を促しています。

メールを受信することが可能なメールアドレスを入力してsignUpメソッドが動作するか確認します。適当なメールアドレスでは動作確認を行うことができません。

メールアドレスとパスワードを入力してSignUpボタンをクリックするとEmail Confirmationも文字列が表示されたEmailConfirmationページにリダイレクトされます。リダイレクトされた際のURLを確認すると入力したメールアドレスがURLに含まれていることが確認できます。


http://localhost:3000/email_confirmation?email=yourmail@gmail.com

Supabaseの管理画面のAuthentificationのUsersを確認するとUsersに入力したメールアドレスが追加されていることが確認できます。

ユーザの登録確認
ユーザの登録確認

Last Sign In列には”Waiting for verification”と表示されています。メールの確認ができていないためにこのメッセージが表示されています。

メールのInboxを確認するとSupabaseからメールが届いていることが確認できます。

Supabaseからのメール確認
Supabaseからのメール確認

“confirm your mail”のリンクをクリックするとhttp://localhost:3000/にリダイレクトされます。リダイレクトされた後は上部に表示されているリンクがDashboardになっていることがわかります。Piniaで設定したauth.isLoggedInにユーザ情報が含まれていることによりHomeView.vueファイル内で行っていたv-ifディレクティブによる分岐で非表示から表示に切り替わったためです。auth.isLoggedInにユーザ情報が含まれるのはApp.vueファイルのsupabase.auth.onAuthStateChangeがサインインによって実行されたためです。

メールのリンクをクリックしてリダイレクト
メールのリンクをクリックしてリダイレクト

ChromeブラウザのExtentionsにDevtoolsをインストールしている場合はPiniaに保存されている値を確認することができます。

DevtoolsによるPiniaの状態確認
DevtoolsによるPiniaの状態確認

ローカルストレージを見るとsupabase.auth.tokenをキーに持つ値が保存されていることも確認できます。

ローカルストレージの確認
ローカルストレージの確認

Supabaseの管理画面のAuthentificationを再度確認すると”Last Sign In”の列が時刻に変わっていることがわかります。

Last Sign In列の情報の確認
Last Sign In列の情報の確認

ページのリロードをしてもApp.vueファイルでsupabase.auth.userメソッドでユーザ情報を取得しているのでDashboardのリンクが表示されたままの状態になります。リロードしてもサインインした情報が残ったままの状態になります。


auth.setUser(supabase.auth.user());

SignOutの設定

DashBoard.vueファイルでsupabase.auth.signOutメソッドによりユーザのサインアウトが行えるようにSignOutボタンを追加します。サインアウトが正常に完了すると/(ルート:HomeView)にリダイレクトされます。


<script setup>
import { supabase } from '../supabase';
import { useRouter } from 'vue-router';
const router = useRouter();

const handleSignOut = async () => {
  try {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
    router.push({
      name: 'home',
    });
  } catch (error) {
    alert(error.message);
  }
};
</script>

<template>
  <div>
    <div>
      <button @click="handleSignOut">SignOut</button>
    </div>
    <h1>DashBoard</h1>
    <p>Welcome to our service</p>
  </div>
</template>
ダッシュボードページの更新
ダッシュボードページの更新

SignOutボタンをクリックすると/(ルート)にリダイレクトされます。サインインしている場合はDashBoardへのリンクが表示されていましたがサインアウトしたのでSignUpとSignInのリンクに変わっています。

Piniaのgettersを利用して表示・非表示を制御
Piniaのgettersを利用して表示・非表示を制御

ローカルストレージを確認するとsupabase.authに関するキーと値が削除されていることが確認できます。

サインアウト後のローカルストレージの確認
サインアウト後のローカルストレージの確認

DevtoolsでPiniaに保存されている状態も確認しておきましょう。stateのuserがnullなのでgettersのisLoggedInの値もnullになっていることが確認できます。

Devtoolsによるサインアウト後のPiniaの値の確認
Devtoolsによるサインアウト後のPiniaの値の確認

SignInの設定

SignUp、SignOutの設定とどのような動作になるのか確認できたのでSignInの設定を行います。SignInの処理はSignUpの処理とほとんど同じです。違いは利用するメソッドがsupabase.auth.signUpからsupabase.auth.signInになった点です。


<script setup>
import { ref } from 'vue';
import { supabase } from '../supabase';
import { useRouter } from 'vue-router';

const router = useRouter();

const email = ref('');
const password = ref('');
const handleSubmit = async () => {
  try {
    const { error, user } = await supabase.auth.signIn({
      email: email.value,
      password: password.value,
    });
    if (error) throw error;
    if (user)
      router.push({
        name: 'dashboard',
      });
  } catch (error) {
    alert(error.message);
  }
};
</script>

<template>
  <div>
    <h1>SignIn</h1>
    <form @submit.prevent="handleSubmit">
      <div>
        <label for="email">Email: </label>
        <input type="email" placeholder="email" v-model="email" />
      </div>
      <div>
        <label for="password">Password: </label>
        <input type="password" placeholder="password" v-model="password" />
      </div>
      <div>
        <button>Sign In</button>
      </div>
    </form>
  </div>
</template>

設定後/(ルート)の画面からSignInのリンクをクリックするとSignIn画面が表示されます。

SignIn画面の確認
SignIn画面の確認

サインアップしたメールアドレスまたはパスワードが異なる場合はalert関数によりメッセージが表示されます。

実際に異なるメールアドレスでサインインを行おうとすると設定通りエラーメッセージが表示されます。

サインインのエラーメッセージの表示
サインインのエラーメッセージの表示

正しいメールアドレスとパスワードを入力すると/dashboardにリダイレクトされます。

ダッシュボードページの更新
サインイン後のダッシュボードページへのリダイレクト

サインインの機能を実装することができました。

アクセス制限

サインイン後にSignOutボタンをクリックしてURLにlocalhost:3000/dashboardを直接入力するとダッシュボード画面が表示されます。サインインを行っていないユーザではアクセスできないようにルーティングに設定を追加します。

ルーティングのbeforeEachメソッドを利用してユーザがサインインしているかどうかチェックを行っています。サインインしていない場合はSignInページにリダイレクトされます。サインインしているかチェックを行いたいルーティングにはmetaオプションを利用してrequiresAuthプロパティを追加し値をtrueに設定しています。


import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import { useAuthStore } from '../stores/auth';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/signup',
      name: 'signup',
      component: () => import('../views/SignUp.vue'),
    },
    {
      path: '/signin',
      name: 'signin',
      component: () => import('../views/SignIn.vue'),
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      component: () => import('../views/DashBoard.vue'),
      meta: {
        requiresAuth: true,
      },
    },
    {
      path: '/email_confirmation',
      name: 'emailConfirmation',
      component: () => import('../views/EmailConfirmation.vue'),
    },
  ],
});

router.beforeEach((to) => {
  const auth = useAuthStore();
  if (!auth.isLoggedIn && to.meta.requiresAuth) {
    return { name: 'signin' };
  }
});

export default router;

サインインをしていない場合は/dashboardへアクセスしようとすると/signinにリダイレクトされます。metaオプションにrequiresAuthにtrueを設定しているページにはサインインを行っていないとアクセスできないようになりました。

Vue3 + Piniaの環境下でSupabaseを利用して認証機能を実装することができました。