前々回でFirestoreを利用してリアルタイムな通信が行えるチャットを、前回はユーザー認証を導入しました。
今回は自分が書き込んだログを削除する機能を追加してみます。
サンプル
実行結果
- 書き込むためにはログインが必要です。閲覧は未ログイン状態でも行えます。
- 自分が書き込んだログは背景がピンク色になり、クリックすると削除できます。
- 自由に投稿していただいて問題ありませんが、公序良俗に反する書き込みはご遠慮ください。
- 適当なタイミングでお掃除します。
ソースコード
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.
セキュリティルールの設定を行っていない場合、他の人のデータも消したい放題ですので必ずルールが正しく設定されたかチェックすることをおすすめします。