├── frontend ├── public │ └── bg.jpg ├── postcss.config.js ├── src │ ├── assets │ │ └── sounds │ │ │ └── notification.mp3 │ ├── zustand │ │ └── useConversation.js │ ├── utils │ │ ├── extractTime.js │ │ └── emoji.js │ ├── components │ │ ├── sidebar │ │ │ ├── Sidebar.jsx │ │ │ ├── LogoutButton.jsx │ │ │ ├── Conversations.jsx │ │ │ ├── Conversation.jsx │ │ │ └── SearchInput.jsx │ │ ├── skeletons │ │ │ └── MessageSkeletons.jsx │ │ └── messages │ │ │ ├── Messages.jsx │ │ │ ├── MessageInput.jsx │ │ │ ├── Message.jsx │ │ │ └── MessageContainer.jsx │ ├── pages │ │ ├── home │ │ │ └── Home.jsx │ │ ├── signup │ │ │ ├── GenderCheckbox.jsx │ │ │ └── SignUp.jsx │ │ └── login │ │ │ └── Login.jsx │ ├── context │ │ ├── AuthContext.jsx │ │ └── SocketContext.jsx │ ├── main.jsx │ ├── hooks │ │ ├── useListenMessages.js │ │ ├── useGetConversations.js │ │ ├── useLogout.js │ │ ├── useGetMessages.js │ │ ├── useSendMessage.js │ │ ├── useLogin.js │ │ └── useSignup.js │ ├── App.jsx │ └── index.css ├── tailwind.config.js ├── index.html ├── vite.config.js ├── README.md ├── .eslintrc.cjs └── package.json ├── backend ├── routes │ ├── user.routes.js │ ├── auth.routes.js │ └── message.routes.js ├── db │ ├── connectToMongoDB.js │ └── connectToRedis.js ├── models │ ├── message.model.js │ ├── conversation.model.js │ └── user.model.js ├── utils │ └── generateToken.js ├── middleware │ └── protectRoute.js ├── socket │ └── socket.js ├── controller │ ├── user.controller.js │ ├── message.controller.js │ └── auth.controller.js └── server.js ├── .gitignore ├── package.json ├── LICENSE └── README.md /frontend/public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriti-raj/messenger-web-app/HEAD/frontend/public/bg.jpg -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/assets/sounds/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriti-raj/messenger-web-app/HEAD/frontend/src/assets/sounds/notification.mp3 -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | // eslint-disable-next-line no-undef 11 | plugins: [require("daisyui")], 12 | } -------------------------------------------------------------------------------- /backend/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import protectRoute from "../middleware/protectRoute.js"; 3 | import { getUsersForSidebar } from "../controller/user.controller.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/", protectRoute, getUsersForSidebar) 8 | 9 | export default router; -------------------------------------------------------------------------------- /backend/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { signup, login, logout } from "../controller/auth.controller.js" 3 | const router = express.Router(); 4 | 5 | router.post("/signup", signup); 6 | 7 | router.post("/login", login); 8 | 9 | router.post("/logout", logout); 10 | 11 | export default router; -------------------------------------------------------------------------------- /frontend/src/zustand/useConversation.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | const useConversation = create(set => ({ 4 | selectedConversation: null, 5 | setSelectedConversation: (selectedConversation) => set({ selectedConversation }), 6 | messages: [], 7 | setMessages: (messages) => set({ messages }), 8 | })) 9 | 10 | export default useConversation; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | 27 | .env -------------------------------------------------------------------------------- /backend/routes/message.routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getMessages, sendMessage } from "../controller/message.controller.js" 3 | import protectRoute from "../middleware/protectRoute.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/:id", protectRoute, getMessages) 8 | router.post("/send/:id", protectRoute, sendMessage) 9 | 10 | export default router; -------------------------------------------------------------------------------- /backend/db/connectToMongoDB.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectToMongoDB = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGO_DB_URI); 6 | console.log("Connected to MongoDB"); 7 | } catch (error) { 8 | console.log("Error connecting to MongoDB", error.message); 9 | } 10 | } 11 | 12 | export default connectToMongoDB; 13 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Messanger App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/utils/extractTime.js: -------------------------------------------------------------------------------- 1 | export function extractTime(dataString) { 2 | const data = new Date(dataString); 3 | const hours = padZero(data.getHours()); 4 | const minutes = padZero(data.getMinutes()); 5 | return `${hours}:${minutes}`; 6 | } 7 | 8 | // Helper function to pad single-digit numbers with a leading zero 9 | function padZero(number) { 10 | return number.toString().padStart(2, '0'); 11 | } -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 5000, 9 | proxy: { 10 | '/api': { 11 | target: 'http://localhost:3000', 12 | changeOrigin: true, 13 | secure: false, 14 | }, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import SearchInput from "./SearchInput"; 2 | import Conversations from "./Conversations"; 3 | import LogoutButton from "./LogoutButton"; 4 | 5 | const Sidebar = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Sidebar; 17 | -------------------------------------------------------------------------------- /frontend/src/pages/home/Home.jsx: -------------------------------------------------------------------------------- 1 | import Sidebar from "../../components/sidebar/Sidebar"; 2 | import MessageContainer from "../../components/messages/MessageContainer"; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Home; 14 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/LogoutButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BiLogOut } from "react-icons/bi"; 3 | import useLogout from "../../hooks/useLogout"; 4 | 5 | const LogoutButton = () => { 6 | const { loading, logout } = useLogout(); 7 | return ( 8 |
9 | {!loading ? ( 10 | 14 | ) : ( 15 | 16 | )} 17 |
18 | ); 19 | }; 20 | 21 | export default LogoutButton; 22 | -------------------------------------------------------------------------------- /backend/models/message.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const messageSchema = new mongoose.Schema({ 4 | senderId: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "User", 7 | required: true 8 | }, 9 | receiverId: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: "User", 12 | required: true 13 | }, 14 | message: { 15 | type: String, 16 | required: true 17 | } 18 | // createdAt, updatedAt 19 | }, { timestamps: true }); 20 | 21 | const Message = mongoose.model("Message", messageSchema); 22 | 23 | export default Message; -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const generateTokenAndSetCookie = (userId, res) => { 4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, { 5 | expiresIn: "15d" 6 | }); 7 | 8 | res.cookie("jwt", token, { 9 | maxAge: 15 * 24 * 60 * 60 * 1000, // 15 days (in milliseconds) 10 | httpOnly: true, // Prevents XSS attacks cross-site scripting attacks 11 | sameSite: "strict", // CSRF attacks cross-site request forgery attacks 12 | secure: process.env.NODE_ENV !== "development" // HTTPS 13 | }); 14 | }; 15 | 16 | export default generateTokenAndSetCookie; -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext } from "react"; 2 | 3 | export const AuthContext = createContext(); 4 | 5 | // eslint-disable-next-line react-refresh/only-export-components 6 | export const useAuthContext = () => { 7 | return useContext(AuthContext); 8 | }; 9 | 10 | export const AuthContextProvider = ({ children }) => { 11 | const [authUser, setAuthUser] = useState( 12 | JSON.parse(localStorage.getItem("chat-user")) || null 13 | ); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /backend/models/conversation.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const conversationSchema = new mongoose.Schema( 4 | { 5 | participants: [ 6 | { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "User", 9 | }, 10 | ], 11 | messages: [{ 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: "Message", 14 | default: [], 15 | }, 16 | ], 17 | }, 18 | { 19 | timestamps: true 20 | } 21 | ); 22 | 23 | const Conversation = mongoose.model("Conversation", conversationSchema); 24 | 25 | export default Conversation; -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { AuthContextProvider } from "./context/AuthContext.jsx"; 7 | import { SocketContextProvider } from "./context/SocketContext.jsx"; 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema({ 4 | fullName: { 5 | type: String, 6 | required: true, 7 | }, 8 | username: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | }, 13 | password: { 14 | type: String, 15 | required: true, 16 | minlength: 6, 17 | }, 18 | gender: { 19 | type: String, 20 | required: true, 21 | enum: ["male", "female"], 22 | }, 23 | profilePic: { 24 | type: String, 25 | default: "", 26 | }, 27 | }, { timestamps: true }); 28 | 29 | const User = mongoose.model("User", userSchema); 30 | 31 | export default User; -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | 'react/prop-types': "off", 21 | "react/jsx-uses-react": "error", 22 | "react/jsx-uses-vars": "error" 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/skeletons/MessageSkeletons.jsx: -------------------------------------------------------------------------------- 1 | const MessageSkeleton = () => { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | ); 19 | }; 20 | export default MessageSkeleton; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transparent-chat-app-socket.io", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "nodemon backend/server.js", 8 | "start": "node backend/server.js", 9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend" 10 | }, 11 | "type": "module", 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cookie-parser": "^1.4.6", 18 | "dotenv": "^16.4.2", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^8.1.1", 22 | "nodemon": "^3.0.3", 23 | "react-hot-toast": "^2.4.1", 24 | "redis": "^4.6.15", 25 | "socket.io": "^4.7.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/db/connectToRedis.js: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | 3 | const connectToRedis = async () => { 4 | try { 5 | const client = createClient({ 6 | password: process.env.REDIS_PASSWORD, 7 | socket: { 8 | host: process.env.REDIS_HOST, 9 | port: process.env.REDIS_PORT 10 | } 11 | }); 12 | 13 | await client.connect(); // Connect to the Redis client 14 | console.log("Connected to Redis"); 15 | 16 | return client; // Return the connected client 17 | } catch (error) { 18 | console.log("Error connecting to Redis:", error.message); 19 | throw error; // Rethrow the error to handle it in the server startup 20 | } 21 | } 22 | 23 | export default connectToRedis; 24 | -------------------------------------------------------------------------------- /frontend/src/hooks/useListenMessages.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import { useSocketContext } from "../context/SocketContext"; 4 | import useConversation from "../zustand/useConversation"; 5 | import notificationSound from "../assets/sounds/notification.mp3" 6 | 7 | const useListenMessages = () => { 8 | const { socket } = useSocketContext(); 9 | const { messages, setMessages } = useConversation(); 10 | 11 | useEffect(() => { 12 | socket?.on("newMessage", (newMessage) => { 13 | newMessage.shouldShake = true; 14 | const sound = new Audio(notificationSound); 15 | sound.play(); 16 | setMessages([...messages, newMessage]); 17 | }) 18 | return () => socket?.off("newMessage") 19 | }, [socket, setMessages, messages]) 20 | } 21 | 22 | export default useListenMessages -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Conversations.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Conversation from "./Conversation.jsx"; 3 | import useGetConversations from "./../../hooks/useGetConversations.js"; 4 | import { getRandomEmoji } from "../../utils/emoji.js"; 5 | 6 | const Conversations = () => { 7 | const { loading, conversations } = useGetConversations(); 8 | return ( 9 |
10 | {conversations.map((conversation, idx) => ( 11 | 17 | ))} 18 | {loading ? : null} 19 |
20 | ); 21 | }; 22 | 23 | export default Conversations; 24 | -------------------------------------------------------------------------------- /frontend/src/utils/emoji.js: -------------------------------------------------------------------------------- 1 | export const funEmojis = [ 2 | "👾", 3 | "⭐", 4 | "🌟", 5 | "🎉", 6 | "🎊", 7 | "🎈", 8 | "🎁", 9 | "🎂", 10 | "🎄", 11 | "🎃", 12 | "🎗", 13 | "🎟", 14 | "🎫", 15 | "🎖", 16 | "🏆", 17 | "🏅", 18 | "🥇", 19 | "🥈", 20 | "🥉", 21 | "⚽", 22 | "🏀", 23 | "🏈", 24 | "⚾", 25 | "🎾", 26 | "🏐", 27 | "🏉", 28 | "🎱", 29 | "🏓", 30 | "🏸", 31 | "🥅", 32 | "🏒", 33 | "🏑", 34 | "🏏", 35 | "⛳", 36 | "🏹", 37 | "🎣", 38 | "🥊", 39 | "🥋", 40 | "🎽", 41 | "⛸", 42 | "🥌", 43 | "🛷", 44 | "🎿", 45 | "⛷", 46 | "🏂", 47 | "🏋️", 48 | "🤼", 49 | "🤸", 50 | "🤺", 51 | "⛹️", 52 | "🤾", 53 | "🏌️", 54 | "🏇", 55 | "🧘", 56 | ]; 57 | 58 | export const getRandomEmoji = () => { 59 | return funEmojis[Math.floor(Math.random() * funEmojis.length)]; 60 | }; -------------------------------------------------------------------------------- /frontend/src/hooks/useGetConversations.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import toast from 'react-hot-toast'; 3 | 4 | const useGetConversations = () => { 5 | const [loading, setLoading] = useState(false); 6 | const [conversations, setConversations] = useState([]); 7 | 8 | useEffect(() => { 9 | const getConversations = async () => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch("/api/users"); 13 | const data = await res.json(); 14 | if (data.error) { 15 | throw new Error(data.error); 16 | } 17 | setConversations(data); 18 | } catch (error) { 19 | toast.error(error.message); 20 | } finally { 21 | setLoading(false); 22 | } 23 | } 24 | getConversations(); 25 | }, []); 26 | return { loading, conversations }; 27 | } 28 | 29 | export default useGetConversations; -------------------------------------------------------------------------------- /frontend/src/hooks/useLogout.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useAuthContext } from "../context/AuthContext" 3 | import toast from 'react-hot-toast'; 4 | 5 | const useLogout = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const logout = async () => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch("/api/auth/logout", { 13 | method: "POST", 14 | headers: { "Content-Type": "applicaion/json" } 15 | }) 16 | const data = await res.json(); 17 | if (data.error) { 18 | throw new Error(data.error); 19 | } 20 | localStorage.removeItem("chat-user"); 21 | setAuthUser(null); 22 | } catch (error) { 23 | toast.error(error.message); 24 | } finally { 25 | setLoading(false); 26 | } 27 | } 28 | return { loading, logout }; 29 | } 30 | 31 | export default useLogout -------------------------------------------------------------------------------- /backend/middleware/protectRoute.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from "../models/user.model.js"; 3 | 4 | const protectRoute = async (req, res, next) => { 5 | try { 6 | const token = req.cookies.jwt; 7 | 8 | if (!token) { 9 | return res.status(401).json({ error: "Unauthorized - No Token Provided" }); 10 | } 11 | 12 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 13 | 14 | if (!decoded) { 15 | return res.status(401).json({ error: "Unauthorized - Invalid Token" }); 16 | } 17 | 18 | const user = await User.findById(decoded.userId).select("-password"); 19 | 20 | if (!user) { 21 | return res.status(404).json({ error: "User not found" }); 22 | } 23 | 24 | req.user = user; 25 | next(); 26 | } catch (error) { 27 | console.log("Error in protectRoute middleware: ", error.message); 28 | res.status(500).json({ error: "Internal server error" }); 29 | } 30 | } 31 | 32 | export default protectRoute; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-hot-toast": "^2.4.1", 16 | "react-icons": "^5.1.0", 17 | "react-router-dom": "^6.22.3", 18 | "socket.io-client": "^4.7.5", 19 | "transparent-chat-app-socket.io": "file:..", 20 | "zustand": "^4.5.4" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.55", 24 | "@types/react-dom": "^18.2.19", 25 | "@vitejs/plugin-react": "^4.2.1", 26 | "autoprefixer": "^10.4.17", 27 | "daisyui": "^4.7.0", 28 | "eslint": "^8.56.0", 29 | "eslint-plugin-react": "^7.33.2", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.4.5", 32 | "postcss": "^8.4.35", 33 | "tailwindcss": "^3.4.1", 34 | "vite": "^5.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kriti Raj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useGetMessages.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import toast from 'react-hot-toast'; 3 | import useConversation from '../zustand/useConversation'; 4 | 5 | const useGetMessages = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { messages, setMessages, selectedConversation } = useConversation(); 8 | useEffect(() => { 9 | const getMessages = async () => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch(`/api/messages/${selectedConversation._id}`); 13 | const data = await res.json(); 14 | if (data.error) { 15 | throw new Error(); 16 | } 17 | setMessages(data); 18 | } catch (error) { 19 | toast.error(error.message); 20 | } finally { 21 | setLoading(false); 22 | } 23 | } 24 | if (selectedConversation?._id) getMessages(); 25 | }, [selectedConversation?._id, setMessages]) 26 | return { messages, loading }; 27 | } 28 | 29 | export default useGetMessages -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom"; 2 | import Home from "./pages/home/Home.jsx"; 3 | import Login from "./pages/login/Login.jsx"; 4 | import SignUp from "./pages/signup/SignUp.jsx"; 5 | import { Toaster } from "react-hot-toast"; 6 | import { useAuthContext } from "./context/AuthContext.jsx"; 7 | 8 | const App = () => { 9 | const { authUser } = useAuthContext(); 10 | return ( 11 |
12 | 13 | : } 16 | /> 17 | : } 20 | /> 21 | : } 24 | /> 25 | : } 28 | /> 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSendMessage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import toast from 'react-hot-toast'; 3 | import useConversation from '../zustand/useConversation'; 4 | 5 | const useSendMessage = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { messages, setMessages, selectedConversation } = useConversation(); 8 | 9 | const sendMessage = async (message) => { 10 | setLoading(true); 11 | try { 12 | const res = await fetch(`/api/messages/send/${selectedConversation._id}`, { 13 | method: 'POST', 14 | headers: { "Content-Type": "application/json" }, 15 | body: JSON.stringify({ message }) 16 | }) 17 | const data = await res.json(); 18 | if (data.error) { 19 | throw new Error(data.error); 20 | } 21 | setMessages([...messages, data]); 22 | } catch (error) { 23 | toast.error(error.message); 24 | } finally { 25 | setLoading(false); 26 | } 27 | } 28 | return { sendMessage, loading } 29 | } 30 | 31 | export default useSendMessage -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), 7 | url(/bg.jpg); 8 | background-repeat: no-repeat; 9 | background-size: cover; 10 | background-position: center; 11 | } 12 | 13 | /* dark mode looking scrollbar */ 14 | ::-webkit-scrollbar { 15 | width: 8px; 16 | } 17 | 18 | ::-webkit-scrollbar-track { 19 | background: #555; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb { 23 | background: #121212; 24 | border-radius: 5px; 25 | } 26 | 27 | ::-webkit-scrollbar-thumb:hover { 28 | background: #242424; 29 | } 30 | 31 | /* SHAKE ANIMATION ON HORIZONTAL DIRECTION */ 32 | .shake { 33 | animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.2s both; 34 | transform: translate3d(0, 0, 0); 35 | backface-visibility: hidden; 36 | perspective: 1000px; 37 | } 38 | 39 | @keyframes shake { 40 | 41 | 10%, 42 | 90% { 43 | transform: translate3d(-1px, 0, 0); 44 | } 45 | 46 | 20%, 47 | 80% { 48 | transform: translate3d(2px, 0, 0); 49 | } 50 | 51 | 30%, 52 | 50%, 53 | 70% { 54 | transform: translate3d(-4px, 0, 0); 55 | } 56 | 57 | 40%, 58 | 60% { 59 | transform: translate3d(4px, 0, 0); 60 | } 61 | } -------------------------------------------------------------------------------- /backend/socket/socket.js: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import http from "http"; 3 | import express from "express"; 4 | 5 | const app = express(); 6 | 7 | const server = http.createServer(app); 8 | const io = new Server(server, { 9 | cors: { 10 | origin: ["http://localhost:5000"], 11 | methods: ["GET", "POST"] 12 | } 13 | }); 14 | 15 | export const getReceiverSocketId = (receiverId) => { 16 | return userSocketMap[receiverId]; 17 | } 18 | 19 | const userSocketMap = {}; // {userId: socketId} 20 | 21 | 22 | io.on("connection", (socket) => { 23 | // console.log("a user connected", socket.id); 24 | 25 | const userId = socket.handshake.query.userId; 26 | if (userId != "undefined") userSocketMap[userId] = socket.id; 27 | 28 | // io.emit() is used to send events to all connected clients 29 | io.emit("getOnlineUsers", Object.keys(userSocketMap)); 30 | 31 | // socket.on() is used to listen to the events. can be used both on client and server side 32 | socket.on("disconnect", () => { 33 | // console.log("user disconnected", socket.id); 34 | delete userSocketMap[userId]; 35 | io.emit("getOnlineUsers", Object.keys(userSocketMap)); 36 | }) 37 | }) 38 | 39 | export { app, io, server }; -------------------------------------------------------------------------------- /frontend/src/components/messages/Messages.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import Message from "./Message.jsx"; 3 | import useGetMessages from "../../hooks/useGetMessages.js"; 4 | import MessageSkeleton from "../skeletons/MessageSkeletons.jsx"; 5 | import useListenMessages from "../../hooks/useListenMessages.js"; 6 | 7 | const Messages = () => { 8 | const { messages, loading } = useGetMessages(); 9 | useListenMessages(); 10 | 11 | const lastMessageRef = useRef(); 12 | 13 | useEffect(() => { 14 | setTimeout(() => { 15 | lastMessageRef.current?.scrollIntoView({ behavior: "smooth" }); 16 | }, 100); 17 | }, [messages]); 18 | 19 | return ( 20 |
21 | {!loading && 22 | messages.length > 0 && 23 | messages.map((message) => ( 24 |
25 | 26 |
27 | ))} 28 | {loading && [...Array(3)].map((_, idx) => )} 29 | {!loading && messages.length === 0 && ( 30 |

Send a message to start the conversation

31 | )} 32 |
33 | ); 34 | }; 35 | 36 | export default Messages; 37 | -------------------------------------------------------------------------------- /frontend/src/pages/signup/GenderCheckbox.jsx: -------------------------------------------------------------------------------- 1 | const GenderCheckbox = ({ onCheckboxChange, selectedGender }) => { 2 | return ( 3 |
4 |
5 | 18 |
19 | 20 |
21 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default GenderCheckbox; 40 | -------------------------------------------------------------------------------- /frontend/src/components/messages/MessageInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { BsSend } from "react-icons/bs"; 3 | import useSendMessage from "../../hooks/useSendMessage"; 4 | 5 | const MessageInput = () => { 6 | const [message, setMessage] = useState(""); 7 | const { sendMessage, loading } = useSendMessage(); 8 | const handleSubmit = async (e) => { 9 | e.preventDefault(); 10 | if (!message) return; 11 | await sendMessage(message); 12 | setMessage(""); 13 | }; 14 | return ( 15 |
16 |
17 | setMessage(e.target.value)} 23 | /> 24 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default MessageInput; 40 | -------------------------------------------------------------------------------- /frontend/src/context/SocketContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | import { useAuthContext } from "./AuthContext"; 3 | import io from "socket.io-client"; 4 | 5 | const SocketContext = createContext(); 6 | 7 | export const useSocketContext = () => { 8 | return useContext(SocketContext); 9 | }; 10 | 11 | export const SocketContextProvider = ({ children }) => { 12 | const [socket, setSocket] = useState(null); 13 | const [onlineUsers, setOnlineUsers] = useState([]); 14 | const { authUser } = useAuthContext(); 15 | 16 | useEffect(() => { 17 | if (authUser) { 18 | const socket = io("https://messenger-web-app.onrender.com", { 19 | query: { 20 | userId: authUser._id, 21 | }, 22 | }); 23 | 24 | setSocket(socket); 25 | 26 | // socket.on() is used to listen to the events. can be used both on client and server side 27 | socket.on("getOnlineUsers", (users) => { 28 | setOnlineUsers(users); 29 | }); 30 | 31 | return () => socket.close(); 32 | } else { 33 | if (socket) { 34 | socket.close(); 35 | setSocket(null); 36 | } 37 | } 38 | }, [authUser]); 39 | 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/components/messages/Message.jsx: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from "../../context/AuthContext"; 2 | import { extractTime } from "../../utils/extractTime"; 3 | import useConversation from "../../zustand/useConversation"; 4 | 5 | const Message = ({ message }) => { 6 | const { authUser } = useAuthContext(); 7 | const { selectedConversation } = useConversation(); 8 | const fromMe = message.senderId === authUser._id; 9 | const formattedTime = extractTime(message.createdAt); 10 | const chatClassName = fromMe ? "chat-end" : "chat-start"; 11 | const profilePic = fromMe 12 | ? authUser.profilePic 13 | : selectedConversation.profilePic; 14 | const bubbleBgColor = fromMe ? "bg-blue-500" : ""; 15 | const shakeClass = message.shouldShake ? "shake" : ""; 16 | 17 | return ( 18 |
19 |
20 |
21 | Tailwind CSS chat bubble component 22 |
23 |
24 |
27 | {/* Hi! What is Upp? */} 28 | {message.message} 29 |
30 |
31 | {formattedTime} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Message; 38 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/Conversation.jsx: -------------------------------------------------------------------------------- 1 | import { useSocketContext } from "../../context/SocketContext"; 2 | import useConversation from "../../zustand/useConversation"; 3 | 4 | const Conversation = ({ conversation, lastIdx, emoji }) => { 5 | const { selectedConversation, setSelectedConversation } = useConversation(); 6 | const isSelected = selectedConversation?._id === conversation._id; 7 | const { onlineUsers } = useSocketContext(); 8 | const isOnline = onlineUsers.includes(conversation._id); 9 | 10 | return ( 11 | <> 12 |
setSelectedConversation(conversation)} 17 | > 18 |
19 |
20 | user avatat 21 |
22 |
23 | 24 |
25 |
26 |

{conversation.fullName}

27 | {emoji} 28 |
29 |
30 |
31 | {!lastIdx &&
} 32 | 33 | ); 34 | }; 35 | 36 | export default Conversation; 37 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import toast from "react-hot-toast"; 3 | import { useAuthContext } from "../context/AuthContext"; 4 | 5 | const useLogin = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const login = async (username, password) => { 10 | 11 | const success = handleInputErrors(username, password); 12 | if (!success) return; 13 | setLoading(true); 14 | try { 15 | const res = await fetch("/api/auth/login", { 16 | method: "POST", 17 | headers: { "Content-Type": "application/json" }, 18 | body: JSON.stringify({ username, password }) 19 | }); 20 | const data = await res.json(); 21 | if (data.error) { 22 | throw new Error(data.error); 23 | } 24 | localStorage.setItem("chat-user", JSON.stringify(data)); 25 | setAuthUser(data); 26 | } catch (error) { 27 | toast.error(error.message); 28 | } finally { 29 | setLoading(false); 30 | } 31 | } 32 | return { loading, login }; 33 | } 34 | 35 | export default useLogin 36 | 37 | function handleInputErrors(username, password) { 38 | if (!username || !password) { 39 | toast.error("Please fill all the fields"); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | -------------------------------------------------------------------------------- /backend/controller/user.controller.js: -------------------------------------------------------------------------------- 1 | import User from "../models/user.model.js"; 2 | 3 | export const getUsersForSidebar = async (req, res) => { 4 | try { 5 | const loggedInUserId = req.user._id; 6 | const client = req.app.locals.redisClient; // Get the Redis client from app locals 7 | 8 | if (!client) { 9 | console.log("Redis client is not initialized"); 10 | return res.status(500).json({ error: "Internal Server Error" }); 11 | } 12 | 13 | // Check if data is already cached in Redis 14 | const cacheKey = `usersForSidebar:${loggedInUserId}`; 15 | const cachedData = await client.get(cacheKey); 16 | if (cachedData) { 17 | // console.log('Data from redis'); 18 | return res.status(200).json(JSON.parse(cachedData)); // Parse cached data before sending response 19 | } 20 | 21 | // Fetch users from MongoDB excluding the logged-in user 22 | const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } }).select("-password"); 23 | 24 | // Store data in Redis with an expiration time 25 | await client.set(cacheKey, JSON.stringify(filteredUsers), { 26 | EX: 300 // Set key with an expiration of 5 minutes 27 | }); 28 | 29 | res.status(200).json(filteredUsers); 30 | } catch (error) { 31 | console.error("Error in getUsersForSidebar: ", error.message); 32 | res.status(500).json({ error: "Internal Server Error" }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/SearchInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaSearch } from "react-icons/fa"; 3 | import useConversation from "../../zustand/useConversation"; 4 | import useGetConversations from "../../hooks/useGetConversations"; 5 | import toast from "react-hot-toast"; 6 | 7 | const SearchInput = () => { 8 | const [search, setSearch] = useState(""); 9 | const { setSelectedConversation } = useConversation(); 10 | const { conversations } = useGetConversations(); 11 | const handleSubmit = (e) => { 12 | e.preventDefault(); 13 | if (!search) return; 14 | if (search.length < 3) { 15 | return toast.error("Search term must be at least 3 characters long"); 16 | } 17 | 18 | const conversation = conversations.find((c) => 19 | c.fullName.toLowerCase().includes(search.toLowerCase()) 20 | ); 21 | 22 | if (conversation) { 23 | setSelectedConversation(conversation); 24 | setSearch(""); 25 | } else toast.error("No such user found!"); 26 | }; 27 | return ( 28 |
29 | setSearch(e.target.value)} 35 | /> 36 | 39 |
40 | ); 41 | }; 42 | 43 | export default SearchInput; 44 | -------------------------------------------------------------------------------- /frontend/src/components/messages/MessageContainer.jsx: -------------------------------------------------------------------------------- 1 | import Messages from "./Messages"; 2 | import MessageInput from "./MessageInput"; 3 | import { TiMessages } from "react-icons/ti"; 4 | import useConversation from "../../zustand/useConversation"; 5 | import { useEffect } from "react"; 6 | import { useAuthContext } from "../../context/AuthContext"; 7 | 8 | const MessageContainer = () => { 9 | const { selectedConversation, setSelectedConversation } = useConversation(); 10 | useEffect(() => { 11 | // cleanup function (unmounts) 12 | return () => setSelectedConversation(null); 13 | }, [setSelectedConversation]); 14 | return ( 15 |
16 | {!selectedConversation ? ( 17 | 18 | ) : ( 19 | <> 20 |
21 | To:{" "} 22 | 23 | {selectedConversation.fullName} 24 | 25 |
26 | 27 | 28 | 29 | )} 30 |
31 | ); 32 | }; 33 | 34 | const NoChatSelected = () => { 35 | const { authUser } = useAuthContext(); 36 | return ( 37 |
38 |
39 |

Welcome 👋 {authUser.fullName} ❄️

40 |

Select a chat to start messaging

41 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default MessageContainer; 48 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import path from "path"; 3 | import express from "express"; 4 | import dotenv from "dotenv"; 5 | import cookieParser from "cookie-parser"; 6 | 7 | import authRoutes from "./routes/auth.routes.js"; 8 | import messageRoutes from "./routes/message.routes.js"; 9 | import userRoutes from "./routes/user.routes.js"; 10 | 11 | import connectToRedis from "./db/connectToRedis.js"; 12 | import connectToMongoDB from "./db/connectToMongoDB.js"; 13 | import { app, server } from "./socket/socket.js"; 14 | 15 | dotenv.config(); 16 | 17 | const __dirname = path.resolve(); 18 | 19 | // PORT should be assigned after calling dotenv.config() 20 | const PORT = process.env.PORT || 3000; 21 | 22 | // Middlewares 23 | app.use(express.json()); // to parse the incoming request with JSON payloads (from req.body) 24 | app.use(cookieParser()); 25 | 26 | app.use("/api/auth", authRoutes); 27 | app.use("/api/messages", messageRoutes); 28 | app.use("/api/users", userRoutes); 29 | 30 | app.use(express.static(path.join(__dirname, "/frontend/dist"))); 31 | 32 | app.get('*', (req, res) => { 33 | res.sendFile(path.join(__dirname, "frontend", "dist", "index.html")); 34 | }); 35 | 36 | // Function to start the server 37 | const startServer = async () => { 38 | try { 39 | await connectToMongoDB(); // Connect to MongoDB 40 | const redisClient = await connectToRedis(); // Connect to Redis 41 | 42 | app.locals.redisClient = redisClient; // Store the Redis client in app locals for later use 43 | 44 | server.listen(PORT, () => { 45 | console.log(`Server Running on port ${PORT}`); 46 | }); 47 | } catch (error) { 48 | console.error('Failed to start server:', error.message); 49 | } 50 | }; 51 | 52 | startServer(); 53 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSignup.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import toast from "react-hot-toast"; 3 | import { useAuthContext } from "../context/AuthContext"; 4 | 5 | const useSignup = () => { 6 | const [loading, setLoading] = useState(false); 7 | const { setAuthUser } = useAuthContext(); 8 | 9 | const signup = async ({ fullName, username, password, confirmPassword, gender }) => { 10 | const success = handleInputErrors({ fullName, username, password, confirmPassword, gender }); 11 | if (!success) return; 12 | setLoading(true); 13 | try { 14 | const res = await fetch("/api/auth/signup", { 15 | method: "POST", 16 | headers: { "Content-Type": "application/json" }, 17 | body: JSON.stringify({ fullName, username, password, confirmPassword, gender }) 18 | }); 19 | 20 | 21 | const data = await res.json(); 22 | if (data.error) { 23 | throw new Error(data.error); 24 | } 25 | 26 | localStorage.setItem("chat-user", JSON.stringify(data)); 27 | setAuthUser(data); 28 | 29 | toast.success('Signup successful'); 30 | } catch (error) { 31 | toast.error(error.message); 32 | } finally { 33 | setLoading(false); 34 | } 35 | }; 36 | return { loading, signup }; 37 | }; 38 | 39 | export default useSignup; 40 | 41 | function handleInputErrors({ fullName, username, password, confirmPassword, gender }) { 42 | if (!fullName || !username || !password || !confirmPassword || !gender) { 43 | toast.error("Please fill all the fields"); 44 | return false; 45 | } 46 | 47 | if (password !== confirmPassword) { 48 | toast.error("Passwords do not match"); 49 | return false; 50 | } 51 | 52 | if (password.length < 6) { 53 | toast.error("Password must be at least 6 characters"); 54 | return false; 55 | } 56 | 57 | return true; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/pages/login/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import useLogin from "../../hooks/useLogin"; 4 | 5 | const Login = () => { 6 | const [username, setUsername] = useState(""); 7 | const [password, setpassword] = useState(""); 8 | 9 | const { loading, login } = useLogin(); 10 | 11 | const handleSubmit = async (e) => { 12 | e.preventDefault(); 13 | await login(username, password); 14 | }; 15 | return ( 16 |
17 |
18 |

19 | Login Messenger 20 |

21 |
22 |
23 | 26 | { 32 | setUsername(e.target.value); 33 | }} 34 | /> 35 |
36 |
37 | 40 | { 46 | setpassword(e.target.value); 47 | }} 48 | /> 49 |
50 | 54 | {"Don't"} have a account? 55 | 56 |
57 | 64 |
65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Login; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Messenger Web App 3 | 4 | ## Introduction 5 | Messenger Web App is a real-time chatting application similar to Facebook Messenger. It features user authentication and hashed password storage to ensure secure communication. 6 | ## Demo 7 | 8 | [Messenger Web App](https://messenger-web-app.onrender.com/) 9 | ## Tech Stack 10 | 11 | **Client:** React, TailwindCSS, Daisy UI, Socket.io 12 | 13 | **Server:** Node, Express, MongoDB, Redis, Socket.io, JWT, cookie-parser 14 | 15 | 16 | ## Features 17 | 18 | 19 | - Real-time messaging using Socket.IO 20 | - User authentication with JWT 21 | - Password hashing with bcrypt 22 | - MongoDB for data storage 23 | - Redis for data caching 24 | ## Dependencies 25 | 26 | The project uses the following major dependencies: 27 | 28 | - Vite for the development server and build tool 29 | - React for building the user interface 30 | - Socket.IO for real-time communication 31 | - JWT for authentication 32 | - MongoDB for database management 33 | - bcrypt for password hashing 34 | - Redis for data cahcing 35 | ## Deployment 36 | 37 | To deploy the Messenger Web App, follow these steps: 38 | 1. Clone the repository: 39 | ```bash 40 | git clone https://github.com/kriti-raj/messenger-web-app.git 41 | ``` 42 | 2. Navigate to the project directory: 43 | ```bash 44 | cd messenger-web-app 45 | ``` 46 | 3. Install the dependencies: 47 | ```bash 48 | npm install 49 | ``` 50 | 4. start the development server: 51 | ```bash 52 | npm run dev 53 | ``` 54 | This will start the server at `http://localhost:5000`. Open your browser and navigate to this URL to see the application in action. 55 | ## Environment Variables 56 | 57 | To run this project, you will need to add the following environment variables to your .env file 58 | 59 | - `PORT` 60 | - `MONGO_DB_URI` 61 | - `JWT_SECRET` 62 | - `NODE_ENV` 63 | - `REDIS_PASSWORD` 64 | - `REDIS_HOST` 65 | - `REDIS_PORT` 66 | ## Documentation 67 | 68 | The project documentation is currently maintained in this README file. For more detailed information on specific topics, please refer to the respective official documentation of the tools and libraries used. 69 | 70 | ## Examples 71 | To see examples of how to use various features of the Messenger Web App, refer to the source code in the src directory. 72 | 73 | ## Troubleshooting 74 | If you encounter any issues, please check the following: 75 | 76 | Ensure all dependencies are installed correctly by running npm install. 77 | Verify that the development server is running on the correct port (http://localhost:5000). 78 | Check the browser console for any error messages. 79 | 80 | ## License 81 | 82 | [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 83 | 84 | ## Contributing 85 | 86 | We welcome contributions from the community. If you'd like to contribute, please fork the repository and submit a pull request. 87 | ## Feedback 88 | 89 | If you have any feedback, please reach out to us at 6517kritiraj@gmail.com 90 | -------------------------------------------------------------------------------- /backend/controller/message.controller.js: -------------------------------------------------------------------------------- 1 | import Conversation from "../models/conversation.model.js"; 2 | import Message from "../models/message.model.js"; 3 | import { getReceiverSocketId, io } from "../socket/socket.js"; 4 | 5 | export const sendMessage = async (req, res) => { 6 | try { 7 | const { message } = req.body; 8 | const { id: receiverId } = req.params; 9 | const senderId = req.user._id; 10 | 11 | let conversation = await Conversation.findOne({ 12 | participants: { $all: [senderId, receiverId] }, 13 | }); 14 | 15 | if (!conversation) { 16 | conversation = await Conversation.create({ 17 | participants: [senderId, receiverId], 18 | }); 19 | } 20 | 21 | const newMessage = new Message({ 22 | senderId, 23 | receiverId, 24 | message, 25 | }); 26 | 27 | if (newMessage) { 28 | conversation.messages.push(newMessage._id); 29 | } 30 | 31 | // Save conversation and message in parallel 32 | await Promise.all([conversation.save(), newMessage.save()]); 33 | 34 | // SOCKET IO FUNCTIONALITY WILL GO HERE 35 | const receiverSocketId = getReceiverSocketId(receiverId); 36 | if (receiverSocketId) { 37 | io.to(receiverSocketId).emit("newMessage", newMessage); 38 | } 39 | 40 | res.status(201).json(newMessage); 41 | } catch (error) { 42 | console.log("Error in sendMessage controller:", error.message); 43 | res.status(500).json({ error: "Internal Server Error" }); 44 | } 45 | }; 46 | 47 | export const getMessages = async (req, res) => { 48 | try { 49 | const { id: userToChatId } = req.params; 50 | const senderId = req.user._id; 51 | const client = req.app.locals.redisClient; // Get the Redis client from app locals 52 | 53 | if (!client) { 54 | console.log("Redis client is not initialized"); 55 | return res.status(500).json({ error: "Internal Server Error" }); 56 | } 57 | 58 | // Check if data is already cached in Redis 59 | const cacheKey = `conversation:${senderId}:${userToChatId}`; 60 | const cachedData = await client.get(cacheKey); 61 | if (cachedData) { 62 | // console.log('Data from redis'); 63 | return res.status(200).json(JSON.parse(cachedData)); // Parse cached data before sending response 64 | } 65 | 66 | const conversation = await Conversation.findOne({ 67 | participants: { $all: [senderId, userToChatId] }, 68 | }).populate("messages"); 69 | 70 | if (!conversation) return res.status(200).json([]); 71 | 72 | const messages = conversation.messages; 73 | 74 | // Store data in Redis with an expiration time 75 | await client.set(cacheKey, JSON.stringify(messages), { 76 | EX: 300 // Set key with an expiration of 5 minutes 77 | }); 78 | 79 | res.status(200).json(messages); 80 | } catch (error) { 81 | console.log("Error in getMessages controller:", error.message); 82 | res.status(500).json({ error: "Internal Server Error" }); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /backend/controller/auth.controller.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import User from "../models/user.model.js"; 3 | import generateTokenAndSetCookie from "../utils/generateToken.js"; 4 | 5 | export const signup = async (req, res) => { 6 | try { 7 | const { fullName, username, password, confirmPassword, gender } = req.body; 8 | 9 | if (password != confirmPassword) { 10 | return res.status(400).json({ error: "Password doesn't match" }); 11 | } 12 | const user = await User.findOne({ username }); 13 | 14 | if (user) { 15 | return res.status(400).json({ error: "Username already exists" }); 16 | } 17 | 18 | // HASH PASSWORD HERE 19 | const salt = await bcrypt.genSalt(10); 20 | const hashedPassword = await bcrypt.hash(password, salt); 21 | 22 | // http://avatar-placeholder.iran.liara.run/ 23 | 24 | const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`; 25 | const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${username}`; 26 | 27 | const newUser = new User({ 28 | fullName, 29 | username, 30 | password: hashedPassword, 31 | gender, 32 | profilePic: gender === "male" ? boyProfilePic : girlProfilePic, 33 | }); 34 | 35 | if (newUser) { 36 | // Generate JWT token here 37 | generateTokenAndSetCookie(newUser._id, res); 38 | await newUser.save(); 39 | 40 | res.status(201).json({ 41 | _id: newUser._id, 42 | fullName: newUser.fullName, 43 | username: newUser.username, 44 | profilePic: newUser.profilePic, 45 | }); 46 | } else { 47 | res.status(400).json({ error: "Invalid user data" }); 48 | } 49 | } catch (error) { 50 | console.log("Error in signup controller", error.message); 51 | res.status(500).json({ 52 | error: "Internal Server Error" 53 | }); 54 | } 55 | }; 56 | 57 | export const login = async (req, res) => { 58 | try { 59 | const { username, password } = req.body; 60 | const user = await User.findOne({ username }); 61 | const isPasswordCorrect = await bcrypt.compare(password, user.password || ""); 62 | 63 | if (!user || !isPasswordCorrect) { 64 | return res.status(400).json({ error: "Invalid username or password" }); 65 | } 66 | 67 | generateTokenAndSetCookie(user._id, res); 68 | 69 | res.status(200).json({ 70 | _id: user._id, 71 | fullName: user.fullName, 72 | username: user.username, 73 | profilePic: user.profilePic, 74 | }); 75 | 76 | } catch (error) { 77 | console.log("Error in login controller", error.message); 78 | res.status(500).json({ 79 | error: "Internal Server Error" 80 | }); 81 | } 82 | }; 83 | 84 | export const logout = (req, res) => { 85 | try { 86 | res.cookie("jwt", "", { maxAge: 0 }); 87 | res.status(200).json({ message: "Logged out successfully" }); 88 | 89 | } catch (error) { 90 | console.log("Error in logout controller", error.message); 91 | res.status(500).json({ 92 | error: "Internal Server Error" 93 | }); 94 | } 95 | }; -------------------------------------------------------------------------------- /frontend/src/pages/signup/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import GenderCheckbox from "./GenderCheckbox"; 3 | import { useState } from "react"; 4 | import useSignup from "../../hooks/useSignup"; 5 | 6 | const SignUp = () => { 7 | const [inputs, setInputs] = useState({ 8 | fullName: "", 9 | username: "", 10 | password: "", 11 | confirmPassword: "", 12 | gender: "", 13 | }); 14 | 15 | // eslint-disable-next-line no-unused-vars 16 | const { loading, signup } = useSignup(); 17 | 18 | const handleCheckboxChange = (gender) => { 19 | setInputs({ ...inputs, gender }); 20 | }; 21 | 22 | const handleSubmit = async (e) => { 23 | e.preventDefault(); 24 | await signup(inputs); 25 | }; 26 | 27 | return ( 28 |
29 |
30 |

31 | Sign Up ChatApp 32 |

33 | 34 |
35 |
36 | 39 | 45 | setInputs({ ...inputs, fullName: e.target.value }) 46 | } 47 | /> 48 |
49 | 50 |
51 | 54 | 60 | setInputs({ ...inputs, username: e.target.value }) 61 | } 62 | /> 63 |
64 | 65 |
66 | 69 | 75 | setInputs({ ...inputs, password: e.target.value }) 76 | } 77 | /> 78 |
79 | 80 |
81 | 84 | 90 | setInputs({ ...inputs, confirmPassword: e.target.value }) 91 | } 92 | /> 93 |
94 | 95 | 99 | 100 | 104 | Already have an account? 105 | 106 |
107 | 117 |
118 | 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default SignUp; 125 | --------------------------------------------------------------------------------