auhtor_image
Volodymyr Terebus

Front End Development Practice Lead

Chat System: How to Manage Conversation

Our goal for this project is to provide a chat system for visitors of the website so they can be easily assisted by the website’s owner in our support department. It is essential for the website’s owner to have the ability to customize the chat widget and have a powerful integration of support in the back office with all visitors and user’s information from the website’s database. This means that any of our chat system customers will have a customizable widget for their websites and their own desk/back office that integrates with their own services.

Overview

In general, all messages should be related to a conversation entity. Let’s say, for example, a room. The room contains all messages and all participants of the conversation.

There are two ways to create a room:

  • The operator manually initializes conversation with the visitor who has no open rooms.
  • The visitor initializes an open conversation and one of the free operators joins in the room.

There is only one way to close a room:

  • The operator has permission to close the conversation, or room. Any new messages must then be started in a new room.

It is possible that some conversations will take a long time. In this case, the visitor will have one room open through all sessions of conversation until the room is closed by the operator.

There are three collections in Firebase Realtime Database. These are:

  1. Room-Metadata – All rooms are stored with ID, state and participants.
  2. Room-Messages – All messages are stored as per the room ID.
  3. Users – All users with an active room or who are online are stored.
Power of Firebase Rules

The primary and advantageous feature of this database is its ability to define strict access to each field of data. For example, non-participants in a particular conversation will not have any access to read the messages in that conversation and only operators or administrators are able to close a conversation.

See examples of these rules below:

{
  // By default, make all data private unless specified otherwise.
  ".read": false,
  ".write": false,
  "room-metadata": {
    ".read": "auth.token.level>1",
    ".write": "auth.token.level>1",
    "$roomId": {
      // Append-only by anyone, and admins can edit or remove rooms as well.
      ".write": "auth != null && (!data.exists() || auth.token.level>1 || data.child('createdByUserId').val()===auth.token.uid)",
      ".read": "data.child('createdByUserId').val()===auth.token.uid || data.child('authorizedUsers').hasChild(auth.token.uid)",
      "id": {
        ".validate": "newData.val() === $roomId"
      },
      "createdByUserId": {
        ".validate": "auth.token.uid === newData.val() || data.val() == newData.val()"
      },
      "isClosed": {
        ".validate": "newData.isBoolean() || !newData.exists()"
      },
      "isWaiting": {
        ".validate": "newData.isBoolean() || !newData.exists()"
      },
      "authorizedUsers": {
        // A list of users that may read messages from this room.
        ".write": "auth != null && (!data.exists() || auth.token.level>1 || data.hasChild(auth.token.uid))"
      },
      "$other": {
        ".validate": false
      }  
    }
  },
  "room-messages": {
    "$roomId": {
      // A list of messages by room, viewable by authorized users
      ".read": "root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.token.uid) || auth.token.level>1",
      "$msgId": {
        // Allow anyone to append to this list and allow admins to edit or remove.
        ".write": "root.child('room-metadata').child($roomId).child('authorizedUsers').hasChild(auth.token.uid))",
        "userId": {
          ".validate": "newData.val()===auth.token.uid || !newData.exists()"
        },
        "name": {
          ".validate": "newData.isString() || !newData.exists()"
        },
        "timestamp": {
          ".validate": "newData.isNumber() || !newData.exists()"
        },
        "message": {
          ".validate": "newData.isString() || !newData.exists()"
        },
        "$other": {
          ".validate": false
        }
      }
    }
   },
  "users": {
    // A list of users and their associated metadata
    // can be updated by the single user or a operator.
    ".write": "auth.token.level>2",
    ".read": "auth.token.level>1",
    "$userId": {
      ".write": "$userId===auth.token.uid || auth.token.level>1",
      ".read": "$userId===auth.token.uid || auth.token.level>1",
      "id": {
        ".validate": "$userId === newData.val()"
      },
      "name": {
        ".validate": "newData.isString() || !newData.exists()"
      },
      "$other": {
        ".validate": false
      }
    }
  },
  "$other": {
    ".validate": false
  }
}

Conclusion

Firebase rules are powerful and vital. They give you the chance to define very strict security protocols for any part of data. In our case, we defined all fields strictly and in their types. The rules also define the access to read and write according to the user role (See Part 1 for more information about roles) and according to the already existing part of data.

Cons
  • Takes some time to design the scheme of data properly.
  • Is not as powerful as the usual imperative rules defined by server-side codes.
Pros
  • It is simple.
  • Does not need important server-side codes to handle access.

In the next article, we show you how to design a JavaScript library.

References

https://firebase.google.com/docs/database/security/

Chat System: Firebase, Authentication and Roles (Part One)