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

前々回で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

チャットの本体です。大まかな変更点は以下です。

  • Firestoreへ書き込む際にFirebase AuthenticationのユーザーIDを記録 (Chat.sendLogCreate())
  • Firestoreから発言を削除するメソッドを追加(Chat.sendLogRemove())
  • 画面に発言を描画する際にユーザー情報を埋め込みつつ、イベントを定義(Chat.addLog())
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Firestore Chat3</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}

      .chatlog-item{cursor:default;}
      .chatlog-item-mine{background-color:lightpink; cursor:pointer;}
  </style>
</head>
<body>
  <h1>Firestore Chat3</h1>

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

  <!-- 入力フォーム -->
  <div id="pleaselogin">
    ※チャットへの書き込みは<a href="/db/firestore4/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: {
        uid: null,
        name: null
      },
      db: null,
      messagesRef: null,

      //----------------------------------------
      // メソッド
      //----------------------------------------
      /**
       * 初期処理
       **/
      init: () => {
        this.db = firebase.firestore();
        this.messagesRef = this.db.collection("chatroom").doc("room3").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.sendLogCreate(msg);
          }
        });
        // submitイベントは(いったん)無視する
        document.getElementById("form1").addEventListener("submit", (e)=>{
          e.preventDefault();
        });
      },

      /**
       * [Firestoreへ送信] ログを追加
       *
       * @param {string} str メッセージ
       **/
      sendLogCreate: (str)=>{
        this.messagesRef.add({
          name: Chat.user.name,
          msg: str,
          date: new Date().getTime(),
          uid: Chat.user.uid
        })
        .then(()=>{
          let msg = document.getElementById("msg");
          msg.focus();
          msg.value = "";
        })
        .catch((error) => {
          console.log(`追加に失敗しました (${error})`);
        });
      },
      /**
       * [Firestoreへ送信] ログを削除
       *
       * @param {string} id
       **/
       sendLogRemove: (id)=>{
        this.messagesRef.doc(id).delete()
        .then(()=>{
          console.log(`削除しました (${id})`);
        })
        .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;                                 // id属性を作成
        li.dataset.uid = data.uid;                    // data-uid属性を作成
        li.classList.add( (data.uid===Chat.user.uid)? "chatlog-item-mine":"chatlog-item");
        li.appendChild(document.createTextNode(log));

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

        // 削除機能をセット
        document.getElementById(id).addEventListener("click", (e)=>{
          let uid = e.target.dataset.uid;
          let id  = e.target.id;

          if( uid === Chat.user.uid && confirm("削除しますか?") ){
            Chat.sendLogRemove(id);
          }
        });
      },

      /**
       * 描画エリアのログを変更
       *
       * @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.uid  = user.uid;
        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

このファイルは前回からの大きな変更はありません。

<!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/firestore4/',

        // 利用する認証機能
        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>

解説

Firestoreにセキュリティルールを追加する

Firebaseのコンソールにログインし、メニュー「Database」→「ルール」とたどり、前回は最終的に以下のような設定を行いました。

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

これを今回は以下のように書き換えます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /chatroom/room3/messages/{document} {
      allow get:  if true;
      allow list: if request.query.limit <= 20;
      allow create: if request.auth.uid != null;
      allow update, delete: if request.auth.uid == resource.data.uid;
    }
  }
}

readと同様にwriteも細かくばらして定義を行うことができます。ここでは以下のような定義を行いました。

  • 新規に作成する際(create)には認証済みであること
  • 更新(update)と削除(delete)は自分自身が作成したレコードであること

requestオブジェクトはFirestoreへの外部から来たリクエストを指し、resourceオブジェクトは操作対象とするFirestore内のデータを指します。ここではresource.data.uidとしていますが、これはuidという名前でドキュメント内にフィールドを用意しているためです。

他者の発言を消そうとしてみる

index.htmlの以下のif文を変更すれば自分以外の発言に対しても、Firestoreへ削除を命じることが可能になるので試しに実行してみます。

// befor
if( uid === Chat.user.uid && confirm("削除しますか?") ){

// after
if( confirm("削除しますか?") ){

すると、正常にルールの設定が行われていれば以下のようにPermission denied的なエラーとなります。

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

セキュリティルールの設定を行っていない場合、他の人のデータも消したい放題ですので必ずルールが正しく設定されたかチェックすることをおすすめします。

続き

blog.katsubemakito.net

参考ページ

firebase.google.com