firebaseのdatabaseのアクセス数制限

# 悪意ある攻撃対策

### ある日突然データが改ざんされた？？
運用していたテニスサークルのデータが改ざんされていました。
色々調べるとセキュリティが甘かった。
セキュリティは色々、触ったけど、firebaseは従量課金のため膨大な読み込みがなされた場合、費用が膨大になる可能性があります。
最悪の事態を回避するため、一日のアクセス数の上限を設定することとしました。
以下は忘備録です。



### functionsでの設定
アクセス数をdatabase のsystemコレクションのglobalAccessといドキュメントに保存して管理を行います。
コレクションとドキュメントは自動で生成されないので、手動で設定します。
ドキュメントには、lastResetTimeというTimestampのものと、requestCountというNumberのプロパティが必要です。
この仕組みでは、ドキュメントにアクセスされる度に、記録されるため結果的にアクセス数は二倍となります。

```:index.js
const cors = require("cors")({ origin: true });
const db = admin.firestore();

exports.limitFirestoreAccess = functions.https.onRequest(async (req, res) => {
  cors(req, res, async () => {
    try {
      const now = new Date();

      const globalRef = db.collection("system").doc("globalAccess");
      const doc = await globalRef.get();

      let requestCount = 0;
      let lastResetTime = now;

      if (doc.exists) {
        const data = doc.data();
        requestCount = data.requestCount || 0;
        lastResetTime = data.lastResetTime;

        if (lastResetTime) {
          const lastResetTimeDate = lastResetTime.toDate();
          const elapsedTime = now.getTime() - lastResetTimeDate.getTime();
          const oneHour = 60 * 60 * 1000;

          if (elapsedTime >= oneHour) {
            console.log("リセット条件を満たしたため、requestCount をリセット");
            requestCount = 0;
            lastResetTime = now;
          }
        }
      }

      if (requestCount >= 3000) {
        return res
          .status(429)
          .json({ error: "⚠️ Firestore のアクセスが制限されています！" });
      }

      requestCount += 1;
      console.log(`Updating access count: ${requestCount}`);

      await globalRef.set(
        {
          requestCount: requestCount,
          lastResetTime: now,
        },
        { merge: true }
      );

      res.status(200).json({ message: "✅ アクセス許可" });
    } catch (error) {
      console.error("Error updating access count:", error);
      res.status(500).json({ error: "内部サーバーエラー" });
    }
  });
});
```

上記はfunctionsにデプロイします。

##チェックする関数を作成

```:CheckAccessLimit.js
export const CheckAccessLimit = async () => {
  try {
    const response = await fetch(
      "https://us-central1-circle-base-a3abd.cloudfunctions.net/limitFirestoreAccess",
      {
        method: "GET",
        mode: "cors",
        headers: {
          "Content-Type": "application/json",
        },
      }
    );

    if (!response.ok) {
      throw new Error("Network response was not ok");
    }

    const contentType = response.headers.get("content-type");
    let data;

    if (contentType && contentType.includes("application/json")) {
      data = await response.json();
    } else {
      data = await response.text();
    }

    if (typeof data === "string") {
      console.log("Response is a string:", data);
    } else {
      console.log("Received JSON data:", data);
    }

    if (data.error) {
      console.error("members", data.error);
      return false;
    }

    return true;
  } catch (error) {
    console.error("アクセス制限の確認中にエラーが発生しました:", error);
    return false;
  }
};
```

##呼び出し
この例では、fetchEventData.jsでeventコレクションのデータを呼び出しています。

```:fetchEventData.js
// src/utils/fetchEventData.js
import { parseISO } from "date-fns";
import { collection, onSnapshot, query, where } from "firebase/firestore";
import { db } from "../firebase"; // Firebaseの設定がされているファイルからdbをインポート
import { convertDurationToMinutes } from "./convertDurationToMinutes";
import { CheckAccessLimit } from "../utils/CheckAccessLimit";

CheckAccessLimit();

export const subscribeToEventData = async (callback) => {
  try {
    const eventCollection = collection(db, "events");
    const canAccess = await CheckAccessLimit();
    if (!canAccess) {
      console.log("アクセス制限がかかっているため、データ取得を中止します。");
      alert(
        "アクセス数が上限のためアクセスができません。しばらくしてからアクセスしてください。"
      );
      return;
    }
    const unsubscribe = onSnapshot(eventCollection, (snapshot) => {
      const eventList = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(), // すべてのデータを取得
      }));

      if (eventList.length > 0 && eventList[0].jsonData) {
        const parseEventList = JSON.parse(eventList[0].jsonData);
        const eventId = eventList[0].id;
        callback(parseEventList, eventId); // idもコールバックに渡す
      } else {
        console.warn("No valid jsonData found in members collection.");
      }
    });

    return unsubscribe; // クリーンアップ用
  } catch (error) {
    console.error("Error subscribing to event data:", error);
    return () => {}; // 何も行わないクリーンアップ関数を返す
  }
};
```