🚅

React で Firebase の管理画面を最速で実装する

2021/02/06に公開

これは何

Firebaseをバックエンド としたアプリ開発をしています。
Firebase console からFirestoreを直接触ってデータの管理をしていたのですが、関連するデータを同時に編集しづらい、データの型を都度選択する必要があり間違えて登録してしまうなどの課題が見えてきたのでReactを使って管理画面を実装しました。
その時に行ったことをまとめました。

権限管理

管理画面からだとユーザが使うよりも多くのデータを変更する必要があります。
そのため、Adminユーザと他のユーザの権限を変えられるようにFirestoreのセキュリティルールを設定します。
以下のように設定すると examples のデータはログイン会員であれば読み込むことができ、Adminユーザだけ書き込みと削除ができるようになり権限管理を実現することができました。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isAdmin() {
      return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
    }
    match /examples/{id} {
      allow read: if request.auth != null;
      allow create, update, delete: if isAdmin();
    }
  }
}

もっと複雑な権限を持たせたい場合は、他のadminsのようなコレクションを追加したり、admins のコレクションに権限のフィールドを追加して実現できます。

FirebaseAuthenticationでログインする

まずはライブラリの追加を行います

create-react-app examples-app
yarn add typesciprt firebase

Firebase Auth を使ったログイン処理を実装する

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

const firebaseApp = firebase.initializeApp({
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
});

export const login = async (email: string, password: string, history: any) => {
  await firebaseApp.auth().signInWithEmailAndPassword(email, password);
}

Firestoreのデータを読み書きする

FirestoreのCLUD処理を行います

export const firebaseDb = firebase.firestore();

export const fetchAll = async (): Promise<Array<any>> => {
  const colRef = firebaseDb.collection("examples");
  const snapshots = await colRef.get();
  return snapshots.docs;
};

export const save = async (data: any) => {
  return firebaseDb.collection("examples").doc(data.id).set(data);
};

export const remove = async (id: string) => {
  return firebaseDb.collection("examples").doc(id).delete();
};

TypeScriptで型の誤りが起きないようにする

FirestoreのCLUD時に型を定義しておくと謝ったデータが登録されるのを防ぐことができます。

セキュリティルールで設定をしてもいいですが今回はTypeScriptでやってみます。

export interface Example {
  title: string;
  description: string;
  order: number;
}

const toExample = (doc: firestore.DocumentSnapshot<any>): Example => {
  const data = doc.data();
  return {
    id: doc.id,
    title: data.title,
    description: data.description,
    order: data.order,
  };
};

const toFirestoreDocument = (example: Example): any => {
  return {
    title: example.title,
    description: concept.description,
    order: concept.order,
  };
};

export const fetchAll = async (): Promise<Example[]> => {
  const colRef = firebaseDb.collection("examples");
  const snapshots = await colRef.get();
  return snapshots.docs.map((doc) => toExample(doc));;
};

export const create = async (example: Example) => {
  const entity = toFirestoreDocument(example);
  return firebaseDb.collection("examples").doc().set(entity);
};

export const remove = async (id: string) => {
  return firebaseDb.collection("examples").doc(id).delete();
};

Reactから作成したメソッドを呼び出す

あとは作成したメソッドをReactのコンポーネントから呼び出せば完成です。

ログイン画面

import React, { useContext } from "react";
import { RouteComponentProps, withRouter } from "react-router";
import { AuthContext } from "../../login";

interface Props extends RouteComponentProps<{}> {}

const Login = (props: Props) => {
  const { login } = useContext(AuthContext);

  const handleSubmit = (event: any) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    login(email.value, password.value, props.history);
  };

  return (
    <>
      <h2>ログイン</h2>
      <form onSubmit={handleSubmit}>
        <label>
          <input name="email" type="email" placeholder="メールアドレス" />
        </label>
        <label>
          <input name="password" type="password" placeholder="パスワード" />
        </label>
        <button type="submit" className={"primary"}>
          ログイン
        </button>
      </form>
    </>
  );
};

export default withRouter(Login);

Firestoreへの書き込み

import React, { useContext } from "react";
import { RouteComponentProps, withRouter } from "react-router";
import { create } from "./model";

interface Props extends RouteComponentProps<{}> {}

const Example = (props: Props) => {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [order, setOrder] = useState("");

  const handleSubmit = async (event: React.MouseEvent<HTMLInputElement>) => {
    event.preventDefault();
    await create({
      title,
      description,
      order: parseInt(order),
    });
  };

  return (
    <>
      <h2>説明</h2>
      <form onSubmit={handleSubmit}>
        <label>
          <input name="title" placeholder="タイトル" />
        </label>
        <label>
          <input name="description" placeholder="説明" />
        </label>
        <label>
          <input name="order" placeholder="順番" />
        </label>
        <button type="submit">
          登録
        </button>
      </form>
    </>
  );
};

export default withRouter(Login);

まとめ

アプリの運用をしていくと、Firebase console だけだとデータの不整合が起きたり、痒い所に手が届かないようになってくるので必要に応じてAdmin機能を作ると運用が楽になります。

Adminの作成には導入には少し時間がかかりますがReact x TypeScript でやっておくと安全に開発をすることができるのでおすすめです

Discussion