Coding Tutorial: Build a Secure Chat App with React, Firebase, and Permit.io
- Share:
All of the code in this article is available in this Git repo
Solid security is essential when building a chat application, especially one involving real-time data and sensitive information. Managing which users have access to which parts of your application requires implementing an authorization model, in this particular case, one based on the relationships between users and resources with varying user privileges.
This guide shows you how to create a secure chat app using React for the user interface, Firebase for real-time data management and authentication, and Permit to implement fine-grained authorization with Relationship-Based Access Control. You’ll learn how to set up different user roles, from moderators with complete control over chat rooms to regular users who can only send and view messages.
Whether new to Permit or experienced with Firebase, this guide will walk you through step-by-step instructions for building a secure, real-time chat application.
Tool Overview
Before we start, here’s a quick overview of the tools we will be using. We will use React to handle the front end for building the interactiveness of our chat applications, Firebase for user authentication and real-time message storage, and Permit to help us manage our ReBAC-based permissions, controlling who can send, receive, or moderate messages. These tools will help us create a secure, dynamic chat app.
Setting up the project
I’ve already created a project demo on GitHub with all the boilerplate code you need to start. You can clone the project by running the following command:
git clone <https://github.com/timonwa/react-chat-permitio-firebase.git>
Once you have cloned the project, navigate to the project directory and install the dependencies:
cd react-chat-app
npm install
This script will install all the necessary dependencies for the project.
Setting up Firebase
Firebase is a backend-as-a-service (BaaS) owned by Google that provides a real-time database and other services like authentication, storage, and hosting. We’ll use Firebase to handle real-time data for our chat application and to authenticate users.
Creating a Firebase project
To set up Firebase for your application, you need to set up a Firebase project to handle real-time data for the chat application. If you don’t have a Firebase account, you can create one by going to the Firebase Console. Once you have created an account, you can create a new project by clicking on the “Go to console” button at the top right corner of the page.
On the next page, click the “Get started with a Firebase project” card if this is your first time using Firebase, or click the “Create a project” card if you already have an existing project.
Enter the name of your project and click “Continue.” You can enable Google Analytics for your project if you'd like. Then, click “Create project” to finalize and create your new Firebase project.
Once the project is ready, you will be redirected to the project dashboard. Click the “Web” icon to add a web app to your project.
Enter a name for your web app and click “Register app.” You will be provided with a snippet of code that you can use to initialize Firebase in your project.
Copy the configuration object from the snippet and store it somewhere safe. Later, we will use this configuration object to initialize Firebase in our React application.
Setting up Firebase Authentication
Firebase offers different authentication methods for users to sign in to your application. For this project, we will use Google as the sign-in method. In the sidebar of the Firebase console, click on the “Build” dropdown, select “Authentication” from the options, and then click on the “Get Started” button to set up authentication for your project.
In the “Sign-in method” tab, click on the “Google” sign-in method and enable it. This allows users to sign in to your application using their Google account. Choose your project support email and click “Save.”
Setting up a Firestore Database
Firestore is a cloud-hosted NoSQL database that allows you to store, sync, and query data in real-time. We’ll use Firestore to store chat messages and user information for our chat application. Firestore offers real-time listeners that enable us to subscribe to changes in the database, ensuring that data updates automatically reflect in the UI without needing to refresh.
To set up Firestore, click on the “Build” dropdown in the Firebase console sidebar, select “Firestore Database” from the options, and then click on the “Create database” button.
Choose a location for your database. Select the closest location to your users and click “Next.” Note that once set, the location cannot be changed again
Next, you must choose between “production” and “test” mode for security. The “test” mode allows open read / write access to your database for 30 days, while the “production” mode secures the database from unauthorized access. For this tutorial, we’ll start with “production” mode and adjust the security rules later.
After creating the database, navigate to the Firestore dashboard and click on the “Rules” tab to set up security rules for your database.
You should update these rules to allow only authenticated users to read and write data. Use the following rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Now that the rules are in place, Firestore will only allow authenticated users to access the database.
Collections and Documents in Firestore
Firestore organizes data into collections and documents. Each collection contains documents, and each document can store fields and subcollections. For our chat application, we will use collections to store chat messages and rooms.
Firestore also allows you to listen to real-time updates from the database. By subscribing to a Firestore collection or document, you can automatically receive updates whenever the data changes. For instance, you can set up a listener on the chat messages collection to ensure that new messages appear immediately in the chat room without manual refreshes.
Here’s a quick example of subscribing to changes in Firestore using a listener:
import { collection, onSnapshot } from "firebase/firestore";
import { db } from "./firebase";
const messagesRef = collection(db, "messages");
onSnapshot(messagesRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
console.log(doc.data());
});
});
With this setup, your chat app will instantly reflect new messages as they are added to the Firestore database.
Integrating Firebase with the React Chat App
Installing Firebase SDK
To integrate Firebase, the first thing to do is to install the Firebase SDK and initialize it inside your app. You can do this by running the following command:
npm install firebase react-firebase-hooks
If you are using the setup I provided, you can skip the installation of the Firebase package, as I have included in the project.
Next, create a file in the src
folder called firebase.js
, add the following code below, and replace the firebase config with the configuration object you got from the Firebase console:
import { initializeApp } from "firebase/app";
import {
browserLocalPersistence,
getAuth,
setPersistence,
} from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
setPersistence(auth, browserLocalPersistence)
.then(() => {
console.log("Firebase auth persistence set to localStorage.");
})
.catch((error) => {
console.error("Error setting Firebase persistence:", error);
});
Here, setPersistence
ensures that the user’s authentication state is persisted in localStorage
, allowing users to stay signed in across page navigation and browser refreshes, improving the app’s usability without needing re-authentication.
Setting up Authentication
Let’s use Firebase authentication to update the sign-in and sign-out functions in the NavBar
component. When users click the “Sign in with Google” button, they will be redirected to the Google sign-in page to access the application. Additionally, when they click the “Log out” button, they will be signed out of the application.
import { auth } from "../firebase";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
const NavBar = ({ chatRoomId }) => {
const [user] = useAuthState(auth);
const googleSignIn = async () => {
const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider).then((result) =>
console.log(result)
);
} catch (error) {
console.log(error);
}
};
const signOut = () => {
auth.signOut();
};
return (
...
)
};
The above code uses the GoogleAuthProvider
class to create a new instance of the Google authentication provider. When the user clicks on the “Sign in with Google” button, the signInWithPopup
function is called with the authentication provider as an argument. This opens a pop-up window where users can sign in with their Google account.
When the user clicks the “Log out” button, the signOut
function is called, which signs the user out of the application. The useAuthState
hook is used to get the current user’s authentication state. If the user is signed in, the “Log out” button is displayed; if the user is signed out, the “Sign in” button is displayed.
The googleSignIn
function in the Welcome.js
file can also be updated to use Firebase authentication. The code for the googleSignIn
function is the same as the code in the NavBar.js
file.
Next is the App.js
file in the src
folder. Update the code to include the following:
import { useEffect } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
return () => unsubscribe();
}, []);
This code listens for changes in the authentication state and updates the user state accordingly. When the authentication state changes (i.e., when the user signs in or signs out), the onAuthStateChanged
function is called with the auth
object and a callback function as arguments. The callback function receives the current user as a parameter and updates the user state with the current user.
The user
state is then used to determine which pages to display based on the user’s authentication state. If the user is signed in, the chat room page is displayed; if the user is signed out, the welcome page is displayed.
Integrating Permit for Relationship-Based Access Control
Permit.io provides an easy way to implement relationship-based access control (ReBAC) in Firebase by giving us a way to define access control rules based on relationships between users and resources. In our chat application, we use Permit to control access to chat rooms based on the relationships between users and specific rooms.
To get started with Permit, you first need to create an account or log in at http://app.permit.io/. Then set up a new workspace for your project. Once you’re logged in, create a project where you’ll define the access policies.
A Permit policy defines how permissions are granted. It follows a hierarchical structure where you create resources, assign actions to those resources, and then define roles that determine what actions users can perform.
Access Control Design
For our chat application, we’ll define:
- A resource called “room”.
- Three roles, each granting distinct levels of access based on a user’s relationship with the room:
- Viewer (default upon signup): Can read messages only.
- Member: Can read, create, and update messages in the rooms they’ve joined.
- Moderator: Can read, create, update, and delete any message in the rooms they moderate.
Upon signing up, a user is assigned the Viewer role, which allows read-only access across all chat rooms. However, when a user joins a room as a Member or Moderator, their role-specific permissions apply solely to that room. This design ensures that permissions are specific to each chat room and that users are granted additional permissions only when added to a room in an elevated role.
Below is a diagram outlining how the Permit’s policy structure works for our chat app:
Looking at the diagram, we can see that this particular user has different roles in different rooms. They are a Viewer in Room 1, a Member in Room 2, and a Moderator in Room 3. This demonstrates how Permit allows for fine-grained access control based on the relationships between users and resources.
Step-by-Step Permit Setup
Navigate to the Resource tab: Go to the Policy tab in Permit’s sidebar, click “Resources”, and then click “Create a Resource”.
Create a Resource: Enter the name of the resource (“room”) and assign the actions (“read”, “create”, “update”, “delete”). Scroll down to the “ReBAC Options” to define the roles for each resource (“moderator”, “member”), then click “Save”.
View Created Roles: Navigate to the Roles tab to view all the roles for your application. The default roles (admin, editor, user) were created by Permit and are not necessary for this tutorial. We will focus on the instance roles tied directly to chat room resources. Note that Permit includes default roles such as admin, editor, and viewer based on Role-Based Access Control (RBAC).
Assign Policies: After setting up the resources and roles, you can assign policies that control which users can perform specific actions based on their role in the chat rooms. This ensures that, for example, a member can post messages but cannot delete them, while a moderator can do both.
Create Instances of the Resource: Create instances of the room resource, where each instance represents a chat room in which users can interact. Click on “Directory” in the sidebar and switch to the “Instances” tab, then click on “Add Instance.” Choose the resource you created earlier and give it a key (e.g., the names of the different chat rooms you might have), then select the tenant to which it belongs.
Now that you’ve set up Permit with resources, roles, and instances, it will enforce these rules, restricting actions based on each user’s relationship with the chat rooms.
Setting up Permit in the Chat Application
Permit offers multiple ways to integrate with your application, including using the Permit SDK and via the Cloud PDP (Policy Decision Point) or Container PDP. For this tutorial, we’ll use the Container PDP.
To use the Permit Container PDP, you must install Docker on your system and pull the Permit PDP container image from Docker Hub into your codebase. Run the following command to pull the container:
docker pull permitio/pdp-v2:latest
Then start the Permit PDP container using the command below:
docker run -it -p 7766:7000 --env
PDP_DEBUG=True --env
PDP_API_KEY=<YOUR_API_KEY>
permitio/pdp-v2:latest
The Permit PDP container now runs on port 7766, allowing your application to communicate with it. Remember to replace <YOUR_API_KEY>
with your Permit API key. You can obtain it by clicking “Projects” in the Permit dashboard sidebar, navigating to the project you are working on, clicking the three dots, and selecting “Copy API Key”.
Creating API Endpoints with Firebase Cloud Functions
We need to create a few API endpoints to manage access and roles in our chat application using Permit. These endpoints will handle user authorization and perform tasks like checking permissions and syncing users. We’ll use Firebase Cloud Functions for this.
Step 1: Set Up Firebase Functions
First, ensure you have Firebase set up in your project. If not, run the following command to initialize Firebase in your project:
firebase init
Choose the Functions option and follow the prompts to set up Cloud Functions for your Firebase project. After initialization, you’ll see a new functions
directory in your project.
Step 2: Install Permit and Get API Key
Next, navigate into the Firebase functions directory and install the Permit library by running:
cd functions
npm install permitio
Step 3: Create Firebase Cloud Functions
Next, let’s create functions to sync users with Permit after they sign up, check a user’s permission on Permit, allow certain users to send messages in the chat rooms, and allow only moderators to delete messages. Navigate to the functions
directory and open the index.js
(or index.ts
if using TypeScript) file. Clear the content and paste the following code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { Timestamp } = require("firebase-admin/firestore");
const Permit = require("permitio").Permit;
admin.initializeApp();
const permit = new Permit({
token: "YOUR_API_KEY"
pdp: "<http://localhost:7766>",
});
Here, we import the necessary modules and initialize the Firebase Admin SDK. We also initialize Permit with the Permit key and the Policy Decision Point (PDP) URL.
// Cloud Function to sync user to Permit.io
exports.syncUser = functions.auth.user().onCreate(async (user) => {
const newUser = {
key: user.uid,
email: user.email,
};
try {
// Sync the user to Permit
await permit.api.createUser(newUser);
await permit.api.assignRole({
role: "viewer",
tenant: "default",
user: user.uid,
});
console.info("User synced successfully to Permit");
} catch (error) {
console.error("Error syncing user to Permit. ==> ", error);
}
});
In the syncUser
Cloud Function, we listen for new user sign-ups. When a new user signs up, we sync the user to Permit and assign the user the viewer role, which is the default role. The createUser
method creates a new user in Permit, while the assignRole
method assigns a role to the user.
// Cloud Function to check user permission
exports.checkPermission = functions.https.onCall(async (data) => {
const { userId, operation, key } = data;
// Validate input parameters
if (!userId || !operation || !key) {
throw new functions.https.HttpsError(
"invalid-argument",
"Missing required parameters."
);
}
try {
// Check the user's permission for the operation in the specified room key
const permitted = await permit.check(userId, operation, {
type: "room",
key,
tenant: "default",
});
if (permitted) {
console.info("User is PERMITTED for the operation");
return { permitted: true };
} else {
console.info("User is NOT PERMITTED for the operation");
return { permitted: false };
}
} catch (error) {
throw new functions.https.HttpsError(
"internal",
"Error occurred while checking user permission."
);
}
});
Next, we create a Cloud Function that checks users’ permissions to operate in a chat room. The function takes the user ID, operation, and room key as input parameters. It then calls the check
method of Permit to check if the user is permitted to operate in the specified room.
// Cloud Function to send a message
exports.sendMessage = functions.https.onCall(async (data, context) => {
const { chatRoomId, message } = data;
// Check if the user is authenticated
if (!context.auth) {
throw new functions.https.HttpsError(
"unauthenticated",
"Request not authenticated."
);
}
const user = context.auth;
try {
// Check if the user has permission to send a message
const isPermitted = await permit.check(user.uid, "create", {
type: "room",
key: chatRoomId,
tenant: "default",
});
if (!isPermitted) {
throw new functions.https.HttpsError(
"permission-denied",
"User does not have permission to send messages in this chat room."
);
}
// Add the message to Firestore if permitted
await admin.firestore().collection(`chatRooms/${chatRoomId}/messages`).add({
text: message,
name: user.token.name,
avatar: user.token.picture,
createdAt: Timestamp.now(),
uid: user.uid,
});
return { success: true, message: "Message sent successfully." };
} catch (error) {
console.error("Error sending message:", error);
throw new functions.https.HttpsError(
"internal",
"Error occurred while sending the message."
);
}
});
The sendMessage
Cloud Function allows users to send messages in chat rooms. The function takes the chat room ID and message as input parameters. It first checks if the user is authenticated and then verifies if the user has permission to send a message in the specified chat room. If the user is permitted, the function adds the message to the Firestore database.
// Cloud Function to delete a message
exports.deleteMessage = functions.https.onCall(async (data, context) => {
const { chatRoomId, messageId } = data;
// Check if the user is authenticated
if (!context.auth) {
throw new functions.https.HttpsError(
"unauthenticated",
"Request not authenticated."
);
}
const userId = context.auth.uid;
try {
// Check if the user is permitted to delete the message
const isModerator = await permit.check(userId, "delete", {
type: "room",
key: chatRoomId,
tenant: "default",
});
if (isModerator) {
await admin
.firestore()
.doc(`chatRooms/${chatRoomId}/messages/${messageId}`)
.delete();
return { success: true, message: "Message deleted successfully." };
} else {
throw new functions.https.HttpsError(
"permission-denied",
"User does not have permission to delete this message."
);
}
} catch (error) {
throw new functions.https.HttpsError(
"internal",
"Error occurred while deleting the message."
);
}
});
The deleteMessage
Cloud Function allows moderators to delete messages in chat rooms. The function takes the chat room ID and message ID as input parameters. It first checks if the user is authenticated and then verifies whether the user is a moderator in the specified chat room. If the user is confirmed as a moderator, the function deletes the message from the Firestore database.
Step 4: Deploy the Function
To deploy the function to Firebase, run:
firebase emulators:start --only functions
This command starts the Firebase emulators and deploys the functions to the Firebase Cloud Functions environment. You can now test the functions locally and connect with the Permit PDP container running on port 7766
. Additionally, a URL for the checkPermission
function will appear in the terminal; make sure to copy it, as we will need it later.
Once the functions are deployed, any time a new user logs into your app, the Firebase function will automatically sync the user to Permit, assigning them the viewer
role and adding them to the default tenant. (A tenant is a group of users and resources that share the same access policies. You can manage your tenants by clicking “Directory” in the sidebar of your Permit workspace.)
To verify that the function works, sign up a new user in your app and check the Permit dashboard to see if the user has been synced. You should see the user listed under the default tenant with the viewer
role assigned.
Building the Chat Application with React and Firebase
Now that users can sign in and sync to Permit, we can build the chat application. Currently, when a user logs in, they are shown a list of chat rooms. However, when they click on a chat room, they cannot see, send, or receive messages. Let’s implement the logic to retrieve messages, allow users to send and receive messages, and enable moderators to delete messages in the chat rooms they moderate.
Step 1: Allow Users to Send and Receive Messages
When users click on a chat room, they should be able to see the messages in that room and send new messages. To achieve this, we must set up a listener to fetch messages from Firestore and display them in the chat room. We also need to create a form for users to send messages.
I have already created the components for the chat room and for sending messages. Let’s update them to include the logic to fetch and send messages.
In the src\\components\\SendMessage.js
file, update the sendMessage
function to send messages to Firestore:
import { auth, } from "../firebase";
const sendMessage = async (event) => {
event.preventDefault();
if (message.trim() === "") {
alert("Enter valid message");
return;
}
if (!user) {
console.error("User not authenticated. Cannot delete message.");
return;
}
if (!chatRoomId || !message) {
console.error("Invalid chat room ID or message. Cannot send message.");
return;
}
try {
const token = await user.getIdToken();
await fetch(
"<http://127.0.0.1:5001/react-chat-3ae5d/us-central1/sendMessage>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ data: { chatRoomId, message } }),
}
);
setMessage("");
scroll.current.scrollIntoView({ behavior: "smooth" });
} catch (error) {
console.error("Error sending message:", error);
throw new Error("Failed to send message. Please try again.");
}
};
When a user clicks the “Send” button, the sendMessage
function is called. The function first checks if the message is not empty, then adds the message to the Firestore database under the chat room’s messages collection. Next, let’s update the ChatRoom.js
file to fetch messages from Firestore and display them in the chat room.
In the src\\components\\ChatRoom.js
file, add a useEffect
hook to fetch messages from Firestore:
import {
query,
collection,
orderBy,
onSnapshot,
limit,
} from "firebase/firestore";
import { useEffect, useRef, useState } from "react";
import { db } from "./firebase";
export default function ChatRoom() {
...
useEffect(() => {
const q = query(
collection(db, "chatRooms", chatRoomId, "messages"),
orderBy("createdAt", "desc"),
limit(50)
);
const unsubscribe = onSnapshot(q, (QuerySnapshot) => {
const fetchedMessages = [];
QuerySnapshot.forEach((doc) => {
fetchedMessages.push({ ...doc.data(), id: doc.id });
});
const sortedMessages = fetchedMessages.sort(
(a, b) => a.createdAt - b.createdAt
);
setMessages(sortedMessages);
});
return () => unsubscribe;
}, [chatRoomId]);
...
}
We are fetching the last 50 messages from the chat room and sorting them by the createdAt
timestamp. The messages are then stored in the messages
state, which is used to display the messages in the chat room.
Lastly, let’s write the function to make the delete buttons work. In the src\\components\\Message.js
file, update the deleteMessage
function to delete messages from Firestore:
import { auth } from "../firebase";
import { useAuthState } from "react-firebase-hooks/auth";
const Message = ({ message }) => {
const [user] = useAuthState(auth);
const deleteMessage = async () => {
if (!user) {
console.error("User not authenticated. Cannot delete message.");
return;
}
if (!chatRoomId || !message.id) {
console.error(
"Invalid chat room ID or message ID. Cannot delete message."
);
return;
}
try {
// Get the user's ID token
const token = await user.getIdToken();
await fetch(
"<http://127.0.0.1:5001/react-chat-3ae5d/us-central1/deleteMessage>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ data: { chatRoomId, messageId: message.id } }),
}
);
console.log("Message deleted successfully.");
} catch (error) {
console.error("Error deleting message:", error);
throw new Error("Failed to delete message. Please try again.");
}
};
return (
<div className={`chat-bubble ${message.uid === user.uid ? "right" : ""}`}>
...
</div>
);
};
When a user clicks the delete button, the deleteMessage
function is called. This function deletes the message from Firestore using the message’s id
.
I also updated the class name of the chat bubble to include or remove the right
class based on whether the current user sent the message. This additional styling will help change the color and position of the chat bubbles differently based on the sender.
When testing these in the browser, you should be able to see messages when you enter a chat room and send a message. When you click the delete button, that message should be deleted.
Step 2: Implement Member and Moderator Permissions
We must ensure that only members and moderators can send messages in the chat room. Members can only read and send messages, while moderators can also delete messages. We’ll use the checkPermission
function we created earlier to check the user’s permissions before allowing them to perform any actions.
Creating a helper function to check permissions will make it easier to manage permissions across the app. In the src\\utils
directory, create a new file called permissions.js
and add the following code:
async function permissions(userId, operation, key) {
try {
const response = await fetch(
"YOUR_CHECK_PERMISSION_FUNCTION_URL",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data: { userId, operation, key } }),
}
);
const result = await response.json();
return result.result;
} catch (error) {
console.error("Error checking user permissions:", error);
throw new Error("Failed to check user permissions. Please try again.");
}
}
export default permissions;
This function uses the fetch
API to send a POST request to the checkPermission
function url we copied earlier. It passes the userId
, operation
, and key
as parameters to check the user’s permissions. The function returns the result of the permission check, which will be either true
or false
.
Next, let’s update the SendMessage
component to check if the user can send messages. If they do, they can type in a message and send it. If they don’t, the text input and button will be disabled.
import { useAuthState } from "react-firebase-hooks/auth";
import permissions from "../utils/permissions";
const SendMessage = ({ scroll, chatRoomId }) => {
const [user] = useAuthState(auth);
const userId = user?.uid || "";
const [isPermitted, setIsPermitted] = useState(false);
useEffect(() => {
const checkUserPermission = async () => {
if (userId && chatRoomId) {
try {
const canCreate = await permissions(userId, "create", chatRoomId);
setIsPermitted(canCreate.permitted);
} catch (error) {
console.error("Failed to check permissions:", error);
setIsPermitted(false);
}
}
};
checkUserPermission();
}, [userId, chatRoomId]);
return (
<form onSubmit={(event) => sendMessage(event)} className="send-message">
<input
disabled={!isPermitted}
/>
<button type="submit" disabled={!isPermitted}>
Send
</button>
</form>
);
};
export default SendMessage;
The checkUserPermission
function runs when the component mounts. It checks whether the user has permission to write to the chat room. If the user has permission, the text input and send button are enabled, allowing the user to send messages. If the user does not have permission, the text input and send button are disabled.
We will do the same for the delete button in the message bubbles. Update the Message.js
component with the following code:
import React, { useEffect, useState } from "react";
import permissions from "../utils/permissions";
const Message = ({ message, chatRoomId }) => {
const [user] = useAuthState(auth);
const userId = user?.uid || "";
const [isPermitted, setIsPermitted] = useState(false);
useEffect(() => {
const checkUserPermission = async () => {
if (userId && chatRoomId) {
try {
const canDelete = await permissions(userId, "delete", chatRoomId);
setIsPermitted(canDelete.permitted);
} catch (error) {
console.error("Failed to check permissions:", error);
setIsPermitted(false);
}
}
};
checkUserPermission();
}, [userId, chatRoomId]);
return (
{isPermitted && (
<button
className="chat-bubble__delete"
type="button"
>
// delete button
</button>
)}
)
}
Here, we check to see if the user can delete messages in the chat room. If the user has permission, the delete button is displayed, allowing the user to delete messages. The delete button is not displayed if the user does not have permission.
Since we haven’t assigned any member or moderator roles to our logged-in user in the Permit dashboard, we can see that the text input and send button are disabled, and the delete button is not displayed.
Step 3: Assigning Instance Roles in Permit
To assign roles to a user, navigate to the Permit dashboard and click on the “Directory” tab in the sidebar. Then, click on the “Users” tab to view all users in your workspace. Select the user you want to assign a role to, click the three dots, and choose “Edit.”
From the side panel, scroll down to the “Instance Access” section, select the resource and role you want to assign the user, and then enter the resource instance key. Click “Save” to assign the role to the user.
From the screenshot above, you can see that the user is now a moderator in the react
chat room and a member in the general
chat room. This means they can read, send, and delete messages in the react
chat room, while in the general
chat room, they can only read and send messages. As for the tech
chat room, They are not a member or moderator, so they can only read messages.
By visiting the browser and testing the new permissions, you see that the user can now send messages in the react
chat room and delete the messages they have sent. They can only send messages in the general
chat room and cannot send messages in the tech
chat room.
Conclusion
This tutorial demonstrates how to use Firebase alongside Permit to build and manage user permissions in your chat application. You can create custom roles and assign them to users based on their relationships with application resources. This allows you to control access to different parts of your application and ensure that users can only perform actions they are authorized to do.
By integrating Permit with Firebase, you can easily check user permissions and enforce access control rules in your application. This provides a secure and scalable way to manage user permissions and ensures that your application is protected from unauthorized access.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker