🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Introducing roles and permissions to your React chat app

  • Lanre Adelowo
April 5th, 2019
You will need Node 8+ and create-react-app installed on your machine.

In this tutorial, I am going to introduce how we can allow granular user access control in a chat room with roles and permissions. For example, in a WhatsApp group, only the admin can delete the group while a regular user/member can’t. Building a chat application has been ridiculously simplified by Pusher Chatkit, communities such as a WhatsApp group can be built within minutes.

By default, every Chatkit instance comes with two sets of roles - default and admin. Each role comes with its own list of permissions, which specify the actions a user can take. For example, the admin role comes with room:delete permission while the default doesn’t. What this means in practice is only a user with an admin role can delete a Chatkit room.

You can always create a new role alongside its permissions using the dashboard and/or API

In this tutorial, I will describe how to build a chatroom where a member can add and/or delete other users to the room. I will also introduce roles and take advantage of that to enrich the UI. In the demo below, you will notice the first user has access to the Add User and Remove User buttons, that is because (s)he has admin access while the next user has a much more fine-grained role that doesn’t permit access to those functionalities.

Prerequisites

  • NodeJS >=8
  • create-react-app . You can install this with npm install -g create-react-app
  • A Pusher Chatkit account and instance.

Building the server

The first thing to do is to create a backend server that will handle user authentication with the Chatkit servers. To get started, create a new pusher-chatkit-roles-permission-app directory for this project. Next cd into it, and create another directory called server. In the server directory, run npm init -y to initialize the project with a package.json file. Following that, run the command below to install all the dependencies we’ll be making use of for building the application server:

    $ npm install express dotenv body-parser cors @pusher/chatkit-server --save

Once the dependencies have been installed, create a new variables.env file in the server root and add in the credentials retrieved from your Chatkit instance dashboard.

    // pusher-chatkit-roles-permission-app/server/variables.env
    CHATKIT_INSTANCE_LOCATOR=PUSHER_CHATKIT_INSTANCE_LOCATOR
    CHATKIT_SECRET_KEY=PUSHER_CHATKIT_SECRET_KEY

Once the above has been done, the next step is to create an index.js file that will contain the backend code. Paste the following code into the index.js file:

    // pusher-chatkit-roles-permission-app/server/index.js

    require('dotenv').config({ path: 'variables.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Chatkit = require('@pusher/chatkit-server');

    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: process.env.CHATKIT_INSTANCE_LOCATOR,
      key: process.env.CHATKIT_SECRET_KEY,
    });

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/users', (req, res) => {
      const { username } = req.body;

      chatkit
        .createUser({
          id: username,
          name: username,
        })
        .then(() => {
          res.sendStatus(201);
        })
        .catch(err => {
          if (err.error === 'services/chatkit/user_already_exists') {
            res.sendStatus(200);
          } else {
            res.status(err.status).json(err);
          }
        });
    });

    app.post('/authenticate', (req, res) => {
      const authData = chatkit.authenticate({
        userId: req.query.user_id,
      });
      res.status(authData.status).send(authData.body);
    });

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running on port ${server.address().port}`);
    });

We have two routes on the server: the /users route takes a userId and creates a Chatkit user while the /authenticate route is meant to authenticate each user that tries to connect to our Chatkit instance and respond with a token (returned by chatkit.authenticate) if the request is valid.

That’s literally all we need to do on the server side for now. You can start the server on port 5200 by running node index.js in the terminal.

Building the frontend app

As mentioned earlier, you will require create-react-app as the frontend app will be built with ReactJS.

    $ npm install -g create-react-app
    $ cd path/to/pusher-chatkit-roles-permission-app
    $ create-react-app client

The above command will create a directory called client that will contain the frontend code. Once it succeeds, you should cd into the client directory so as to install the additional dependencies we will need.

    $ npm install @pusher/chatkit-client axios --save

Once the dependencies have been installed, you will need to run npm run start to start the application. You can now visit localhost:3000.

Creating the Login component

Since we need to allow for authentication and identification of each user, the next thing to do is to build a login page, this page will be the default and only after authentication will the user be allowed to join the chatroom.

Create a new file called Login.js, this should be located in the src directory, except stated otherwise all files to be created henceforth should be in the src directory. In the newly created file, paste the following code:

    // pusher-chatkit-roles-permission-app/client/src/Login.js

    import React, { useState } from 'react';
    import Title from './Title';

    const Login = props => {
      const [input, setInput] = useState('');
      const [isLoginButtonDisabled, setLoginButtonClickStatus] = useState(false);

      return (
        <div className="app">
          <Title title={'Login page'} />

          <div className="login-box">
            <div className="login-form">
              <input
                type="text"
                value={input}
                onChange={e => setInput(e.target.value)}
                placeholder="Username"
              />
              <br />
              <button
                type="submit"
                onClick={() => {
                  setLoginButtonClickStatus(!isLoginButtonDisabled);
                  props.login(input);
                }}
                className="login-button"
                disabled={isLoginButtonDisabled}
              >
                Login
              </button>
              <br />
            </div>
          </div>
        </div>
      );
    };

    export default Login;

Perhaps the most interesting thing in the above snippet is the usage of React hooks instead of regular classes as they seem to be much cleaner and more terse. You might have also noticed that there is a line that says import Title from './Title';. That means you will also need to create a Title.js file. That can be done with

    $ touch Title.js

Once done, you will need to paste the following content in the Title.js file:

    // pusher-chatkit-roles-permission-app/client/src/Title.js

    import React from 'react';

    const Title = props => {
      return (
        <p className="title">
          {props.title.trim() ? props.title.trim() : 'Chat room'}
        </p>
      );
    };

    export default Title;

Connecting the Login component to the application

The next step will be to make sure the login component is the first page that is displayed on accessing localhost:3000. React already ships with a App.js file where the entire application is bootstrapped, so you will need to open that file, after which you can delete all of it’s content and replace it with the following:

    // pusher-chatkit-roles-permission-app/client/src/App.js

    import React from 'react';
    import './App.css';
    import Chat from './Chat';
    import Login from './Login';
    import axios from 'axios';

    class App extends React.Component {
      state = {
        username: '',
      };

      logIn = username => {
        if (username.trim().length === 0) {
          alert('Please provide your username');
          return;
        }

        axios
          .post('http://localhost:5200/users', { username })
          .then(res => {
            this.setState({ username });
          })
          .catch(err => {
            console.log(err);
            alert('We could not log you in');
          });
      };

      render() {
        if (this.state.username === '') {
          return <Login login={this.logIn} />;
        }

        return <Chat username={this.state.username} />;
      }
    }

    export default App;

As you can see on Line 32, we check for the value of this.state.username. If it is empty, we render the Login component so the user can authenticate against the backend server else we render the Chat component.

Building the Chat component

Your guess is right, a Chat component doesn’t exists yet so it will need to be created. You can go ahead to create a Chat.js file with the following command:

    $ touch Chat.js

In the newly created Chat.js file, paste the following code:

    // pusher-chatkit-roles-permission-app/client/src/Chat.js

    import React, { Component } from 'react';
    import { ChatManager, TokenProvider } from '@pusher/chatkit-client';
    import { SendMessageForm, MessageList } from './Message';
    import Title from './Title';
    import Popup from './Popup';
    import './App.css';

    const instanceLocator = 'PUSHER_CHATKIT_INSTANCE_LOCATOR';
    const ROOM_ID = 'PUSHER_ROOM_ID';

    class Chat extends Component {
      state = {
        messages: [],
        currentUser: null,
        showPopup: false,
        popUpText: '',
        handleInput: null,
      };

      componentDidMount() {
        const chatManager = new ChatManager({
          instanceLocator: instanceLocator,
          userId: this.props.username,
          tokenProvider: new TokenProvider({
            url: 'http://localhost:5200/authenticate',
          }),
        });

        chatManager
          .connect()
          .then(currentUser => {
            this.setState({ currentUser });

            this.state.currentUser.subscribeToRoom({
              roomId: ROOM_ID,
              hooks: {
                onMessage: message => {
                  this.setState({
                    messages: [...this.state.messages, message],
                  });
                },
              },
            });
          })
          .catch(err => {
            console.log(err);
          });
      }

      dismissModal = () => {
        this.setState({ showPopup: false });
      };

      deleteUserFromRoom = () => {
        const fn = username => {
          return this.state.currentUser.removeUserFromRoom({
            userId: username,
            roomId: ROOM_ID,
          });
        };

        this.setState({
          handleInput: fn,
          showPopup: true,
          popUpText: 'Remove a user from this room',
        });
      };

      addUserToRoom = () => {
        const fn = username => {
          return this.state.currentUser.addUserToRoom({
            userId: username,
            roomId: ROOM_ID,
          });
        };

        this.setState({
          handleInput: fn,
          showPopup: true,
          popUpText: 'Add a user to this room',
        });
      };

      sendMessage = text => {
        this.state.currentUser.sendMessage({
          text,
          roomId: ROOM_ID,
        });
      };

      render() {
        if (this.state.showPopup) {
          return (
            <Popup
              handleInput={this.state.handleInput}
              text={this.state.popUpText}
              dismiss={this.dismissModal}
            />
          );
        }

        return (
          <div className="app">
            <Title title={''} />
            {this.state.currentUser !== null && (
              <Header
                addUserToRoom={this.addUserToRoom}
                removeUserFromRoom={this.deleteUserFromRoom}
              />
            )}
            <MessageList
              roomId={this.state.roomId}
              messages={this.state.messages}
            />
            <SendMessageForm sendMessage={this.sendMessage} />
          </div>
        );
      }
    }

    const Header = props => {
      return (
        <p className="title">
          <button className="header-button" onClick={() => props.addUserToRoom()}>
            Add user
          </button>
          <button
            className="header-button"
            onClick={() => props.removeUserFromRoom()}
          >
            Remove user
          </button>
        </p>
      );
    };

    export default Chat;

Please replace PUSHER_CHATKIT_INSTANCE_LOCATOR and PUSHER_ROOM_ID with their original values. In the case of PUSHER_ROOM_ID, you will need to create a room via the console in the Chatkit dashboard and copy the ID of the room.

The next step is to create two more files, Message.js and Popup.js. That can be done with:

    $ touch Message.js Popup.js

Inside of the Message.js file, paste the following code:

    // pusher-chatkit-roles-permission-app/client/src/Message.js

    import React, { Component } from 'react';

    const MessageList = props => {
      return (
        <ul className="message-list">
          {props.messages.map((message, index) => {
            return (
              <li key={message.id} className="message">
                <div>{message.senderId}</div>
                <div>{message.text}</div>
              </li>
            );
          })}
        </ul>
      );
    };

    class SendMessageForm extends Component {
      constructor() {
        super();
        this.state = {
          message: '',
        };
      }

      handleChange = e => {
        this.setState({
          message: e.target.value,
        });
      };

      handleSubmit = e => {
        e.preventDefault();
        this.props.sendMessage(this.state.message);
        this.setState({
          message: '',
        });
      };

      render() {
        return (
          <form onSubmit={this.handleSubmit} className="send-message-form">
            <input
              onChange={this.handleChange}
              value={this.state.message}
              placeholder="Type your message and hit ENTER"
              type="text"
            />
          </form>
        );
      }
    }

    export { MessageList, SendMessageForm };

As for the Popup.js file, paste the following:

    // pusher-chatkit-roles-permission-app/client/src/Popup.js

    import React, { useState } from 'react';

    const Popup = props => {
      const [value, setValue] = useState('');
      const [disableButtons, setButtonState] = useState(false);
      const [errorMessage, setErrorMessage] = useState('');

      return (
        <div className="popup">
          <div className="popup_inner">
            <h1>{props.text}</h1>
            <p style={{ color: 'red' }}>{errorMessage}</p>
            <input
              type="text"
              value={value}
              placeholder={'user ID'}
              onChange={e => setValue(e.target.value)}
            />
            <button
              type="submit"
              onClick={() => {
                setButtonState(true);
                props
                  .handleInput(value)
                  .then(() => {
                    setValue('');
                    setButtonState(false);
                    setErrorMessage(''); // clear any previous error message
                    alert('Success');
                  })
                  .catch(err => {
                    let message = err.info.error_description;

                    if (
                      err.info.error ===
                      'services/chatkit_authorizer/authorization/missing_permission'
                    ) {
                      message =
                        "You don't have enough permissions to perform this action";
                    }

                    setErrorMessage(message);
                    setButtonState(false);
                    console.log(err);
                  });
              }}
              disabled={disableButtons}
            >
              Submit
            </button>
            <button
              style={{ backgroundColor: 'red' }}
              onClick={() => {
                props.dismiss();
              }}
              disabled={disableButtons}
            >
              Close modal
            </button>
          </div>
        </div>
      );
    };

    export default Popup;

A final step before you visit the application is to create some users and a role in the Chatkit dashboard. You will create a role called admin. As it’s name depict, this role will be given to only select users as it comes with a lots of permissions (thus powers).

Once you click the CREATE ROLE button, you will be able to select a list of permissions that will be attached to the role. As it is named admin, you should select all available permissions. As at the time of writing, there are 18 available permissions.

The next step is to select the role named default. Note that you don’t have to create it as it is created automatically on the creation of a Chatkit instance, it is also assigned to all users. You will need to toggle off some permissions mainly room:members:add and room:members:remove.

The last step is to create some sample users. You can create as much as you like as long as you assign the admin role to one of them. In my own case, I have assigned the admin role to the user identified by adelowo. To assign a role to a user, select the role ( in the case, admin), then click the ASSIGN ROLE TO A USER button, after which you will be presented with a screen that looks like:

You will also need to update App.css as the file is quite large, I am going to link to it instead. You should copy and paste the content into App.css. The file can be found here.

You can go visit localhost:3000 now to see the application in action. If it is not accessible, run npm run start again. The application should look like the following:

An admin user:

A default user:

As seen above, a regular user cannot add and/or remove a member in the chatroom. That is because the role assigned to the user doesn’t have room:members:add and room:members:remove.

Although this works, one more thing you need to do is to make sure only a user with the admin role can see the buttons to perform the action. This is purely from a user experience point of view. To do this, you will need to fetch the list of roles assigned to the user and check if the admin role is included, that information is then passed to the frontend which then hides or shows the buttons as needed.

You will need to update server/index.js. What we are trying to achieve is in the following snippet:

if (err.error === 'services/chatkit/user_already_exists') {
    chatkit.getUserRoles({ userId: username }).then(roles => {
      const adminRole = roles.find(permission => {
        return permission.role_name === ADMIN_ROLE;
      });

      // The frontend will get access to is_admin and hide the buttons if false
      res.status(200).json({ is_admin: adminRole !== undefined });
    });
    return;
  }

Now open server/index.js again and replace it’s content with the following:

    // pusher-chatkit-roles-permission-app/server/index.js

    require('dotenv').config({ path: 'variables.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Chatkit = require('@pusher/chatkit-server');

    const ADMIN_ROLE = 'admin';

    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: process.env.CHATKIT_INSTANCE_LOCATOR,
      key: process.env.CHATKIT_SECRET_KEY,
    });

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/users', (req, res) => {
      const { username } = req.body;

      chatkit
        .createUser({
          id: username,
          name: username,
        })
        .then(() => {
          res.status(201);
          res.send({ is_admin: false });
        })
        .catch(err => {
          if (err.error === 'services/chatkit/user_already_exists') {
            chatkit.getUserRoles({ userId: username }).then(roles => {
              const adminRole = roles.find(permission => {
                return permission.role_name === ADMIN_ROLE;
              });

              res.status(200).json({ is_admin: adminRole !== undefined });
            });
            return;
          }

          res.status(err.status).json(err);
        });
    });

    app.post('/authenticate', (req, res) => {
      const authData = chatkit.authenticate({
        userId: req.query.user_id,
      });
      res.status(authData.status).send(authData.body);
    });

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running on port ${server.address().port}`);
    });

Remember to stop and restart the server with node index.js.

In client/src/App.js, you will also need to replace Line 21 with the following code this.setState({ username, isUserAdmin: res.data.is_admin }); and Line 36 with <Chat isAdmin={this.state.isUserAdmin} username={this.state.username} />. Once done, App,js should look like this:

    // pusher-chatkit-roles-permission-app/client/src/App.js

    import React from 'react';
    import './App.css';
    import Chat from './Chat';
    import Login from './Login';
    import axios from 'axios';

    class App extends React.Component {
      state = {
        username: '',
        isUserAdmin: false,
      };

      logIn = username => {
        if (username.trim().length === 0) {
          alert('Please provide your username');
          return;
        }

        axios
          .post('http://localhost:5200/users', { username })
          .then(res => {
            this.setState({ username, isUserAdmin: res.data.is_admin });
          })
          .catch(err => {
            console.log(err);
            alert('We could not log you in');
          });
      };

      render() {
        if (this.state.username === '') {
          return <Login login={this.logIn} />;
        }

        return (
          <Chat isAdmin={this.state.isUserAdmin} username={this.state.username} />
        );
      }
    }

    export default App;

Finally, to actually make use of the is_admin value from the backend to hide or display the buttons, Chat.js should be updated. Before the return in Line 104, a new line should be added that contains the following const showHeader = this.state.currentUser !== null && this.props.isAdmin; after which Line 107 should be replaced with {showHeader && (. Once this is done, the entire Chat.js should look like this:

    // pusher-chatkit-roles-permission-app/client/src/Chat.js

    import React, { Component } from 'react';
    import { ChatManager, TokenProvider } from '@pusher/chatkit-client';
    import { SendMessageForm, MessageList } from './Message';
    import Title from './Title';
    import Popup from './Popup';
    import './App.css';

    const instanceLocator = 'PUSHER_CHATKIT_INSTANCE_LOCATOR';
    const ROOM_ID = 'PUSHER_ROOM_ID';

    class Chat extends Component {
      state = {
        messages: [],
        currentUser: null,
        showPopup: false,
        popUpText: '',
        handleInput: null,
      };

      componentDidMount() {
        const chatManager = new ChatManager({
          instanceLocator: instanceLocator,
          userId: this.props.username,
          tokenProvider: new TokenProvider({
            url: 'http://localhost:5200/authenticate',
          }),
        });

        chatManager
          .connect()
          .then(currentUser => {
            this.setState({ currentUser });

            this.state.currentUser.subscribeToRoom({
              roomId: ROOM_ID,
              hooks: {
                onMessage: message => {
                  this.setState({
                    messages: [...this.state.messages, message],
                  });
                },
              },
            });
          })
          .catch(err => {
            console.log(err);
          });
      }

      dismissModal = () => {
        this.setState({ showPopup: false });
      };

      deleteUserFromRoom = () => {
        const fn = username => {
          return this.state.currentUser.removeUserFromRoom({
            userId: username,
            roomId: ROOM_ID,
          });
        };

        this.setState({
          handleInput: fn,
          showPopup: true,
          popUpText: 'Remove a user from this room',
        });
      };

      addUserToRoom = () => {
        const fn = username => {
          return this.state.currentUser.addUserToRoom({
            userId: username,
            roomId: ROOM_ID,
          });
        };

        this.setState({
          handleInput: fn,
          showPopup: true,
          popUpText: 'Add a user to this room',
        });
      };

      sendMessage = text => {
        this.state.currentUser.sendMessage({
          text,
          roomId: ROOM_ID,
        });
      };

      render() {
        if (this.state.showPopup) {
          return (
            <Popup
              handleInput={this.state.handleInput}
              text={this.state.popUpText}
              dismiss={this.dismissModal}
            />
          );
        }

        const showHeader = this.state.currentUser !== null && this.props.isAdmin;

        return (
          <div className="app">
            <Title title={''} />
            {showHeader && (
              <Header
                addUserToRoom={this.addUserToRoom}
                removeUserFromRoom={this.deleteUserFromRoom}
              />
            )}
            <MessageList
              roomId={this.state.roomId}
              messages={this.state.messages}
            />
            <SendMessageForm sendMessage={this.sendMessage} />
          </div>
        );
      }
    }

    const Header = props => {
      return (
        <p className="title">
          <button className="header-button" onClick={() => props.addUserToRoom()}>
            Add user
          </button>
          <button
            className="header-button"
            onClick={() => props.removeUserFromRoom()}
          >
            Remove user
          </button>
        </p>
      );
    };

    export default Chat;

Visit http://localhost:3000 again and login with a user that doesn’t have the admin role. You will notice that the buttons are no longer visible since the user is not an admin.

Conclusion

In this tutorial, I have introduced roles and permissions in Chatkit in order to assign fine-grained permissions to users. I have also described how to build a chatroom that describes how to make use of roles and permission to limit users’ action.

As always, the code can be found on GitHub.

Clone the project repository
  • Chat
  • chatroom
  • JavaScript
  • React
  • Social
  • Social Interactions
  • Node.js
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.