Sending and receiving text messages in recent times are frequently done online and in real-time. Text messages in real-time chat applications are sent and received immediately, depending on the speed of the router. Realtime messaging is very essential in chat applications, and Socket.io grants us this privilege.
In this tutorial, you’ll build a chat application hovering around:
To follow this tutorial, be sure you have the following:
18
or v20
),14
or higher, with a minimum requirement of version 12
), andThis article is a modification of How to Build a Real-time Chat Forum using Strapi, Socket.io, React, and MongoDB.
Now that everything is correct let’s set up the PostgreSQL database.
Search and open up PgAdmin in the start menu of your computer. PgAdmin will help create and manage your Strapi database.
PgAdmin is included with your PostgreSQL installation. Upon opening PgAdmin for the first time, you'll be prompted to enter the password you set during the PostgreSQL setup.
On the left navigation bar, click on Servers and click on PostgreSQL 14.
Right-click on Databases, hover over Create and click on Database.
You can name the database anything you desire, but in this tutorial, the name of the database is chatapp
. Once you're done naming the database, hit save.
The name of the database, chatapp
, will be shown on the left navigation bar as shown below:
In this last section, you learned how to create a database in PostgreSQL. In this section, you’ll learn how to connect PostgreSQL with Strapi using the Quickstart
option and the Custom setup
option.
Open up your terminal and create a folder for this application. This folder is the project’s root directory for this application. In this tutorial, the name of this folder is strapified-chat
. Not a bad name, right?
1mkdir strapified-chat
Open the folder in your desired code editor or run the code below in your terminal to open the folder with Vs Code:
1cd strapified-chat
2code .
After starting your code editor, open the integrated terminal in your code editor to quickly initialize and create a package.json file for the project using the code below:
1npm init --y
The following npm
command will install the latest Strapi instance.
1npx create-strapi@latest chatapp
If you prefer yarn
instead, in your terminal, run the command;
1yarn create strapi
When you press enter
, the terminal will ask for a project name. For this instance name it, chatapp
.
You can check out the official Strapi page to learn about the different ways to install Strapi.
The terminal will prompt you to either log in
or sign up
for Strapi Cloud (to begin your free 14-day trial projects) or skip this step. Use the arrow keys to navigate and press Enter to select your option.
For this demo, skip this step, but in a live production app, this just means you will have to host the project yourself.
During the Strapi installation process, when prompted with:
1? Do you want to use the default database (sqlite)? Y/n
Select No
by typing n
and pressing Enter. This action allows you to customize the database settings. Use the arrow keys to navigate through the options, and select postgres
as your database client. Confirm your selection by pressing Enter.
For the database name, type chatapp
and press Enter to proceed to the next prompt.
The terminal will ask you a bunch more questions. For each of these questions, input the following configuration:
Once the series of questions is over, Strapi will create the project using the custom options you specified. When the installation is done, Strapi sends a success message and outlines the basic commands that help get your project started.
Now that you have installed Strapi into the application, it’s time to get access to Strapi admin. Move into the created project and run the Strapi build command below to build the Strapi admin UI:
1cd chatapp
2npm run build
The output is shown below:
To start Strapi in development mode, run the following command:
1npm run develop
This command launches the Strapi local development server, which will be accessible on port 1337
Once the installation is complete, open the folder named strapified-chat
in a code editor. Click on chatapp, then select database.js
from the config folder.
Replace the following code into the database.js
file to configure the PostgreSQL database.
Replace the following code into the database.js file to configure the PostgreSQL database.
1module.exports = ({ env }) => {
2 const client = env("DATABASE_CLIENT", "sqlite");
3
4 const connections = {
5 mysql: {
6 connection: {
7 host: env("DATABASE_HOST", "localhost"),
8 port: env.int("DATABASE_PORT", 3306),
9 database: env("DATABASE_NAME", "strapi"),
10 user: env("DATABASE_USERNAME", "strapi"),
11 password: env("DATABASE_PASSWORD", "strapi"),
12 ssl: env.bool("DATABASE_SSL", false) && {
13 key: env("DATABASE_SSL_KEY", undefined),
14 cert: env("DATABASE_SSL_CERT", undefined),
15 ca: env("DATABASE_SSL_CA", undefined),
16 capath: env("DATABASE_SSL_CAPATH", undefined),
17 cipher: env("DATABASE_SSL_CIPHER", undefined),
18 rejectUnauthorized: env.bool(
19 "DATABASE_SSL_REJECT_UNAUTHORIZED",
20 true,
21 ),
22 },
23 },
24 pool: {
25 min: env.int("DATABASE_POOL_MIN", 2),
26 max: env.int("DATABASE_POOL_MAX", 10),
27 },
28 },
29 postgres: {
30 connection: {
31 connectionString: env("DATABASE_URL"),
32 host: env("DATABASE_HOST", "127.0.0.1"),
33 port: env.int("DATABASE_PORT", 5432),
34 database: env("DATABASE_NAME", "chatapp"),
35 user: env("DATABASE_USERNAME", "postgres"),
36 password: env("DATABASE_PASSWORD", "your_db_password"),
37 ssl: env.bool("DATABASE_SSL", false) && {
38 key: env("DATABASE_SSL_KEY", undefined),
39 cert: env("DATABASE_SSL_CERT", undefined),
40 ca: env("DATABASE_SSL_CA", undefined),
41 capath: env("DATABASE_SSL_CAPATH", undefined),
42 cipher: env("DATABASE_SSL_CIPHER", undefined),
43 rejectUnauthorized: env.bool(
44 "DATABASE_SSL_REJECT_UNAUTHORIZED",
45 true,
46 ),
47 },
48 schema: env("DATABASE_SCHEMA", "public"),
49 },
50 pool: {
51 min: env.int("DATABASE_POOL_MIN", 2),
52 max: env.int("DATABASE_POOL_MAX", 10),
53 },
54 },
55 sqlite: {
56 connection: {
57 filename: path.join(
58 __dirname,
59 "..",
60 env("DATABASE_FILENAME", ".tmp/data.db"),
61 ),
62 },
63 useNullAsDefault: true,
64 },
65 };
66
67 return {
68 connection: {
69 client,
70 ...connections[client],
71 acquireConnectionTimeout: env.int("DATABASE_CONNECTION_TIMEOUT", 60000),
72 },
73 };
74};
Once you’re done inputing the code above in your database.js
file, hit save and let Strapi restart the server.
After connecting PostgreSQL with Strapi, the next step is to install NextJS into the Chat application.
Run the create-next-app
command below to spin up the next application in a new folder.
1npx create-next-app next-chat
Follow the prompt to install create-next-app
.
Nodemailer is a NodeJS module that enables you to send emails from your email server. Nodemailer makes use of your email service's credentials to send these emails. In this section, you will learn how to set up Nodemailer for your different email services. Now, open your terminal in the next-chat directory and install Nodemailer.
1cd next-chat
2npm i nodemailer
After installing Nodemailer, navigate to the pages/api
directory in your next-chat project and create a new file named mail.js
. Your file structure should now include: pages/api/mail.js
.
To send an email using nodemailer requires an email service. This tutorial makes use of the Google Mail service (Gmail), but you can choose a different email or SMTP service.
To make use of the Google Mailing Service with nodemailer, you need to allow access to a less secured app.
Open the mail.js
file and add the following code to configure Nodemailer.
1export default function (req, res) {
2 const nodemailer = require("nodemailer");
3 const transporter = nodemailer.createTransport({
4 port: 465,
5 host: "smtp.gmail.com",
6 secure: "true",
7 auth: {
8 user: "examaple@gmail.com", //Replace with your email address
9 pass: "example", // Replace with the password to your email.
10 },
11 });
12 const mailData = {
13 from: "Chat API",
14 to: req.body.email,
15 subject: `Verify your email`,
16 text: req.body.message,
17 };
18 transporter.sendMail(mailData, function (err, info) {
19 if (err)
20 return res.status(500).json({ message: `an error occurred ${err}` });
21 res.status(200).json({ message: info });
22 de;
23 });
24}
Now that nodemailer has been configured, let's create a simple login form that will accept the email from the user.
Open up the index.js
file in the pages folder and replace the code with the following code.
1import styles from "../styles/Home.module.css";
2export default function Home() {
3 return (
4 <div className={styles.container}>
5 <form className={styles.main}>
6 <h1>Login</h1>
7 <label htmlFor="name">Email: </label>
8 <input type="email" id="name" />
9 <br />
10 <input type="submit" />
11 </form>
12 </div>
13 );
14}
After refactoring the index.js file and creating a simple form, the next step is to add functionality.
useState
dependency from react in the index.js
file. import { useState }
from "react";
onChange
event that will listen for any change in the input and store it in a state variable.1export default function Home() {
2 const [email, setEmail] = useState("");
3 const [user, setUser] = useState("");
4 return (
5 <div className={styles.container}>
6 <form className={styles.main}>
7 <h1>Login</h1>
8 <label htmlFor="user">Username: </label>
9 <input
10 type="text"
11 id="user"
12 value={user}
13 onChange={(e) => setUser(e.target.value)} // Getting the inputs
14 />
15 <br />
16 <label htmlFor="name">Email: </label>
17 <input
18 type="email"
19 id="name"
20 onChange={(e) => setEmail(e.target.value)} // Getting the inputs
21 />
22 <br />
23 <input type="submit" />
24 </form>
25 </div>
26 );
27}
api/mail
.1export default function Home() {
2 const [email, setEmail] = useState("");
3 const [user, setUser] = useState("");
4 const handlesubmit = (e) => {
5 e.preventDefault();
6 let message = "Testing, Testing..... It works🙂";
7 let data = {
8 email, // User's email
9 message,
10 };
11 fetch("/api/mail", {
12 method: "POST", // POST request to /api//mail
13 headers: {
14 "Content-Type": "application/json",
15 },
16 body: JSON.stringify(data),
17 }).then(async (res) => {
18 if (res.status === 200) {
19 console.log(await res.json());
20 } else {
21 console.log(await res.json());
22 }
23 });
24 setEmail("");
25 setUser("");
26 };
27 return (
28 <div className={styles.container}>
29 <form className={styles.main}>
30 <h1>Login</h1>
31 <label htmlFor="user">Username: </label>
32 <input
33 type="text"
34 id="user"
35 value={user}
36 onChange={(e) => setUser(e.target.value)} // Getting the inputs
37 />{" "}
38 <br />
39 <label htmlFor="name">Email: </label>
40 <input
41 type="email"
42 id="name"
43 value={email}
44 onChange={(e) => setEmail(e.target.value)} // Getting the inputs
45 />
46 <br />
47 <input type="submit" onClick={handlesubmit} /> // Handling Submit
48 </form>
49 </div>
50 );
51}
Once you are done with adding the code above and hitting save, head on to localhost:3000 to try out the form.
According to the documentation, JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. JWT generates a unique token that can authenticate and authorize a user.
Open up your terminal in the next-chat
folder and install the JSON Web Token dependency.
1npm i jsonwebtoken
Add the following code to the handleSubmit
function in the index.js
file.
1import jwt from "jsonwebtoken";
2
3const handleSubmit = (e) => {
4 e.preventDefault();
5
6 const SECRET = "this is a secret"; // JWT secret
7 const account = { email: "example@example.com" }; // Use meaningful data (e.g., email)
8 const token = jwt.sign(account, SECRET, { expiresIn: "1h" }); // Create the token with expiration
9
10 console.log("Generated Token:", token); // Log the token
11};
For security purposes, the
SECRET
variable for JWT should be a random set of strings and should be stored in an environmental variable.
Strapi is an open-source headless CMS that gives developers the freedom to choose their favorite tools and frameworks and allows editors to manage and distribute their content using their application's admin panel. Now that you’ve generated the token using JWT, it’s time to store the username
, email
, and token
using my favorite CMS, Strapi.
Ensure that your Strapi server is up and running.
1cd chatapp
2npm run develop
Navigate to http://localhost:1337/admin and log in with your Strapi admin credentials.
After a successful login, click on Content-Type Builder on the side-nav bar and click on Create new collection type.
Give it a name of your choice, but in this tutorial, the name of the collection type is Account.
After naming the collection type, hit continue, choose Text, and name it username
.
Click on Add another field to configure the field for email
and token
. Choose the Email field for Email and Text field for Token, then hit finish. The output is shown below.
Click on Save at the top-right of the screen and let Strapi restart the server.
To increase security, Strapi blocks all CRUD requests to a newly created collection type by default. The user’s credentials need to be sent to the account collection type, so this feature will not be needed in this application.
To allow requests, click on Settings on the side-nav bar and click on Roles under USERS & PERMISSIONS PLUGIN.
Scroll down to Accounts, click on the drop-down, Select all, and hit Save at the top-right of the screen.
Now that you’ve learned how to edit the permission of a collection type, it’s time to learn how to store the user’s details in that collection type using Strapi.
Make sure your NextJS application is running.
1cd next-chat
2npm run dev
The next step is to make a post request to "http://localhost:1337/api/accounts", passing the credentials of the user as a parameter.
Open your index.js
file in the pages
folder and add the following code to the handlesubmit
function.
1const handleSubmit = (e) => {
2 e.preventDefault();
3
4 const message = "Testing, Testing..... It works 🙂"; // Demo message
5 const data = {
6 email, // User's email
7 message,
8 };
9
10 // JWT payload and secret
11 const account = { email };
12 const SECRET = "this is a secret";
13 const token = jwt.sign(account, SECRET, { expiresIn: "1h" }); // Token with expiration
14
15 // Strapi data payload
16 const strapiData = {
17 data: {
18 username: user, // User's name
19 email,
20 token, // JWT token
21 },
22 };
23
24 // Send data to Strapi
25 fetch("http://localhost:1337/api/accounts", {
26 method: "POST",
27 headers: {
28 "Content-Type": "application/json",
29 },
30 body: JSON.stringify(strapiData), // Payload
31 })
32 .then(async (res) => {
33 if (res.ok) {
34 const response = await res.json();
35 console.log("Strapi Response:", response); // Success
36 } else {
37 const error = await res.json();
38 console.error("Strapi Error:", error); // Error
39 }
40 })
41 .catch((err) => console.error("Fetch Error:", err));
42};
Load up localhost:3000 in your favorite browser to test the request.
Enter your Username and Email, then click Submit. If successful, a confirmation message will appear, indicating the details were added successfully.
Click on Content Manager in your Strapi’s admin panel to check if it was added to Strapi.
After setting up Strapi, the next step is to use the token stored in strapi to authenticate the user. To do this, a link containing the token will be sent to the user’s email.
Open up your index.js
file in the pages folder and add the following code to send the link to the user’s email in the handlesubmit
function.
1const handleSubmit = (e) => {
2 e.preventDefault();
3
4 // JWT payload and secret
5 const account = { email }; // Use meaningful data like email
6 const SECRET = "this is a secret";
7 const token = jwt.sign(account, SECRET, { expiresIn: "1h" }); // Token with expiration
8
9 // Create message with the token
10 const message = `http://localhost:3000/chat/${token}`;
11
12 // Prepare email data
13 const data = {
14 email, // User's email
15 message,
16 };
17
18 // Send the email data to the API
19 fetch("/api/mail", {
20 method: "POST",
21 headers: {
22 "Content-Type": "application/json",
23 },
24 body: JSON.stringify(data),
25 })
26 .then(async (res) => {
27 if (res.ok) {
28 console.log("Email sent:", await res.json());
29 } else {
30 console.error("Email error:", await res.json());
31 }
32 })
33 .catch((err) => console.error("Fetch error:", err));
34
35 // Clear input fields
36 setEmail("");
37 setUser("");
38};
Navigate to "http://localhost:3000", fill in a valid email, and check your inbox to see if the message was successfully sent.
Once the link has been sent to the user and the user clicks on it, we need to get the token and verify it. This will prevent the user from generating random strings and in turn, it increases security.
Create a folder in the pages
folder called chat
and create a file in the chat
folder named token.js
Add the following code to the token.js
file to get the token from the URL and verify it using the JWT Secret.
1import { useRouter } from "next/router";
2import { useEffect, useState } from "react";
3import jwt from "jsonwebtoken";
4
5export default function Chat() {
6 const router = useRouter();
7 const SECRET = "this is a secret"; // JWT Secret
8 const [done, setDone] = useState("");
9 const token = router.query.token; // Extract token from the URL
10
11 useEffect(() => {
12 if (!router.isReady) return; // Ensure the router is ready
13
14 try {
15 const payload = jwt.verify(token, SECRET); // Verify the token
16 console.log("Payload:", payload); // Debugging payload
17 setDone("done"); // Grant access
18 } catch (error) {
19 console.error("Token verification failed:", error.message); // Log the error
20 router.push("/"); // Redirect on failure
21 }
22 }, [token, router.isReady]); // Watch for token changes
23
24 return (
25 <div>
26 {done !== "done" ? (
27 <h1>Verifying token... Please wait</h1>
28 ) : (
29 <h1>Welcome to the Group Chat</h1>
30 )}
31 </div>
32 );
33}
After verifying the token and extracting the payload, the next step is to check if the verified token matches the one stored in Strapi. To do this, the email
from the token's payload is used to query Strapi.
Add the following code to the token.js
file to query Strapi using the email
stored in the token payload and verify the token.
1export default function Chat() {
2 // Rest of the code
3 useEffect(() => {
4 // Rest of the code
5 try {
6 const payload = jwt.verify(token, SECRET); // Verifying the token using the secret
7
8 async function fetchData() {
9 const response = await fetch(
10 `http://localhost:1337/api/accounts?filters[email][$eq]=${payload.email}`,
11 );
12 const json = await response.json();
13
14 if (!json.data || json.data.length === 0) {
15 throw new Error("Account not found in Strapi");
16 }
17
18 const account = json.data[0]; // Fetch the first matching account
19 if (account.token !== token) {
20 throw new Error("Token mismatch");
21 }
22
23 console.log(account); // Debug the fetched account
24 setDone("done"); // Grant access to the chat page
25 }
26
27 fetchData();
28 } catch (error) {
29 console.error("Error:", error.message); // Log the error
30 router.push("/"); // Redirect the user to the home page
31 }
32 }, [token]); // Listens for changes in the token
33 // Rest of the code
34}
After retrieving the user’s data from Strapi, the next step is to compare the token in the URL with the one stored in Strapi. This ensures that the user is valid and authorized.
Add the following code to verify the tokens match.
1// Rest of the code
2
3useEffect(() => {
4 // Ensure the router is ready
5 if (!router.isReady) return;
6
7 async function fetchData() {
8 try {
9 const payload = jwt.verify(token, SECRET); // Verify the token using the secret
10
11 // Query Strapi using the email from the payload
12 const response = await fetch(
13 `http://localhost:1337/api/accounts?filters[email][$eq]=${payload.email}`,
14 );
15 const json = await response.json();
16
17 if (!json.data || json.data.length === 0) {
18 throw new Error("Account not found in Strapi");
19 }
20
21 const account = json.data[0]; // Get the first matching account
22 if (account.token !== token) {
23 // Compare tokens
24 throw new Error("Token mismatch");
25 }
26
27 // Tokens match; grant access
28 console.log("Tokens match:", account);
29 setDone("done");
30 } catch (error) {
31 console.error("Error:", error.message); // Log the error
32 router.push("/"); // Redirect to home page on failure
33 }
34 }
35
36 fetchData();
37}, [token, router.isReady]); // Listen for changes in token and router readiness
38
39// Rest of the code
Now that security has been added to the chat application, it’s time to set up our chat environment.
This article is a modification of a previous article. Refer to it for clarity on the various features.
Run npm install
to install all the dependencies for this chat application. Once the installation is complete, run npm run dev
to start up the chat application.
Please ensure that you add the right Gmail credentials to the mail.js file. Also, ensure your strapi server is running. To do this, open your terminal and run
npm run develop
in your Strapi project folder.
Clone, fork or download the next-chat file for this chat application using this GitHub link.
When the chat application is up and running, head on to "http://localhost:3000", create a new account and check your inputted email for the verification link. Once you click on the link you will be brought to a clean User Interface shown below.
After starting up the chat application, the next step is to add chat functionality to the application.
Create a new collection named message in Content-Type Builder and add 2 fields, a user field, and a message field.
Now, open up the chatapp
folder and add the backend functionality for socket.io to the index.js
file in the src
folder.
1"use strict";
2const { Server } = require("socket.io"); // Use CommonJS syntax
3
4module.exports = {
5 register(/*{ strapi }*/) {},
6
7 bootstrap(/*{ strapi }*/) {
8 const io = new Server(strapi.server.httpServer, {
9 cors: {
10 origin: "http://localhost:3000",
11 methods: ["GET", "POST"],
12 allowedHeaders: ["my-custom-header"],
13 credentials: true,
14 },
15 });
16
17 io.on("connection", function (socket) {
18 socket.on("join", ({ username }) => {
19 console.log("user connected");
20 console.log("username is ", username);
21 if (username) {
22 socket.join("group");
23 socket.emit("welcome", {
24 user: "bot",
25 text: `${username}, Welcome to the group chat`,
26 userData: username,
27 });
28 } else {
29 console.log("An error occurred");
30 }
31 });
32
33 socket.on("sendMessage", async (data) => {
34 const axios = require("axios");
35 const strapiData = {
36 data: {
37 user: data.user,
38 message: data.message,
39 },
40 };
41
42 await axios
43 .post("http://localhost:1337/api/messages", strapiData)
44 .then(() => {
45 socket.broadcast.to("group").emit("message", {
46 user: data.username,
47 text: data.message,
48 });
49 })
50 .catch((e) => console.log("error", e.message));
51 });
52 });
53 },
54};
Add the following code to the index.js
file in next-chat/components
to set up socket.io and send messages.
1import React, { useEffect, useState } from "react";
2import { Input } from "antd";
3import "antd/dist/antd.css";
4import "font-awesome/css/font-awesome.min.css";
5import Header from "./Header";
6import Messages from "./Messages";
7import List from "./List";
8import { io } from "socket.io-client";
9import {
10 ChatContainer,
11 StyledContainer,
12 ChatBox,
13 StyledButton,
14 SendIcon,
15} from "../pages/chat/styles";
16
17function ChatRoom({ username, id }) {
18 const [messages, setMessages] = useState([]);
19 const [message, setMessage] = useState("");
20 const [users, setUsers] = useState([]);
21 const [socket, setSocket] = useState(null); // Store socket instance
22 const BASE_URL = "http://localhost:1337";
23
24 useEffect(() => {
25 // Initialize socket connection
26 const ioInstance = io(BASE_URL);
27 setSocket(ioInstance);
28
29 // Handle "join" event
30 ioInstance.emit("join", { username }, (error) => {
31 if (error) alert(error);
32 });
33
34 // Listen for "welcome" event
35 ioInstance.on("welcome", async (data) => {
36 const welcomeMessage = {
37 user: data.user,
38 message: data.text,
39 };
40 setMessages((prev) => [welcomeMessage, ...prev]);
41
42 // Fetch all messages from Strapi
43 try {
44 const res = await fetch(`${BASE_URL}/api/messages`);
45 const response = await res.json();
46 const allMessages = response.data.map((one) => one.attributes);
47 setMessages((prev) => [...prev, ...allMessages]);
48 } catch (err) {
49 console.error("Error fetching messages:", err.message);
50 }
51 });
52
53 // Listen for "message" event
54 ioInstance.on("message", async () => {
55 try {
56 const res = await fetch(`${BASE_URL}/api/messages`);
57 const response = await res.json();
58 const allMessages = response.data.map((one) => one.attributes);
59 setMessages(allMessages);
60 } catch (err) {
61 console.error("Error fetching new messages:", err.message);
62 }
63 });
64
65 // Clean up on component unmount
66 return () => {
67 ioInstance.disconnect();
68 };
69 }, [username]);
70
71 const sendMessage = () => {
72 if (!message.trim()) {
73 alert("Message can't be empty");
74 return;
75 }
76
77 if (socket) {
78 socket.emit("sendMessage", { message, user: username }, (error) => {
79 if (error) alert(error);
80 });
81 setMessage("");
82 }
83 };
84
85 const handleChange = (e) => {
86 setMessage(e.target.value);
87 };
88
89 const handleClick = () => {
90 sendMessage();
91 };
92
93 return (
94 <ChatContainer>
95 <Header room="Group Chat" />
96 <StyledContainer>
97 <List users={users} id={id} usersname={username} />
98 <ChatBox>
99 <Messages messages={messages} username={username} />
100 <Input
101 type="text"
102 placeholder="Type your message"
103 value={message}
104 onChange={handleChange}
105 />
106 <StyledButton onClick={handleClick}>
107 <SendIcon>
108 <i className="fa fa-paper-plane" />
109 </SendIcon>
110 </StyledButton>
111 </ChatBox>
112 </StyledContainer>
113 </ChatContainer>
114 );
115}
116
117export default ChatRoom;
Add the following code to the index.js
file in next-chat/components/Messages
. This code will loop through the messages and display them on the screen.
1import React, { useEffect, useRef } from "react";
2import Message from "./Message/";
3import styled from "styled-components";
4function Messages(props) {
5 const { messages, username: user } = props;
6 const messagesEndRef = useRef(null);
7 const scrollToBottom = () => {
8 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); //Scroll to bottom functionality.
9 };
10 useEffect(() => {
11 scrollToBottom();
12 }, [messages]);
13 return (
14 <StyledMessages>
15 {messages.map((message, i) => (
16 <div key={i} ref={messagesEndRef}>
17 <Message message={message} username={user} />
18 </div>
19 ))}
20 </StyledMessages>
21 );
22}
23export default Messages;
24const StyledMessages = styled.div`
25 padding: 5% 0;
26 overflow: auto;
27 flex: auto;
28`;
Lastly, add the following code to the index.js
file in next-chat/components/Messages/Message
. This code will neatly format the messages and position the messages in their rightful place.
1import React from "react";
2import { MessagesContainer, MessageBox, MessageText, SentBy } from "./styles";
3
4function Message(props) {
5 const {
6 username,
7 message: { user, message },
8 } = props;
9 let sentByCurrentUser = false;
10
11 if (user === username) {
12 sentByCurrentUser = true;
13 }
14
15 const background = sentByCurrentUser ? "blue" : "dark";
16 const textPosition = sentByCurrentUser ? "end" : "start";
17 const textColor = sentByCurrentUser ? "white" : "dark";
18 const sentBy = sentByCurrentUser ? "right" : "left";
19 return (
20 <MessagesContainer textPosition={textPosition}>
21 <MessageBox background={background}>
22 <MessageText color={textColor}>{message}</MessageText>
23 </MessageBox>
24 <SentBy sentBy={sentBy}>{user}</SentBy>
25 </MessagesContainer>
26 );
27}
28
29export default Message;
The previous section covered how to set up socket.io for both the client and server sides of the application. This section will cover how to implement role-based authentication in the chat application.
Create an active user collection type in Strapi, add a users
field and socketid
field, make sure it is set to a unique field and configure the roles permission.
Add the following code to the index.js
file in chat-app/src
to make a POST and request to the active users collection type.
1//rest of the code
2io.on("connection", function (socket) {
3 socket.on("join", ({ username }) => {
4 console.log("user connected");
5 console.log("username is ", username);
6 if (username) {
7 socket.join("group");
8 socket.emit("welcome", {
9 user: "bot",
10 text: `${username}, Welcome to the group chat`,
11 userData: username,
12 });
13 } else {
14 console.log("An error occurred");
15 }
16 });
17
18 socket.on("sendMessage", async (data) => {
19 const axios = require("axios");
20 const strapiData = {
21 data: {
22 user: data.user,
23 message: data.message,
24 },
25 };
26
27 await axios
28 .post("http://localhost:1337/api/messages", strapiData)
29 .then(() => {
30 socket.broadcast.to("group").emit("message", {
31 user: data.username,
32 text: data.message,
33 });
34 })
35 .catch((e) => console.log("error", e.message));
36 });
37});
38//rest of the code
After storing and sending the user data from the server, add the following code to the index.js
file in next-chat/components
to listen for the server's roomData
event and fetch the list of active users.
1useEffect(() => {
2 // Initialize socket connection
3 const ioInstance = io("http://localhost:1337");
4
5 // Listen for "roomData" event
6 ioInstance.on("roomData", async () => {
7 try {
8 const res = await fetch("http://localhost:1337/api/active-users");
9 const response = await res.json();
10 setUsers(response); // Fetch and store active users
11 } catch (err) {
12 console.error("Error fetching active users:", err.message);
13 }
14 });
15
16 // Clean up on component unmount
17 return () => {
18 ioInstance.disconnect();
19 };
20}, [username]);
Add the code below to the index.js
file in next-chat/components/List
to get the users from the props
passed to the List component and display them.
1import React from "react";
2import styled from "styled-components";
3import { List as AntdList, Avatar } from "antd";
4
5function List({ users, username }) {
6 const userList = users?.data || []; // Safely access users array
7
8 return (
9 <StyledList>
10 <ListHeading>Active Users</ListHeading>
11 <AntdList
12 itemLayout="horizontal"
13 dataSource={userList}
14 renderItem={(user) => (
15 <AntdList.Item>
16 <AntdList.Item.Meta
17 avatar={
18 <Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
19 }
20 title={user.users} // Display user name from Strapi data
21 />
22 <button
23 style={
24 user.users === "Admin" || username !== "Admin"
25 ? { display: "none" }
26 : null
27 } // Only show delete button for Admin
28 >
29 Delete
30 </button>
31 </AntdList.Item>
32 )}
33 />
34 </StyledList>
35 );
36}
37
38export default List;
39
40const StyledList = styled(AntdList)`
41 margin-right: 10px;
42 flex: 0 0 35%;
43 padding: 20px;
44 .ant-list-item-meta-content {
45 flex-grow: 0;
46 }
47 h4 {
48 font-size: 25px;
49 }
50 a {
51 color: #097ef0;
52 }
53`;
54
55const ListHeading = styled.div`
56 color: #757591;
57 font-size: 20px;
58 font-style: oblique;
59 border-bottom: 1px solid #757591;
60`;
Populate the Account collection type with Admin as username. Head on to http://localhost:3000 to do so.
Click on Content-Type Builder in your Strapi’s admin dashboard and edit the username field to take in unique sets of usernames for the Account collection type.
Hit finish and click on save.
This delete function is a feature made accessible to only the Admin user.
Open up the index.js
file in chatapp/src
and add the code below to set up the backend.
1"use strict";
2const { Server } = require("socket.io");
3const axios = require("axios");
4
5module.exports = {
6 register() {},
7
8 bootstrap() {
9 const io = new Server(strapi.server.httpServer, {
10 cors: {
11 origin: "http://localhost:3000",
12 methods: ["GET", "POST"],
13 allowedHeaders: ["my-custom-header"],
14 credentials: true,
15 },
16 });
17
18 io.on("connection", (socket) => {
19 console.log("User connected with socket ID:", socket.id);
20
21 // Handle user join
22 socket.on("join", async ({ username }) => {
23 if (username) {
24 socket.join("group");
25 socket.emit("welcome", {
26 user: "bot",
27 text: `${username}, Welcome to the group chat`,
28 });
29
30 const strapiData = { data: { users: username, socketid: socket.id } };
31
32 try {
33 await axios.post(
34 "http://localhost:1337/api/active-users",
35 strapiData,
36 );
37 socket.emit("roomData", { done: "true" });
38 } catch (error) {
39 if (error.response?.status === 400) {
40 socket.emit("roomData", { done: "existing" });
41 } else {
42 console.error("Error adding user:", error.message);
43 }
44 }
45 }
46 });
47
48 // Handle "kick" event
49 socket.on("kick", (data) => {
50 io.sockets.sockets.forEach((s) => {
51 if (s.id === data.socketid) {
52 s.disconnect(); // Disconnect the user
53 s.removeAllListeners(); // Cleanup socket listeners
54 console.log("User kicked:", s.id);
55 } else {
56 console.log("Couldn't kick user:", s.id);
57 }
58 });
59 });
60
61 // Handle user disconnect
62 socket.on("disconnect", async () => {
63 try {
64 await axios.delete(
65 `http://localhost:1337/api/active-users/${socket.id}`,
66 );
67 console.log("User disconnected:", socket.id);
68 } catch (error) {
69 console.error("Error removing user:", error.message);
70 }
71 });
72 });
73 },
74};
75("use strict");
76const { Server } = require("socket.io");
77const axios = require("axios");
78
79module.exports = {
80 register() {},
81
82 bootstrap() {
83 const io = new Server(strapi.server.httpServer, {
84 cors: {
85 origin: "http://localhost:3000",
86 methods: ["GET", "POST"],
87 allowedHeaders: ["my-custom-header"],
88 credentials: true,
89 },
90 });
91
92 io.on("connection", (socket) => {
93 console.log("User connected with socket ID:", socket.id);
94
95 // Handle user join
96 socket.on("join", async ({ username }) => {
97 if (username) {
98 socket.join("group");
99 socket.emit("welcome", {
100 user: "bot",
101 text: `${username}, Welcome to the group chat`,
102 });
103
104 const strapiData = { data: { users: username, socketid: socket.id } };
105
106 try {
107 await axios.post(
108 "http://localhost:1337/api/active-users",
109 strapiData,
110 );
111 socket.emit("roomData", { done: "true" });
112 } catch (error) {
113 if (error.response?.status === 400) {
114 socket.emit("roomData", { done: "existing" });
115 } else {
116 console.error("Error adding user:", error.message);
117 }
118 }
119 }
120 });
121
122 // Handle "kick" event
123 socket.on("kick", (data) => {
124 io.sockets.sockets.forEach((s) => {
125 if (s.id === data.socketid) {
126 s.disconnect(); // Disconnect the user
127 s.removeAllListeners(); // Cleanup socket listeners
128 console.log("User kicked:", s.id);
129 } else {
130 console.log("Couldn't kick user:", s.id);
131 }
132 });
133 });
134
135 // Handle user disconnect
136 socket.on("disconnect", async () => {
137 try {
138 await axios.delete(
139 `http://localhost:1337/api/active-users/${socket.id}`,
140 );
141 console.log("User disconnected:", socket.id);
142 } catch (error) {
143 console.error("Error removing user:", error.message);
144 }
145 });
146 });
147 },
148};
After setting up the backend, open up your index.js
file in next-chat/components/List
and add the following code. The code below will get the socket id, emit it to the server and delete the user from the database.
1import React from "react";
2import styled from "styled-components";
3import { List as AntdList, Avatar } from "antd";
4import socket from "socket.io-client";
5
6function List(props) {
7 const users = props.users.data; // Strapi 5 no longer uses `attributes`, so data is already structured directly.
8
9 const handleClick = async (id, socketid) => {
10 const io = socket("http://localhost:1337");
11
12 try {
13 // Delete user from active users collection
14 await fetch(`http://localhost:1337/api/active-users/${id}`, {
15 method: "DELETE",
16 headers: { "Content-Type": "application/json" },
17 });
18
19 // Emit "kick" event to disconnect the user
20 io.emit("kick", { socketid }, (error) => {
21 if (error) return alert("Failed to kick user:", error);
22 });
23
24 // Refresh the page after a short delay
25 setTimeout(() => location.reload(), 3000);
26 } catch (error) {
27 console.error("Error deleting user:", error.message);
28 setTimeout(() => location.reload(), 3000);
29 }
30 };
31
32 return (
33 <StyledList>
34 <ListHeading>Active Users</ListHeading>
35 <AntdList
36 itemLayout="horizontal"
37 dataSource={users}
38 renderItem={(user) => (
39 <AntdList.Item>
40 <AntdList.Item.Meta
41 avatar={
42 <Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
43 }
44 title={user.users} // Directly access the `users` field.
45 />
46 <button
47 style={
48 user.users === "Admin" || props.username !== "Admin"
49 ? { display: "none" }
50 : null
51 }
52 onClick={() => handleClick(user.id, user.socketid)} // Use `user.id` and `user.socketid`.
53 >
54 Delete
55 </button>
56 </AntdList.Item>
57 )}
58 />
59 </StyledList>
60 );
61}
62
63export default List;
64
65// Styled Components
66const StyledList = styled(AntdList)`
67 margin-right: 10px;
68 flex: 0 0 35%;
69 padding: 20px;
70`;
71
72const ListHeading = styled.div`
73 color: #757591;
74 font-size: 20px;
75 font-style: oblique;
76 border-bottom: 1px solid #757591;
77`;
This article primarily aimed at teaching you how to set up different authenticating techniques in your application as well as enlightening you on how to utilize Strapi 5.
Feel free to clone or fork the final code from the GitHub repo. As a next step, try to use this knowledge gained to build more complex applications using Strapi.
Full Stack Web Developer. Loves JavaScript.
Student Developer ~ Yet Another Open Source Guy ~ JavaScript/TypeScript Developer & a Tech Outlaw...