【Nuxt.js・TailwindCSS】ログイン実装① FirebaseAuthでメールログイン/ログアウト/新規登録

#はじめに
こんにちは。
こちらの記事では、FirebaseAuthでメールログイン/ログアウト/新規登録を行う方法を記しています。

Vuetifyを使ったログイン実装の記事はよく見てきたのですが、使っていない人からすると学習コストがかかってしまい、遠回りになってしまうと感じたので、Vuetifyを使わない人向けに記事にしました。

誤っている点がございましたらコメントいただけると幸いです。

#前提

* Nuxtの新規プロジェクト作成が既に済んでいる
* Firebaseのアカウントを作成して連携まで済んでいる

→ まだFirebaseとの連携が完了していない方は、[こちらの記事](https://nuxt.alizlab.com/%e3%80%90nuxt-js%e3%80%91firebase%e5%b0%8e%e5%85%a5%e7%b7%a8rdb%e7%89%88%ef%bc%9a%e3%83%87%e3%83%bc%e3%82%bf%e3%81%ae%e8%bf%bd%e5%8a%a0-%e5%8f%96%e5%be%97%e3%82%92%e3%81%97%e3%82%88%e3%81%86/)のStep4まで取り組んでみてください。
→ firebase-toolsについては[こちら](https://qiita.com/taichi0514/items/357087af3626138a6b76)。



#対象者

* Nuxt.jsとFirebaseAuthでログイン機能を実装したい方
* Vuetify（UIフレームワーク）を使わない方
* TailwindCSSを使いたい方（他のCSSでも可。この記事のメインはログイン機能実装なので、詳細な説明は省きます。詳しくは[こちら](https://tailwindcss.com/)）

#使用技術

| 使用技術 | バーション |
|:-:|:-:|
|  nuxt |  2.15.7 |
|  nuxtjs / tailwindcss |  4.2.0 |
|  firebase |  9.1.3 |
|  firebase-tools |  9.21.0 |


#実装手順
新規登録機能、ログイン機能、ログアウト機能の順に実装していきます。
ディレクトリ構造は以下のようになります。（ログイン機能に関係しないものは省略しています）

>プロジェクト名/
　┣ pages/
　┃ ┣ index.vue
　┃ ┣ login.vue
　┃ ┣ register.vue
　┃ ┗ home.vue
　┣ plugins/
　┃ ┗ firebase.js
　┣  store/
　┃ ┗ index.js
　┗ nuxt.config.js
>

| ファイル名 | 説明 |
|:-|:-:|
| pages / index.vue  | ウェルカムページ  |
|  pages / login.vue |  ログインページ |
|  pages / register.vue |  新規登録ページ |
| pages / home.vue  |  ログイン後ページ |
| plugins / firebase.js  | firebase導入  |
| store / index.js  |  Vuexストア |
| nuxt.config.js  |  プラグインやモジュールなどの設定 |


##新規登録機能の実装
>firebase公式：https://firebase.google.com/docs/auth/web/password-auth#create_a_password-based_account

###1. Firebase側の設定と連携
実際にコードを書いていく前に、firebase側でメールアドレスを使えるようにしておきます。
【Firebase管理画面＞Authentication＞Sign-in melhod】で、メールアドレスを有効化しておく。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/2eb6120f-7caf-303a-a24f-d6a3ee6089ce.png">

また、FirebaseAuthを使えるように`plugins/firebase.js`に記載しておきます。

```javascript:plugins/firebase.js
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';

const config = {
  //APIキー
}

firebase.initializeApp(config)

const auth = firebase.auth()

export default function (context, inject) {
    inject('auth', auth) //this.$auth で呼び出せる。
}
```
injectを使用することで、`this.$~`のように短い記述でimportせずに他のページでも使用することができます。詳しくは下記の記事に記載しております！

https://qiita.com/YuStarrr/items/e6821add123d3cb0631a

<br>

`plugins/firebase.js`を使用できるように、`nuxt.config.js`にpluginsの記載をしておきます、

```javascript:nuxt.config.js
plugins: [
  '~/plugins/firebase.js'
]
```

###2. Vuexストアの設定

>storeディレクトリ：https://nuxtjs.org/ja/docs/directory-structure/store/

Vuexストアを利用してログイン状態を保持するので、`store/index.js`にも記載しておきます。

```javascript:store/index.js
export const state = () => ({
  user: {
    uid: "",
    email: "",
    login: false
  }
});

export const getters = {
  user: state => {
    return state.user;
  }
};

export const actions = {
  checkLogin({ commit }) {
    this.$auth.onAuthStateChanged(function(user) {
      if (user) {
        commit("getData", { uid: user.uid, email: user.email });
        commit("switchLogin");
      } else {
        commit("deleteLogin");
      }
    });
  }
};

export const mutations = {
  getData(state, payload) {
    state.user.uid = payload.uid;
    state.user.email = payload.email;
  },
  switchLogin(state) {
    state.user.login = true;
  },
  deleteLogin(state) {
    state.user.login = false;
  },
  signOut(state) {
    state.user.uid = "";
    state.user.email = "";
    state.user.login = false;
  }
};
```
stateで、ユーザーのID・メールアドレス・ログイン状態を保管。
gettersで、stateの情報を取得。
actionsで、`checkLogin`が呼ばれた時にログインの切り替えを行う。
mutationsで、ユーザの状態に合わせてstateの情報を書き換える。


###3. 新規登録画面の作成
`pages/register.vue`ファイルを作成し、コードを記述していきます。

```javascript:register.vue
<template>
  <div id="register">
    <div class="mt-12 px-4 py-40 bg-gray-100 md:mt-20">
      <div class="container mx-auto py-8 bg-white md:w-160">
        <h2 class="text-center text-2xl font-bold">新規登録</h2>
        <form @submit.prevent class="px-20 md:px-40" novalidate>
          <div>
            <div class="mt-12 w-full">
              <input
                type="email"
                name="email"
                required="required"
                placeholder="メールアドレス"
                v-model="email"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
                autofocus
              />
            </div>
            <div class="mt-12 w-full">
              <input
                type="password"
                name="password"
                required="required"
                placeholder="パスワード"
                v-model="password"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
              />
            </div>
            <button
              type="submit"
              @click="register"
              class="block mt-8 mx-auto w-2/3 h-12 text-white text-xl bg-gray-200 hover:bg-red-500 rounded-md cursor-pointer transition duration-300"
            >
              登録
            </button>
          </div>
        </form>
        <div class="py-8 text-center text-base">
          <p class="text-blue-500">
            <nuxt-link to="/login" class="text-blue-500"
              >ログインはこちら</nuxt-link
            >
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    user() {
      return this.$store.getters["user"];
    }
  },
  data: function() {
    return {
      email: "",
      password: ""
    };
  },
  methods: {
    register() {
      this.$auth
        .createUserWithEmailAndPassword(this.email, this.password)
        .then(user => {
          alert("登録が完了しました。");
          this.$store.dispatch("checkLogin");
          this.$router.push("/home");
        })
        .catch(error => {
          console.log({ code: error.code, message: error.message });
          alert("エラーにより登録ができませんでした。恐れ入りますが再度お試しください。");
          }
        });
    }
  }
};
</script>
```
ポイントとしては、formタグに`@submit.prevent`を記載することでフォームの自動送信を防ぎ、入力ミスなどは正規表現で制御するので、`novalidate`でブラウザのバリデーションを無効化しています。
また、メールアドレスとパスワードのv-modelで、dataプロパティの値をバインディングしているので、`this.email`のようにして参照することができるようになりましました。

登録ボタンをクリックすると、metodsの`register()`イベントが発火します。

####正常に登録できた場合（.then）
* アラートで登録が完了した通知
* `store/index.js`のactionsに登録した`checkLogin`を呼び出して、ログイン済みに変更
* ログイン後ページへ遷移

####エラーが起きた場合（.catch）
* コンソールにエラーコード・メッセージを表示
* アラートでエラーが発生した旨を伝える通知

上記のように処理を実行する。
（methodsに記載した処理は、`store/index.js`のactionsに記述して呼び出すこともできます。ですが、登録完了後にページ遷移をさせたいため、pagesディレクトリ内にこのような処理を行なっています。）

作成した画面は以下のように表示されます。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/504ed6d6-cbe7-32b4-d9c2-3ef1cb04567c.png">

###4.正規表現を用いたバリデーション
正しい表記で入力されているかチェックするため、以下のように追記しておきます。

```javascript:register.vue
<template>
  <div id="register">
    <div class="mt-12 px-4 py-40 bg-gray-100 md:mt-20">
      <div class="container mx-auto py-8 bg-white md:w-160">
        <h2 class="text-center text-2xl font-bold">新規登録</h2>
        <form @submit.prevent class="px-20 md:px-40" novalidate>
          <div>
            <div class="mt-12 w-full">
              <input
                type="email"
                name="email"
                required="required"
                placeholder="メールアドレス"
                v-model="email"
                @input="isInput"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
                autofocus
              />
              <p class="text-red-400">{{ emailErrorMassage }}</p>
            </div>
            <div class="mt-12 w-full">
              <input
                type="password"
                name="password"
                required="required"
                placeholder="パスワード"
                v-model="password"
                @input="isInput"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
              />
              <p class="text-red-400">{{ passwordErrorMassage }}</p>
            </div>
            <button
              type="submit"
              @click="register"
              class="block mt-8 mx-auto w-2/3 h-12 text-white text-xl bg-gray-200 hover:bg-red-500 rounded-md cursor-pointer transition duration-300"
            >
              登録
            </button>
          </div>
        </form>
        <div class="py-8 text-center text-base">
          <p class="text-blue-500">
            <nuxt-link to="/login" class="text-blue-500"
              >ログインはこちら</nuxt-link
            >
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    user() {
      return this.$store.getters["user"];
    }
  },
  data: function() {
    return {
      email: "",
      password: "",
      emailErrorMassage: "",
      passwordErrorMassage: "",
      emailRegexp: /^[a-z\d][\w.-]*@[\w.-]+\.[a-z\d]+$/i,
      passwordRegexp: /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,20}$/i
    };
  },
  methods: {
    register() {
      if (!this.emailRegexp.test(this.email)) {
        this.emailErrorMassage =
          "このメールアドレスは無効です。正しく入力してください。";
      }
      if (!this.passwordRegexp.test(this.password)) {
        this.passwordErrorMassage =
          "このパスワードは無効です。半角英数字を含んで8-20文字の範囲内で入力してください。";
      }
      if (this.email === "") {
        this.emailErrorMassage = "メールアドレスを入力してください。";
      }
      if (this.password === "") {
        this.passwordErrorMassage = "パスワードを入力してください。";
      }
      if (this.emailErrorMassage !== "" || this.passwordErrorMassage !== "") {
        return;
      }
      this.$auth
        .createUserWithEmailAndPassword(this.email, this.password)
        .then(user => {
          alert("登録が完了しました。");
          this.$store.dispatch("checkLogin");
          this.$router.push("/home");
        })
        .catch(error => {
          console.log({ code: error.code, message: error.message });
          if (error.code === "auth/invalid-email") {
            this.emailErrorMassage = "このメールアドレスは無効です。";
          } else if (error.code === "auth/email-already-in-use") {
            this.emailErrorMassage = "このメールアドレスは既に使用されています。";
          } else {
            alert("エラーにより登録ができませんでした。恐れ入りますが再度お試しください。");
          }
        });
    },
    isInput() {
      this.emailErrorMassage = "";
      this.passwordErrorMassage = "";
    }
  }
};
</script>

```
メールアドレスとパスワードを確認して、入力が誤っている場合はエラーメッセージを表示させています。正規表現で拾えなかったエラーメッセージに関しては、`.catch`で対応しています。また、入力中はエラーメッセージを非表示にできるように、inputタグに`@input="isInput"`を設定しています。
エラーメッセージは、[こちら](https://firebase.google.com/docs/auth/admin/errors?hl=ja)から参照できます。

今回利用する正規表現については、以下の記事で詳しく解説しております！

https://qiita.com/YuStarrr/items/03f206fec44a6d52d0d0

###5. 動作確認
入力の形式が正しくない場合はエラーメッセージが表示される。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/a564fcc1-e004-3223-1dac-3c6086a9099d.png">

正常に登録できた場合はアラートが表示され、【Firebase管理画面＞Authentication＞Users】に、メールアイコンのアカウントが追加される。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/c41ad866-744a-8587-1c53-f8dcafaa058a.png">


##メールアドレスログイン機能の実装

>firebase公式：https://firebase.google.com/docs/auth/web/start#sign_in_existing_users

###1. ログイン画面の作成
`pages/login.vue`ファイルを作成し、コードを記述していきます。（内容は新規登録画面とほぼ同じ）

```javascript:pages/login.vue
<template>
  <div id="login">
    <div class="mt-12 px-4 py-20 bg-gray-100 md:mt-20">
      <div class="container mx-auto bg-white md:w-160">
        <h2 class="pt-8 text-center text-2xl font-bold">ログイン</h2>
        <form @submit.prevent class="px-20 md:px-40" novalidate>
          <div>
            <div class="mt-12 w-full">
              <input
                type="email"
                name="email"
                required="required"
                placeholder="メールアドレス"
                v-model="email"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
                autofocus
              />
            </div>
            <div class="mt-12 w-full">
              <input
                type="password"
                name="password"
                required="required"
                placeholder="パスワード"
                v-model="password"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
              />
            </div>
            <button
              type="submit"
              @click="login"
              class="block mt-8 mx-auto w-2/3 h-12 text-white text-xl bg-gray-200 hover:bg-red-500 rounded-md cursor-pointer transition duration-300"
            >
              ログイン
            </button>
          </div>
        </form>
        <div class="py-8 text-center text-base">
          <p class="text-blue-500">
            <nuxt-link to="/register">初めての方はこちら</nuxt-link>
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    user() {
      return this.$store.getters["user"];
    }
  },
  data: function() {
    return {
      email: "",
      password: "",
      emailErrorMassage: "",
      passwordErrorMassage: ""
    };
  },
  methods: {
    login() {
      this.$auth
        .signInWithEmailAndPassword(this.email, this.password)
        .then(user => {
          alert("ログインに成功しました！");
          this.$store.dispatch("checkLogin");
          this.$router.push("/home");
        })
        .catch(error => {
          console.log(error);
          alert("メールアドレスもしくはパスワードが違う可能性があります");
        });
    }
  }
};
</script>

```
ログインボタンをクリックすると、`login()`イベントが発火します。
完成した画面は以下のようになります。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/629aee0d-dfea-40b7-a147-33d5c00ba7db.png">

###2. バリデーションの実装
新規登録するときに、メールアドレスに`@`をつけたり、パスワードに半角英数字などや文字数制御をしているので、ログインページでは未入力の場合のみ、エラーメッセージを表示させます。

```javascript:pages/login.vue
<template>
  <div id="login">
    <div class="mt-12 px-4 py-20 bg-gray-100 md:mt-20">
      <div class="container mx-auto bg-white md:w-160">
        <h2 class="pt-8 text-center text-2xl font-bold">ログイン</h2>
        <form @submit.prevent class="px-20 md:px-40" novalidate>
          <div>
            <div class="mt-12 w-full">
              <input
                type="email"
                name="email"
                required="required"
                placeholder="メールアドレス"
                v-model="email"
                @input="isInput"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
                autofocus
              />
              <p class="text-red-400">{{ emailErrorMassage }}</p>
            </div>
            <div class="mt-12 w-full">
              <input
                type="password"
                name="password"
                required="required"
                placeholder="パスワード"
                v-model="password"
                @input="isInput"
                class="pl-4 w-full h-14 text-base border-2 border-solid border-black border-opacity-25 hover:border-opacity-50 rounded-md transition duration-300"
              />
              <p class="text-red-400">{{ passwordErrorMassage }}</p>
            </div>
            <button
              type="submit"
              @click="login"
              class="block mt-8 mx-auto w-2/3 h-12 text-white text-xl bg-gray-200 hover:bg-red-500 rounded-md cursor-pointer transition duration-300"
            >
              ログイン
            </button>
          </div>
        </form>
        <div class="py-8 text-center text-base">
          <p class="text-blue-500">
            <nuxt-link to="/register">初めての方はこちら</nuxt-link>
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    user() {
      return this.$store.getters["user"];
    }
  },
  data: function() {
    return {
      email: "",
      password: "",
      emailErrorMassage: "",
      passwordErrorMassage: ""
    };
  },
  methods: {
    login() {
      if (this.email === "") {
        this.emailErrorMassage = "メールアドレスを入力してください。";
      }
      if (this.password === "") {
        this.passwordErrorMassage = "パスワードを入力してください。";
      }
      if (this.emailErrorMassage !== "" || this.passwordErrorMassage !== "") {
        return;
      }
      this.$auth
        .signInWithEmailAndPassword(this.email, this.password)
        .then(user => {
          alert("ログインに成功しました！");
          this.$store.dispatch("checkLogin");
          this.$router.push("/home");
        })
        .catch(error => {
          console.log(error);
          alert("メールアドレスもしくはパスワードが違う可能性があります");
        });
    },
    isInput() {
      this.emailErrorMassage = "";
      this.passwordErrorMassage = "";
    }
  }
};
</script>

```


###3. 動作確認
未入力の場合のエラーメッセージ。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/49467c69-fc8d-06f9-6c11-2ed93176e1b5.png">

登録したメールアドレス・パスワードでのログインが成功すると、アラートで表示が出ます。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/1f79785f-a076-190f-af68-4924d67b1dc2.png">

##ログアウト機能の実装

>firebase公式：https://firebase.google.com/docs/auth/web/password-auth#next_steps

###1. ログアウト画面の実装
ログイン後に表示される`pages/home.vue`ファイルを作成し、コードを記述していきます。
ログアウトボタンをpagesに表示させていますが、ログイン後のヘッダーに表示させるほうが一般的かもしれません。

```javascript:pages/home.vue
<template>
  <div id="home">
    <button @click="signOut">
      ログアウト
    </button>
  </div>
</template>

<script>
export default {
  methods: {
    signOut() {
      this.$auth
        .signOut()
        .then(() => {
          alert("ログアウトしました");
          this.$store.commit("signOut");
          this.$router.push("/");
        })
        .catch(error => {
          console.log(error);
          alert("ログアウトに失敗しました");
        });
    }
  }
};
</script>

```
ログインボタンをクリックすると、`signOut()`イベントが発火します。
`store/index.js`のmutaitionsで持っていたユーザのID・メールアドレスを空にして、ログイン状態をfalseに設定します。

###2. 動作確認
ログアウトするとアラートが表示されて、ウェルカムページへ遷移します。

<img width=“200” src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/966101/8b4fd4d4-7407-f4af-706e-1369a340231f.png">


#参考

* [【Nuxt.js】firebase基礎編(Auth版)：メールアドレスログインをできるようにしよう](https://nuxt.alizlab.com/%e3%80%90nuxt-js%e3%80%91firebase%e5%9f%ba%e7%a4%8e%e7%b7%a8auth%e7%89%88%ef%bc%9a%e3%83%a1%e3%83%bc%e3%83%ab%e3%82%a2%e3%83%89%e3%83%ac%e3%82%b9%e3%83%ad%e3%82%b0%e3%82%a4%e3%83%b3%e3%82%92/)
* [【Nuxt.js】firebase基礎編(Auth版)：ログアウトしよう](https://nuxt.alizlab.com/%e3%80%90nuxt-js%e3%80%91firebase%e5%9f%ba%e7%a4%8e%e7%b7%a8auth%e7%89%88%ef%bc%9a%e3%83%ad%e3%82%b0%e3%82%a2%e3%82%a6%e3%83%88%e3%81%97%e3%82%88%e3%81%86/)
* [【Nuxt.js】firebase基礎編(Auth版)：アカウント作成をできるようにしよう](https://nuxt.alizlab.com/%e3%80%90nuxt-js%e3%80%91firebase%e5%9f%ba%e7%a4%8e%e7%b7%a8auth%e7%89%88%ef%bc%9a%e3%82%a2%e3%82%ab%e3%82%a6%e3%83%b3%e3%83%88%e4%bd%9c%e6%88%90%e3%82%92%e3%81%a7%e3%81%8d%e3%82%8b%e3%82%88-2/)
* [
Firebase Authentication と NUXT（Vue.Js）で簡単にユーザー登録＆認証を作成する](https://www.metrocode.co/blog/post/firebase-authentication-nuxt-vuejs)

<br>


#おわりに
ここまでFirebaseAuthで、新規登録 / メールログイン / ログアウトを実装する方法についてまとめました。
かなり気合を込めて執筆したので、少しでも内容が良ければ、LGTMを押していただけると嬉しいです！

次回以降は、**「Googleログイン機能」**や**「匿名ログイン機能」**、ログイン前後のリダイレクト処理などを随時記事にしていきたいと思いますので、[フォロー](https://qiita.com/YuStarrr)してお待ちいただければと思います！
以上、最後まで読んでいただきありがとうございました！