かべブログ

TypeScript, React, Goなどでまったり個人開発。たまに競プロ。

Firebase Authentication(+ Next.js + TypeScript) 素振り

はじめに

こんばんは、かべです。

ちゃんとした Web アプリを作るにはやっぱりユーザー認証機能が欲しい!ということで新規ユーザー作成、ログイン機能を実装する練習をしました。先日 Firebase の Realtime Database を使うことがあったので、流れで FIrebase Authentication を使うことにしました。

いろいろやり方を探していると良さげな Zennの記事 を見つけたため、これをなぞりながら自分でも少し機能を足していこうと思います。今回の目標は次の通りです。

  • 新規ユーザー登録がメールアドレスとパスワードで出来る
  • 登録したユーザーのメールアドレスとパスワードでログインできる
  • ログインするとログイン後のページに飛び、「ようこそ、〇〇さん!」のように表示される
  • ログインしていない時にログイン後のページを見ようとしても戻される

構成としては、Next.js + TypeScript で行います。

環境

今回の開発は以下の環境で行いました。

  • Ubuntu 20.04.1
  • yarn 1.22.5
  • firebase 8.3.2
  • Next 10.1.3
  • React 17.0.2
  • TypeScript 4.0

Firebase の設定

  1. Firebase のコンソールから「プロジェクトを追加」
  2. 適当にプロジェクト名等入力
  3. 左サイドバーの「Authentication」をクリック
  4. Sign-in method」から「メール/パスワード」を有効にする
  5. 左サイドバーの「プロジェクトを設定」から Web アプリを追加し、Firebase SDK を追加する

という手順で簡単に設定できます。最後の手順で出てくる var firebaseConfig は保存しておいてください。接続の際に必要です。

Next.js の初期設定と Firebase への接続

いつもの create-next-app で作ります。今回は TypeScript を使いたいので npx create-next-app --example with-typescript [好きなプロジェクト名] で行きます。完成したらいらないファイルを消して、そこにいろいろ書いていきましょう。

まず、Firebase SDK にある Firebase への接続キーを Next.js 上で読めるようにします。Next.js では .env ファイルに書くだけでは環境変数をよしなに読み取ってくれないようで、next.config.js ファイルにも書く必要があります。生成されたコードのルートディレクトリに次のコードを追加しましょう。

.env.local

FIREBASE_API_KEY='hoge'
FIREBASE_AUTH_DOMAIN='hoge'
FIREBASE_PROJECT_ID='hoge'
FIREBASE_STORAGE_BUCKET='hoge'
FIREBASE_MESSAGING_SENDER_ID='hoge'
FIREBASE_APP_ID='hoge'

hoge には適宜得られた値を代入します。

next.config.js

module.exports = {
  env: {
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
  },
};

環境変数が設定出来たら、次は Next.js 内で firebase のあれこれを使えるように firebase 利用の大元となるファイルを作ります。

/utils/firebase.ts

import "firebase/auth";
import "firebase/firestore";

import firebase from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  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(firebaseConfig);
}

export const auth = firebase.auth();

ユーザー登録

今回は localhost:3000/ をログイン用のページ、/signup をユーザー登録用のページ、/contents をログイン成功後のページとして作成していきます。

まずはユーザー情報を Context として持つための Provider などを /context/Auth.tsx に作ります。

/context/Auth.tsx

import firebase from "firebase/app";
import { FC, createContext, useEffect, useState } from "react";
import { auth } from "../utils/firebase";

// 現在のユーザーを持つ Context の型
type AuthContextProps = {
  currentUser: firebase.User | null | undefined;
};

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined });

// Context を全体で使えるようにする Provider
const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<
    firebase.User | null | undefined
  >(undefined);

  // ログイン状態が変わった時に発火
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
    });
  }, []);

  return (
    <AuthContext.Provider value={{ currentUser }}>
      {children}
    </AuthContext.Provider>
  );
};

export { AuthContext, AuthProvider };

/pages/_app.tsx で全体に当てるのも忘れずに。

/pages/_app.tsx

import { AppProps } from "next/app";
import { AuthProvider } from "../context/Auth";

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
};

export default MyApp;

ここまで下準備が出来たら、登録用のフォームを作ります。これは firebase.auth() が持つ createUserWithEmailAndPassword を使って簡単に実装出来ます。

/pages/signup.tsx

import { NextPage } from "next";
import { useRouter } from "next/router";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";

import { auth } from "../utils/firebase";

const SignUp: NextPage = () => {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  // 既にサインインしている時はサインアップの必要が無いので
  // ログイン後のページに飛ばす
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push("/contents");
    });
  }, []);

  // 新規ユーザー登録
  const createUser = async (e: FormEvent) => {
    // form の動作を止める
    e.preventDefault();

    try {
      // firebase のユーザー登録用メソッド
      await auth.createUserWithEmailAndPassword(email, password);
      router.push("/");
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <>
      <form onSubmit={createUser}>
        <div>
          <label htmlFor="email">メールアドレス: </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード: </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">登録</button>
      </form>
      <Link href="/">ログイン</Link>
    </>
  );
};

export default SignUp;

ログイン

次にログイン機能を実装していきましょう。これも firebase.auth() が持つ signInWithEmailAndPassword で簡単に実装出来ます。

/pages/index.tsx

import { NextPage } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { FormEvent, useEffect, useState } from "react";
import { auth } from "../utils/firebase";

const IndexPage: NextPage = () => {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  // 既にサインインしている時はサインアップの必要が無いので
  // ログイン後のページに飛ばす
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      user && router.push("/contents");
    });
  }, []);

  // ログイン処理
  const logIn = async (e: FormEvent) => {
    // form の動作を止める
    e.preventDefault();

    try {
      await auth.signInWithEmailAndPassword(email, password);
      router.push("/contents");
    } catch (err) {
      alert(err.message);
    }
  };

  return (
    <>
      <form onSubmit={logIn}>
        <div>
          <label htmlFor="email">メールアドレス: </label>
          <input
            id="email"
            type="email"
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード: </label>
          <input
            id="password"
            type="password"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">ログイン</button>
      </form>
      <Link href="/signup">新規登録</Link>
    </>
  );
};

export default IndexPage;

ログインが出来たら /contents に飛ばしてあげます。/contents ではログインしたユーザーのメールアドレスを表示してみましょう。ここでは context を使って実装しています。

/pages/contents.tsx

import { NextPage } from "next";
import { useRouter } from "next/router";
import { useContext, useEffect } from "react";

import { auth } from "../utils/firebase";
import { AuthContext } from "../context/Auth";

const Contents: NextPage = () => {
  const router = useRouter();
  // Context を使用
  const currentUser = useContext(AuthContext);

  useEffect(() => {
    // ログインしていない状態で見ようとした時にログイン画面へ戻す
    auth.onAuthStateChanged((user) => {
      user || router.push("/");
    });
  }, []);

  const logOut = async () => {
    try {
      await auth.signOut();
      router.push("/");
    } catch (error) {
      alert(error.message);
    }
  };

  // ログインしていない時に見ようとしても見られないようにする
  return (
    <>
      {currentUser.currentUser ? (
        <>
          <div>ようこそ、{currentUser.currentUser?.email}さん</div>
          <button onClick={logOut}>ログアウト</button>
        </>
      ) : (
        <></>
      )}
    </>
  );
};

export default Contents;

JSX 部分で3項演算子での分岐をしているのは、ログインしていない状態で見ようとした時に何もレンダリングされないようにするためです。この部分を消すと localhost:3000/contents を直接叩いたときに「ようこそ、さん」と一瞬表示されてしまいます。

完成

これで完成です!ユーザー登録、ログインなどが出来るか確認してみましょう。

f:id:okb_okb:20210407235710p:plain
新規登録

f:id:okb_okb:20210407235744p:plain
ログイン

f:id:okb_okb:20210407235759p:plain
ログイン後の画面

終わりに

Firebase Authentication でログイン機能を簡単に実装することが出来ました。次は NextAuth,js でも同じようなことをして比べてみたいですね。

今回のコードはこちらにアップロードされています。

参考