Mern Chat application
Read This on Github
What is MERN Stack?
MERN is a full stack JavaScript framework that is used to build web applications. It is a combination of four technologies: MongoDB, Express, React, and Node.js.
-
React
- FRONTEND LIBRARY BUILT BY FACEBOOK
- REUSABLE COMPONENT(Basically we don't need to write the same code again and again for different pages)
- VIRTUAL DOM(When you click on a button, it doesn't refresh the whole page, it just refreshes the part that you have clicked on)
- SUPER FAST
-
Node.js
- JAVASCRIPT RUNTIME BUILT ON CHROME'S V8 ENGINE(We can run javascript outside the browser)
- SCALABLE WEB SERVER(We can handle a lot of requests at the same time)
- NODE PACKAGE MANAGER(NPM)
- DEVELOP REAL TIME SYSTEMS(CHAT APPLICATIONS, REAL TIME DATA)
-
Express
- NODE JS FRAMEWORK(We can use node js without express but express makes it easier)
- POWERFUL ROUTING API(We can create different routes for different pages)
- DETAILED DOCUMENTATION(https://expressjs.com/en/guide/routing.html (opens in a new tab))
- HIGH PERFORMANCE(We can handle a lot of requests at the same time)
- MANY THIRD PARTY PLUGINS(We can use many plugins to make our work easier)
-
MongoDB
- CROSS PLATFORM, NO SQL AND DOCUMENT-ORIENTED(We can use it on any platform and we don't need to create tables)
- ALWAYS ON(We don't need to turn it on and off)
- HIGHLY SCALABLE(We can handle a lot of requests at the same time)
- FLEXIBLE SCHEMA(We can add or remove fields from the database without any problem)
Data Flow in Our App
First, our React app sends a request to the Express web framework using an endpoint such as /api/chat
for a GET request to retrieve all chat data or /api/chat/:id
for a GET request to retrieve a specific chat by its ID. The Express server receives the request and forwards it to the Node.js server. The Node.js server uses Mongoose to connect to the MongoDB database and makes a query to retrieve or manipulate data from the database. For example, for a GET request to retrieve all chat data, the query might look like Chat.find({})
. For a POST request to create a new chat, the query might look like Chat.create(newChat)
. For a PUT request to update an existing chat, the query might look like Chat.findByIdAndUpdate(id, updatedChat)
. For a DELETE request to delete an existing chat, the query might look like Chat.findByIdAndDelete(id)
. The MongoDB database processes the request and sends the data back to the Node.js server. The Node.js server then sends the data to the Express server, which formats it into JSON and sends it back to the React app. Finally, the React app receives the data and displays it on the screen.
Tools Used
- Visual Studio Code - https://code.visualstudio.com/ (opens in a new tab)
- Extention
- Prettier - https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode (opens in a new tab)
- ESLint - https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint (opens in a new tab)
- Material Icon Theme - https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme (opens in a new tab)
- Live Server - https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer (opens in a new tab)
- Bracket Pair Colorizer - https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer (opens in a new tab)
- AutRename Tag - https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag (opens in a new tab)
- Auto Close Tag - https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-close-tag (opens in a new tab)
- ES7 React/Redux/GraphQL/React-Native snippets - https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets (opens in a new tab)
- JavaScript (ES6) code snippets - https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptS (opens in a new tab)
- Use Formats on Save - Settings > Text Editor > Formatting > Format On Save
- MongoDB Atlas - https://www.mongodb.com/cloud/atlas (opens in a new tab)
- Postman - https://www.postman.com/ (opens in a new tab)
- Heroku - https://www.heroku.com/ (opens in a new tab)
- Git - https://git-scm.com/ (opens in a new tab)
- Github - https://github.com/ (opens in a new tab)
- NPM - https://www.npmjs.com/ (opens in a new tab)
- Node.js - https://nodejs.org/en/ (opens in a new tab)
- Express - https://expressjs.com/ (opens in a new tab)
- React - https://reactjs.org/ (opens in a new tab)
- MongoDB - https://www.mongodb.com/ (opens in a new tab)
- Mongoose - https://mongoosejs.com/ (opens in a new tab)
- Socket.io - https://socket.io/ (opens in a new tab)
- React Router - https://reactrouter.com/ (opens in a new tab)
- React Scroll - https://www.npmjs.com/package/react-scroll (opens in a new tab)
- React Scroll To Bottom - https://www.npmjs.com/package/react-scroll-to-bottom (opens in a new tab)
Creating Node JS Server and Express JS API
- Create Folder of your project
- Open terminal and type
npm init
- You can follow this step according to your project
package name: (mern_chat_app) octochat-test
version: (1.0.0)
description:
entry point: (index.js) server.js
test command:
git repository: (https://github.com/Subham-Maity/MERN_Chat_App.git)
keywords:
author: Subham Maity
license: (ISC)
About to write to F:\mains\MERN_Chat_App\package.json:
{
"name": "octochat-test",
"version": "1.0.0",
"description": "I'm trying",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Subham-Maity/MERN_Chat_App.git"
},
"author": "Subham Maity",
"license": "ISC",
"bugs": {
"url": "https://github.com/Subham-Maity/MERN_Chat_App/issues"
},
"homepage": "https://github.com/Subham-Maity/MERN_Chat_App#readme"
}
Is this OK? (yes)
- Install Express
npm install express
- Express helps us to create server and routes easily
- Create a folder named
backend
and create a file namedserver.js
inside it and write the following code
const express = require("express");
const app = express();
app.listen(5000, console.log("Server started on port 5000"));
- This code will create a server on port 5000 and will print
Server started on port 5000
in the terminal
- Install nodemon because it will automatically restart the server when we make any changes in the code
npm install nodemon
- now change the
package.json
file
"scripts": {
"start": "node backend/server.js",
"server": "nodemon backend/server.js"
},
- It will do the same thing if you run
npm start
ornpm run server
- Let's create a small route
app.get("/", (req, res) => {
res.send("API is running...");
});
- Now run
npm run server
and go tohttp://localhost:5000/
and you will seeAPI is running...
in the browser
- Let's add some data to the server
backend> data > data.js
- This is a dummy data
const chats = [
{
isGroupChat: false,
users: [
{
name: "Akshay Singh",
email: "akshay.singh@yahoo.co.in",
},
{
name: "Sudhanshu Patel",
email: "sudhanshu54@rediffmail.com",
},
],
_id: "617a077e18c25468bc7c4dd4",
chatName: "Akshay Singh",
},
{
isGroupChat: false,
users: [
{
name: "Rajesh Sharma",
email: "rajesh.sharma2@gmail.com",
},
{
name: "Aakash Verma",
email: "aakash.verma89@yahoo.co.in",
},
],
_id: "617a077e18c25468b27c4dd4",
chatName: "Rajesh Sharma",
},
{
isGroupChat: true,
users: [
{
name: "Deepak Gupta",
email: "deepak.gupta333@gmail.com",
},
{
name: "Rudra Pratap Singh",
email: "rudra_pratap_singh@hotmail.com",
},
{
name: "Chhavi Malhotra",
email: "chhavi_malhotra21@yahoo.co.in",
},
],
_id: "617a518c4081150716472c78",
chatName: "College Friends",
groupAdmin: {
name: "Chhavi Malhotra",
email: "chhavi_malhotra21@yahoo.co.in",
},
},
{
isGroupChat: false,
users: [
{
name: "Vijay Kumar",
email: "vijay_kumar72@rediffmail.com",
},
{
name: "Arjun Singh",
email: "arjun.singh3@yahoo.co.in",
},
],
_id: "617a518c4081150016472c78",
chatName: "Chill Zone",
groupAdmin: {
name: "Guest User",
email: "guest@example.com",
},
},
];
module.exports = chats;
- Make an another api to get all the chats
app.get("/api/chats", (req, res) => {
res.json(chats);
});
- Now run
npm run server
and go tohttp://localhost:5000/api/chats
and you will see all the chats in the browser
- Let's create a route to get a single chat by id
app.get("/api/chats/:id", (req, res) => {
//console.log(req);
//console.log(req.params.id);
const chat = chats.find((c) => c._id === req.params.id);
res.send(chat);
});
- first if you hit on the browser
http://localhost:5000/api/chats/617a077e18c25468bc7c4dd4
using console.log(req) you will see the request object in the terminal
- we are gonna take this id variable
req.params.id
and now reload the page and useconsole.log(req.params.id);
and you will see the id in the terminal - now we are gonna use this id to find the chat and send it to the browser
- now go to the browser and hit
http://localhost:5000/api/chats/617a077e18c25468bc7c4dd4
and you will see the chat in the browser
{"isGroupChat":false,"users":[{"name":"Akshay Singh","email":"akshay.singh@yahoo.co.in"},{"name":"Sudhanshu Patel","email":"sudhanshu54@rediffmail.com"}],"_id":"617a077e18c25468bc7c4dd4","chatName":"Akshay Singh"}
- We will create a .env file to store the port number
- create a file named
.env
in the root directory and write the following code
PORT = 5000
Install package dotenv
npm install dotenv
- Now we will use this port number in the server.js file
const dotenv = require("dotenv");
dotenv.config();
const PORT = process.env.PORT || 5000;
app.listen(PORT, console.log(`Server started on port ${PORT}`));
Frontend Setup
- Open you root directory in the terminal and run the following command
npx create-next-app frontend
- Choose according to your choice
- Now go to the frontend directory and run the following command
npm install axios
- For now we will use charkra ui for styling
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
go to _app.tsx
and add the following code
import { ChakraProvider } from "@chakra-ui/react";
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
For testing purpose we will add a div in the index.tsx
file
import { Button } from "@chakra-ui/react";
function Home() {
return (
<div className="_app">
<Button colorScheme="teal" variant="solid">
{" "}
Button{" "}
</Button>
</div>
);
}
export default Home;
It will create a button
- If you want to connect frontend to backend then you have to add proxy in the
package.json
file
"proxy": "http://127.0.0.1:5000",
Open your browser for frontend and hit
http://localhost:3000/
and you will see the button
- Install react router dom
npm install react-router-dom
- In next js we don't need this react router dom but if you want to use it you can
- GO to
pages
folder and inside that create a two fileshomepage.tsx
andChatPage.tsx
and add the following codeHomepage.tsx
import React from "react";
const Homepage = () => {
return <div>Homepage</div>;
};
export default Homepage;
ChatPage.tsx
import React from "react";
const ChatPage = () => {
return <div>ChatPage</div>;
};
export default ChatPage;
You can also use
rafce
snippet to create a functional component
Now go to your index.tsx and add the following code
import { Button } from "@chakra-ui/react";
import Homepage from "./Homepage";
import ChatPage from "./ChatPage";
function Home() {
return (
<div className="_app">
<Homepage />
<ChatPage />
</div>
);
}
export default Home;
No in your browser you will see two divs one is
Homepage
and another isChatPage
- If you visit
http://localhost:3000/
you will see the homepage and chatpage both but we want to show only one page at a time
- but in next js no need to use react router dom and exact keyword or path keyword you can you useRouter hook
- by default if you visit
http://localhost:3000/ChatPage
it will show you the ChatPage and if you visithttp://localhost:3000/Homepage
it will show you the Homepage
- You want if you visit
http://localhost:3000/
it will show you the Homepage and if you visithttp://localhost:3000/Chats
it will show you the ChatPage (you don't wanna visithttp://localhost:3000/ChatPage
to see the ChatPage)
For that go to index.tsx
and add the following code
import Homepage from "./Homepage";
function Home() {
return (
<div className="_app">
<Homepage />
</div>
);
}
export default Home;
Now if you visit
http://localhost:3000/
you will see the Homepage
But if you visit http://localhost:3000/Chats
it will show you the error page because we don't have any Chats page so we have to create a chats path
- go to
_app.tsx
and add the following code and use the useRouter hook androuter.asPath
to get the current path
// pages/_app.tsx
import { ChakraProvider } from "@chakra-ui/react";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import ChatPage from "./ChatPage";
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
if (router.asPath === "/chats" || router.asPath === "/ChatPage") {
return <ChatPage />;
}
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
- Now create a dynamic route in the
index.tsx
filepages > chats > index.tsx
// pages/chats/index.js
import ChatPage from "../ChatPage";
export default function Chats() {
return <ChatPage />;
}
- Make sure you import the ChatPage component in the
index.tsx
file
Now if you visit
http://localhost:3000/
you will see the Homepage and if you visithttp://localhost:3000/Chats
you will see the ChatPage also if you visithttp://localhost:3000/ChatPage
you will see the ChatPage
If you use dynamic route you can remove the
router.asPath === "/ChatPage"
from the_app.tsx
file
_app.tsx
import { ChakraProvider } from "@chakra-ui/react";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
- Let's make a api call to the backend and get the data and display it in the frontend
We need to install axios
npm install axios
Now go to the ChatPage.tsx
file and add the following code
import React from "react";
import axios from "axios";
const ChatPage = () => {
const fetchChats = async () => {
const data = await axios.get("http://localhost:5000/api/chats");
console.log(data);
};
React.useEffect(() => {
fetchChats();
}, []);
return <div>ChatPage</div>;
};
export default ChatPage;
You can also destructure data from response so
const { data } = await axios.get("http://localhost:5000/api/chats");
- Now if you visit
http://localhost:3000/Chats
you will see the ChatPage and in the console you will see the data from the backend
- Now let's try to render this data in the frontend
- Go to the
ChatPage.tsx
file and add the following code
we need useState hook to store the data from the backend
import React, { useEffect, useState } from "react";
import axios from "axios";
interface Chat {
_id: string;
chatName: string;
// ... other properties of the chat data
}
const ChatPage = () => {
const [chats, setChats] = useState<Chat[]>([]);
const fetchChats = async () => {
const { data } = await axios.get("http://localhost:5000/api/chats");
setChats(data);
};
useEffect(() => {
fetchChats();
}, []);
return (
<div>
{chats.map((chat) => (
<div key={chat._id}>{chat.chatName}</div>
))}
</div>
);
};
export default ChatPage;
Now if you visit
http://localhost:3000/Chats
you will see the ChatPage and in the console you will see the data from the backend and also you will see the chatName in the frontend
- First we need to store data in the state (useState hook - you can think of it as a variable that can be changed and it will re-render the component When the state changes, the component will re-render with the updated state) and then we need to map over the data and display it in the frontend
- The useState hook returns an array with two elements: the current state value and a function to update the state. Chats is the current state value that holds the data from the backend, and setChats is the function to update the state with new data. The fetchChats function is used to fetch data from the backend and then update the state using setChats. This way, when new data is fetched, it can be stored in the chats state and the component will re-render with the updated data.
- Now pass the data to the
setChats
function so the data will be stored in the chats state and the component will re-render with the updated - Now we need to map over the data and display it in the frontend so in the return statement first use chats.map and then pass the chat as a parameter and then return the chatName
- But you will get an error related to the key prop so add a key prop to the div and pass the chat._id as a value
- This is typescript so we need to add the type of the data so add the interface Chat and add the properties of the data and then pass the Chat type to the useState hook
useState<Chat[]>
Schema Design
- ChatName
- isGroupChat
- users
- latestMessage
- groupAdmin
- First we need mongoose so install mongoose
npm install mongoose
- In the backend folder create a folder called models and in the models folder create a file called chatModel.js
Insde the codeblock you will understand what is going on
// Import mongoose library
const mongoose = require("mongoose");
// Define a schema object using mongoose.Schema constructor
// A schema is a configuration object that defines the structure and properties of the documents in a MongoDB collection
// A schema also defines validation, getters, setters, virtuals, indexes and middleware for the documents
const chatModel = mongoose.Schema(
{
// chatName is a string property that holds the name of the chat
chatName: {
type: String, // Specify the type of the property using the type key
// The type key tells mongoose what data type this property should have and how to cast it
// Mongoose supports several built-in types like String, Number, Date, etc. as well as custom types
trim: true // Specify additional options using other keys, such as trim
// The trim option tells mongoose to remove any whitespace from the beginning and end of the value before saving it to the database
},
// isGroupChat is a boolean property that indicates whether the chat is a group chat or not
isGroupChat: {
type: Boolean,
default: false // Specify a default value using the default key
// The default option tells mongoose what value to use if this property is not specified when creating a new document
},
// users is an array property that references the User model
users: [
{
type: mongoose.Schema.Types.ObjectId, // Specify the type of the elements in the array using mongoose.Schema.Types
// mongoose.Schema.Types is an object that contains all the valid types for schema properties
// ObjectId is a special type that represents a 12-byte MongoDB ObjectId
ref: "User" // Specify the model that this property references using the ref key
// The ref option tells mongoose which model to use when populating this property with actual documents from another collection
// Population is a feature that lets you replace the ids in an array with the corresponding documents from another collection
}
],
// latestMessage is a property that references the Message model
latestMessage: {
type: mongoose.Schema.Types.ObjectId,
ref: "Message"
},
// groupAdmin is a property that references the User model
groupAdmin: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
},
{
timestamps: true // Specify schema-level options using the second argument of the mongoose.Schema constructor
// The timestamps option tells mongoose to automatically add createdAt and updatedAt fields to the chat document
// These fields store the date and time when the chat was created and last modified
}
);
// Convert the schema into a model using mongoose.model function
const Chat = mongoose.model("Chat", chatModel);
// A model is a class that represents a collection in MongoDB and provides methods to create, read, update and delete documents in that collection
// The first argument of mongoose.model is the name of the model and the collection in MongoDB (in singular form)
// The second argument is the schema that defines the structure and properties of the documents in that collection
// Export the model so it can be used in other modules
module.exports = Chat;
- Now create message model so inside model folder create a file called messageModel.js
// require mongoose, a library that provides a high-level interface for interacting with MongoDB
const mongoose = require("mongoose");
// require bcryptjs, a library that provides a secure way of hashing and comparing passwords
const bcrypt = require("bcryptjs");
// define a schema for the user collection using mongoose.Schema
const userSchema = mongoose.Schema(
{
// a string that contains the user's name, which is a required field
name: { type: "String", required: true },
// a string that contains the user's email address, which is a required and unique field
email: { type: "String", unique: true, required: true },
// a string that contains the user's password, which is a required field
password: { type: "String", required: true },
// a string that contains the user's profile picture URL, which is a required field with a default value
pic: {
type: "String",
required: true,
//if someone doesn't upload a profile picture, a default profile picture will be used
default:
"https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg",
},
// a boolean that indicates whether the user is an administrator or not, which is a required field with a default value of false
isAdmin: {
type: Boolean,
required: true,
default: false,
},
},
// an option object that enables the creation of createdAt and updatedAt fields for each document in the collection
{ timestaps: true }
);
// create a User model using mongoose.model with the user schema
const User = mongoose.model("User", userSchema);
// export the User model for use in other modules using module.exports
module.exports = User;
User Authentication Forms in NextJs
- Index.tsx for the background
import Homepage from "./Homepage";
import styles from "./index.module.css";
import { ChakraProvider } from "@chakra-ui/react";
function Home() {
return (
<ChakraProvider>
<div className={styles.App}>
<Homepage />
</div>
</ChakraProvider>
);
}
export default Home;
- CSS file for the background(index.module.css)
@import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@300&display=swap");
.App {
min-height: 100vh;
display: flex;
background: #092756;
background: -moz-radial-gradient(
0% 100%,
ellipse cover,
rgba(104, 128, 138, 0.4) 10%,
rgba(138, 114, 76, 0) 40%
),
-moz-linear-gradient(top, rgba(57, 173, 219, 0.25) 0%, rgba(42, 60, 87, 0.4) 100%),
-moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(
0% 100%,
ellipse cover,
rgba(104, 128, 138, 0.4) 10%,
rgba(138, 114, 76, 0) 40%
),
-webkit-linear-gradient(top, rgba(57, 173, 219, 0.25) 0%, rgba(
42,
60,
87,
0.4
) 100%),
-webkit-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -o-radial-gradient(
0% 100%,
ellipse cover,
rgba(104, 128, 138, 0.4) 10%,
rgba(138, 114, 76, 0) 40%
),
-o-linear-gradient(top, rgba(57, 173, 219, 0.25) 0%, rgba(42, 60, 87, 0.4) 100%),
-o-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -ms-radial-gradient(
0% 100%,
ellipse cover,
rgba(104, 128, 138, 0.4) 10%,
rgba(138, 114, 76, 0) 40%
),
-ms-linear-gradient(top, rgba(57, 173, 219, 0.25) 0%, rgba(42, 60, 87, 0.4) 100%),
-ms-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(
0% 100%,
ellipse cover,
rgba(104, 128, 138, 0.4) 10%,
rgba(138, 114, 76, 0) 40%
),
linear-gradient(
to bottom,
rgba(57, 173, 219, 0.25) 0%,
rgba(42, 60, 87, 0.4) 100%
),
linear-gradient(135deg, #670d10 0%, #092756 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#3E1D6D', endColorstr='#092756', GradientType=1);
background-position: center;
background-size: cover;
}
- Now use some Charkra ui and Tailwind then make a basic page so open your
Homepage.tsx
file and add the following code
import React from "react";
import {
Box,
Container,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from "@chakra-ui/react";
import Login from "@/components/Authentication/Login";
import Signup from "@/components/Authentication/Signup";
const Homepage = () => {
return (
<Container maxW="xl" centerContent mt="8">
<Box
bg="rgba(31, 41, 55, 0.8)"
rounded="lg"
border="1px"
borderColor="gray.700"
p="6"
mb="4"
w="full"
>
<h1 className="text-white text-4xl font-semibold">Octo Meet</h1>
</Box>
<Box
bg="rgba(31, 41, 55, 0.4)"
rounded="lg"
border="1px"
borderColor="gray.700"
p="6"
mb="4"
w="full"
>
<Tabs isFitted variant="soft-rounded">
<TabList mb="4">
<Tab
className="rounded-lg border border-gray-700 p-2 m-2 w-full text-center transition-colors duration-300 hover:bg-gray-700 hover:border-gray-800 hover:text-gray-200"
_focus={{ outline: "none", boxShadow: "outline" }}
>
Login
</Tab>
<Tab
className="text-white rounded-lg border border-gray-700 p-2 m-2 w-full text-center transition-colors duration-300 hover:bg-gray-700 hover:border-gray-800 hover:text-gray-200"
_focus={{ outline: "none", boxShadow: "outline" }}
>
Sign Up
</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Login />
</TabPanel>
<TabPanel>
<Signup />
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</Container>
);
};
export default Homepage;
- Now create a folder called
components
and inside that folder create a folder calledAuthentication
and inside that folder create two filesLogin.tsx
andSignup.tsx
and add the following code
Signup.tsx
import { Button } from "@chakra-ui/button";
import { VStack } from "@chakra-ui/layout";
import { FormControl, FormLabel } from "@chakra-ui/form-control";
import { InputGroup, InputRightElement } from "@chakra-ui/input";
import React, { useState } from "react";
import styles from "./Signup.module.css";
const Signup = () => {
const [show, setShow] = useState(false); //data type as boolean
const [name, setName] = useState<string>(""); //data type as string
const [email, setEmail] = useState<string>("");
const [confirmpassword, setConfirmpassword] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [pic, setPic] = useState();
const handleClick = () => setShow(!show);
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {};
const postDetails = (pics: FileList | null) => {}; //We will change it future ❗
return (
<VStack spacing="5px" color="gray.700">
{/*//Name of the user*/}
<div className={styles.inputContainer} id="first-name">
<input
id={styles.inputField}
type="text"
required
placeholder="What's should we call you? 🙂"
onChange={(e) => setName(e.target.value)}
/>
<label className={styles.usernameLabel} htmlFor={styles.inputField}>
Name{" "}
</label>
<svg viewBox="0 0 448 512" className={styles.userIcon}>
<path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"></path>
</svg>
</div>
{/*//Email of the user*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="email" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Email Address
</FormLabel>
</FormControl>
<input
id={styles.inputField}
type="email"
required
placeholder="Now Enter Your Email ✨"
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{/*//Password of the user*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="password" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Password
</FormLabel>
</FormControl>
<InputGroup>
<input
id={styles.inputField}
type={show ? "text" : "password"}
required
className={`${styles.input} input`}
placeholder="Enter Strong password 🔐"
onChange={(e) => setPassword(e.target.value)}
/>
<InputRightElement width="4.5rem">
<Button
className="text-white input-group-button"
onClick={handleClick}
variant="ghost"
fontSize="sm"
fontFamily="sans-serif"
colorScheme="gray"
_hover={{ bg: "gray.700" }}
bg="transparent"
textColor={show ? "gray.300" : "gray.500"}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
</div>
{/*Confirmation of password*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="password" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Confirm Password
</FormLabel>
</FormControl>
<InputGroup>
<input
id={styles.inputField}
type={show ? "text" : "password"}
required
className={`${styles.input} input`}
placeholder="Confirm your password 🗝️"
onChange={(e) => setConfirmpassword(e.target.value)}
/>
<InputRightElement width="4.5rem">
<Button
className="text-white input-group-button"
onClick={handleClick}
variant="ghost"
fontSize="sm"
fontFamily="sans-serif"
colorScheme="gray"
_hover={{ bg: "gray.700" }}
bg="transparent"
textColor={show ? "gray.300" : "gray.500"}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
</div>
{/*//Profile picture of the user*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="pic" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Upload Profile Picture 📸
</FormLabel>
</FormControl>
<InputGroup>
<input
id={styles.inputField}
type="file"
accept="image/*"
className={`${styles.input} input`}
placeholder="Upload Profile Pictur🗝️"
//We will change it future ❗
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
postDetails(e.target.files);
}
}}
/>
</InputGroup>
</div>
<Button colorScheme="blue" width="100%" style={{ marginTop: 15 }}>
Sign Up
</Button>
</VStack>
);
};
export default Signup;
- Add css file for the signup page so make a file named
Signup.module.css
in the same folder and add the following code in it.
.inputContainer {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
}
#inputField {
width: 460px;
max-width: 100%;
height: 45px;
border: none;
outline: none;
padding: 0px 7px;
font-size: 15px;
color: white;
background-color: transparent;
box-shadow: 3px 3px 10px rgba(0, 0, 0, 1),
-1px -1px 6px rgba(255, 255, 255, 0.4);
border-radius: 10px;
font-weight: 500;
caret-color: rgb(155, 78, 255);
transition: width 0.3s ease; /* add transition property */
font-family: Whitney, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
#inputField:focus {
width: 480px;
max-width: 100%;
height: 45px;
border: none;
outline: none;
padding: 0px 7px;
font-size: 15px;
background-color: transparent;
box-shadow: 3px 3px 10px rgb(26, 42, 44),
-1px -1px 6px rgba(255, 255, 255, 0.4),
inset 3px 3px 10px rgba(0, 0, 0, 1),
inset -1px -1px 6px rgba(255, 255, 255, 0.4);
border-radius: 10px;
color: #c17d97;
font-weight: 500;
caret-color: rgb(155, 78, 255);
transition: width 0.3s ease; /* add transition property */
font-family: Whitney, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
@media (max-width: 767px) {
#inputField {
width: 100%;
}
#inputField:focus {
width: 100%;
}
}
.userIcon {
position: absolute;
fill: rgb(155, 78, 255);
width: 12px;
height: 12px;
top: -23px;
left: -15px;
opacity: 0;
transition: .2s linear;
}
.usernameLabel {
position: absolute;
top: -25px;
left: 5px;
color: #b0afaf;
font-size: 14px;
font-weight: 500;
font-family: Whitney, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
overflow: hidden;
transition: .2s linear;
opacity: 0;
}
#inputField:focus ~ .usernameLabel,
#inputField:valid ~ .usernameLabel {
transform: translateX(20px);
opacity: 1;
font-size: 12px;
}
#inputField:focus ~ .userIcon,
#inputField:valid ~ .userIcon {
transform: translateX(20px);
opacity: 1;
}
#inputField:focus,
#inputField:valid {
background-color: #37344f;
transition-duration: .3s;
}
- Now just do same for the
login page
and you are done with the frontend part of the project.
import { Button } from "@chakra-ui/button";
import { VStack } from "@chakra-ui/layout";
import { FormControl, FormLabel } from "@chakra-ui/form-control";
import { InputGroup, InputRightElement } from "@chakra-ui/input";
import React, { useState } from "react";
import styles from "./Signup.module.css";
const Login = () => {
const [show, setShow] = useState(false); //data type as boolean
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const handleClick = () => setShow(!show);
const submitHandler = () => {};
return (
<VStack spacing="5px" color="gray.700">
{/*//Email of the user*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="email" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Email Address
</FormLabel>
</FormControl>
<input
id={styles.inputField}
type="email"
required
placeholder="Now Enter Your Email ✨"
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{/*//Password of the user*/}
<div className="mt-18"></div>
<div className={styles.inputContainer}>
<FormControl id="password" isRequired>
<FormLabel
style={{
opacity: 0.2,
fontFamily: "Helvetica Neue",
fontSize: 16,
fontWeight: "bold",
letterSpacing: "0.5px",
}}
className="text-transparent tex bg-clip-text bg-gradient-to-l from-slate-500 via-red-800 to-purple-300 pl-2 mt-2"
>
Password
</FormLabel>
</FormControl>
<InputGroup>
<input
id={styles.inputField}
type={show ? "text" : "password"}
required
className={`${styles.input} input`}
placeholder="Enter Strong password 🔐"
onChange={(e) => setPassword(e.target.value)}
/>
<InputRightElement width="4.5rem">
<Button
className="text-white input-group-button"
onClick={handleClick}
variant="ghost"
fontSize="sm"
fontFamily="sans-serif"
colorScheme="gray"
_hover={{ bg: "gray.700" }}
bg="transparent"
textColor={show ? "gray.300" : "gray.500"}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
</div>
<Button colorScheme="blue" width="100%" style={{ marginTop: 15 }}>
Login
</Button>
<Button
variant="solid"
colorScheme="red"
width="100%"
onClick={() => {
setEmail("guest@example.com");
setPassword("123456");
}}
>
Access as a Guest User
</Button>
</VStack>
);
};
export default Login;
We borrow same css from the signup page and use it in the login page.
Connect MongoDB
- First Setup a MongoDB Atlas account and create a cluster.
- Then copy the connection string and paste it in the
.env
file.
MONGODB_URI = "your connection string"
Replace your password with
<password>
and your database name with<dbname>
.
- Now create a config folder and create a
db.js
file. - In the
db.js
file, write the following code. // you can also use colors package to make your console logs colorful.
const mongoose = require("mongoose");
const colors = require("colors");
const connectDB = async () => {
try {
// Remove the useFindAndModify option from the connection options object
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
// useFindAndModify: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
} catch (error) {
console.log(`Error: ${error.message}`.red.bold);
process.exit();
}
};
module.exports = connectDB;
- With explanation
// Define an asynchronous function to connect to the database
// An asynchronous function is a special kind of function that can pause and resume its execution until some operation is completed
// This allows the code to run without blocking other tasks while waiting for the result of the operation
// The async keyword before the function declaration indicates that it is an asynchronous function
const connectDB = async () => {
// Use a try-catch block to handle any errors that might occur during the connection process
// The try block contains the code that attempts to connect to the database
// The catch block contains the code that handles any errors that are thrown by the try block
try {
// Use the mongoose.connect method to return a promise that resolves to a connection object
// A promise is an object that represents the eventual completion or failure of an asynchronous operation
// The mongoose.connect method takes two arguments: a connection string and an options object
// The connection string specifies the location and name of the database to connect to
// The options object specifies some configuration settings for the connection
// The await keyword before the mongoose.connect method indicates that the code should wait until the promise is resolved or rejected before moving on to the next line
// If the promise is resolved, it returns a connection object that can be used to interact with the database
// If the promise is rejected, it throws an error that can be caught by the catch block
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: true,
});
// Log a message to the console indicating that the connection was successful and showing the host name of the database server
// The conn.connection.host property contains the host name of the database server
// The ${} syntax inside the backticks (`) allows us to insert a variable or expression into a string template
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (err) {
// If there is an error during the connection process, log the error message to the console and exit the process with a non-zero code
// The err.message property contains the error message that describes what went wrong
// The console.error method logs an error message to the console with a red color and a stack trace
// The process.exit method terminates the current process and exits with a given code
// A non-zero exit code indicates that something went wrong and the process did not complete successfully
console.error(`Error: ${err.message}`);
process.exit();
}
};
- No go to server.js and type the following code.
// Import the connectDB function from the db.js file
const connectDB = require("./config/db");
// Connect to the database
connectDB();
Serve.js file
Don't do this mistakes the dotenv module after calling the connectDB function in your server.js file. This means that the MONGO_URI environment variable is not available when you pass it to the mongoose.connect() method. You need to load the dotenv module before calling the connectDB function.
const express = require("express");
const chats = require("./data/data");
const cors = require("cors");
// Load the dotenv module before calling connectDB
const dotenv = require("dotenv");
dotenv.config();
const app = express();
const connectDB = require("./config/db");
app.use(cors());
connectDB();
app.get("/", (req, res) => {
res.send("API is running...");
});
app.get("/api/chats", (req, res) => {
res.send(chats);
});
app.get("/api/chats/:id", (req, res) => {
//console.log(req);
// console.log(req.params.id);
const chat = chats.find((c) => c._id === req.params.id);
res.send(chat);
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, console.log(`Server started on port ${PORT}`));
Authentication
Json Web Token
Part 1
- Install the following packages.
npm i jsonwebtoken
npm i express-async-handler
- controller folder create a
userController.js
file then create a filegenerateToken.js
in the config folder andt inside theroute
folder create auserRoutes.js
file.
backend/controllers/userControllers.js
const asyncHandler = require("express-async-handler"); // Import the 'express-async-handler' middleware to handle asynchronous errors
const User = require("../models/userModel"); // Import the User model, assuming it represents a user in the database
const generateToken = require("../config/generateToken"); // Import the 'generateToken' function from the '../config/generateToken.js' file
// Register a new user
const registerUser = asyncHandler(async (req, res) => {
const { name, email, password, pic } = req.body; // Destructure the request body to get name, email, password, and pic
// If any of the fields are empty, throw an error
if (!name || !email || !password) {
res.status(400);
throw new Error("Please fill all the fields");
}
// Check if the user already exists
const userExists = await User.findOne({ email }); // Use the User model to find a user with the given email in the database
if (userExists) {
// If the user already exists, throw an error
res.status(400);
throw new Error("User already exists");
}
// If the user does not exist, create a new user
const user = await User.create({ // Use the User model to create a new user with the provided data and save it to the database
name,
email,
password,
pic,
});
// If the user is created successfully, send the user data back to the frontend
if (user) {
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: generateToken(user._id), // Generate a token for the user using the 'generateToken' function and include it in the response
});
} else {
// If the user is not created, throw an error
res.status(400);
throw new Error("Invalid user data");
}
});
module.exports = registerUser; // Export the registerUser function as the default export of this module
Problem (With user property so fix it)
backend/controllers/userControllers.js
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const generateToken = require("../config/generateToken");
const registerUser = asyncHandler(async (req, res) => {
const { name, email, password, pic } = req.body;
//if any of the fields are empty, throw an error
if (!name || !email || !password) {
res.status(400);
throw new Error("Please fill all the fields");
}
const userExists = await User.findOne({ email }); // Update variable name to User instead of user
//if the user already exists, throw an error
if (userExists) {
res.status(400);
throw new Error("User already exists");
}
//if the user does not exist, create a new user
const newUser = await User.create({
// Update variable name to newUser instead of user
name,
email,
password,
pic,
});
//if the user is created, send the user data back to the frontend
if (newUser) {
res.status(201).json({
_id: newUser._id,
name: newUser.name,
email: newUser.email,
pic: newUser.pic,
token: generateToken(newUser._id),
});
} else {
//if the user is not created, throw an error
res.status(400);
throw new Error("Invalid user data");
}
});
module.exports = registerUser;
In summary, the registerUser function is an asynchronous function that handles the registration of a new user. It uses the asyncHandler middleware to handle any asynchronous errors that may occur during its execution. It first checks if the required fields (name, email, and password) are provided in the request body and throws an error if any of them are missing. Then, it checks if the user already exists in the database based on the provided email, and throws an error if the user already exists. If the user does not exist, it creates a new user using the User.create() method from the User model, saves it to the database, and sends the user data back to the frontend, including a token generated using the generateToken function. If the user is not created successfully, it throws an error with a message "Invalid user data".
backend/models/userModel.js
const mongoose = require("mongoose");
const userSchema = mongoose.Schema(
{
name: { type: "String", required: true },
email: { type: "String", unique: true, required: true },
password: { type: "String", required: true },
pic: {
type: "String",
default:
"https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg",
},
isAdmin: {
type: Boolean,
required: true,
default: false,
},
},
{ timestaps: true }
);
const User = mongoose.model("User", userSchema);
module.exports = User;
backend/server.js
const express = require("express");
const dotenv = require("dotenv");
const { chats } = require("./data/data");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");// Import the userRoutes from the '../routes/userRoutes.js' file
const cors = require("cors");
dotenv.config();
connectDB();
const app = express();
//Since we’re sending data from the frontend to the server, we need to parse it to JSON and specify its format.
app.use(express.json());
app.use(cors());
app.get("/", (req, res) => {
res.send("API is running...");
});
app.use("/api/user", userRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, console.log(`Server started on port ${PORT}`));
backend/config/generateToken.js
const jwt = require("jsonwebtoken");
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: "30d",
});
};
module.exports = generateToken;
backend/routes/userRoutes.js
const express = require("express");
const router = express.Router();
const registerUser = require("../controllers/userControllers");
router.route("/").post(registerUser);
// router.post("/login", authUser);
module.exports = router;
Part 2 (Postman)
- Let's test it on postman name a folder name
Authentication
by clicking on the+
icon and then click on thesave
button. - Make another folder name
Authentication
and then click on thesave
button. - Inside that create a filename
Registration
and then click on thesave
button. - Inside that GET box type
http://localhost:5000/api/user
and you can sethttp://localhost:5000
as a variable.- Go to new tab (top left) and click on
Environment
- Give it a name
- Insice the Variable tab add
URL
and set the value tohttp://localhost:5000
and then click on thesave
button. - Now if you go to top right corner under
Upgrade
you will see the name of the environment you created select this and right side you can see eye icon click on that and you will see the variable you created.
- Go to new tab (top left) and click on
- Now in the
Registration
file change thehttp://localhost:5000/api/user
to{{URL}}/api/user
and then click on thesave
button. - Now go to
Body
tab select raw and then select JSON - Set the method POST because our backend is expecting a POST request.
- Now in the
Body
tab you can see the JSON format we need to send to the backend.
{
"name": "Subham",
"email": "subham@codexam.com",
"password": "123456"
}
- Now you can see something like this
{
"_id": "6442b690811d9895c66ad7ed",
"name": "Subham Maity",
"email": "subham@codexam.com",
"pic": "https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg",
"token": "cCI6IkpXVCJ9.eyJpZCI6IjY0NDJiNjkwODExZDk4OTVjNjZhZDdlZCIsImlhdCI6MTY4MjA5MzcxMiwiZXhwIjoxNjg0Njg1NzEyfQ.mt_K5LzpAZDy49VfG3Vdzizpk3XB1oRIpzQAyl_XdeE"
}
- Now check your database you will see the user is created in the database (MongoDB Atlas -> Browse Collections)
{"_id":{"$oid":"6443a62d373fe47e47fc7961"},
"name":"Subham Maity",
"email":"subham@codexam.com",
"password":"123456",
"pic":"https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg",
"isAdmin":false,"__v":{"$numberInt":"0"}}
Part 3 ( authUser function )
backend/routes/userRoutes.js
router.post("/login", authUser);
backend/controllers/userControllers.js
//userRoutes.js -> authUser function
const authUser = asyncHandler(async (req, res) => {
//We are gonna take the email and password from the request body
const { email, password } = req.body;
//find the user exist in the database or not
const user = await User.findOne({ email });
//if the user exists
if (user && ()) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
pic: user.pic,
token: generateToken(user._id),
});
} else {
res.status(401);
throw new Error("Invalid email or password");
}
});
Pending Secure password
Part 4 ( Password Encryption )
We don't wanna store the password in plain text in the database so we are gonna use bcrypt
to encrypt the password.
- Install
bcryptjs
by runningnpm i bcryptjs
backend/models/userModel.js
userSchema.pre("save");
This means before saving the user to the database we are gonna run this function.
- Now we are gonna create a function that will match the password that we have in the database with the password that we are passing in.
userSchema.pre("save", async function (next) {
if (!this.isModified) {//if the password is not modified then we don't need to do anything
next();//move on to the next middleware or function if we don't use next() then it will be stuck here
}
//higher the number is, more secure the password is.
const salt = await bcrypt.genSalt(10);
//hashing the password
this.password = await bcrypt.hash(this.password, salt);
});
- inside the
userControllers
we are created function where that will match the password that we have in the database with the password that we are passing in `if (user && (await user.matchPasswor(password)))`` - for this we need to create a matchPassword function inside the
userModel.js
file
// Controller -> userControlles->user.matchPasswor(password)
userSchema.methods.matchPassword = async function (enteredPassword) {
//This compares the password that we have in the database with the password that we are passing in
return await bcrypt.compare(enteredPassword, this.password); //this.password is the password that we have in the database and enteredPassword is the password that we are passing in the function
};
Entire userModel.js now
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const userSchema = mongoose.Schema(
{
name: { type: "String", required: true },
email: { type: "String", unique: true, required: true },
password: { type: "String", required: true },
pic: {
type: "String",
default:
"https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg",
},
isAdmin: {
type: Boolean,
required: true,
default: false,
},
},
{ timestamps: true }
);
// Controller -> userControlles->user.matchPasswor(password)
userSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// before saving the user to the database we are going to run this function.
userSchema.pre("save", async function (next) {
if (!this.isModified) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
const User = mongoose.model("User", userSchema);
module.exports = User;
- Now we are gonna import authUser in userRoutes.js
const { registerUser, authUser } = require("../controllers/userControllers");
router.post("/login", authUser);
controllers -> userControlle.js
if (user && (await user.matchPasswor(password))) {
....
}
module.exports = { registerUser, authUser };
Entire userController.js now
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const generateToken = require("../config/generateToken");
const registerUser = asyncHandler(async (req, res) => {
const { name, email, password, pic } = req.body;
//if any of the fields are empty, throw an error
if (!name || !email || !password) {
res.status(400);
throw new Error("Please fill all the fields");
}
const userExists = await User.findOne({ email }); // Update variable name to User instead of user
//if the user already exists, throw an error
if (userExists) {
res.status(400);
throw new Error("User already exists");
}
//if the user does not exist, create a new user
const newUser = await User.create({
// Update variable name to newUser instead of user
name,
email,
password,
pic,
});
//if the user is created, send the user data back to the frontend
if (newUser) {
res.status(201).json({
_id: newUser._id,
name: newUser.name,
email: newUser.email,
pic: newUser.pic,
token: generateToken(newUser._id),
});
} else {
//if the user is not created, throw an error
res.status(400);
throw new Error("Invalid user data");
}
});
//userRoutes.js -> authUser function
const authUser = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (user && (await user.matchPasswor(password))) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
pic: user.pic,
token: generateToken(user._id),
});
} else {
res.status(401);
throw new Error("Invalid email or password");
}
});
module.exports = { registerUser, authUser };
- Now test the password Encryption in postman
Part 5 ( Let's check login -> postman)
Part 6 ( Login -> Error Handling)
Lets say if I pass http://localhost:5000/api/users/lo
in the postman, it will give me an error Cannot GET /api/users/lo
- In server file, we are gonna create a middleware function that will handle all the errors that we are getting in the backend.
app.use(notFound);
app.use(errorHandler);
- Now create a
middleware
folder and create aerrorMiddleware.js
file
const notFound = (req, res, next) => {
const error = new Error(`Not Found - ${req.originalUrl} `);
res.status(404);
next(error);
};
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === "production" ? null : err.stack,
});
};
module.exports = { notFound, errorHandler };
Let me explain what we are doing here.
// This function makes an error message if the URL is not found
// and tells the next function to handle it
const notFound = (req, res, next) => {
// req has information about the request
// res can send a response back to the client
// next can call the next function in the stack
const error = new Error(`Not Found - ${req.originalUrl} `); // make an error message with the URL
res.status(404); // tell the client that the URL is not found
next(error); // tell the next function to handle the error
};
// This function handles any error that is passed to it
// It sends a JSON response with the error message and stack trace (if not in production mode)
const errorHandler = (err, req, res, next) => {
// err has information about the error
const statusCode = res.statusCode === 200 ? 500 : res.statusCode; // check the status code and change it to 500 if it is 200
// This is because Express sets the default status code to 200
// Changing it to 500 tells the client that something went wrong on the server
// The ternary operator is a short way of writing if-else
// It means if res.statusCode === 200 is true, then use 500, otherwise use res.statusCode
res.status(statusCode); // tell the client the status code
res.json({
message: err.message, // tell the client the error message
stack: process.env.NODE_ENV === "production" ? null : err.stack, // tell the client the error stack trace if not in production mode
// The stack trace shows where and how the error happened
// In production mode, we don't want to show it to the client for security reasons
// That's why we check process.env.NODE_ENV and use null if it is "production"
// process.env.NODE_ENV is a variable that tells us which mode the app is running in
// It can be set manually or by using tools like dotenv or cross-env
// If it is not set, it will be undefined by default
});
};
// This exports the two functions so they can be used in other files
module.exports = { notFound, errorHandler };
The code block defines two functions: notFound and errorHandler. They are error-handling middleware functions that can catch and handle errors that occur in the Express app.
- notFound: This function creates a new Error object with a custom message that includes the original URL of the request (req.originalUrl). It then sets the status code of the response (res) to 404 (Not Found), which indicates that the server could not find what was requested. Finally, it passes the error object to the next middleware function using the next function.
- errorHandler: This function handles any error object that is passed to it by any other middleware or route handler. It checks the status code of the response (res.statusCode) and sets it to 500 (Internal Server Error) if it is 200 (OK). This is because Express sets the default status code to 200 unless otherwise specified. Setting it to 500 indicates that something went wrong on the server side and prevents sending a misleading response. It then sends a JSON response with the following properties:
- message: The error message extracted from the error object (err.message).
- stack: The stack trace of the error extracted from the error object (err.stack). However, in production environment (process.env.NODE_ENV set to "production"), it is set to null to prevent sensitive information from being exposed.
The last line exports the two functions so they can be imported and used in other files.
Some terms that are used in this explanation are:
- req: The request object that contains information about the incoming HTTP request, such as URL, query parameters, request headers, etc.
- res: The response object that is used to send the HTTP response back to the client. It includes methods for setting the response status code, sending response headers, and sending response body.
- next: A function that passes control to the next middleware function in the chain. It is typically used to pass control to the next middleware function after completing the current middleware's processing.
- statusCode: The HTTP response status code, which indicates the outcome of the HTTP request.
- err: The error object, which contains information about the error that occurred during the request processing. It may include an error message, error stack trace, and other details.
- Now import this in the server file
const { notFound, errorHandler } = require("./middlewares/errorMiddleware");
Part 7 ( Picture Upload)
We will use cloudinary to upload the picture. So go to cloudinary.com and create an account. After creating an account, you will get an API key and API secret. Copy them .
Install next-env
npm i next-env
- Create a file called .env.local in the root directory of your project. This file will contain your environment variables.
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET="your_upload_preset"
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="your_cloud_name"
NEXT_PUBLIC_API_KEY="your_api_key"
- Make sure that your .env.local file is ignored by your version control system, such as Git. You can add it to your .gitignore file to prevent it from being committed.
# .gitignore
.env.local
-
Restart your Next.js development server if it is running. Next.js will automatically load the environment variables from the .env.local file into process.env.
-
In your code, you can access the environment variables using the process.env object:
// in Signup.tsx
import React from "react";
const Signup = () => {
// Access environment variables
const cloudinaryUploadPreset = process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET ?? "";
const cloudinaryCloudName = process.env.NEXT_PUBLIC_CLOUDINARY_