[Firebase] Firestoreでリアルタイムなチャットを作る (Web編) その2

前回、Firestoreを利用してリアルタイムな通信が行えるチャットを作成しましたが、今回はこのチャットに書き込みためにはログインが必要な状態に仕様変更したいと思います。

サンプル

実行結果

miku3.net

  • 書き込むためにはログインが必要です。閲覧は未ログイン状態でも行えます。
  • 自由に投稿していただいて問題ありませんが、公序良俗に反する書き込みはご遠慮ください。
  • 適当なタイミングでお掃除します。

ソースコード

config.js

Firebaseのコンソールで表示される内容をそのままコピペしconfig.jsというファイル名で保存しました。前回と同じです。

// コンソールの内容をそのままコピペ
var config = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "test-f76bc.firebaseapp.com",
  databaseURL: "https://test-f76bc.firebaseio.com",
  projectId: "test-f76bc",
  storageBucket: "test-f76bc.appspot.com",
  messagingSenderId: "1111111111111111"
};
firebase.initializeApp(config);

index.html

チャットの本体です。処理内容は前回とほぼ同じですが、発言を書き込むためのフォームをログインしないと表示しないようにしています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Firestore Chat2</title>
  <style>
      #chatlog{ width:520px; height:300px; border:1px solid gray; overflow-y:scroll; }
      #pleaselogin{ width:540px; border:1px solid gray; padding:10px; background-color:lightgrey; }
      #uname{ width:130px; float:left; margin-right:10px; padding-top:5px; text-align:center}
      #msg{ width:300px; height:30px; margin-right:10px; font-size:12pt;}
      #sbmt{ width:100px; height:30px; }
      .hide{display:none}
  </style>
</head>
<body>
  <h1>Firestore Chat2</h1>

  <!-- 発言が表示される領域 -->
  <ul id="chatlog"></ul>

  <!-- 入力フォーム -->
  <div id="pleaselogin">
    ※チャットへの書き込みは<a href="/db/firestore3/login.html">ログイン</a>が必要です。
  </div>
  <form id="form1" class="hide">
    <div id="uname"></div>
    <input type="text" id="msg"><button type="button" id="sbmt">送信</button>
  </form>

  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-firestore.js"></script>
  <script src="/js/config.js"></script>
  <script>
    /**
     * Chatオブジェクト
     **/
    var Chat = {
      //----------------------------------------
      // プロパティ
      //----------------------------------------
      user: {
        name: null
      },
      db: null,
      messagesRef: null,

      //----------------------------------------
      // メソッド
      //----------------------------------------
      /**
       * 初期処理
       **/
      init: () => {
        this.db = firebase.firestore();
        this.messagesRef = this.db.collection("chatroom").doc("room2").collection("messages");

        //---------------------
        // 同期処理
        //---------------------
        this.messagesRef.orderBy("date", "asc").limit(20).onSnapshot( (snapshot) => {
          snapshot.docChanges().forEach((change) => {
            // 追加
            if ( change.type === 'added' ) {
              Chat.addLog(change.doc.id, change.doc.data());
            }
            // 更新
            else if( change.type === 'modified' ){
              Chat.modLog(change.doc.id, change.doc.data());
            }
            // 削除
            else if ( change.type === 'removed' ) {
              Chat.removeLog(change.doc.id);
            }
          });
        });

        //---------------------
        // 送信ボタン
        //---------------------
        document.getElementById("sbmt").addEventListener("click", ()=>{
          let msg = document.getElementById("msg").value;
          if( msg.length !== 0 ){
            Chat.sendLog(msg);
          }
        });
        // submitイベントは(いったん)無視する
        document.getElementById("form1").addEventListener("submit", (e)=>{
          e.preventDefault();
        });
      },

      /**
       * Firestoreへ送信
       *
       * @param {string} str 送信内容
       **/
      sendLog: (str)=>{
        this.messagesRef.add({
          name: Chat.user.name,
          msg: str,
          date: new Date().getTime()
        })
        .then(()=>{
          let msg = document.getElementById("msg");
          msg.focus();
          msg.value = "";
        })
        .catch((error) => {
          console.log(`追加に失敗しました (${error})`);
        });
      },

      /**
       * 描画エリアにログを追加
       *
       * @param {string} id
       * @param {object} data
       **/
       addLog: (id, data)=>{
        // 追加するHTMLを作成
        let log = `${data.name}: ${data.msg} (${getStrTime(data.date)})`;
        let li  = document.createElement('li');
        li.id   = id;
        li.appendChild(document.createTextNode(log));

        // 表示エリアへ追加
        let chatlog = document.getElementById("chatlog");
        chatlog.insertBefore(li, chatlog.firstChild);
      },

      /**
       * 描画エリアのログを変更
       *
       * @param {string} id
       * @param {object} data
       **/
       modLog: (id, data)=>{
        let log = document.getElementById(id);
        if( log !== null ){
          log.innerText = `${data.name}: ${data.msg} (${getStrTime(data.date)})`;
        }
      },

      /**
       * 描画エリアのログを削除
       *
       * @param {string} id
       **/
       removeLog: (id)=>{
        let log = document.getElementById(id);
        if( log !== null ){
          log.parentNode.removeChild(log);
        }
      }
    };  // Chat


    /**
     * 描画エリアのログを変更
     *
     * @param {string} id
     * @param {object} data
     **/
    firebase.auth().onAuthStateChanged( (user) => {
      // ログイン状態なら書き込みフォームを開放
      if( user !== null ){
        //隠す
        document.getElementById("pleaselogin").classList.add("hide");

        //表示
        document.getElementById("chatlog").classList.remove("hide");
        document.getElementById("form1").classList.remove("hide");

        // ユーザー名を確保
        Chat.user.name = user.displayName;
        document.getElementById("uname").innerText = Chat.user.name;
      }

      // Firestore処理開始
      Chat.init();
    });

    /**
     * UNIX TIME => MM-DD hh:mm
     **/
     function getStrTime(time){
      let t = new Date(time);
      return(
        ("0" + (t.getMonth() + 1)).slice(-2) + "-" +
        ("0" + t.getDate()       ).slice(-2) + " " +
        ("0" + t.getHours()      ).slice(-2) + ":" +
        ("0" + t.getMinutes()    ).slice(-2)
      );
    }
  </script>
</body>
</html>

login.html

こちらでFirebaseのAuthを利用してログインを行います。今回はメールアドレスでの認証のみ利用しています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Firestore Chat3</title>
  <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css" />
  <style>#auth{text-align: center;}</style>
</head>
<body>
  <section id="auth">
    <h1>Firestore Chat3</h1>
    <p>ログインまたは新規登録をしてください</p>
    <div id="firebaseui-auth-container"></div>
  </section>

  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/ui/3.5.2/firebase-ui-auth__ja.js"></script>
  <script src="/js/config.js"></script>
  <script>
    //----------------------------------------------
    // Firebase UIの設定
    //----------------------------------------------
    var uiConfig = {
        // ログイン完了時のリダイレクト先
        signInSuccessUrl: '/db/firestore3/',

        // 利用する認証機能
        signInOptions: [
          firebase.auth.EmailAuthProvider.PROVIDER_ID  //メール認証
        ],

        // 利用規約のURL(任意で設定)
        tosUrl: 'http://example.com/kiyaku/',
        // プライバシーポリシーのURL(任意で設定)
        privacyPolicyUrl: 'http://example.com/privacy'
      };

      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
  </script>
</body>
</html>

解説

Firebase Authenticationで認証する

認証については過去の記事で取り上げてますので、詳しくは以下を参照ください。 blog.katsubemakito.net

Firestoreにセキュリティルールを設定する

Firebaseのコンソールにログインし、メニュー「Database」→「ルール」とたどるとデフォルトでは誰でも触れるような状態になっています(前回の記事でそのような設定をしたためですw)

これを以下のように書き換え、書き込みは認証済みのユーザーでないと行えないよう変更します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /chatroom/room2/messages/{document} {
      allow read:  if true;
      allow write: if request.auth.uid != null;
    }
  }
}

未ログイン状態で書き込んでみる

form要素に設定されているhideクラス

を削除すればフォームが閲覧可能になりますので、この状態で別のブラウザなどでアクセスします。

試しにテキストボックスに適当な文字列を入力し、送信ボタンを押すと…無事にPermission deniedのエラーが現れました。

FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

Firestoreはクライアントから直接操作することが可能ですが、このように不正な操作が行われることを念頭に置く必要があります。そのため実際のプロジェクトではこのセキュリティルールの設定が非常に重要になってきます。

一度に取得する件数を制限する

Firestoreはデータを操作した件数によって課金が発生しますし、何らかのバグなどで一度に大量にデータを取得されるとパフォーマンスにも大きく影響します。

というわけで、以下のように取得件数に対して制限をかけることができます。以下のように記述すると一度に3件を超えるリストを取得しようとするとエラーとなります。

service cloud.firestore {
  match /databases/{database}/documents {
    match /chatroom/room2/messages/{document} {
      allow get:  if true;
      allow list: if request.query.limit <= 3;
      allow write: if request.auth.uid != null;
    }
  }
}

無理矢理、3件を超えるデータを取得しようとすると以下のようなエラーが発生します。

FirebaseError: Missing or insufficient permissions.

権限readは上記のようにgetlistに分割して指定することが可能で、それぞれに個別の制限を設けることができます。

続き

blog.katsubemakito.net

参考ページ

firebase.google.com