🍚

Remix & Cloudflare Pagesのプロジェクトで環境変数を使う

2022/09/18に公開

はじめに

Remix & Cloudflare Pages の環境に Firebase Authentication を導入するため環境変数の設定を行ったのですが、結構時間とられたので記事にしようと思います。

前提

以下の設定が完了している前提で進めていきます。

  • Cloudflare Pages でデプロイする前提の Remix プロジェクトを作成している
    • npx create-remix@latestを入力
    • What type of app do you want to create?Just the basicsを選択
    • Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets.Cloudflare Pagesを選択
  • Cloudflare Pages でデプロイできる
  • Firebase のプロジェクトが作成されている
  • Firebase Authentication でメールアドレスとパスワードによる認証が設定されている

環境変数の呼び出し方

package.json と同じ階層に.dev.varsというファイルを作成します。ファイルの中身は.env ファイルと同じように書きます。環境変数は git 管理しないので.gitignoreに追記します。

SECRET_KEY = secret-key;

loader 関数内で環境変数の呼び出しができます。

export const loader: LoaderFunction = async ({ context }) => {
  return {
    SECRET_KEY: context.SECRET_KEY,
  };
};

.dev.vars に環境変数を設定する

.dev.varsに firebase app の項目を設定します。

.dev.vars
API_KEY = YOUR_API_KEY
AUTH_DOMAIN = YOUR_AUTH_DOMAIN
PROJECT_ID = YOUR_PROJECT_ID
STORAGE_BUCKET = YOUR_STORAGE_BUCKET
MESSAGING_SENDER_ID = YOUR_MESSAGING_SENDER_ID
APP_ID = YOUR_APP_ID
MEASUREMENT_ID = YOUR_MEASUREMENT_ID

Firebase App を初期化する

ここではroot.tsx内で firebase app の初期化を行います。loader 関数で環境変数を呼び出します。useLoaderData()を使って loader 関数で return した json を取得します。呼び出した環境変数を使って firebase app を initialize します。

root.tsx
root.tsx
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
} from "@remix-run/react";
import type { LoaderFunction, MetaFunction } from "@remix-run/cloudflare";
import type { FirebaseOptions } from "firebase/app";
import { initializeApp } from "firebase/app";

export const loader: LoaderFunction = async ({ context }) => {
  return {
    API_KEY: context.API_KEY,
    AUTH_DOMAIN: context.AUTH_DOMAIN,
    PROJECT_ID: context.PROJECT_ID,
    STORAGE_BUCKET: context.STORAGE_BUCKET,
    MESSAGING_SENDER_ID: context.MESSAGING_SENDER_ID,
    APP_ID: context.APP_ID,
    MEASUREMENT_ID: context.MEASUREMENT_ID,
  };
};

export const meta: MetaFunction = () => ({
  charset: "utf-8",
  title: "New Remix App",
  viewport: "width=device-width,initial-scale=1",
});

export default function App() {
  const {
    API_KEY,
    AUTH_DOMAIN,
    PROJECT_ID,
    STORAGE_BUCKET,
    MESSAGING_SENDER_ID,
    APP_ID,
    MEASUREMENT_ID,
  } = useLoaderData();
  const firebaseConfig: FirebaseOptions = {
    apiKey: API_KEY,
    authDomain: AUTH_DOMAIN,
    projectId: PROJECT_ID,
    storageBucket: STORAGE_BUCKET,
    messagingSenderId: MESSAGING_SENDER_ID,
    appId: APP_ID,
    measurementId: MEASUREMENT_ID,
  };
  initializeApp(firebaseConfig);

  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

Firebase Authentication で新規登録する

新規登録画面としてapp\routes\login\entry.tsxを作成します。
ユーザー名とパスワードを入力して新規登録ボタンを押すと、actionに POST されます。getAuth()の部分で、root.tsx で initialize した FirebaseApp に関連付けられた Auth インスタンスを返します。action内でユーザーの新規登録を行います。

entry.tsx
app\routes\login\entry.tsx
import { Form } from "@remix-run/react";
import type { ActionFunction } from "@remix-run/cloudflare";
import { redirect } from "@remix-run/cloudflare";
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const email = formData.get("email")?.toString();
  const password = formData.get("password")?.toString();

  if (email !== undefined && password !== undefined) {
    try {
      const firebaseAuth = getAuth();
      const userCredential = await createUserWithEmailAndPassword(
        firebaseAuth,
        email,
        password
      );
      console.log("createUser=", userCredential.user);
      return redirect("/login");
    } catch (error) {
      console.log(error);
      return redirect("/");
    }
  }
  return redirect("/");
};

export default function Entry() {
  return (
    <>
      <div>新規登録</div>
      <Form method="post">
        <fieldset>
          <div>
            <label htmlFor="email">ユーザー名</label>
            <input type="email" name="email" id="email" />
          </div>
          <div>
            <label htmlFor="password">パスワード</label>
            <input type="password" name="password" id="password" />
          </div>
          <div>
            <button type="submit">新規登録</button>
          </div>
        </fieldset>
      </Form>
    </>
  );
}

Firebase Authentication でログインする

ログイン画面としてapp\routes\login\index.tsxを作成します。
ユーザー名とパスワードを入力してログインボタンを押すと、actionに POST されます。getAuth()の部分で、root.tsx で initialize した FirebaseApp に関連付けられた Auth インスタンスを返します。action内でユーザーのログインを行います。

index.tsx
app\routes\login\index.tsx
import { Form } from "@remix-run/react";
import type { ActionFunction } from "@remix-run/cloudflare";
import { redirect } from "@remix-run/cloudflare";

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const email = formData.get("email")?.toString();
  const password = formData.get("password")?.toString();

  if (email !== undefined && password !== undefined) {
    try {
      const firebaseAuth = getAuth();
      const userCredential = await signInWithEmailAndPassword(
        firebaseAuth,
        email,
        password
      );
      console.log("loginUser=", userCredential.user);
      return redirect("/top");
    } catch (error) {
      console.log(error);
      return redirect("/");
    }
  }
  return redirect("/");
};

export default function Login() {
  return (
    <>
      <div>ログイン画面</div>
      <Form method="post">
        <fieldset>
          <div>
            <label htmlFor="email">ユーザー名</label>
            <input type="email" name="email" id="email" />
          </div>
          <div>
            <label htmlFor="password">パスワード</label>
            <input type="password" name="password" id="password" />
          </div>
          <div>
            <button type="submit">ログイン</button>
          </div>
        </fieldset>
      </Form>
    </>
  );
}

ログインに成功した場合のみ遷移する画面としてapp\routes\top\index.tsxを作成しておきます。

index.tsx
app\routes\top\index.tsx
export default function Top() {
  return <div>トップページ</div>;
}

実行すると、、、

npm run devでローカルサーバーを起動します。
登録していないユーザーでログインしようとするとauth/user-not-foundというエラーが返ってきます。今の実装では/にリダイレクトされます。

code: 'auth/user-not-found',

新規登録画面からユーザーを登録するとaccessTokenなどのユーザー情報が返ってきます。今の実装では/loginに遷移します。また、Firebase Authentication には入力したメールアドレスが登録されます。

すでに登録されているユーザーで新規登録しようとするとauth/email-already-in-useというエラーが返ってきます。今の実装では/にリダイレクトされます。

code: 'auth/email-already-in-use',

登録されているユーザーでログインすると、accessToken等のユーザー情報が返ってきて/topに遷移します。

最後に

Remix & Cloudflare Pages の環境で環境変数を呼ぶのは特殊な作業が必要だったので、解消するまでに時間がかかりました。.tsx内に登録・ログイン処理を書きましたが、この部分は他ファイルに分けて、ロジックと表示の責任を分けるのがよいと思います。
本番環境で環境変数を使うには別途設定が必要なので、また記事にしようと思います。

Discussion