├── frontend ├── .env.development ├── src │ ├── hooks │ │ ├── index.ts │ │ └── useDebounce.ts │ ├── layouts │ │ ├── index.ts │ │ ├── auth.tsx │ │ └── dashboard.tsx │ ├── pages │ │ ├── auth │ │ │ ├── index.ts │ │ │ ├── sign-in.tsx │ │ │ └── sign-up.tsx │ │ └── dashboard │ │ │ ├── index.ts │ │ │ ├── notification.tsx │ │ │ ├── sidebar.tsx │ │ │ └── home.tsx │ ├── components │ │ ├── index.ts │ │ ├── wrapper.tsx │ │ ├── Image.tsx │ │ └── Account.tsx │ ├── services │ │ ├── index.ts │ │ ├── auth.ts │ │ ├── instance.ts │ │ └── dashboard.ts │ ├── redux │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── visibility.ts │ │ │ ├── listMessage.ts │ │ │ └── currentUser.ts │ │ ├── store.ts │ │ ├── initState.ts │ │ └── reducers │ │ │ ├── rootReducer.ts │ │ │ ├── visibility.ts │ │ │ ├── listMessage.ts │ │ │ └── currentUser.ts │ ├── socket.ts │ ├── setupTests.ts │ ├── index.tsx │ ├── reportWebVitals.ts │ ├── App.tsx │ ├── routes.tsx │ ├── types.ts │ └── style.css ├── .env.production ├── public │ ├── robots.txt │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── logo.svg │ └── index.html ├── postcss.config.js ├── .gitignore ├── vercel.json ├── tsconfig.json ├── tailwind.config.js └── package.json ├── backend ├── nodemon.json ├── src │ ├── utils │ │ └── types.ts │ ├── routes │ │ ├── authRoute.ts │ │ ├── messageRoute.ts │ │ ├── index.ts │ │ └── userRoute.ts │ ├── configs │ │ └── db.ts │ ├── models │ │ ├── MessageModel.ts │ │ └── UserModel.ts │ ├── server.ts │ ├── middleware │ │ ├── token.ts │ │ └── auth.ts │ ├── socket.ts │ └── controllers │ │ ├── userController.ts │ │ └── messageController.ts ├── .env ├── build │ ├── utils │ │ └── types.js │ ├── models │ │ ├── MessageModel.js │ │ └── UserModel.js │ ├── routes │ │ ├── index.js │ │ ├── authRoute.js │ │ ├── messageRoute.js │ │ └── userRoute.js │ ├── server.js │ ├── configs │ │ └── db.js │ ├── middleware │ │ ├── token.js │ │ └── auth.js │ ├── socket.js │ └── controllers │ │ ├── userController.js │ │ └── messageController.js ├── package.json └── tsconfig.json ├── .gitignore ├── restAPI.http └── README.md /frontend/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:3001/api 2 | -------------------------------------------------------------------------------- /frontend/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export {default as useDebounce } from './useDebounce'; -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://chat-app-u9j0.onrender.com/api 2 | -------------------------------------------------------------------------------- /frontend/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./dashboard"; 3 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./sign-in"; 2 | export * from "./sign-up"; 3 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vo-Huy-Khoa/chat-app-mern/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vo-Huy-Khoa/chat-app-mern/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Account"; 2 | export * from "./Image"; 3 | export * from "./wrapper"; 4 | -------------------------------------------------------------------------------- /frontend/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./instance"; 2 | export * from "./auth"; 3 | export * from "./dashboard"; 4 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./home"; 2 | export * from "./notification"; 3 | export * from "./sidebar"; 4 | -------------------------------------------------------------------------------- /frontend/src/redux/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./currentUser"; 2 | export * from "./listMessage"; 3 | export * from "./visibility"; 4 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | module.exports = { 3 | plugins: [require("tailwindcss"), require("autoprefixer")], 4 | }; 5 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["backend/src"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "npx ts-node ./src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/wrapper.tsx: -------------------------------------------------------------------------------- 1 | export const Wrapper = ({ children }: any) => { 2 | return
{children}
; 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /backend/node_modules 5 | /frontend/node_modules 6 | -------------------------------------------------------------------------------- /frontend/src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import rootReducer from "./reducers/rootReducer"; 3 | 4 | const store = createStore(rootReducer); 5 | 6 | export default store; 7 | -------------------------------------------------------------------------------- /frontend/src/socket.ts: -------------------------------------------------------------------------------- 1 | import io from "socket.io-client"; 2 | 3 | const url = "https://chat-app-u9j0.onrender.com"; 4 | const socket = io(url, { 5 | transports: ["websocket"], 6 | }); 7 | 8 | export default socket; 9 | -------------------------------------------------------------------------------- /backend/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export const URL_LOCALHOST = 2 | process.env.URL_LOCALHOST || "http://localhost:3000"; 3 | export const URL_PRODUCTION = 4 | process.env.URL_PRODUCTION || "https://chatapp-vo-huy-khoa.vercel.app"; 5 | export const PORT = process.env.PORT || "3001"; 6 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | PORT = 3001 2 | MONGO_URL=mongodb+srv://vohuykhoa:vohuykhoa@cluster0.nppkasl.mongodb.net/?retryWrites=true&w=majority 3 | JWT_SECRET=jwttoken 4 | REFRESH_TOKEN_SECRET=refreshtoken 5 | URL_LOCALHOST=http://localhost:3000 6 | URL_PRODUCTION=https://chatapp-vo-huy-khoa.vercel.app 7 | -------------------------------------------------------------------------------- /frontend/src/components/Image.tsx: -------------------------------------------------------------------------------- 1 | export const Image = ({ src, width = "32px", height = "32px" }: any) => { 2 | return ( 3 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/routes/authRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as Auth from "../middleware/auth"; 3 | 4 | const router = Router(); 5 | 6 | router.post("/login", Auth.Login); 7 | router.post("/logout", Auth.Logout); 8 | router.post("/refreshToken", Auth.RefreshToken); 9 | router.post("/register", Auth.Register); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /backend/build/utils/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.PORT = exports.URL_PRODUCTION = exports.URL_LOCALHOST = void 0; 4 | exports.URL_LOCALHOST = process.env.URL_LOCALHOST || "http://localhost:3000"; 5 | exports.URL_PRODUCTION = process.env.URL_PRODUCTION || "https://chatapp-vo-huy-khoa.vercel.app"; 6 | exports.PORT = process.env.PORT || "3001"; 7 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/src/configs/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | mongoose.set("strictQuery", false); 3 | const connect = async () => { 4 | const url = process.env.MONGO_URL || ""; 5 | return await mongoose 6 | .connect(url) 7 | .then(() => { 8 | console.log("Database connect successfully"); 9 | }) 10 | .catch(() => { 11 | console.log("Database connect fail"); 12 | }); 13 | }; 14 | 15 | export default connect; 16 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Provider } from "react-redux"; 4 | import App from "./App"; 5 | import store from "./redux/store"; 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement 8 | ); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /frontend/src/redux/initState.ts: -------------------------------------------------------------------------------- 1 | import { IUser, selectMessageType } from "../types"; 2 | 3 | export const Visibility = "sidebar"; 4 | export const initialState: IUser = { 5 | _id: "", 6 | avatar: 7 | "https://cdn.shopify.com/shopifycloud/shopify/assets/no-image-2048-5e88c1b20e087fb7bbe9a3771824e743c244f437e4f8ba93bbf7b11b53f7824c_grande.gif", 8 | fullname: "", 9 | username: "", 10 | }; 11 | export const initMessage: selectMessageType = []; 12 | -------------------------------------------------------------------------------- /backend/src/routes/messageRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as Token from "../middleware/token"; 3 | import messageController from "../controllers/messageController"; 4 | 5 | const router = Router(); 6 | 7 | router.post("/list", Token.authToken, messageController.list); 8 | router.post("/find", Token.authToken, messageController.find); 9 | router.post("/create", Token.authToken, messageController.create); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /frontend/src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useDebounce = (value: string, delay: number) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => setDebouncedValue(value), delay); 8 | return () => clearTimeout(handler); 9 | }, [value, delay]); 10 | 11 | return debouncedValue; 12 | }; 13 | 14 | export default useDebounce; 15 | -------------------------------------------------------------------------------- /frontend/src/layouts/auth.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import { routes } from "../routes"; 3 | 4 | export const Auth = () => { 5 | return ( 6 |
7 | 8 | {routes.map( 9 | ({ layout, pages }) => 10 | layout === "auth" && 11 | pages.map(({ path, element }) => ( 12 | 13 | )) 14 | )} 15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import Router from "express"; 3 | import authRoute from "./authRoute"; 4 | import userRoute from "./userRoute"; 5 | import messageRoute from "./messageRoute"; 6 | 7 | const router = Router(); 8 | const routes = (app: express.Application) => { 9 | router.use("/auth", authRoute); 10 | router.use("/user", userRoute); 11 | router.use("/message", messageRoute); 12 | 13 | return app.use("/api", router); 14 | }; 15 | 16 | export default routes; 17 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/src/redux/actions/visibility.ts: -------------------------------------------------------------------------------- 1 | import { SelectVisibilityAction } from "../../types"; 2 | 3 | export enum VisibilityActionTypes { 4 | SET_VISIBILITY = "SET_VISIBILITY", 5 | CLEAR_VISIBILITY = "CLEAR_VISIBILITY", 6 | } 7 | 8 | export const setVisibility = (visibility: string): SelectVisibilityAction => ({ 9 | type: VisibilityActionTypes.SET_VISIBILITY, 10 | payload: visibility, 11 | }); 12 | 13 | export const clearVisibility = (): SelectVisibilityAction => ({ 14 | type: VisibilityActionTypes.CLEAR_VISIBILITY, 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route, Navigate, BrowserRouter } from "react-router-dom"; 2 | import { Auth, Dashboard } from "./layouts"; 3 | import "./style.css"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | } /> 10 | } /> 11 | } /> 12 | 13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /frontend/src/redux/actions/listMessage.ts: -------------------------------------------------------------------------------- 1 | import { ClearSelectMessageAction, SetSelectMessageAction } from "../../types"; 2 | 3 | export enum SelectMessageActionTypes { 4 | SET_MESSAGE = "SET_MESSAGE", 5 | CLEAR_MESSAGE = "CLEAR_MESSAGE", 6 | } 7 | 8 | export const setSelectMessage = (message: any): SetSelectMessageAction => ({ 9 | type: SelectMessageActionTypes.SET_MESSAGE, 10 | payload: message, 11 | }); 12 | 13 | export const clearSelectMessage = (): ClearSelectMessageAction => ({ 14 | type: SelectMessageActionTypes.CLEAR_MESSAGE, 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/api/(.*)", 5 | "headers": [ 6 | { "key": "Access-Control-Allow-Credentials", "value": "true" }, 7 | { "key": "Access-Control-Allow-Origin", "value": "*" }, 8 | { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" }, 9 | { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" } 10 | ] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/redux/reducers/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { currentUserReducer, currentReceiverReducer } from "./currentUser"; 3 | import currentMessageReducer from "./listMessage"; 4 | import currentVisibilityReducer from "./visibility"; 5 | 6 | const rootReducer = combineReducers({ 7 | currentUser: currentUserReducer, 8 | currentMessage: currentMessageReducer, 9 | currentVisibility: currentVisibilityReducer, 10 | currentReceiver: currentReceiverReducer, 11 | }); 12 | 13 | export type RootState = ReturnType; 14 | 15 | export default rootReducer; 16 | -------------------------------------------------------------------------------- /backend/build/models/MessageModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const mongoose_1 = require("mongoose"); 4 | const messageSchema = new mongoose_1.Schema({ 5 | senderID: { 6 | type: mongoose_1.Schema.Types.ObjectId, 7 | ref: "User", 8 | }, 9 | receiverID: { 10 | type: mongoose_1.Schema.Types.ObjectId, 11 | ref: "User", 12 | }, 13 | message: { type: String }, 14 | }, { 15 | timestamps: true, 16 | }); 17 | const MessageModel = (0, mongoose_1.model)("Message", messageSchema); 18 | exports.default = MessageModel; 19 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/redux/reducers/visibility.ts: -------------------------------------------------------------------------------- 1 | import { SelectVisibilityAction } from "../../types"; 2 | import { VisibilityActionTypes } from "../actions/visibility"; 3 | import { Visibility } from "../initState"; 4 | 5 | const currentVisibilityReducer = ( 6 | state = Visibility, 7 | action: SelectVisibilityAction 8 | ) => { 9 | switch (action.type) { 10 | case VisibilityActionTypes.SET_VISIBILITY: 11 | return action.payload; 12 | case VisibilityActionTypes.CLEAR_VISIBILITY: 13 | return Visibility; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default currentVisibilityReducer; 20 | -------------------------------------------------------------------------------- /frontend/src/redux/reducers/listMessage.ts: -------------------------------------------------------------------------------- 1 | import { SelectMessageAction } from "../../types"; 2 | import { SelectMessageActionTypes } from "../actions/listMessage"; 3 | import { initMessage } from "../initState"; 4 | 5 | const currentMessageReducer = ( 6 | state = initMessage, 7 | action: SelectMessageAction 8 | ) => { 9 | switch (action.type) { 10 | case SelectMessageActionTypes.SET_MESSAGE: 11 | return [...action.payload]; 12 | case SelectMessageActionTypes.CLEAR_MESSAGE: 13 | return [...initMessage]; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default currentMessageReducer; 20 | -------------------------------------------------------------------------------- /backend/src/routes/userRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as Token from "../middleware/token"; 3 | import userController from "../controllers/userController"; 4 | 5 | const router = Router(); 6 | 7 | router.post("/search", Token.authToken, userController.search); 8 | router.get("/profile/:id", Token.authToken, userController.profile); 9 | router.put("/update/:id", Token.authToken, userController.update); 10 | router.delete("/delete/:id", Token.authToken, userController.destroy); 11 | router.delete("/destroy", Token.authToken, userController.destroyAll); 12 | router.get("/", Token.authToken, userController.list); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /backend/src/models/MessageModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | 3 | interface IMessage { 4 | senderID: { 5 | type: Schema.Types.ObjectId; 6 | ref: "User"; 7 | }; 8 | receiverID: { 9 | type: Schema.Types.ObjectId; 10 | ref: "User"; 11 | }; 12 | message: string; 13 | } 14 | 15 | const messageSchema = new Schema( 16 | { 17 | senderID: { 18 | type: Schema.Types.ObjectId, 19 | ref: "User", 20 | }, 21 | receiverID: { 22 | type: Schema.Types.ObjectId, 23 | ref: "User", 24 | }, 25 | message: { type: String }, 26 | }, 27 | { 28 | timestamps: true, 29 | } 30 | ); 31 | 32 | const MessageModel = model("Message", messageSchema); 33 | 34 | export default MessageModel; 35 | -------------------------------------------------------------------------------- /backend/build/routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const authRoute_1 = __importDefault(require("./authRoute")); 8 | const userRoute_1 = __importDefault(require("./userRoute")); 9 | const messageRoute_1 = __importDefault(require("./messageRoute")); 10 | const router = (0, express_1.default)(); 11 | const routes = (app) => { 12 | router.use("/auth", authRoute_1.default); 13 | router.use("/user", userRoute_1.default); 14 | router.use("/message", messageRoute_1.default); 15 | return app.use("/api", router); 16 | }; 17 | exports.default = routes; 18 | -------------------------------------------------------------------------------- /frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "./pages/dashboard"; 2 | import { Login, Register } from "./pages/auth"; 3 | 4 | export const routes = [ 5 | { 6 | layout: "auth", 7 | pages: [ 8 | { 9 | path: "/sign-in", 10 | element: , 11 | }, 12 | { 13 | path: "/sign-up", 14 | element: , 15 | }, 16 | ], 17 | }, 18 | { 19 | layout: "dashboard", 20 | pages: [ 21 | { 22 | path: "/home", 23 | element: , 24 | }, 25 | ], 26 | }, 27 | ]; 28 | 29 | export const API_URL = { 30 | SIGN_IN: "auth/login", 31 | SIGN_UP: "auth/register", 32 | LOGOUT: "auth/logout", 33 | REFRESH_TOKEN: "auth/refreshToken", 34 | PROFILE: "user/profile/", 35 | SEARCH: "user/search", 36 | USER: "user/", 37 | MESSAGES: "message/list", 38 | MESSAGE: "message/find", 39 | CREATE_MESSAGE: "message/create", 40 | }; 41 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import connect from "./configs/db"; 4 | import dotenv from "dotenv"; 5 | import routes from "./routes"; 6 | import http from "http"; 7 | import { handleSocket } from "./socket"; 8 | import { PORT, URL_LOCALHOST, URL_PRODUCTION } from "./utils/types"; 9 | 10 | dotenv.config(); 11 | 12 | const app = express(); 13 | 14 | const server = http.createServer(app); 15 | 16 | const allowedOrigins: string[] = [URL_LOCALHOST, URL_PRODUCTION]; 17 | 18 | const corsOptions: cors.CorsOptions = { 19 | origin: allowedOrigins, 20 | credentials: true, 21 | }; 22 | 23 | app.use(cors(corsOptions)); 24 | 25 | app.use(express.json()); 26 | 27 | app.use(express.urlencoded({ extended: true })); 28 | 29 | routes(app); 30 | 31 | connect(); 32 | 33 | handleSocket(server); 34 | 35 | server.listen(PORT, () => { 36 | console.log(`Server listing at port: ${PORT}`); 37 | }); 38 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | module.exports = { 3 | mode: "jit", 4 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: { 8 | height: { 9 | 120: "120px", 10 | 240: "240px", 11 | }, 12 | }, 13 | colors: { 14 | primary: "#2a2731", 15 | black: "rgb(10, 23, 33)", 16 | white: "#ffffff", 17 | gray: "#656566", 18 | blue: "#76a9ff", 19 | dark: "#252f3c", 20 | red: "#DC143C", 21 | green:"#008000" 22 | }, 23 | screens: { 24 | sm: { min: "320px", max: "639px" }, 25 | md: { min: "640px", max: "767px" }, 26 | lg: { min: "768px", max: "1023px" }, 27 | xl: { min: "1024px", max: "1279px" }, 28 | "2xl": { min: "1280px" }, 29 | }, 30 | left: { 31 | "1/5": "20%", 32 | }, 33 | }, 34 | variants: { 35 | extend: {}, 36 | }, 37 | plugins: [], 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/models/UserModel.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import bcrypt from "bcrypt"; 3 | 4 | interface IUser { 5 | fullname: string; 6 | email: string; 7 | password: string; 8 | avatar: string; 9 | refreshToken: string; 10 | socketID: string; 11 | } 12 | 13 | const userSchema = new Schema( 14 | { 15 | fullname: { type: String }, 16 | email: { type: String }, 17 | password: { type: String }, 18 | avatar: { type: String }, 19 | refreshToken: { type: String }, 20 | socketID: { type: String }, 21 | }, 22 | { 23 | timestamps: true, 24 | } 25 | ); 26 | 27 | userSchema.methods.encryptPassword = function (password: string) { 28 | return bcrypt.hashSync(password, bcrypt.genSaltSync(5)); 29 | }; 30 | 31 | userSchema.methods.validPassword = function (password: string) { 32 | return bcrypt.compareSync(password, this.password); 33 | }; 34 | 35 | const UserModel = model("User", userSchema); 36 | 37 | export default UserModel; 38 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "npx nodemon", 9 | "build": "rimraf ./build && tsc", 10 | "start": "npm run build && node ./build/server.js", 11 | "product": "node ./build/server.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@types/cors": "^2.8.13", 18 | "bcrypt": "^5.1.0", 19 | "body-parser": "^1.20.1", 20 | "dotenv": "^16.0.3", 21 | "express": "^4.18.2", 22 | "jsonwebtoken": "^9.0.0", 23 | "mongoose": "^6.8.0", 24 | "morgan": "^1.10.0", 25 | "typescript": "^4.9.4", 26 | "@types/socket.io": "^3.0.2" 27 | }, 28 | "devDependencies": { 29 | "@types/bcrypt": "^5.0.0", 30 | "@types/express": "^4.17.15", 31 | "@types/jsonwebtoken": "^8.5.9", 32 | "@types/node": "^18.11.17", 33 | "rimraf": "^3.0.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/build/models/UserModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const mongoose_1 = require("mongoose"); 7 | const bcrypt_1 = __importDefault(require("bcrypt")); 8 | const userSchema = new mongoose_1.Schema({ 9 | fullname: { type: String }, 10 | email: { type: String }, 11 | password: { type: String }, 12 | avatar: { type: String }, 13 | refreshToken: { type: String }, 14 | socketID: { type: String }, 15 | }, { 16 | timestamps: true, 17 | }); 18 | userSchema.methods.encryptPassword = function (password) { 19 | return bcrypt_1.default.hashSync(password, bcrypt_1.default.genSaltSync(5)); 20 | }; 21 | userSchema.methods.validPassword = function (password) { 22 | return bcrypt_1.default.compareSync(password, this.password); 23 | }; 24 | const UserModel = (0, mongoose_1.model)("User", userSchema); 25 | exports.default = UserModel; 26 | -------------------------------------------------------------------------------- /frontend/src/redux/actions/currentUser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClearCurrentReceiverAction, 3 | ClearCurrentUserAction, 4 | IUser, 5 | SetCurrentReceiverAction, 6 | SetCurrentUserAction, 7 | } from "../../types"; 8 | 9 | export enum CurrentUserActionTypes { 10 | SET_CURRENT_USER = "SET_CURRENT_USER", 11 | CLEAR_CURRENT_USER = "CLEAR_CURRENT_USER", 12 | SET_CURRENT_RECEIVER = "SET_CURRENT_RECEIVER", 13 | CLEAR_CURRENT_RECEIVER = "CLEAR_CURRENT_RECEIVER", 14 | } 15 | 16 | export const setCurrentUser = (user: IUser): SetCurrentUserAction => ({ 17 | type: CurrentUserActionTypes.SET_CURRENT_USER, 18 | payload: user, 19 | }); 20 | 21 | export const clearCurrentUser = (): ClearCurrentUserAction => ({ 22 | type: CurrentUserActionTypes.CLEAR_CURRENT_USER, 23 | }); 24 | 25 | export const setCurrentReceiver = (user: IUser): SetCurrentReceiverAction => ({ 26 | type: CurrentUserActionTypes.SET_CURRENT_RECEIVER, 27 | payload: user, 28 | }); 29 | 30 | export const clearCurrentReceiver = (): ClearCurrentReceiverAction => ({ 31 | type: CurrentUserActionTypes.CLEAR_CURRENT_RECEIVER, 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/redux/reducers/currentUser.ts: -------------------------------------------------------------------------------- 1 | import { initialState } from "../initState"; 2 | import { CurrentUserActionTypes } from "../actions/currentUser"; 3 | import { CurrentReceiverAction, CurrentUserAction, IUser } from "../../types"; 4 | 5 | const currentUserReducer = ( 6 | state = initialState, 7 | action: CurrentUserAction 8 | ): IUser => { 9 | switch (action.type) { 10 | case CurrentUserActionTypes.SET_CURRENT_USER: 11 | return { 12 | ...state, 13 | ...action.payload, 14 | }; 15 | case CurrentUserActionTypes.CLEAR_CURRENT_USER: 16 | return { 17 | ...initialState, 18 | }; 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | const currentReceiverReducer = ( 25 | state = initialState, 26 | action: CurrentReceiverAction 27 | ): IUser => { 28 | switch (action.type) { 29 | case CurrentUserActionTypes.SET_CURRENT_RECEIVER: 30 | return { 31 | ...state, 32 | ...action.payload, 33 | }; 34 | case CurrentUserActionTypes.CLEAR_CURRENT_RECEIVER: 35 | return { 36 | ...initialState, 37 | }; 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | export { currentUserReducer, currentReceiverReducer }; 44 | -------------------------------------------------------------------------------- /backend/build/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const cors_1 = __importDefault(require("cors")); 8 | const db_1 = __importDefault(require("./configs/db")); 9 | const dotenv_1 = __importDefault(require("dotenv")); 10 | const routes_1 = __importDefault(require("./routes")); 11 | const http_1 = __importDefault(require("http")); 12 | const socket_1 = require("./socket"); 13 | const types_1 = require("./utils/types"); 14 | dotenv_1.default.config(); 15 | const app = (0, express_1.default)(); 16 | const server = http_1.default.createServer(app); 17 | const allowedOrigins = [types_1.URL_LOCALHOST, types_1.URL_PRODUCTION]; 18 | const corsOptions = { 19 | origin: allowedOrigins, 20 | credentials: true, 21 | }; 22 | app.use((0, cors_1.default)(corsOptions)); 23 | app.use(express_1.default.json()); 24 | app.use(express_1.default.urlencoded({ extended: true })); 25 | (0, routes_1.default)(app); 26 | (0, db_1.default)(); 27 | (0, socket_1.handleSocket)(server); 28 | server.listen(types_1.PORT, () => { 29 | console.log(`Server listing at port: ${types_1.PORT}`); 30 | }); 31 | -------------------------------------------------------------------------------- /frontend/src/layouts/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import { routes } from "../routes"; 3 | import { Sidebar, Notification } from "../pages/dashboard"; 4 | import { useSelector } from "react-redux"; 5 | import { RootState } from "../redux/reducers/rootReducer"; 6 | 7 | const Dashboard = () => { 8 | const isVisible = useSelector((state: RootState) => state.currentVisibility); 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 | {routes.map( 19 | ({ layout, pages }) => 20 | layout === "dashboard" && 21 | pages.map(({ path, element }) => ( 22 | 23 | )) 24 | )} 25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export { Dashboard }; 36 | -------------------------------------------------------------------------------- /backend/src/middleware/token.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | const createToken = (user: any) => { 5 | const JWT_SECRET = process.env.JWT_SECRET || ""; 6 | const payload = { id: user.id, username: user.username }; 7 | let token = null; 8 | try { 9 | token = jwt.sign(payload, JWT_SECRET, { expiresIn: "120s" }); 10 | } catch (error) { 11 | console.error(error); 12 | } 13 | 14 | return token; 15 | }; 16 | 17 | const refreshToken = (data: any, token: string) => { 18 | const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || ""; 19 | let payload = { username: data.username }; 20 | try { 21 | token = jwt.sign(payload, REFRESH_TOKEN_SECRET); 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | 26 | return token; 27 | }; 28 | 29 | const authToken = (req: Request, res: Response, next: NextFunction) => { 30 | const authorizationHeader = req.headers["authorization"]; 31 | const token = authorizationHeader?.split(" ")[1] || ""; 32 | if (!token) { 33 | res.status(401).json({ message: "Token is not provided" }); 34 | } 35 | 36 | try { 37 | const key = process.env.JWT_SECRET || ""; 38 | let isVeriToken = jwt.verify(token, key); 39 | if (isVeriToken) { 40 | next(); 41 | } 42 | } catch (error) { 43 | res.status(403).json(error); 44 | } 45 | }; 46 | 47 | export { createToken, authToken, refreshToken }; 48 | -------------------------------------------------------------------------------- /backend/build/configs/db.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const mongoose_1 = __importDefault(require("mongoose")); 16 | mongoose_1.default.set("strictQuery", false); 17 | const connect = () => __awaiter(void 0, void 0, void 0, function* () { 18 | const url = process.env.MONGO_URL || ""; 19 | return yield mongoose_1.default 20 | .connect(url) 21 | .then(() => { 22 | console.log("Database connect successfully"); 23 | }) 24 | .catch(() => { 25 | console.log("Database connect fail"); 26 | }); 27 | }); 28 | exports.default = connect; 29 | -------------------------------------------------------------------------------- /backend/build/routes/authRoute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | Object.defineProperty(exports, "__esModule", { value: true }); 26 | const express_1 = require("express"); 27 | const Auth = __importStar(require("../middleware/auth")); 28 | const router = (0, express_1.Router)(); 29 | router.post("/login", Auth.Login); 30 | router.post("/logout", Auth.Logout); 31 | router.post("/refreshToken", Auth.RefreshToken); 32 | router.post("/register", Auth.Register); 33 | exports.default = router; 34 | -------------------------------------------------------------------------------- /frontend/src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from "../routes"; 2 | import socket from "../socket"; 3 | import { instanceAxios } from "./instance"; 4 | 5 | const refreshToken = () => { 6 | const userID = JSON.parse(sessionStorage.getItem("user") || "")?.id; 7 | const refreshToken = sessionStorage.getItem("refreshToken"); 8 | const body = { 9 | id: userID, 10 | token: refreshToken, 11 | }; 12 | 13 | instanceAxios.post(API_URL.REFRESH_TOKEN, JSON.stringify(body)); 14 | }; 15 | 16 | const handleRegister = async (body: any) => { 17 | const response = await instanceAxios.post(API_URL.SIGN_UP, JSON.stringify(body)); 18 | return response; 19 | }; 20 | 21 | const handleLogin = async (body: any) => { 22 | const response = await instanceAxios 23 | .post(API_URL.SIGN_IN, JSON.stringify(body)) 24 | .then((response) => { 25 | sessionStorage.setItem("token", response.data.token); 26 | sessionStorage.setItem("refreshToken", response.data.refreshToken); 27 | sessionStorage.setItem("user", JSON.stringify(response.data.user)); 28 | socket.emit("login", response.data.user.id); 29 | }); 30 | return response; 31 | }; 32 | 33 | const handleLogout = async () => { 34 | try { 35 | const userID = JSON.parse(sessionStorage.getItem("user") || "")?.id; 36 | const body = { 37 | id: userID, 38 | }; 39 | socket.emit(API_URL.LOGOUT, userID); 40 | sessionStorage.clear(); 41 | await instanceAxios.post(API_URL.LOGOUT, JSON.stringify(body)); 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | }; 46 | 47 | const getToken = () => { 48 | const token = sessionStorage.getItem("token") || null; 49 | return token; 50 | }; 51 | 52 | export { getToken, refreshToken, handleLogout, handleLogin, handleRegister }; 53 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^6.2.1", 7 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 8 | "@fortawesome/react-fontawesome": "^0.2.0", 9 | "@reduxjs/toolkit": "^1.9.2", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "@tippyjs/react": "^4.2.6", 14 | "@types/jest": "^27.5.2", 15 | "@types/node": "^16.18.10", 16 | "@types/react": "^18.0.26", 17 | "@types/react-dom": "^18.0.9", 18 | "@types/socket.io": "^3.0.2", 19 | "autoprefixer": "^10.4.14", 20 | "axios": "^1.2.1", 21 | "classnames": "^2.3.2", 22 | "moment": "^2.29.4", 23 | "nodemon": "^2.0.20", 24 | "postcss": "^8.4.22", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-redux": "^8.0.5", 28 | "react-scripts": "5.0.1", 29 | "sass": "^1.57.0", 30 | "socket.io-client": "^4.6.0", 31 | "typescript": "^4.9.4", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "file-loader": "^6.2.0", 60 | "react-router-dom": "^6.5.0", 61 | "tailwindcss": "^3.3.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/build/routes/messageRoute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | const express_1 = require("express"); 30 | const Token = __importStar(require("../middleware/token")); 31 | const messageController_1 = __importDefault(require("../controllers/messageController")); 32 | const router = (0, express_1.Router)(); 33 | router.post("/list", Token.authToken, messageController_1.default.list); 34 | router.post("/find", Token.authToken, messageController_1.default.find); 35 | router.post("/create", Token.authToken, messageController_1.default.create); 36 | exports.default = router; 37 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 28 | Chat App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /backend/build/middleware/token.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.refreshToken = exports.authToken = exports.createToken = void 0; 7 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); 8 | const createToken = (user) => { 9 | const JWT_SECRET = process.env.JWT_SECRET || ""; 10 | const payload = { id: user.id, username: user.username }; 11 | let token = null; 12 | try { 13 | token = jsonwebtoken_1.default.sign(payload, JWT_SECRET, { expiresIn: "120s" }); 14 | } 15 | catch (error) { 16 | console.error(error); 17 | } 18 | return token; 19 | }; 20 | exports.createToken = createToken; 21 | const refreshToken = (data, token) => { 22 | const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || ""; 23 | let payload = { username: data.username }; 24 | try { 25 | token = jsonwebtoken_1.default.sign(payload, REFRESH_TOKEN_SECRET); 26 | } 27 | catch (error) { 28 | console.log(error); 29 | } 30 | return token; 31 | }; 32 | exports.refreshToken = refreshToken; 33 | const authToken = (req, res, next) => { 34 | const authorizationHeader = req.headers["authorization"]; 35 | const token = (authorizationHeader === null || authorizationHeader === void 0 ? void 0 : authorizationHeader.split(" ")[1]) || ""; 36 | if (!token) { 37 | res.status(401).json({ message: "Token is not provided" }); 38 | } 39 | try { 40 | const key = process.env.JWT_SECRET || ""; 41 | let isVeriToken = jsonwebtoken_1.default.verify(token, key); 42 | if (isVeriToken) { 43 | next(); 44 | } 45 | } 46 | catch (error) { 47 | res.status(403).json(error); 48 | } 49 | }; 50 | exports.authToken = authToken; 51 | -------------------------------------------------------------------------------- /backend/src/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import http from "http"; 3 | import UserModel from "./models/UserModel"; 4 | import messageController from "./controllers/messageController"; 5 | import { Socket } from "socket.io"; 6 | import { URL_LOCALHOST, URL_PRODUCTION } from "./utils/types"; 7 | 8 | const listSocketID: any = []; 9 | 10 | const handleLogin = async (socket: Socket, userId: string) => { 11 | listSocketID.push(socket.id); 12 | await UserModel.findOneAndUpdate({ _id: userId }, { socketID: socket.id }); 13 | socket.emit("login_success", { userId, socketId: socket.id }); 14 | }; 15 | 16 | const handleLogout = async (socket: Socket, userId: string) => { 17 | const index = listSocketID.indexOf(socket.id); 18 | if (index !== -1) { 19 | listSocketID.splice(index, 1); 20 | } 21 | await UserModel.findOneAndUpdate({ _id: userId }, { socketID: "" }); 22 | socket.emit("logout_success", { userId, socketId: socket.id }); 23 | }; 24 | 25 | const handleMessage = async (socket: Socket, io: any, data: any) => { 26 | await messageController.create(data, io, socket, listSocketID); 27 | }; 28 | 29 | const handleGetMessage = async (socket: Socket, io: any, data: any) => { 30 | await messageController.find(data, io, socket, listSocketID); 31 | }; 32 | 33 | export const handleSocket = (server: http.Server) => { 34 | const io = new Server(server, { 35 | cors: { 36 | origin: [URL_LOCALHOST, URL_PRODUCTION], 37 | credentials: true, 38 | }, 39 | }); 40 | 41 | io.on("connection", (socket: Socket) => { 42 | socket.on("login", async (userId: string) => { 43 | await handleLogin(socket, userId); 44 | }); 45 | 46 | socket.on("logout", async (userId: string) => { 47 | await handleLogout(socket, userId); 48 | }); 49 | 50 | socket.on("message", async (data: any) => { 51 | await handleMessage(socket, io, data); 52 | }); 53 | 54 | socket.on("get-message", async (data: any) => { 55 | await handleGetMessage(socket, io, data); 56 | }); 57 | }); 58 | 59 | return io; 60 | }; 61 | -------------------------------------------------------------------------------- /backend/src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import UserModel from "../models/UserModel"; 3 | 4 | class userController { 5 | async list(req: Request, res: Response) { 6 | try { 7 | const userList = await UserModel.find(); 8 | res.status(200).json(userList); 9 | } catch (error) { 10 | res.status(400).json(error); 11 | } 12 | } 13 | 14 | async profile(req: Request, res: Response) { 15 | try { 16 | const user = await UserModel.findById(req.params.id); 17 | res.status(200).json(user); 18 | } catch (error) { 19 | res.status(400).json(error); 20 | } 21 | } 22 | 23 | async update(req: Request, res: Response) { 24 | try { 25 | const updatedUser = await UserModel.findOneAndUpdate( 26 | { _id: req.body.params }, 27 | { 28 | username: req.body.username, 29 | password: req.body.password, 30 | }, 31 | { new: true } // return the updated document 32 | ); 33 | res.status(200).json(updatedUser); 34 | } catch (error) { 35 | res.status(400).json(error); 36 | } 37 | } 38 | 39 | async search(req: Request, res: Response) { 40 | try { 41 | const regex = new RegExp(req.body.fullname, "i"); 42 | const user = await UserModel.find({ 43 | fullname: regex, 44 | }); 45 | res.status(200).json(user); 46 | } catch (error) { 47 | res.status(400).json(error); 48 | } 49 | } 50 | 51 | async destroy(req: Request, res: Response) { 52 | try { 53 | const user = await UserModel.deleteOne({ _id: req.body.params }); 54 | res.status(200).json(user); 55 | } catch (error) { 56 | res.status(400).json(error); 57 | } 58 | } 59 | 60 | async destroyAll(req: Request, res: Response) { 61 | try { 62 | const deletedUser = await UserModel.deleteMany({}); 63 | res.status(200).json(deletedUser); 64 | } catch (error) { 65 | res.status(400).json(error); 66 | } 67 | } 68 | } 69 | 70 | export default new userController(); 71 | -------------------------------------------------------------------------------- /frontend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CurrentUserActionTypes, 3 | SelectMessageActionTypes, 4 | VisibilityActionTypes, 5 | } from "./redux/actions"; 6 | 7 | export interface IUser { 8 | _id: string; 9 | avatar: string; 10 | fullname: string; 11 | username: string; 12 | } 13 | 14 | export interface IMessage { 15 | _id: string; 16 | senderID: IUser; 17 | receiverID: IUser; 18 | message: string; 19 | createdAt: string; 20 | } 21 | 22 | export interface selectMessageType extends Array {} 23 | 24 | export interface MessageContextValue { 25 | selectMessage: selectMessageType; 26 | getSelectMessage: React.Dispatch>; 27 | } 28 | 29 | export interface SetCurrentUserAction { 30 | type: CurrentUserActionTypes.SET_CURRENT_USER; 31 | payload: IUser; 32 | } 33 | 34 | export interface ClearCurrentUserAction { 35 | type: CurrentUserActionTypes.CLEAR_CURRENT_USER; 36 | } 37 | 38 | export interface SetCurrentReceiverAction { 39 | type: CurrentUserActionTypes.SET_CURRENT_RECEIVER; 40 | payload: IUser; 41 | } 42 | 43 | export interface ClearCurrentReceiverAction { 44 | type: CurrentUserActionTypes.CLEAR_CURRENT_RECEIVER; 45 | } 46 | 47 | export type CurrentUserAction = SetCurrentUserAction | ClearCurrentUserAction; 48 | 49 | export type CurrentReceiverAction = 50 | | SetCurrentReceiverAction 51 | | ClearCurrentReceiverAction; 52 | 53 | export interface SetSelectMessageAction { 54 | type: SelectMessageActionTypes.SET_MESSAGE; 55 | payload: []; 56 | } 57 | 58 | export interface ClearSelectMessageAction { 59 | type: SelectMessageActionTypes.CLEAR_MESSAGE; 60 | } 61 | 62 | export type SelectMessageAction = 63 | | SetSelectMessageAction 64 | | ClearSelectMessageAction; 65 | 66 | export interface SetVisibilityAction { 67 | type: VisibilityActionTypes.SET_VISIBILITY; 68 | payload: string; 69 | } 70 | 71 | export interface ClearVisibilityAction { 72 | type: VisibilityActionTypes.CLEAR_VISIBILITY; 73 | } 74 | 75 | export type SelectVisibilityAction = 76 | | SetVisibilityAction 77 | | ClearVisibilityAction; 78 | -------------------------------------------------------------------------------- /backend/build/routes/userRoute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | const express_1 = require("express"); 30 | const Token = __importStar(require("../middleware/token")); 31 | const userController_1 = __importDefault(require("../controllers/userController")); 32 | const router = (0, express_1.Router)(); 33 | router.post("/search", Token.authToken, userController_1.default.search); 34 | router.get("/profile/:id", Token.authToken, userController_1.default.profile); 35 | router.put("/update/:id", Token.authToken, userController_1.default.update); 36 | router.delete("/delete/:id", Token.authToken, userController_1.default.destroy); 37 | router.delete("/destroy", Token.authToken, userController_1.default.destroyAll); 38 | router.get("/", Token.authToken, userController_1.default.list); 39 | exports.default = router; 40 | -------------------------------------------------------------------------------- /frontend/src/services/instance.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { API_URL } from "../routes"; 3 | 4 | const API_URLUrl = process.env.REACT_APP_API_URL; 5 | 6 | const getRefreshToken = async () => { 7 | const id = JSON.parse(sessionStorage.getItem("user") || "")?.id; 8 | const token = sessionStorage.getItem("refreshToken") || ""; 9 | 10 | const body = { 11 | id, 12 | token, 13 | }; 14 | return await instanceAxios 15 | .post(API_URL.REFRESH_TOKEN, JSON.stringify(body)) 16 | .then((response) => { 17 | localStorage.removeItem("token"); 18 | localStorage.removeItem("refreshToken"); 19 | sessionStorage.setItem("token", response.data.token); 20 | }); 21 | }; 22 | 23 | 24 | const instanceAxios = axios.create({ 25 | baseURL: API_URLUrl, 26 | headers: { 27 | "Content-Type": "application/json", 28 | "Access-Control-Allow-Origin": "*", 29 | }, 30 | }); 31 | 32 | // Add a request interceptor 33 | instanceAxios.interceptors.request.use( 34 | function (config) { 35 | // Do something before request is sent 36 | config.withCredentials = true; 37 | const token = sessionStorage.getItem("token") || ""; 38 | if (token !== "") { 39 | config.headers = { 40 | ...config.headers, 41 | Authorization: `Bearer ${token}`, 42 | }; 43 | } 44 | return config; 45 | }, 46 | function (error) { 47 | // Do something with request error 48 | return Promise.reject(error); 49 | } 50 | ); 51 | 52 | // Add a response interceptor 53 | instanceAxios.interceptors.response.use( 54 | function (response) { 55 | return response; 56 | }, 57 | async function (error) { 58 | const originalRequest = error.config; 59 | if (error.response.status === 403 && !originalRequest._retry) { 60 | originalRequest._retry = true; 61 | const access_token = await getRefreshToken(); 62 | instanceAxios.defaults.headers.common["Authorization"] = 63 | "Bearer " + access_token; 64 | return instanceAxios(originalRequest); 65 | } 66 | // Any status codes that falls outside the range of 2xx cause this function to trigger 67 | // Do something with response error 68 | return Promise.reject(error); 69 | } 70 | ); 71 | 72 | export { instanceAxios }; 73 | -------------------------------------------------------------------------------- /frontend/src/services/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from "../routes"; 2 | import { IMessage, IUser, selectMessageType } from "../types"; 3 | import { instanceAxios } from "./instance"; 4 | 5 | const getProfile = async () => { 6 | let id: null = null; 7 | const user = sessionStorage.getItem("user") || ""; 8 | if (user !== "") { 9 | id = JSON.parse(user).id; 10 | } 11 | 12 | const response = await instanceAxios.get(`${API_URL.PROFILE}${id}`); 13 | return response; 14 | }; 15 | 16 | const handleSearch = async (fullname: string) => { 17 | const response = await instanceAxios.post( 18 | API_URL.SEARCH, 19 | JSON.stringify({ fullname: fullname }) 20 | ); 21 | return response; 22 | }; 23 | 24 | const getListUser = async () => { 25 | const response = await instanceAxios.get(API_URL.USER); 26 | return response; 27 | }; 28 | 29 | const getListMessage = async () => { 30 | let userId: null = null; 31 | const user = sessionStorage.getItem("user") || ""; 32 | if (user !== "") userId = JSON.parse(user).id; 33 | 34 | const response = await instanceAxios.post( 35 | API_URL.MESSAGES, 36 | JSON.stringify({ senderID: userId, receiverID: userId }) 37 | ); 38 | return response; 39 | }; 40 | 41 | const getMessage = async (senderID: string, receiverID: string) => { 42 | const response = await instanceAxios.post( 43 | API_URL.MESSAGE, 44 | JSON.stringify({ senderID, receiverID }) 45 | ); 46 | return response; 47 | }; 48 | 49 | const createMessage = async ( 50 | senderID: string, 51 | receiverID: string, 52 | message: string 53 | ) => { 54 | const response = await instanceAxios.post( 55 | API_URL.CREATE_MESSAGE, 56 | JSON.stringify({ senderID, receiverID, message }) 57 | ); 58 | return response; 59 | }; 60 | 61 | const handleFilterMessage = ( 62 | listMessage: selectMessageType, 63 | currentUser: IUser 64 | ): selectMessageType => { 65 | const uniqueMessage = Array.from( 66 | new Map( 67 | listMessage.map((message: IMessage) => [ 68 | message?.senderID?._id, 69 | message, 70 | ]) && 71 | listMessage.map((message: IMessage) => [ 72 | message?.receiverID?._id, 73 | message, 74 | ]) 75 | ).values() 76 | ); 77 | 78 | const filterArray = uniqueMessage.filter((message) => { 79 | return message.receiverID._id !== currentUser._id; 80 | }); 81 | 82 | return filterArray; 83 | }; 84 | 85 | export { 86 | getProfile, 87 | handleSearch, 88 | getListMessage, 89 | getMessage, 90 | createMessage, 91 | getListUser, 92 | handleFilterMessage, 93 | }; 94 | -------------------------------------------------------------------------------- /restAPI.http: -------------------------------------------------------------------------------- 1 | 2 | # url localhost 3 | @url = http://localhost:3001/api 4 | # url product 5 | @token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzYjkzYTdhNGIxYWRmMWU2OWI1NzQyZCIsInVzZXJuYW1lIjoiYW5odGh5IiwiZW1haWwiOiJhbmh0aHlAZ21haWwuY29tIiwiaWF0IjoxNjc2MDk2Njg4LCJleHAiOjE2NzYwOTY4MDh9.kRWaC_2Kqdki66XN-L4E6-Tn6pNZj7iuMh5J6UiCtLI 6 | @refreshToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRoYW5odGh1eSIsImlhdCI6MTY3NDcyNTI2MX0.WttXYRfnVlQuZTPqlE9JSdpwsu91t_xYvdYOyQzI7wo 7 | 8 | # =============AUTH================== 9 | 10 | ### Register 11 | POST {{url}}/auth/register 12 | Content-Type: application/json 13 | 14 | { 15 | "username": "khoavh", 16 | "password": "1", 17 | "avatar": "https://cdn.diemnhangroup.com/seoulcenter/2022/11/gai-xinh-2.jpg" 18 | } 19 | 20 | ### Login 21 | POST {{url}}/auth/login 22 | Content-Type: application/json 23 | 24 | { 25 | "email": "khoavh@gmail.com", 26 | "password": "1" 27 | } 28 | 29 | ### Logout 30 | POST {{url}}/auth/logout 31 | Content-Type: application/json 32 | 33 | { 34 | "id": "63b93a7a4b1adf1e69b5742d" 35 | } 36 | 37 | ### refreshToken 38 | POST {{url}}/auth/refreshToken 39 | Content-Type: application/json 40 | 41 | { 42 | "id": "63b93a7a4b1adf1e69b5742d", 43 | "token": "{{refreshToken}}" 44 | } 45 | 46 | # ==================USER========================= 47 | 48 | ### List All USer 49 | GET {{url}}/user 50 | Authorization: Beaer {{token}} 51 | 52 | ### search 53 | POST {{url}}/user/search 54 | Authorization: Beaer {{token}} 55 | Content-Type: application/json 56 | 57 | { 58 | "username": "khoavh" 59 | } 60 | 61 | ### Destroy All 62 | DELETE {{url}}/user/destroy 63 | Authorization: Beaer {{token}} 64 | 65 | ### profile 66 | GET {{url}}/user/profile/63b673f6f878cb9c68675fe6 67 | Authorization: Beaer {{token}} 68 | 69 | ### delete a users 70 | DELETE {{url}}/users/delete/63baa329b593996a69735666 71 | Authorization: Beaer {{token}} 72 | 73 | 74 | # ======================MESSAGE========================= 75 | 76 | ### List All Message 77 | POST {{url}}/message/list 78 | Authorization: Beaer {{token}} 79 | Content-Type: application/json 80 | 81 | { 82 | "senderID": "63b93a7a4b1adf1e69b5742d" 83 | } 84 | 85 | ### Message 86 | POST {{url}}/message/find 87 | Authorization: Beaer {{token}} 88 | Content-Type: application/json 89 | 90 | { 91 | "senderID": "63b93a7a4b1adf1e69b5742d", 92 | "receiverID": "63d5360bbfe103866f4ee972" 93 | } 94 | 95 | ### Create Message Thy 96 | POST {{url}}/message/create 97 | Authorization: Beaer {{token}} 98 | Content-Type: application/json 99 | 100 | { 101 | "message": "How are you?", 102 | "senderID": "63b93a7a4b1adf1e69b5742d", 103 | "receiverID": "63d5360bbfe103866f4ee972" 104 | } 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /backend/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import UserModel from "../models/UserModel"; 3 | import bcrypt from "bcrypt"; 4 | import * as Token from "./token"; 5 | import jwt from "jsonwebtoken"; 6 | 7 | const Register = async (req: Request, res: Response) => { 8 | try { 9 | const { email, fullname, avatar, password } = req.body; 10 | const hashedPassword = bcrypt.hashSync(password, bcrypt.genSaltSync(10)); 11 | const newUser = new UserModel({ 12 | email, 13 | fullname, 14 | avatar, 15 | password: hashedPassword, 16 | }); 17 | await newUser.save(); 18 | res.status(201).json(newUser); 19 | } catch (error) { 20 | console.error(error); 21 | res.status(400).json({ error: "Failed to register user" }); 22 | } 23 | }; 24 | 25 | const Login = async (req: Request, res: Response) => { 26 | try { 27 | const { email, password } = req.body; 28 | const user = await UserModel.findOne({ email }); 29 | 30 | if (!user || !bcrypt.compareSync(password, user.password)) { 31 | return res.status(400).json("Login Fail!"); 32 | } 33 | 34 | const token = Token.createToken(user) || ""; 35 | const refreshToken = Token.refreshToken(user, token); 36 | 37 | await UserModel.updateOne({ email }, { refreshToken }); 38 | 39 | return res.status(200).json({ 40 | user: { id: user.id }, 41 | token, 42 | refreshToken, 43 | }); 44 | } catch (err) { 45 | console.error(err); 46 | return res.status(500).json("Internal Server Error"); 47 | } 48 | }; 49 | 50 | const RefreshToken = async (req: Request, res: Response) => { 51 | const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || ""; 52 | const JWT_SECRET = process.env.JWT_SECRET || ""; 53 | const { token: refreshToken, id: userId } = req.body; 54 | if (!refreshToken || !userId) { 55 | return res.sendStatus(401); 56 | } 57 | try { 58 | const user = await UserModel.findById(userId); 59 | if (!user || user.refreshToken !== refreshToken) { 60 | return res.sendStatus(403); 61 | } 62 | jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err: any, decoded: any) => { 63 | if (err) { 64 | return res.sendStatus(403); 65 | } 66 | const accessToken = jwt.sign( 67 | { id: userId, username: decoded.username }, 68 | JWT_SECRET, 69 | { expiresIn: "3600s" } 70 | ); 71 | return res.status(201).json({ token: accessToken }); 72 | }); 73 | } catch (error) { 74 | console.error(error); 75 | return res.sendStatus(500); 76 | } 77 | }; 78 | 79 | const Logout = async (req: Request, res: Response) => { 80 | const userId = req.body.id; 81 | await UserModel.updateOne({ _id: userId }, { refreshToken: "" }).then(() => { 82 | res.sendStatus(200); 83 | }); 84 | }; 85 | export { Register, Login, RefreshToken, Logout }; 86 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/sign-in.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink, useNavigate } from "react-router-dom"; 2 | import { useRef, useState } from "react"; 3 | import { handleLogin } from "../../services/auth"; 4 | import { useDispatch } from "react-redux"; 5 | import { setVisibility } from "../../redux/actions"; 6 | 7 | const Login = () => { 8 | const navigate = useNavigate(); 9 | const dispatch = useDispatch(); 10 | const emailRef = useRef(null); 11 | const passwordRef = useRef(null); 12 | const [validate, setValidate] = useState(false); 13 | 14 | const handleSubmit = async (e: React.FormEvent) => { 15 | const body = { 16 | email: emailRef.current?.value, 17 | password: passwordRef.current?.value, 18 | }; 19 | e.preventDefault(); 20 | try { 21 | await handleLogin(body); 22 | dispatch(setVisibility("home")); 23 | navigate("/dashboard/home"); 24 | } catch (error) { 25 | console.error(error); 26 | setValidate(true); 27 | } 28 | }; 29 | 30 | return ( 31 |
32 |
33 |

Login

34 |
35 |
36 | 39 | 48 |
49 |
50 | 53 | 62 |
63 | {validate && ( 64 |
65 | Invalid email or password 66 |
67 | )} 68 | 71 |
72 | 73 | Sign Up? 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | export { Login }; 81 | -------------------------------------------------------------------------------- /backend/src/controllers/messageController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import MessageModel from "../models/MessageModel"; 3 | import UserModel from "../models/UserModel"; 4 | 5 | class MessageController { 6 | async list(req: Request, res: Response) { 7 | const { senderID, receiverID } = req.body; 8 | try { 9 | const listMessage = await MessageModel.find({ 10 | $or: [{ senderID }, { receiverID }], 11 | }) 12 | .populate("senderID") 13 | .populate("receiverID"); 14 | res.status(200).json(listMessage); 15 | } catch (error) { 16 | res.status(400).json(error); 17 | } 18 | } 19 | async create(data: any, io: any, socket: any, listSocketID: any) { 20 | const { senderID, receiverID, message } = data; 21 | const newMessage = new MessageModel({ senderID, receiverID, message }); 22 | 23 | try { 24 | await newMessage.save(); 25 | 26 | const [messagesFromSender, messagesFromReceiver, receiver] = 27 | await Promise.all([ 28 | MessageModel.find({ senderID, receiverID }) 29 | .populate("senderID") 30 | .populate("receiverID"), 31 | MessageModel.find({ senderID: receiverID, receiverID: senderID }) 32 | .populate("senderID") 33 | .populate("receiverID"), 34 | UserModel.findById(receiverID), 35 | ]); 36 | 37 | const messages = [...messagesFromSender, ...messagesFromReceiver]; 38 | 39 | if (receiver?.socketID && listSocketID.includes(receiver.socketID)) { 40 | io.to(receiver.socketID).emit("message", messages); 41 | } 42 | 43 | socket.emit("message", messages); 44 | 45 | const listMessage = await MessageModel.find({ 46 | $or: [{ senderID: senderID }, { receiverID: senderID }], 47 | }) 48 | .populate("senderID") 49 | .populate("receiverID"); 50 | 51 | socket.emit("listMessage", listMessage); 52 | } catch (error) { 53 | // res.status(400).json(error); 54 | } 55 | } 56 | 57 | async find(data: any, io: any, socket: any, listSocketID: any) { 58 | const { senderID, receiverID } = data; 59 | 60 | try { 61 | const [messageSender, messageReceiver, user] = await Promise.all([ 62 | MessageModel.find({ senderID, receiverID }) 63 | .populate("senderID") 64 | .populate("receiverID"), 65 | MessageModel.find({ senderID: receiverID, receiverID: senderID }) 66 | .populate("senderID") 67 | .populate("receiverID"), 68 | UserModel.findById(receiverID), 69 | ]); 70 | 71 | const messages = [...messageSender, ...messageReceiver]; 72 | 73 | if (user?.socketID && listSocketID.includes(user.socketID)) { 74 | io.to(user.socketID).emit("message", messages); 75 | } 76 | 77 | socket.emit("message", messages); 78 | const listMessage = await MessageModel.find({ 79 | $or: [{ senderID: senderID }, { receiverID: senderID }], 80 | }) 81 | .populate("senderID") 82 | .populate("receiverID"); 83 | socket.emit("listMessage", listMessage); 84 | } catch (error) { 85 | // res.status(400).json(error); 86 | } 87 | } 88 | } 89 | 90 | export default new MessageController(); 91 | -------------------------------------------------------------------------------- /backend/build/socket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.handleSocket = void 0; 16 | const socket_io_1 = require("socket.io"); 17 | const UserModel_1 = __importDefault(require("./models/UserModel")); 18 | const messageController_1 = __importDefault(require("./controllers/messageController")); 19 | const types_1 = require("./utils/types"); 20 | const listSocketID = []; 21 | const handleLogin = (socket, userId) => __awaiter(void 0, void 0, void 0, function* () { 22 | listSocketID.push(socket.id); 23 | yield UserModel_1.default.findOneAndUpdate({ _id: userId }, { socketID: socket.id }); 24 | socket.emit("login_success", { userId, socketId: socket.id }); 25 | }); 26 | const handleLogout = (socket, userId) => __awaiter(void 0, void 0, void 0, function* () { 27 | const index = listSocketID.indexOf(socket.id); 28 | if (index !== -1) { 29 | listSocketID.splice(index, 1); 30 | } 31 | yield UserModel_1.default.findOneAndUpdate({ _id: userId }, { socketID: "" }); 32 | socket.emit("logout_success", { userId, socketId: socket.id }); 33 | }); 34 | const handleMessage = (socket, io, data) => __awaiter(void 0, void 0, void 0, function* () { 35 | yield messageController_1.default.create(data, io, socket, listSocketID); 36 | }); 37 | const handleGetMessage = (socket, io, data) => __awaiter(void 0, void 0, void 0, function* () { 38 | yield messageController_1.default.find(data, io, socket, listSocketID); 39 | }); 40 | const handleSocket = (server) => { 41 | const io = new socket_io_1.Server(server, { 42 | cors: { 43 | origin: [types_1.URL_LOCALHOST, types_1.URL_PRODUCTION], 44 | credentials: true, 45 | }, 46 | }); 47 | io.on("connection", (socket) => { 48 | socket.on("login", (userId) => __awaiter(void 0, void 0, void 0, function* () { 49 | yield handleLogin(socket, userId); 50 | })); 51 | socket.on("logout", (userId) => __awaiter(void 0, void 0, void 0, function* () { 52 | yield handleLogout(socket, userId); 53 | })); 54 | socket.on("message", (data) => __awaiter(void 0, void 0, void 0, function* () { 55 | yield handleMessage(socket, io, data); 56 | })); 57 | socket.on("get-message", (data) => __awaiter(void 0, void 0, void 0, function* () { 58 | yield handleGetMessage(socket, io, data); 59 | })); 60 | }); 61 | return io; 62 | }; 63 | exports.handleSocket = handleSocket; 64 | -------------------------------------------------------------------------------- /backend/build/controllers/userController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const UserModel_1 = __importDefault(require("../models/UserModel")); 16 | class userController { 17 | list(req, res) { 18 | return __awaiter(this, void 0, void 0, function* () { 19 | try { 20 | const userList = yield UserModel_1.default.find(); 21 | res.status(200).json(userList); 22 | } 23 | catch (error) { 24 | res.status(400).json(error); 25 | } 26 | }); 27 | } 28 | profile(req, res) { 29 | return __awaiter(this, void 0, void 0, function* () { 30 | try { 31 | const user = yield UserModel_1.default.findById(req.params.id); 32 | res.status(200).json(user); 33 | } 34 | catch (error) { 35 | res.status(400).json(error); 36 | } 37 | }); 38 | } 39 | update(req, res) { 40 | return __awaiter(this, void 0, void 0, function* () { 41 | try { 42 | const updatedUser = yield UserModel_1.default.findOneAndUpdate({ _id: req.body.params }, { 43 | username: req.body.username, 44 | password: req.body.password, 45 | }, { new: true } // return the updated document 46 | ); 47 | res.status(200).json(updatedUser); 48 | } 49 | catch (error) { 50 | res.status(400).json(error); 51 | } 52 | }); 53 | } 54 | search(req, res) { 55 | return __awaiter(this, void 0, void 0, function* () { 56 | try { 57 | const regex = new RegExp(req.body.fullname, "i"); 58 | const user = yield UserModel_1.default.find({ 59 | fullname: regex, 60 | }); 61 | res.status(200).json(user); 62 | } 63 | catch (error) { 64 | res.status(400).json(error); 65 | } 66 | }); 67 | } 68 | destroy(req, res) { 69 | return __awaiter(this, void 0, void 0, function* () { 70 | try { 71 | const user = yield UserModel_1.default.deleteOne({ _id: req.body.params }); 72 | res.status(200).json(user); 73 | } 74 | catch (error) { 75 | res.status(400).json(error); 76 | } 77 | }); 78 | } 79 | destroyAll(req, res) { 80 | return __awaiter(this, void 0, void 0, function* () { 81 | try { 82 | const deletedUser = yield UserModel_1.default.deleteMany({}); 83 | res.status(200).json(deletedUser); 84 | } 85 | catch (error) { 86 | res.status(400).json(error); 87 | } 88 | }); 89 | } 90 | } 91 | exports.default = new userController(); 92 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/sign-up.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink, useNavigate } from "react-router-dom"; 2 | import { useRef, useState } from "react"; 3 | import { handleRegister } from "../../services/auth"; 4 | 5 | const Register = () => { 6 | const navigate = useNavigate(); 7 | const fullNameRef = useRef(null); 8 | const emailRef = useRef(null); 9 | const passwordRef = useRef(null); 10 | const [avatar, setAvatar] = useState( 11 | "https://cdn.shopify.com/shopifycloud/shopify/assets/no-image-2048-5e88c1b20e087fb7bbe9a3771824e743c244f437e4f8ba93bbf7b11b53f7824c_grande.gif" 12 | ); 13 | 14 | async function handleSubmit(e: React.FormEvent) { 15 | e.preventDefault(); 16 | 17 | const body = { 18 | fullname: fullNameRef?.current?.value, 19 | email: emailRef?.current?.value, 20 | password: passwordRef?.current?.value, 21 | avatar: avatar, 22 | }; 23 | try { 24 | await handleRegister(body); 25 | navigate("/login"); 26 | } catch (error) { 27 | console.error(error); 28 | alert("Register failed!"); 29 | } 30 | } 31 | 32 | return ( 33 |
34 |
35 |

Register

36 |
37 |
38 | avatar 43 | { 46 | setAvatar(e.currentTarget.value); 47 | }} 48 | placeholder="URL avatar..." 49 | className="h-16 px-4 border-none rounded-2xl border-b-1 border-gray-400 text-black text-2xl" 50 | /> 51 |
52 |
53 | 54 | 61 |
62 |
63 | 64 | 71 |
72 |
73 | 76 | 83 |
84 | 90 |
91 |
92 | 93 | Sign In? 94 | 95 |
96 |
97 |
98 | ); 99 | }; 100 | 101 | export { Register }; 102 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | /* src/index.css */ 2 | @import 'tailwindcss/base'; 3 | @import 'tailwindcss/components'; 4 | @import 'tailwindcss/utilities'; 5 | 6 | * { 7 | box-sizing: border-box; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | :root { 13 | --primary: #2a2731; 14 | --black: rgb(10, 23, 33); 15 | --white: #ffffff; 16 | --gray: #656566; 17 | --blue: #76a9ff; 18 | } 19 | 20 | ::-webkit-scrollbar:horizontal { 21 | height: 8px; 22 | width: 5px !important; 23 | } 24 | 25 | ::-webkit-scrollbar { 26 | border-radius: 0; 27 | width: 8px; 28 | } 29 | 30 | ::-webkit-scrollbar-thumb { 31 | border-radius: 4px; 32 | background-color: var(--gray); 33 | } 34 | 35 | ::-webkit-scrollbar-track { 36 | border-radius: 0; 37 | background-color: rgba(0, 0, 0, 0); 38 | } 39 | 40 | ::-webkit-scrollbar-thumb:active { 41 | cursor: pointer; 42 | } 43 | 44 | ::-webkit-scrollbar:vertical { 45 | height: 20px; 46 | } 47 | 48 | 49 | html { 50 | font-size: 62.5%; 51 | } 52 | 53 | body { 54 | font-family: "IBM Plex Sans", sans-serif; 55 | font-size: 1.6 rem; 56 | line-height: 1.5; 57 | text-rendering: optimizeSpeed; 58 | height: 100%; 59 | } 60 | 61 | input:focus { 62 | outline: none; 63 | } 64 | 65 | 66 | @media (min-width: 1024px) { 67 | .custom-w-sidebar { 68 | left: 0; 69 | width: 25%; 70 | } 71 | 72 | .custom-w-home { 73 | left: 25%; 74 | width: 55%; 75 | } 76 | 77 | .custom-w-notification { 78 | right: 0; 79 | width: 20%; 80 | } 81 | } 82 | 83 | 84 | 85 | @media (max-width: 1024px) { 86 | .custom-w-sidebar { 87 | left: 0; 88 | width: 30%; 89 | } 90 | 91 | .custom-w-home { 92 | left: 30%; 93 | width: 45%; 94 | } 95 | 96 | .custom-w-notification { 97 | right: 0; 98 | width: 25%; 99 | } 100 | } 101 | 102 | .wrapper { 103 | box-sizing: border-box; 104 | display: flex; 105 | flex-direction: column; 106 | box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.12); 107 | background-color: var(--black); 108 | color: var(--white); 109 | padding: 20px; 110 | border-radius: 8px; 111 | padding-top: 8px; 112 | overflow-y: scroll; 113 | font-size: 1.5rem; 114 | } 115 | 116 | .wrapper ul { 117 | list-style-type: none; 118 | display: flex; 119 | flex-direction: column; 120 | align-items: center; 121 | } 122 | 123 | .wrapper ul li { 124 | display: flex; 125 | flex-direction: row; 126 | align-items: center; 127 | padding-left: 15px; 128 | } 129 | 130 | .wrapper ul li:hover { 131 | cursor: pointer; 132 | background-color: #3c3844; 133 | border-radius: 20px; 134 | } 135 | 136 | 137 | @media (min-width: 992px) { 138 | .wrapper { 139 | width: 35vh; 140 | font-size: 1.5rem; 141 | gap: 10px; 142 | } 143 | 144 | .wrapper ul { 145 | margin-top: 20px; 146 | gap: 5px; 147 | } 148 | 149 | .wrapper ul li { 150 | width: 100%; 151 | height: 40px; 152 | gap: 10px; 153 | } 154 | 155 | .search { 156 | width: 35vh; 157 | height: 35vh; 158 | } 159 | } 160 | 161 | @media (max-width: 992px) and (min-width: 426px) { 162 | .wrapper { 163 | width: 40vh; 164 | font-size: 1.5rem; 165 | gap: 10px; 166 | } 167 | 168 | .wrapper ul { 169 | gap: 5px; 170 | } 171 | 172 | .wrapper ul li { 173 | width: 100%; 174 | height: 40px; 175 | gap: 10px; 176 | } 177 | 178 | .search { 179 | width: 40vh; 180 | height: 40vh; 181 | } 182 | } 183 | 184 | @media (max-width: 425px) { 185 | .wrapper { 186 | width: 350px; 187 | font-size: 1.5rem; 188 | } 189 | 190 | .wrapper ul { 191 | gap: 5px; 192 | } 193 | 194 | .wrapper ul li { 195 | width: 100%; 196 | height: 40px; 197 | gap: 10px; 198 | } 199 | 200 | .search { 201 | width: 270px; 202 | height: 270px; 203 | } 204 | } -------------------------------------------------------------------------------- /backend/build/controllers/messageController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const MessageModel_1 = __importDefault(require("../models/MessageModel")); 16 | const UserModel_1 = __importDefault(require("../models/UserModel")); 17 | class MessageController { 18 | list(req, res) { 19 | return __awaiter(this, void 0, void 0, function* () { 20 | const { senderID, receiverID } = req.body; 21 | try { 22 | const listMessage = yield MessageModel_1.default.find({ 23 | $or: [{ senderID }, { receiverID }], 24 | }) 25 | .populate("senderID") 26 | .populate("receiverID"); 27 | res.status(200).json(listMessage); 28 | } 29 | catch (error) { 30 | res.status(400).json(error); 31 | } 32 | }); 33 | } 34 | create(data, io, socket, listSocketID) { 35 | return __awaiter(this, void 0, void 0, function* () { 36 | const { senderID, receiverID, message } = data; 37 | const newMessage = new MessageModel_1.default({ senderID, receiverID, message }); 38 | try { 39 | yield newMessage.save(); 40 | const [messagesFromSender, messagesFromReceiver, receiver] = yield Promise.all([ 41 | MessageModel_1.default.find({ senderID, receiverID }) 42 | .populate("senderID") 43 | .populate("receiverID"), 44 | MessageModel_1.default.find({ senderID: receiverID, receiverID: senderID }) 45 | .populate("senderID") 46 | .populate("receiverID"), 47 | UserModel_1.default.findById(receiverID), 48 | ]); 49 | const messages = [...messagesFromSender, ...messagesFromReceiver]; 50 | if ((receiver === null || receiver === void 0 ? void 0 : receiver.socketID) && listSocketID.includes(receiver.socketID)) { 51 | io.to(receiver.socketID).emit("message", messages); 52 | } 53 | socket.emit("message", messages); 54 | const listMessage = yield MessageModel_1.default.find({ 55 | $or: [{ senderID: senderID }, { receiverID: senderID }], 56 | }) 57 | .populate("senderID") 58 | .populate("receiverID"); 59 | socket.emit("listMessage", listMessage); 60 | } 61 | catch (error) { 62 | // res.status(400).json(error); 63 | } 64 | }); 65 | } 66 | find(data, io, socket, listSocketID) { 67 | return __awaiter(this, void 0, void 0, function* () { 68 | const { senderID, receiverID } = data; 69 | try { 70 | const [messageSender, messageReceiver, user] = yield Promise.all([ 71 | MessageModel_1.default.find({ senderID, receiverID }) 72 | .populate("senderID") 73 | .populate("receiverID"), 74 | MessageModel_1.default.find({ senderID: receiverID, receiverID: senderID }) 75 | .populate("senderID") 76 | .populate("receiverID"), 77 | UserModel_1.default.findById(receiverID), 78 | ]); 79 | const messages = [...messageSender, ...messageReceiver]; 80 | if ((user === null || user === void 0 ? void 0 : user.socketID) && listSocketID.includes(user.socketID)) { 81 | io.to(user.socketID).emit("message", messages); 82 | } 83 | socket.emit("message", messages); 84 | const listMessage = yield MessageModel_1.default.find({ 85 | $or: [{ senderID: senderID }, { receiverID: senderID }], 86 | }) 87 | .populate("senderID") 88 | .populate("receiverID"); 89 | socket.emit("listMessage", listMessage); 90 | } 91 | catch (error) { 92 | // res.status(400).json(error); 93 | } 94 | }); 95 | } 96 | } 97 | exports.default = new MessageController(); 98 | -------------------------------------------------------------------------------- /frontend/src/components/Account.tsx: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { RootState } from "../redux/reducers/rootReducer"; 4 | import { setCurrentReceiver, setVisibility} from "../redux/actions"; 5 | import { useEffect, useRef } from "react"; 6 | import socket from "../socket"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { faThumbsUp } from "@fortawesome/free-solid-svg-icons"; 9 | 10 | const AccountItem = ({ ...rest }) => { 11 | const currentUser = useSelector((state: RootState) => state.currentUser); 12 | const dispatch = useDispatch(); 13 | const { searchUser } = rest; 14 | const senderID = currentUser?._id; 15 | const receiverID = searchUser?._id; 16 | 17 | const handleSubmit = () => { 18 | dispatch(setVisibility("home")); 19 | dispatch(setCurrentReceiver(searchUser)); 20 | const data = { 21 | senderID: receiverID, 22 | receiverID: senderID, 23 | }; 24 | socket.emit("get-message", data); 25 | }; 26 | 27 | return ( 28 |
handleSubmit()} 31 | > 32 | 37 | {searchUser?.fullname} 38 |
39 | ); 40 | }; 41 | 42 | const AccountMessage = ({ ...rest }) => { 43 | const dispatch = useDispatch(); 44 | const { currentMessage, searchUser } = rest; 45 | const divRef = useRef(null); 46 | const currentUser = useSelector((state: RootState) => state.currentUser); 47 | const currentReceiver = useSelector( 48 | (state: RootState) => state.currentReceiver 49 | ); 50 | const senderID = currentUser?._id; 51 | const receiverID = searchUser?._id; 52 | 53 | useEffect(() => { 54 | if ( 55 | divRef.current && 56 | divRef.current.parentElement?.children[0] === divRef.current 57 | ) { 58 | divRef.current.click(); 59 | } 60 | }, []); 61 | 62 | const handleSubmit = () => { 63 | dispatch(setVisibility("home")); 64 | dispatch(setCurrentReceiver(searchUser)); 65 | const data = { 66 | senderID: senderID, 67 | receiverID: receiverID, 68 | }; 69 | socket.emit("get-message", data); 70 | }; 71 | 72 | 73 | return ( 74 |
81 | avatar 90 | 91 |
92 |

93 | {currentMessage.senderID === currentUser 94 | ? currentMessage?.senderID?.fullname 95 | : currentMessage?.receiverID?.fullname} 96 |

97 | {currentMessage?.message !== ":like" ? ( 98 |
99 |

{currentMessage?.message}

100 | 101 | {moment(currentMessage?.createdAt).format("HH:mm")} 102 | 103 |
104 | ) : ( 105 | 109 | )} 110 |
111 | 112 |
113 | ); 114 | }; 115 | 116 | const AccountStatus = ({ ...rest }) => { 117 | const dispatch = useDispatch(); 118 | const { searchUser } = rest; 119 | const currentUser = useSelector((state: RootState) => state.currentUser); 120 | const receiverID = searchUser?._id; 121 | const senderID = currentUser?._id; 122 | const handleSubmit = () => { 123 | dispatch(setVisibility("home")); 124 | dispatch(setCurrentReceiver(searchUser)); 125 | const data = { 126 | senderID: receiverID, 127 | receiverID: senderID, 128 | }; 129 | socket.emit("get-message", data); 130 | }; 131 | 132 | return ( 133 |
handleSubmit()} 136 | > 137 | {searchUser?.fullname} 142 |

{searchUser?.fullname}

143 |
144 | ); 145 | }; 146 | 147 | export { AccountItem, AccountMessage, AccountStatus }; 148 | -------------------------------------------------------------------------------- /backend/build/middleware/auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | var __importDefault = (this && this.__importDefault) || function (mod) { 35 | return (mod && mod.__esModule) ? mod : { "default": mod }; 36 | }; 37 | Object.defineProperty(exports, "__esModule", { value: true }); 38 | exports.Logout = exports.RefreshToken = exports.Login = exports.Register = void 0; 39 | const UserModel_1 = __importDefault(require("../models/UserModel")); 40 | const bcrypt_1 = __importDefault(require("bcrypt")); 41 | const Token = __importStar(require("./token")); 42 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); 43 | const Register = (req, res) => __awaiter(void 0, void 0, void 0, function* () { 44 | try { 45 | const { email, fullname, avatar, password } = req.body; 46 | const hashedPassword = bcrypt_1.default.hashSync(password, bcrypt_1.default.genSaltSync(10)); 47 | const newUser = new UserModel_1.default({ 48 | email, 49 | fullname, 50 | avatar, 51 | password: hashedPassword, 52 | }); 53 | yield newUser.save(); 54 | res.status(201).json(newUser); 55 | } 56 | catch (error) { 57 | console.error(error); 58 | res.status(400).json({ error: "Failed to register user" }); 59 | } 60 | }); 61 | exports.Register = Register; 62 | const Login = (req, res) => __awaiter(void 0, void 0, void 0, function* () { 63 | try { 64 | const { email, password } = req.body; 65 | const user = yield UserModel_1.default.findOne({ email }); 66 | if (!user || !bcrypt_1.default.compareSync(password, user.password)) { 67 | return res.status(400).json("Login Fail!"); 68 | } 69 | const token = Token.createToken(user) || ""; 70 | const refreshToken = Token.refreshToken(user, token); 71 | yield UserModel_1.default.updateOne({ email }, { refreshToken }); 72 | return res.status(200).json({ 73 | user: { id: user.id }, 74 | token, 75 | refreshToken, 76 | }); 77 | } 78 | catch (err) { 79 | console.error(err); 80 | return res.status(500).json("Internal Server Error"); 81 | } 82 | }); 83 | exports.Login = Login; 84 | const RefreshToken = (req, res) => __awaiter(void 0, void 0, void 0, function* () { 85 | const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || ""; 86 | const JWT_SECRET = process.env.JWT_SECRET || ""; 87 | const { token: refreshToken, id: userId } = req.body; 88 | if (!refreshToken || !userId) { 89 | return res.sendStatus(401); 90 | } 91 | try { 92 | const user = yield UserModel_1.default.findById(userId); 93 | if (!user || user.refreshToken !== refreshToken) { 94 | return res.sendStatus(403); 95 | } 96 | jsonwebtoken_1.default.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, decoded) => { 97 | if (err) { 98 | return res.sendStatus(403); 99 | } 100 | const accessToken = jsonwebtoken_1.default.sign({ id: userId, username: decoded.username }, JWT_SECRET, { expiresIn: "3600s" }); 101 | return res.status(201).json({ token: accessToken }); 102 | }); 103 | } 104 | catch (error) { 105 | console.error(error); 106 | return res.sendStatus(500); 107 | } 108 | }); 109 | exports.RefreshToken = RefreshToken; 110 | const Logout = (req, res) => __awaiter(void 0, void 0, void 0, function* () { 111 | const userId = req.body.id; 112 | yield UserModel_1.default.updateOne({ _id: userId }, { refreshToken: "" }).then(() => { 113 | res.sendStatus(200); 114 | }); 115 | }); 116 | exports.Logout = Logout; 117 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/notification.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { 3 | faArrowLeft, 4 | faBars, 5 | faBell, 6 | faPhone, 7 | faToggleOff, 8 | faUser, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | import { Image } from "../../components/Image"; 11 | import { NavLink } from "react-router-dom"; 12 | import { useDispatch, useSelector } from "react-redux"; 13 | import { RootState } from "../../redux/reducers/rootReducer"; 14 | import { setVisibility } from "../../redux/actions"; 15 | const Notification = () => { 16 | const dispatch = useDispatch(); 17 | const currentReceiver = useSelector( 18 | (state: RootState) => state.currentReceiver 19 | ); 20 | const isVisible = useSelector((state: RootState) => state.currentVisibility); 21 | 22 | const handleHome = () => { 23 | dispatch(setVisibility("home")); 24 | }; 25 | return ( 26 |
27 |
28 | {isVisible === "notification" && ( 29 | 34 | )} 35 | {isVisible === "notification" && ( 36 | 37 | )} 38 |
39 |
40 | 46 |

{currentReceiver.fullname}

47 |
48 |
49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 |

Notifications

57 | 58 |
59 |
60 | 61 |

Bookmarks

62 | 63 |
64 |
65 |
66 |
67 |
68 | share medias 69 | 70 | View all 71 | 72 |
73 |
74 | 79 | 84 | 89 | 94 |
95 |
96 |
97 |
98 | share files 99 | 100 | View all 101 | 102 |
103 |
104 |
105 | 110 |
111 | File Name 112 |

File Description

113 |
114 |
115 |
116 | 121 |
122 | File Name 123 |

File Description

124 |
125 |
126 |
127 | 132 |
133 | File Name 134 |

File Description

135 |
136 |
137 |
138 |
139 |
140 |
141 | ); 142 | }; 143 | 144 | export { Notification }; 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Typescript](https://img.shields.io/badge/-TypeScript-007acc?logo=typescript&logoColor=white&style=for-the-badge) 2 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 3 | ![Node.js](https://img.shields.io/badge/node.js-339933.svg?style=for-the-badge&logo=Node%2Ejs&logoColor=white) 4 | ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 5 | ![MongoDB](https://img.shields.io/badge/MongoDB-47A248.svg?style=for-the-badge&logo=MongoDB&logoColor=white) 6 | ![Socket.io](https://img.shields.io/badge/socket.io-010101.svg?style=for-the-badge&logo=Socket%2Eio&logoColor=white) 7 | 8 | # Chat App MERN 9 | The Real Time Chat Application is a messaging platform that enables users to communicate with each other in real-time. It is built using Typescript, React, Express, Mongoose, and Socket.io, which are all modern technologies for developing web applications. 10 | 11 | ## Index 12 | 13 | - [Demo](#demo) 14 | - [Features](#features) 15 | - [Installation](#installation) 16 | - [How It Works](#how-it-works) 17 | - [Structure](#structure) 18 | 19 | ## Demo 20 | 21 |

Live: Website Chat App

22 | 23 | 24 | ### Desktop: 25 | chat app 26 | 27 | ### Smart Phone: 28 | 29 |
30 | chat app 31 |
32 | 33 |
34 | 35 | ## Features 36 | 37 | - Use Express to build the backend. 38 | - Use React to build the frontend. 39 | - Authenticates via email and password. 40 | - Real-time communication between a client and a server using [Socket.io](https://github.com/socketio/socket.io). 41 | - Uses [MongoDB](https://github.com/mongodb/mongo), [Mongoose](https://github.com/Automattic/mongoose) for storing and querying data. 42 | 43 | ## Installation 44 | 45 | ### Running Locally 46 | 47 | Make sure you have [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 48 | 49 | 1. Clone repository: 50 | 51 | ``` 52 | $ git clone https://github.com/Vo-Huy-Khoa/chat-app-mern.git 53 | $ cd chat-app-mern 54 | ``` 55 | 56 | Run Backend: 57 | 58 | ```bash 59 | cd backend 60 | npm i 61 | npm run dev 62 | ``` 63 | 64 | Run Frontend: 65 | 66 | ```bash 67 | cd frontend 68 | npm i 69 | npm run start 70 | ``` 71 | 72 | ## How It Works 73 | 74 | #### MongoDB 75 | 76 | You need to create a database on MongoDB, then create a database user, get the `MongoDB URI`, and assign it to `dbURI`. 77 | 78 | ### Database 79 | 80 | Mongoose is used to interact with a MongoDB. 81 | 82 | #### Schemas 83 | 84 | There are two schemas; users and rooms. 85 | 86 | Each user has a username, password,and picture. If the user is logged via username and password, and the if logged in via a social account, then the password will be null. 87 | 88 | ### Models 89 | 90 | Each model wraps Mongoose Model object, overrides and provides some methods. 91 | 92 | ### Sockets 93 | 94 | Having an active connection opened between the client and the server so client can send and receive data. This allows real-time communication using sockets. This is made possible by [Socket.io](https://github.com/socketio/socket.io). 95 | 96 | The client starts by connecting to the server through a socket (maybe also assigned to a specific namespace) . Once connections is successful, client and server can emit and listen to events. 97 | 98 | ## Structure of the project: 99 | 100 | ### BackEnd 101 | 102 | ```text 103 | src 104 | | 105 | ├── configs 106 | | └── db.ts 107 | ├── controllers 108 | | └── UserController.ts 109 | │ └── MessageController.ts 110 | ├── middleware 111 | | └── auth.ts 112 | │ └── token.ts 113 | ├── models 114 | | └── User.ts 115 | | └──Message.ts 116 | ├── routes 117 | | └── index.ts 118 | └── server.ts 119 | ``` 120 | 121 | ### FrontEnd 122 | 123 | ```text 124 | src 125 | ├── components 126 | │ └── Account.tsx 127 | │ └── Image.tsx 128 | │ └── index.ts 129 | ├── hooks 130 | │ └── useDebounce 131 | │ └── index 132 | ├── layouts 133 | │ └── auth.tsx 134 | │ └── dashboard.tsx 135 | │ └── index.ts 136 | ├── pages 137 | │ └── auth 138 | │ └── sign-in.tsx 139 | │ └── sign-up.tsx 140 | │ └── index.ts 141 | │ └── dashboard 142 | │ └── home.tsx 143 | │ └── notification.tsx 144 | │ └── sidebar.tsx 145 | │ └── index.ts 146 | ├── redux 147 | │ └── actions 148 | │ └── sign-in.tsx 149 | │ └── index.ts 150 | │ └── reducers 151 | │ └── visibility.ts 152 | │ └── rootReducer.ts 153 | │ └── initState.ts 154 | │ └── store.ts 155 | ├── services 156 | │ └── auth.ts 157 | │ └── dashboard.ts 158 | │ └── index.ts 159 | ├── socket.tsx 160 | ├── route.tsx 161 | ├── App.tsx 162 | └── index.tsx 163 | 164 | ``` 165 | 166 | 200 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "../../components/Image"; 2 | import HeadlessTippy from "@tippyjs/react/headless"; 3 | import { 4 | AccountItem, 5 | AccountMessage, 6 | AccountStatus, 7 | } from "../../components/Account"; 8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 9 | import { 10 | faBell, 11 | faBug, 12 | faChevronDown, 13 | faDeleteLeft, 14 | faGear, 15 | faRightFromBracket, 16 | faSearch, 17 | faUser, 18 | } from "@fortawesome/free-solid-svg-icons"; 19 | import { useEffect, useState } from "react"; 20 | import { useDebounce } from "../../hooks"; 21 | import { Wrapper as PopperWrapper } from "../../components"; 22 | import { 23 | handleSearch, 24 | getListMessage, 25 | getListUser, 26 | handleFilterMessage, 27 | } from "../../services/dashboard"; 28 | import { IMessage, IUser, selectMessageType } from "../../types"; 29 | import { useSelector } from "react-redux"; 30 | import { RootState } from "../../redux/reducers/rootReducer"; 31 | import { handleLogout } from "../../services"; 32 | import { NavLink } from "react-router-dom"; 33 | 34 | const Sidebar = () => { 35 | const currentUser = useSelector((state: RootState) => state.currentUser); 36 | const [valueSearch, setValueSearch] = useState(""); 37 | const debounceValue = useDebounce(valueSearch, 500); 38 | const [listUserSearch, setListSearch] = useState([]); 39 | const [listUser, setListUser] = useState([]); 40 | const [listMessage, setListMessage] = useState([]); 41 | 42 | useEffect(() => { 43 | if (!debounceValue.trim()) { 44 | return; 45 | } 46 | handleSearch(debounceValue).then((response) => { 47 | setListSearch(response.data); 48 | }); 49 | }, [debounceValue]); 50 | 51 | useEffect(() => { 52 | async function fetchMessagesAndUsers() { 53 | try { 54 | const messageResponse = await getListMessage(); 55 | const listMessage = handleFilterMessage( 56 | messageResponse.data, 57 | currentUser 58 | ); 59 | setListMessage(listMessage); 60 | 61 | const userResponse = await getListUser(); 62 | setListUser(userResponse.data); 63 | } catch (error) { 64 | // Handle errors 65 | } 66 | } 67 | 68 | fetchMessagesAndUsers(); 69 | }, [currentUser]); 70 | 71 | return ( 72 |
73 |
74 |
75 | ( 81 |
82 | 83 |
    84 |
  • 85 | 86 | Profile 87 |
  • 88 |
  • 89 | 90 | Settings 91 |
  • 92 |
  • 93 | 94 | Report 95 |
  • 96 |
  • 97 | 101 | 102 | Logout 103 | 104 |
  • 105 |
106 |
107 |
108 | )} 109 | > 110 |
111 | 115 | 116 |
117 |
118 |

{currentUser.username}

119 |
120 |
121 | 125 | 126 | 1 127 | 128 |
129 |
130 |
131 | ( 137 |
138 | 139 | {listUserSearch.map((user, index) => { 140 | return ( 141 | 146 | ); 147 | })} 148 | 149 |
150 | )} 151 | > 152 |
153 | 157 | { 159 | setValueSearch(e.currentTarget.value); 160 | }} 161 | value={valueSearch} 162 | type="text" 163 | placeholder="Search" 164 | className="w-full h-14 bg-black rounded-3xl text-white p-8 pl-14 text-2xl" 165 | /> 166 | { 170 | setValueSearch(""); 171 | }} 172 | /> 173 |
174 |
175 |
176 |

All User

177 |
178 | {listUser.map((user: IUser, index) => { 179 | return ( 180 | 185 | ); 186 | })} 187 |
188 |
189 |
190 |
191 | {listMessage.map((message: IMessage, index) => { 192 | return ( 193 | 202 | ); 203 | })} 204 |
205 |
206 | ); 207 | }; 208 | 209 | export { Sidebar }; 210 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard/home.tsx: -------------------------------------------------------------------------------- 1 | import socket from "../../socket"; 2 | import { Image } from "../../components/Image"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { 5 | faArrowLeft, 6 | faCircleInfo, 7 | faCopy, 8 | faFileImage, 9 | faMicrophone, 10 | faPhone, 11 | faThumbsUp, 12 | faVideoCamera, 13 | } from "@fortawesome/free-solid-svg-icons"; 14 | import { useEffect, useState } from "react"; 15 | import { IMessage } from "../../types"; 16 | import { getProfile } from "../../services"; 17 | import { setCurrentUser, setSelectMessage, setVisibility } from "../../redux/actions"; 18 | import { useDispatch, useSelector } from "react-redux"; 19 | import { RootState } from "../../redux/reducers/rootReducer"; 20 | 21 | const Home = () => { 22 | const dispatch = useDispatch(); 23 | 24 | const currentUser = useSelector((state: RootState) => state.currentUser); 25 | const currentReceiver = useSelector( 26 | (state: RootState) => state.currentReceiver 27 | ); 28 | const selectMessage = useSelector((state: RootState) => state.currentMessage); 29 | const [valueMessage, setValueMessage] = useState(""); 30 | const currentSenderID = currentUser?._id; 31 | const currentReceiverID = currentReceiver?._id; 32 | const countMessage = currentReceiver._id.length ? 1 : null; 33 | 34 | const emitMessage = (message: string) => { 35 | const data = { 36 | senderID: currentSenderID, 37 | receiverID: currentReceiverID, 38 | message: message, 39 | }; 40 | socket.emit("message", data); 41 | setValueMessage(""); 42 | }; 43 | 44 | const handleCreateMessage = (event: any) => { 45 | event.preventDefault(); 46 | emitMessage(valueMessage); 47 | }; 48 | 49 | const handleCreateLike = (event: any) => { 50 | event.preventDefault(); 51 | emitMessage(":like"); 52 | }; 53 | 54 | const handleReturnSidebar = ()=>{ 55 | dispatch(setVisibility("sidebar")); 56 | } 57 | const handleReturnNotification = ()=>{ 58 | dispatch(setVisibility("notification")); 59 | } 60 | 61 | useEffect(() => { 62 | const handleNewMessage = (data: any = []) => { 63 | const sortedMessages = data.sort( 64 | (a: IMessage, b: IMessage) => 65 | Date.parse(a.createdAt) - Date.parse(b.createdAt) 66 | ); 67 | dispatch(setSelectMessage(sortedMessages)); 68 | }; 69 | 70 | socket.on("message", handleNewMessage); 71 | 72 | return () => { 73 | socket.off("message", handleNewMessage); 74 | }; 75 | }, [dispatch]); 76 | 77 | useEffect(() => { 78 | getProfile().then((res) => { 79 | dispatch(setCurrentUser(res.data)); 80 | }); 81 | }, [dispatch]); 82 | 83 | return ( 84 |
85 |
86 |
87 | 92 | {countMessage !== null && ( 93 |
94 | {currentReceiver?.fullname} 95 |

{currentReceiver?.fullname}

96 |
97 | )} 98 |
99 | 100 |
101 | 102 | 103 | 106 |
107 | 108 |
109 |
110 |
111 | {selectMessage.map((message, index) => { 112 | if (message.senderID._id !== currentUser._id) { 113 | return ( 114 |
115 | sender 120 | {message.message !== ":like" ? ( 121 |
122 |

{message.message}

123 |
124 | ) : ( 125 |
126 | 130 |
131 | )} 132 |
133 | ); 134 | } else { 135 | return ( 136 |
140 | sender 145 | {message.message !== ":like" ? ( 146 |
147 |

{message.message}

148 |
149 | ) : ( 150 |
151 | 155 |
156 | )} 157 |
158 | ); 159 | } 160 | })} 161 |
162 |
163 | ( 164 | {countMessage !== null && ( 165 |
166 |
167 |
168 | 172 | 176 | 180 |
181 | { 185 | setValueMessage(event.target.value); 186 | }} 187 | /> 188 | 189 | {valueMessage !== "" ? ( 190 | 196 | ) : ( 197 | 206 | )} 207 |
208 |
209 | )} 210 | ) 211 |
212 | ); 213 | }; 214 | 215 | export { Home }; 216 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src/" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------