【Next.js】Firebase AuthenticationとHOCを使用してサインイン機能を実装する

f:id:nekorokkekun:20190925000003p:plain:w1000
本記事では、Next.jsプロジェクトにFirebase Authenticationを組み込み、サインイン機能を実装するとともに、HOC(高階コンポーネント)を使用してサインインユーザーのみが閲覧できるページを作成していきます。

Next.jsプロジェクトの準備

まずはNext.jsプロジェクトの準備をしていきましょう。

npx create-next-app next-firebase-auth
cd next-firebase-auth
yarn devhttp://localhost:3000/

http://localhost:3000/にアクセスしてみると以下のような画面が出てくるはずです。
f:id:nekorokkekun:20190924225728p:plain

サインインユーザーのみ見られるコンポーネントの作成

次にサインインユーザーのみが見ることのできるコンポーネントを作成していきましょう。

cd pages
touch dashboard.js

pages/dashboard.js

import React from 'react';
import Nav from '../components/nav';
class Dashboard extends React.Component {
 render() {
   return (
     <div>
          <Nav />
          <h1>Dashboard  Page</h1>
           <p>You can't go into this page if you are not authenticated.</p>
      </div>
     )
   }
}
export default Dashboard;

まだHOCに設定していないためアクセス可能です。

必要となるパッケージやDB接続ファイルの準備

次にFirebaseの接続に必要となるパッケージやDB接続用ファイルの準備をしていきます。

まずはルートディレクトリに.envファイルを作成します。

touch .env

ここにFirebaseでプロジェクトを作成した際に発行された

  • api key
  • auth domain
  • database url
  • project id
  • storage buchet
  • messaging sender id
  • app id

たちを定数として収めていきます。

Firebaseにおけるプロジェクトの作成から上記のキーの発行手順については以下の記事に書かれています。
nekorokkekun.hatenablog.com


取得したキーたちを以下のように.envに書き込んでいきましょう。
.env

FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_AUTH_DOMAIN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_DATABASE_URL=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_PROJECT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_STORAGE_BUCKET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_MESSAGING_SENDER_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FIREBASE_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


次にnext.config.jsをルートディレクトリ直下に作成します。

touch next.config.js

next.config.js

require("dotenv").config();
const path = require("path");
const Dotenv = require("dotenv-webpack");
module.exports = {
  webpack: config => {
    config.plugins = config.plugins || [];
    config.plugins = [
      ...config.plugins,
      // Read the .env file
      new Dotenv({
        path: path.join(__dirname, ".env"),
        systemvars: true
      })
    ];
    return config;
  }
};

最後にFirebaseへ接続するためのファイルを作成しましょう。

// ルートディレクトリから
mkdir src
cd src
mkdir firebase
cd firebase
touch index.js

src/firebase/index.js

import firebase from "firebase/app";
import "firebase/auth";
const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.FIREBASE_DATABASE_URL,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID
};
if (!firebase.apps.length) {
  firebase.initializeApp(config);
}
const auth = firebase.auth();
export { auth, firebase };

これでprocess.env.〇〇(定数名)という形でプロジェクト内の他ファイルから.envで設定した定数名を使用するための下準備ができました!


そして以下の2つのパッケージをインストールすることで.envで設定した定数がprocess.env.〇〇(定数名)といった形でプロジェクト内の他のファイルからアクセス可能となります。

yarn add firebase dotenv
yarn add dotenv-webpack

FirebaseでAuthentication設定

次にFirebaseのコンソール上からAuthenticationの設定をしておきましょう。
先ほど作成したFirebaseのプロジェクトにアクセスします。
そしてサイドメニューの「Authentication」をクリックしましょう。
f:id:nekorokkekun:20190924232207p:plain


画像の順番に従い、
① タブメニューの「ログイン方法」をクリック
② 「有効にする」をオン
③ プロジェクトのサポートメールを設定
という順序で設定を完了させましょう。
f:id:nekorokkekun:20190924232600p:plain

index.jsの書き換え

独自のUIにしていますが、ポイントはfirebaseからauthをimportしているというところです。

import React from "react";
import Link from "next/link";
import Nav from "../components/nav";
import { auth, firebase } from "../src/firebase";
import Head from "next/head"

class Home extends React.Component {
  render() {
    return (
      <div>
        <Head title="Home" />
        <Nav />
        <div className="hero">
          <h1 className="title">
            Welcome to Firebase Authentication in Next.js!
          </h1>
          <p className="description">
            Click on the Dashboard link to visit the dashboard page.
          </p>
          <div className="row">
            <Link href="/dashboard">
              <a className="card">
                <h3>Go to Dashboard&rarr;</h3>
                <p>Visit Dashboard</p>
              </a>
            </Link>
          </div>
        </div>
        <style jsx>{`
          .hero {
            width: 100%;
            color: #333;
          }
          .title {
            margin: 0;
            width: 100%;
            padding-top: 80px;
            line-height: 1.15;
            font-size: 48px;
          }
          .title,
          .description {
            text-align: center;
          }
          .row {
            max-width: 880px;
            margin: 80px auto 40px;
            display: flex;
            flex-direction: row;
            justify-content: space-around;
          }
          .card {
            padding: 18px 18px 24px;
            width: 220px;
            text-align: left;
            text-decoration: none;
            color: #434343;
            border: 1px solid #9b9b9b;
          }
          .card:hover {
            border-color: #067df7;
          }
          .card h3 {
            margin: 0;
            color: #067df7;
            font-size: 18px;
          }
          .card p {
            margin: 0;
            padding: 12px 0 0;
            font-size: 13px;
            color: #333;
          }
        `}</style>
      </div>
    );
  }
}
export default Home;

HOC(高階コンポーネント)の作成

いよいよHOC(高階コンポーネント)を作成します。
高階コンポーネントについて今回は詳しく解説しませんが、「通常のコンポーネントに機能を追加するもの」という認識でOKです。

今回のHOCで追加する機能が「サインイン後に見ることができる」というものですね。

// src直下で
mkdir helpers
cd helpers
touch withAuth.js

src/helpers/withAuth.js

import React from "react";
import router from "next/router";
import { auth } from "../firebase";

const withAuth = Component => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        status: "LOADING"
      };
    }
    componentDidMount() {
      auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.setState({
            status: "SIGNED_IN"
          });
        } else {
          router.push("/");
        }
      });
    }
    renderContent() {
      const { status } = this.state;
      if (status == "LOADING") {
        return <h1>Loading ......</h1>;
      } else if (status == "SIGNED_IN") {
        return <Component {...this.props} />;
      }
    }
    render() {
      return <>{this.renderContent()}</>;
    }
  };
};
export default withAuth;

こちらは細かく解説していきましょう。

引数でComponentを受ける

const withAuth = Component => {

後ほど「サインイン後しか見られないコンポーネント」を設定する際に

export default withAuth(コンポーネント名)

という書き方でexportします。

この「コンポーネント名」で指定されたコンポーネントが、withAuthの引数に設定された「Component」を指しています。

ユーザー認証されるまではstateで読み込みが「Loading」

    constructor(props) {
      super(props);
      this.state = {
        status: "LOADING"
      };
    }

stateの状態は基本的に「LOADING」(=見認証の状態)にしておき、
ユーザー認証されて初めてstateが「SIGNED IN」の状態となります。

authUserで認証の可否を判別

    componentDidMount() {
      auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.setState({
            status: "SIGNED_IN"
          });
        } else {
          router.push("/");
        }
      });
    }

サインインが成功した場合、「authUser」インスタンスの中には、ユーザー情報が入ることになります。そうするとif文でstateのstatusが「SIGNED_ID」になり、コンテンツを見ることができるようになります。

uid(=ユーザーID)やメールアドレスなどが入っているため、サインイン後にも利用できそうですね!

もしユーザーが未認証の場合にはauthUserは空のままのため、elseの方に行き、そのままトップページに遷移することとなります。

statusがLOADINGのままだと、LOADINGという画面が表示されることとなります。それが以下のものですね。

    renderContent() {
      const { status } = this.state;
      if (status == "LOADING") {
        return <h1>Loading ......</h1>;
      } else if (status == "SIGNED_IN") {
        return <Component {...this.props} />;
      }
    }

そしてstatusがSIGNED_INだと、HOCでサインインユーザーのみが見られるComponentを表示できるようになっています。

dashboard.jsへ追記

先ほど作成したdashboard.jsにwithAuth.jsを合体させる形で追記しましょう。

pages/dashboard.js

import React from 'react';
import Nav from '../components/nav';
// 追加
import withAuth from "../src/helpers/withAuth";

class Dashboard extends React.Component {
 render() {
   return (
     <div>
          <Nav />
          <h1>Dashboard  Page</h1>
           <p>You can't go into this page if you are not authenticated.</p>
      </div>
     )
   }
}
// withAuthの引数にdashboard.jsを追加
export default withAuth(Dashboard);

これでサインインユーザーのみがdashboard.jsを見られるような設定となりました!

index.jsに追記

次にサインインなどの処理を行うためのコードをpages/index.jsに追記します。

pages/index.js

import React from "react";
import Link from "next/link";
import Nav from "../components/nav";
import { auth, firebase } from "../src/firebase";
import Head from "next/head"

class Home extends React.Component {
// ここから下を追加
  handleSignIn = () => {
    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/contacts.readonly");
    auth
      .signInWithPopup(provider)
      .then(() => {
        alert("You are signed In");
      })
      .catch(err => {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };
  handleSignout = () => {
    auth
      .signOut()
      .then(function() {
        alert("Logout successful");
      })
      .catch(function(error) {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };
// ここまでを追加
  render() {
    return (
      <div>
        <Head title="Home" />
        <Nav />
        <div className="hero">
          <h1 className="title">
            Welcome to Firebase Authentication in Next.js!
          </h1>
          <p className="description">
            Click on the Dashboard link to visit the dashboard page.
          </p>
          <div className="row">
            <Link href="/dashboard">
              <a className="card">
                <h3>Go to Dashboard&rarr;</h3>
                <p>Visit Dashboard</p>
              </a>
            </Link>
   {/* ここから下2行を追加 */}
            <button onClick={this.handleSignIn}>Sign In using google</button> 
            <button onClick={this.handleSignout}>Signout</button>
          </div>
        </div>
        <style jsx>{`
          .hero {
            width: 100%;
            color: #333;
          }
          .title {
            margin: 0;
            width: 100%;
            padding-top: 80px;
            line-height: 1.15;
            font-size: 48px;
          }
          .title,
          .description {
            text-align: center;
          }
          .row {
            max-width: 880px;
            margin: 80px auto 40px;
            display: flex;
            flex-direction: row;
            justify-content: space-around;
          }
          .card {
            padding: 18px 18px 24px;
            width: 220px;
            text-align: left;
            text-decoration: none;
            color: #434343;
            border: 1px solid #9b9b9b;
          }
          .card:hover {
            border-color: #067df7;
          }
          .card h3 {
            margin: 0;
            color: #067df7;
            font-size: 18px;
          }
          .card p {
            margin: 0;
            padding: 12px 0 0;
            font-size: 13px;
            color: #333;
          }
        `}</style>
      </div>
    );
  }
}
export default Home;


こちらも少し解説をします。

以下はサインイン・サインアウトの処理を行うイベントの設定です。

handleSignInでは、ボタンを押すとサインインモーダルウィンドウがポップアップし、サインイン処理ができるという流れになります。

こちらは私たちが作り込む必要はありません。

  handleSignIn = () => {
    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/contacts.readonly");
    auth
      .signInWithPopup(provider)
      .then(() => {
        alert("You are signed In");
      })
      .catch(err => {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };

handleSignoutも同じくで、Signout()メソッドを使えば簡単にユーザーを未承認の状態に戻してくれます。

  handleSignout = () => {
    auth
      .signOut()
      .then(function() {
        alert("Logout successful");
      })
      .catch(function(error) {
        alert("OOps something went wrong check your console");
        console.log(err);
      });
  };

そして、上記2つのイベントを呼び出すのが以下のbuttonですね。

            <button onClick={this.handleSignIn}>Sign In using google</button> 
            <button onClick={this.handleSignout}>Signout</button>

サインインをしてみる

それでは

yarn dev

でローカルサーバを起動して実際にサインインをしてみましょう。

まずはサインインしていない状態でDashboardに移ってみましょう。画面遷移しないですね。

次にサインインボタンをクリックします。

すると以下のようなウィンドウが出るはずです。
f:id:nekorokkekun:20190924235640p:plain


サインインのプロセスを経ると、Dashboardもみることができました。

最後にサインアウトできれば成功です!
お疲れ様でした。