Parcourir la source

deployment-v1

main
Apoorv Pandey il y a 1 an
révision
3ee4f23a7c
35 fichiers modifiés avec 9582 ajouts et 0 suppressions
  1. +2
    -0
      .gitignore
  2. +120
    -0
      auth/mongoAuthState.js
  3. +21
    -0
      client/.eslintrc.cjs
  4. +24
    -0
      client/.gitignore
  5. +8
    -0
      client/README.md
  6. +13
    -0
      client/index.html
  7. +5361
    -0
      client/package-lock.json
  8. +38
    -0
      client/package.json
  9. +6
    -0
      client/postcss.config.js
  10. +47
    -0
      client/src/App.jsx
  11. +28
    -0
      client/src/components/Message.jsx
  12. +10
    -0
      client/src/components/PrivateRoute.jsx
  13. +26
    -0
      client/src/components/ReloadCheck.jsx
  14. +16
    -0
      client/src/components/Tag.jsx
  15. +9
    -0
      client/src/connection/socket.js
  16. +7
    -0
      client/src/index.css
  17. +12
    -0
      client/src/main.jsx
  18. +133
    -0
      client/src/pages/Messages.jsx
  19. +51
    -0
      client/src/pages/QRScan.jsx
  20. +68
    -0
      client/src/pages/Signin.jsx
  21. +79
    -0
      client/src/pages/Signup.jsx
  22. +19
    -0
      client/src/redux/socket/socketSlice.js
  23. +14
    -0
      client/src/redux/store.js
  24. +22
    -0
      client/src/redux/user/userSlice.js
  25. +44
    -0
      client/src/utils/http.js
  26. +8
    -0
      client/tailwind.config.js
  27. +15
    -0
      client/vite.config.js
  28. +8
    -0
      config/error.js
  29. +14
    -0
      db/dbConnect.js
  30. +88
    -0
      helper/jwt_helper.js
  31. +328
    -0
      index.js
  32. +44
    -0
      models/user.js
  33. +2857
    -0
      package-lock.json
  34. +30
    -0
      package.json
  35. +12
    -0
      validation/user.validity.js

+ 2
- 0
.gitignore Voir le fichier

@@ -0,0 +1,2 @@
node_modules/
.env

+ 120
- 0
auth/mongoAuthState.js Voir le fichier

@@ -0,0 +1,120 @@
const { proto } = require("@whiskeysockets/baileys/WAProto");
const {
Curve,
signedKeyPair,
} = require("@whiskeysockets/baileys/lib/Utils/crypto");
const {
generateRegistrationId,
} = require("@whiskeysockets/baileys/lib/Utils/generics");
const { randomBytes } = require("crypto");

const initAuthCreds = () => {
const identityKey = Curve.generateKeyPair();
return {
noiseKey: Curve.generateKeyPair(),
signedIdentityKey: identityKey,
signedPreKey: signedKeyPair(identityKey, 1),
registrationId: generateRegistrationId(),
advSecretKey: randomBytes(32).toString("base64"),
processedHistoryMessages: [],
nextPreKeyId: 1,
firstUnuploadedPreKeyId: 1,
accountSettings: {
unarchiveChats: false,
},
};
};

const BufferJSON = {
replacer: (k, value) => {
if (
Buffer.isBuffer(value) ||
value instanceof Uint8Array ||
value?.type === "Buffer"
) {
return {
type: "Buffer",
data: Buffer.from(value?.data || value).toString("base64"),
};
}

return value;
},

reviver: (_, value) => {
if (
typeof value === "object" &&
!!value &&
(value.buffer === true || value.type === "Buffer")
) {
const val = value.data || value.value;
return typeof val === "string"
? Buffer.from(val, "base64")
: Buffer.from(val || []);
}

return value;
},
};

module.exports = useMongoDBAuthState = async (collection) => {
const writeData = (data, id) => {
const informationToStore = JSON.parse(
JSON.stringify(data, BufferJSON.replacer)
);
const update = {
$set: {
...informationToStore,
},
};
return collection.updateOne({ _id: id }, update, { upsert: true });
};
const readData = async (id) => {
try {
const data = JSON.stringify(await collection.findOne({ _id: id }));
return JSON.parse(data, BufferJSON.reviver);
} catch (error) {
return null;
}
};
const removeData = async (id) => {
try {
await collection.deleteOne({ _id: id });
} catch (_a) {}
};
const creds = (await readData("creds")) || (0, initAuthCreds)();
return {
state: {
creds,
keys: {
get: async (type, ids) => {
const data = {};
await Promise.all(
ids.map(async (id) => {
let value = await readData(`${type}-${id}`);
if (type === "app-state-sync-key") {
value = proto.Message.AppStateSyncKeyData.fromObject(data);
}
data[id] = value;
})
);
return data;
},
set: async (data) => {
const tasks = [];
for (const category of Object.keys(data)) {
for (const id of Object.keys(data[category])) {
const value = data[category][id];
const key = `${category}-${id}`;
tasks.push(value ? writeData(value, key) : removeData(key));
}
}
await Promise.all(tasks);
},
},
},
saveCreds: () => {
return writeData(creds, "creds");
},
};
};

+ 21
- 0
client/.eslintrc.cjs Voir le fichier

@@ -0,0 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"no-unused-vars": "off",
},
};

+ 24
- 0
client/.gitignore Voir le fichier

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

+ 8
- 0
client/README.md Voir le fichier

@@ -0,0 +1,8 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

+ 13
- 0
client/index.html Voir le fichier

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

+ 5361
- 0
client/package-lock.json
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 38
- 0
client/package.json Voir le fichier

@@ -0,0 +1,38 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.7",
"@tanstack/react-query": "^5.0.0-beta.35",
"axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-loader-spinner": "^5.4.5",
"react-redux": "^8.1.3",
"react-router-dom": "^6.17.0",
"socket.io-client": "^4.7.2",
"whatsapp-api": "file:.."
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"vite": "^5.0.0"
}
}

+ 6
- 0
client/postcss.config.js Voir le fichier

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

+ 47
- 0
client/src/App.jsx Voir le fichier

@@ -0,0 +1,47 @@
import React from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./utils/http";
import SignIn from "./pages/Signin";
import Signup from "./pages/Signup";
import PrivateRoute from "./components/PrivateRoute";
import QRScan from "./pages/QRScan";
import Messages from "./pages/Messages";
import ReloadCheck from "./components/ReloadCheck";

const router = createBrowserRouter([
{
element: <ReloadCheck />,
children: [
{
path: "/",
element: <SignIn />,
},
{
path: "/signup",
element: <Signup />,
},
{
element: <PrivateRoute />,
children: [
{
path: "/authcheck",
element: <QRScan />,
},
{
path: "/messages",
element: <Messages />,
},
],
},
],
},
]);

export default function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}

+ 28
- 0
client/src/components/Message.jsx Voir le fichier

@@ -0,0 +1,28 @@
/* eslint-disable react/prop-types */
import React from "react";

const Message = (props) => {
const { message } = props;

const messageDate = new Date(message.timestamp);
const formattedDate = messageDate.toLocaleDateString("en-US", {
day: "numeric",
month: "short",
year: "2-digit",
});

return (
<div className='flex flex-col gap-1 bg-white p-4 rounded-lg shadow-lg'>
<div className='flex flex-col gap-1 text-lg font-semibold text-zinc-800'>
<h1>{message.username}</h1>
<h1>{message.phoneNumber}</h1>
</div>
<div>
<h1>{message.conversation}</h1>
<h1>{formattedDate}</h1>
</div>
</div>
);
};

export default Message;

+ 10
- 0
client/src/components/PrivateRoute.jsx Voir le fichier

@@ -0,0 +1,10 @@
import { useSelector } from "react-redux";
import { Navigate, Outlet } from "react-router-dom";

const PrivateRoute = () => {
const { currentUser } = useSelector((state) => state.user);
console.log(currentUser);
return currentUser ? <Outlet /> : <Navigate to='/signup' />;
};

export default PrivateRoute;

+ 26
- 0
client/src/components/ReloadCheck.jsx Voir le fichier

@@ -0,0 +1,26 @@
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { Outlet } from "react-router-dom";
import { signInSuccess } from "../redux/user/userSlice";
import { useEffect, useState } from "react";

const ReloadCheck = () => {
const dispatch = useDispatch();
const [getMeFetched, setGetMeFetched] = useState(false);

useEffect(() => {
axios
.get("/api/getMe")
.then((response) => {
dispatch(signInSuccess(response.data));
setGetMeFetched(true);
})
.catch((err) => {
setGetMeFetched(true);
});
}, []);

return getMeFetched && <Outlet />;
};

export default ReloadCheck;

+ 16
- 0
client/src/components/Tag.jsx Voir le fichier

@@ -0,0 +1,16 @@
/* eslint-disable react/prop-types */
import React from "react";

const Tag = (props) => {
const { tag, tagDeleteSubmitHandler } = props;
return (
<div
onClick={() => tagDeleteSubmitHandler(tag)}
className='flex text-xs font-bold bg-green-500 rounded-2xl p-2 text-white cursor-pointer transition shadow-md hover:opacity-95 hover:shadow-lg'
>
{tag}
</div>
);
};

export default Tag;

+ 9
- 0
client/src/connection/socket.js Voir le fichier

@@ -0,0 +1,9 @@
import { io } from "socket.io-client";

const socket = io("http://localhost:3000", {
reconnection: true, // Enable reconnection
reconnectionAttempts: 5, // Maximum number of reconnection attempts
reconnectionDelay: 1000,
});

export default socket;

+ 7
- 0
client/src/index.css Voir le fichier

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
background-color: rgb(241, 245, 241);
}

+ 12
- 0
client/src/main.jsx Voir le fichier

@@ -0,0 +1,12 @@
import React from "react";
import { store } from "./redux/store.js";
import { Provider } from "react-redux";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);

+ 133
- 0
client/src/pages/Messages.jsx Voir le fichier

@@ -0,0 +1,133 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useMutation, useQuery } from "@tanstack/react-query";
import React, { useEffect, useRef, useState } from "react";
import { allMessages, delTag, newTag, queryClient } from "../utils/http";
import socket from "../connection/socket";
import Message from "../components/Message";
import { useNavigate } from "react-router-dom";
import { CiSquarePlus } from "react-icons/ci";
import { useDispatch, useSelector } from "react-redux";
import Tag from "../components/Tag";
import { signInSuccess } from "../redux/user/userSlice";

const Messages = () => {
// const { data, isLoading } = useQuery({
// queryKey: ["messages"],
// queryFn: getMessages,
// });
const dispatch = useDispatch();
const { currentUser } = useSelector((state) => state.user);
const [data, setData] = useState([]);
const navigate = useNavigate();

const tagRef = useRef();

useEffect(() => {
if (localStorage.getItem("firstLoadDone") === null) {
// If it's the first load, set the flag in local storage to true and reload the page
localStorage.setItem("firstLoadDone", 1);
} else {
socket.emit("whatsapp connect", currentUser._id);
}

socket.on("new message", (message) => {
setData((messages) => [message, ...messages]);
});

socket.on("user disconnected", () => {
navigate("/authcheck", { replace: true });
});

return () => {
// Clean up socket event listeners when the component unmounts
socket.off("user disconnected");
socket.off("new message");
};
}, []);

const { data: oldMessages } = useQuery({
queryKey: ["Messages"],
queryFn: allMessages,
});

const { mutate: addTag } = useMutation({
mutationFn: newTag,
onSuccess: (data) => {
dispatch(signInSuccess(data));
tagRef.current.value = "";
queryClient.invalidateQueries(["Messages"]);
},
});

const { mutate: removeTag } = useMutation({
mutationFn: delTag,
onSuccess: (data) => {
dispatch(signInSuccess(data));
queryClient.invalidateQueries(["Messages"]);
},
});

const onAddSubmitHandler = (event) => {
event.preventDefault();
addTag({ formData: { tag: tagRef.current.value } });
};

const onRemoveSubmitHandler = (tag) => {
removeTag({ formData: { tag } });
};

return (
<div className='max-w-4xl mx-auto p-6'>
<div className='mb-5 flex gap-2'>
<form
className='relative flex-[1_1_auto] h-fit'
onSubmit={onAddSubmitHandler}
>
<input
type='text'
className='p-4 border rounded-lg w-full'
ref={tagRef}
placeholder='Add new tag...'
/>
<button
style={{ top: "50%", transform: "translateY(-50%)" }}
className='absolute right-5 text-black hover:opacity-80'
>
<CiSquarePlus style={{ width: "1.75em", height: "1.75em" }} />
</button>
</form>
<div className='self-stretch flex flex-wrap gap-2 flex-[8_8_auto] p-3 bg-white rounded-lg'>
{currentUser.tags.map((tag, i) => (
<Tag
key={i}
tag={tag}
tagDeleteSubmitHandler={onRemoveSubmitHandler}
/>
))}
</div>
</div>
<p className='text-center text-3xl font-bold mb-8'>Messages</p>
<div className='flex flex-col gap-5 mb-8'>
<div className='flex flex-col gap-5'>
{data.map((message, i) => (
<Message
key={i}
message={message}
/>
))}
</div>
<div className='flex flex-col gap-5'>
{oldMessages &&
oldMessages.map((message, i) => (
<Message
key={i}
message={message}
/>
))}
</div>
</div>
</div>
);
};

export default Messages;

+ 51
- 0
client/src/pages/QRScan.jsx Voir le fichier

@@ -0,0 +1,51 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import socket from "../connection/socket";

const QRScan = () => {
const [qr, setQR] = useState(null);
const navigate = useNavigate();
const [presentState, setPresentState] = useState("status-check");
const { currentUser } = useSelector((state) => state.user);
useEffect(() => {
socket.emit("whatsapp connect", currentUser._id);

socket.on("qrCode", (qrCodeDataURL) => {
setQR(qrCodeDataURL);
setPresentState("scan-qr");
});

socket.on("user connected", () => {
navigate("/messages", { replace: true });
setPresentState("authenticated");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
return () => {
// Clean up socket event listeners when the component unmounts
socket.off("user connected");
};
}, []);

return (
<>
{qr ? (
<div className='flex flex-col items-center justify-center gap-1 mt-16'>
<img src={qr} />
<p className='text-center font-medium'>
scan this QR code to continue
</p>
</div>
) : (
<h1 className='text-center mt-16'>Checking User status...</h1>
)}
<div className='flex flex-col text-lg items-center font-bold justify-center gap-1 mt-4'>
<p>{currentUser.username}</p>
<p>{currentUser.email}</p>
</div>
</>
);
};

export default QRScan;

+ 68
- 0
client/src/pages/Signin.jsx Voir le fichier

@@ -0,0 +1,68 @@
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { signInHelper } from "../utils/http";
import { useMutation } from "@tanstack/react-query";
import { signInSuccess } from "../redux/user/userSlice";
import { useDispatch } from "react-redux";

const SignIn = () => {
const [user, setUser] = useState({ email: "", password: "" });
const dispatch = useDispatch();
const navigate = useNavigate();

const { mutate, isPending, isError, error } = useMutation({
mutationFn: signInHelper,
onSuccess: (data) => {
dispatch(signInSuccess(data));
navigate("/authcheck", { replace: true });
},
});

const formSubmitHandler = (e) => {
e.preventDefault();
mutate({ formData: user });
};

const formInputChangeHandler = (event) => {
setUser((prevUser) => ({
...prevUser,
[event.target.id]: event.target.value,
}));
};

return (
<div className='max-w-lg mx-auto mt-32'>
<h1 className='text-center text-3xl font-bold'>Sign in</h1>
<form
onSubmit={formSubmitHandler}
className='flex flex-col gap-3 mt-8'
>
<input
type='text'
className='p-3 rounded-lg border'
onChange={formInputChangeHandler}
id='email'
placeholder='Email'
/>
<input
type='password'
id='password'
onChange={formInputChangeHandler}
className='p-3 rounded-lg border'
placeholder='Password'
/>
<button className='p-3 rounded-[32px] text-white uppercase font-bold disabled:opacity-80 transition duration-300 shadow hover:shadow-lg hover:opacity-95 bg-teal-600 hover:bg-teal-700'>
Submit
</button>
</form>
<div className='flex gap-2 mt-2'>
<p>{"Don't have an account?"}</p>
<Link to={"/signup"}>
<span className='text-blue-700'>Sign up</span>
</Link>
</div>
</div>
);
};

export default SignIn;

+ 79
- 0
client/src/pages/Signup.jsx Voir le fichier

@@ -0,0 +1,79 @@
import { useMutation } from "@tanstack/react-query";
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { signUpHelper } from "../utils/http";
import { useDispatch } from "react-redux";
import { signInSuccess } from "../redux/user/userSlice";

const Signup = () => {
const [user, setUser] = useState({ username: "", password: "", email: "" });

const dispatch = useDispatch();
const navigate = useNavigate();

const { mutate, isPending, isError, error } = useMutation({
mutationFn: signUpHelper,
onSuccess: (data) => {
dispatch(signInSuccess(data));
navigate("/authcheck", { replace: true });
},
});

const formSubmitHandler = (e) => {
e.preventDefault();
mutate({ formData: user });
};

const formInputChangeHandler = (event) => {
setUser((prevUser) => ({
...prevUser,
[event.target.id]: event.target.value,
}));
};

return (
<div className='max-w-lg mx-auto mt-32'>
<h1 className='text-center text-3xl font-bold'>Sign Up</h1>
<form
onSubmit={formSubmitHandler}
className='flex flex-col gap-3 mt-8'
>
<input
id='username'
onChange={formInputChangeHandler}
type='text'
className='p-3 rounded-lg border'
placeholder='Username'
/>
<input
onChange={formInputChangeHandler}
id='email'
type='text'
className='p-3 rounded-lg border'
placeholder='Email'
/>
<input
onChange={formInputChangeHandler}
id='password'
type='password'
className='p-3 rounded-lg border'
placeholder='Password'
/>
<button
disabled={isPending}
className='p-3 rounded-[32px] text-white uppercase font-bold transition duration-300 shadow hover:shadow-lg hover:opacity-95 bg-teal-600 hover:bg-teal-700'
>
{isPending ? "Submitting..." : `Submit`}
</button>
</form>
<div className='flex gap-2 mt-2'>
<p>Have an account?</p>
<Link to={"/"}>
<span className='text-blue-700'>Sign in</span>
</Link>
</div>
</div>
);
};

export default Signup;

+ 19
- 0
client/src/redux/socket/socketSlice.js Voir le fichier

@@ -0,0 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";

const initialSocketState = {
socketValue: null,
};

const socketSlice = createSlice({
name: "socket",
initialState: initialSocketState,
reducers: {
setNewSocket: (state, action) => {
state.socketValue = action.payload;
},
},
});

export const { setNewSocket } = socketSlice.actions;

export default socketSlice.reducer;

+ 14
- 0
client/src/redux/store.js Voir le fichier

@@ -0,0 +1,14 @@
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./user/userSlice.js";
import socketReducer from "./socket/socketSlice.js";

export const store = configureStore({
reducer: {
user: userReducer,
socket: socketReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: true,
}),
});

+ 22
- 0
client/src/redux/user/userSlice.js Voir le fichier

@@ -0,0 +1,22 @@
import { createSlice } from "@reduxjs/toolkit";

const initialUserState = {
currentUser: null,
};

const userSlice = createSlice({
name: "user",
initialState: initialUserState,
reducers: {
signInSuccess: (state, action) => {
state.currentUser = action.payload;
},
signOutSuccess: (state) => {
state.currentUser = null;
},
},
});

export const { signInSuccess, signOutSuccess } = userSlice.actions;

export default userSlice.reducer;

+ 44
- 0
client/src/utils/http.js Voir le fichier

@@ -0,0 +1,44 @@
import { QueryClient } from "@tanstack/react-query";
import axios from "axios";

export const queryClient = new QueryClient();

export const signUpHelper = async ({ formData }) => {
const response = await axios.post("/api/signup", formData);
return response.data;
};

export const signInHelper = async ({ formData }) => {
const response = await axios.post("/api/signin", formData);
return response.data;
};

export const createWhatsappServer = async () => {
const response = await axios.get("/api/createSession");
return response.statusText;
};

export const getMessages = async () => {
const response = await axios.get("/api");
return response.data;
};

export const allMessages = async () => {
try {
const response = await axios.get("/api/refreshMessages");
return response.data;
} catch (error) {
console.error("Error fetching messages:", error);
throw error;
}
};

export const newTag = async ({ formData }) => {
const response = await axios.post("/api/tag/add", formData);
return response.data;
};

export const delTag = async ({ formData }) => {
const response = await axios.post("/api/tag/del", formData);
return response.data;
};

+ 8
- 0
client/tailwind.config.js Voir le fichier

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

+ 15
- 0
client/vite.config.js Voir le fichier

@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

// https://vitejs.dev/config/
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
secure: false,
},
},
},
plugins: [react()],
});

+ 8
- 0
config/error.js Voir le fichier

@@ -0,0 +1,8 @@
class MyError extends Error {
constructor(message, statusCode) {
super(message);
this.status = statusCode;
}
}

module.exports = MyError;

+ 14
- 0
db/dbConnect.js Voir le fichier

@@ -0,0 +1,14 @@
const mongoose = require("mongoose");

const mongoConnection = () => {
mongoose
.connect(process.env.mongo_url)
.then(() => {
console.log("Mongo DB connected");
})
.catch((err) => {
console.log("Error connecting");
});
};

module.exports = mongoConnection;

+ 88
- 0
helper/jwt_helper.js Voir le fichier

@@ -0,0 +1,88 @@
const JWT = require("jsonwebtoken");
const User = require("../models/user");

exports.signAccessToken = (userId) => {
return new Promise((resolve, reject) => {
const payload = {};
const access_key = process.env.ACCESS_TOKEN_SECRET;
const options = {
audience: userId,
issuer: "NeonFlake",
expiresIn: "10d",
};
JWT.sign(payload, access_key, options, (err, data) => {
if (err) reject({ status: 500, message: err.message });
resolve(data);
});
});
};

exports.verifyAccessToken = (req, res, next) => {
if (!req?.cookies.jwt)
return res
.status(401)
.json({ status: 401, message: "Access Token is required" });
const token = req.cookies.jwt;

JWT.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, payload) => {
if (err) {
if (err.name === "JsonWebTokenError")
throw next({ status: 403, message: "Authentication failed" });
else throw next({ status: 401, message: err.message });
}
req.userId = payload.aud;
User.findById(req.userId)
.then((data) => {
if (data) {
req.data = data;
next();
} else {
throw next({ status: 401, message: "User not found" });
}
})
.catch((err) => {
next({ status: 500, message: err.message });
});
});
};

exports.signRefreshToken = (userId) => {
return new Promise((resolve, reject) => {
const payload = {};
const refresh_key = process.env.REFRESH_TOKEN_SECRET;
const options = {
audience: userId,
issuer: "Apoorv Pandey",
expiresIn: "1d",
};

JWT.sign(payload, refresh_key, options, (err, result) => {
if (err) reject({ status: 500, message: err.message });

resolve(result);
});
});
};

exports.verifyRefreshToken = (req, res, next) => {
if (!req.cookies.jwt)
return next({
status: 401,
message: "Error no refresh token provided",
});
const token = req.cookies.jwt;

JWT.verify(token, process.env.REFRESH_TOKEN_SECRET, (err, payload) => {
if (err && err.name !== "TokenExpiredError")
return next({ status: 401, message: err.message });
else if (err && err.name === "TokenExpiredError") {
const output = JWT.verify(token, process.env.REFRESH_TOKEN_SECRET, {
ignoreExpiration: true,
});
req.userInfo = { token: output.aud, isExpired: true };
} else {
req.userInfo = { token: payload.aud, isExpired: false };
}
next();
});
};

+ 328
- 0
index.js Voir le fichier

@@ -0,0 +1,328 @@
require("dotenv").config();
const { DisconnectReason } = require("@whiskeysockets/baileys");
const makeWASocket = require("@whiskeysockets/baileys").default;
const express = require("express");
const app = express();
const { Server } = require("socket.io");
const http = require("http");
const cors = require("cors");
const qrCode = require("qrcode");
const mongoConnection = require("./db/dbConnect");
const cookieParser = require("cookie-parser");
const bcrypt = require("bcryptjs");
const User = require("./models/user");
const path = require("path");
const { signAccessToken, verifyAccessToken } = require("./helper/jwt_helper");
const {
createSignUpValidation,
createSignInValidation,
} = require("./validation/user.validity");
const MyError = require("./config/error");
const useMongoDBAuthState = require("./auth/mongoAuthState");
const { MongoClient } = require("mongodb");

mongoConnection();
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(express.static(path.join(__dirname, "client", "dist")));

const server = http.createServer(app);
const io = new Server(server, {
cors: "http://localhost:3000",
});

const mongoClient = new MongoClient(process.env.mongodb_url, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoClient.connect().then(() => console.log("mongoClient connected"));

const sock = {};

// Function to set a global variable for a specific ID
function setSock(id, value) {
sock[id] = value;
}

let groupMessageEventListener;
let connectionUpdateListener;

async function generateQRCode(data) {
return new Promise((resolve, reject) => {
qrCode.toDataURL(data, (err, url) => {
if (err) {
reject(err);
} else {
resolve(url);
}
});
});
}

async function connectionLogic(id, socket, isError) {
// const { state, saveCreds } = await useMultiFileAuthState("auth_info_baileys");
const user = await User.findById(id);
console.log(user);

const collection = mongoClient
.db("whatsapp_api")
.collection(`auth_info_${id}`);

const chatsCollection = mongoClient
.db("whatsapp_chats")
.collection(`all_chats_${id}`);

const { state, saveCreds } = await useMongoDBAuthState(collection);

try {
if (isError || user.isLogged === false) {
let sock = makeWASocket({
printQRInTerminal: true,
auth: state,
});
setSock(id, sock);
} else {
sock[id].ev.off("messages.upsert", groupMessageEventListener);
sock[id].ev.off("connection.update", connectionUpdateListener);
}
} catch (error) {
console.error(error);
}

connectionUpdateListener = async (update) => {
const { connection, lastDisconnect } = update;
if (!connection && update?.qr) {
const qrCodeDataURL = await generateQRCode(update.qr);
console.log("QR created");
socket.emit("qrCode", qrCodeDataURL);
}

if (connection === "close") {
const shouldReconnect =
lastDisconnect?.error?.output?.statusCode !==
DisconnectReason.loggedOut;
console.log(
"connection closed due to ",
lastDisconnect.error,
", reconnecting ",
shouldReconnect
);

if (
lastDisconnect.error?.output?.statusCode === DisconnectReason.loggedOut
) {
console.log("User logged out Rereun the connection");
// Handle user logout, perform cleanup, or redirect as needed
await mongoClient.db("whatsapp_api").dropCollection(`auth_info_${id}`);
user.isLogged = false;
await user.save();
socket.emit("user disconnected");
}

// if (lastDisconnect.error?.output?.statusCode === 440) {
// await mongoClient.db("whatsapp_api").dropCollection(`auth_info_${id}`);
// connectionLogic(id, socket);
// }

if (shouldReconnect) {
connectionLogic(id, socket, true);
}
} else if (connection === "open") {
console.log("opened connection");
user.isLogged = true;
await user.save();
socket.emit("user connected");
}
};

groupMessageEventListener = async (messageInfoUpsert) => {
if (
messageInfoUpsert.messages[0].key.remoteJid.split("@")[1] === "g.us" &&
messageInfoUpsert.messages[0].message?.conversation
) {
const user = await User.findById(id);
const tags = user.tags;
const newMessage = messageInfoUpsert.messages[0].message?.conversation;
const isRequiredMessage =
tags.length === 0
? true
: tags.reduce((accum, curr) => {
if (accum) return accum;
else {
const regex = new RegExp(curr, "i");
console.log(accum, curr, newMessage);
console.log(regex.test(newMessage));
return regex.test(newMessage);
}
}, false);
console.log(isRequiredMessage);
const newGrpMessage = {
conversation: messageInfoUpsert.messages[0].message?.conversation,
username: messageInfoUpsert.messages[0].pushName,
phoneNumber: messageInfoUpsert.messages[0].key.participant
.split("@")[0]
.slice(2),
timestamp: new Date(
messageInfoUpsert.messages[0].messageTimestamp * 1000
).toISOString(),
};
// console.log(messageInfoUpsert.messages[0]);
if (isRequiredMessage) {
socket.emit("new message", newGrpMessage);
chatsCollection.insertOne(newGrpMessage);
console.log(newGrpMessage);
}
}
};

sock[id].ev.on("connection.update", connectionUpdateListener);
sock[id].ev.on("messages.upsert", groupMessageEventListener);

sock[id].ev.on("creds.update", saveCreds);

if (user.isLogged === true) {
socket.emit("user connected");
}
}

app.get("/", (req, res) => {
res.status(200).json("Hey this is done");
});

app.post("/api/signup", async (req, res, next) => {
try {
await createSignUpValidation.validateAsync(req.body);
const { username, password, email } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ username, password: hashedPassword, email });
await newUser.save();
const token = await signAccessToken(newUser.id);
const maxAgeInSeconds = 10 * 24 * 60 * 60;
res.cookie("jwt", token, {
httpOnly: true,
maxAge: maxAgeInSeconds,
});
res.status(201).json(newUser);
} catch (error) {
next(error);
}
});

app.get("/api/getMe", verifyAccessToken, async (req, res, next) => {
try {
const data = req.data;
res.status(200).json(data);
} catch (error) {
next(error);
}
});

app.post("/api/signin", async (req, res, next) => {
try {
await createSignInValidation.validateAsync(req.body);
const { email, password } = req.body;
console.log(email, password);
const validUser = await User.checkUser(email, password);

if (!validUser) throw new MyError("Invalid email or password");
const token = await signAccessToken(validUser.id);
const maxAgeInSeconds = 10 * 24 * 60 * 60;
res.cookie("jwt", token, {
httpOnly: true,
maxAge: maxAgeInSeconds,
});

res.status(200).json(validUser);
} catch (error) {
next(error);
}
});

app.get("/api/refreshMessages", verifyAccessToken, async (req, res, next) => {
try {
const data = req.data;
const chatsCollection = mongoClient
.db("whatsapp_chats")
.collection(`all_chats_${data.id}`);
console.log(sock);

const messages = await chatsCollection
.find({})
.sort({ timestamp: -1 })
.limit(20)
.toArray();
res.status(200).json(messages);
} catch (error) {
next(error);
}
});

app.post("/api/tag/add", verifyAccessToken, async (req, res, next) => {
try {
const data = req.data;
const { tag } = req.body;
const userTag = await User.findByIdAndUpdate(
data.id,
{ $addToSet: { tags: tag } },
{ new: true }
);
if (userTag) {
return res.status(201).json(userTag);
} else {
return res.status(404).json("User not found");
}
} catch (error) {
next(error);
}
});

app.post("/api/tag/del", verifyAccessToken, async (req, res, next) => {
try {
const data = req.data;
const { tag: deleteTag } = req.body;
let user = await User.findById(data.id);

let tags = user.tags;
tags = tags.filter((tag) => tag !== deleteTag);
user.tags = tags;
await user.save();

res.status(200).json(user);
} catch (error) {
next(error);
}
});

// app.get("/createServer", verifyAccessToken, (req, res) => {
// const data = req.data;
// connectionLogic(data.id);
// res.status(200).json("Server created");
// });

process.on("exit", async () => {
User.updateMany({}, { isLogged: false });
});

io.on("connection", (socket) => {
console.log("a user connected");
socket.on("whatsapp connect", (id) => {
connectionLogic(id, socket, false);
});
});

app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "client", "dist", "index.html"));
});

app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
status: err.status || 500,
message: err.message || "Internal Server error",
});
});

server.listen(3000, () => {
console.log("listening on *:3000");
});

+ 44
- 0
models/user.js Voir le fichier

@@ -0,0 +1,44 @@
const { Schema, model } = require("mongoose");
const bcrypt = require("bcryptjs");

const userSchema = new Schema({
username: {
type: String,
reuired: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
phoneNumber: {
type: String,
},
isLogged: {
type: Boolean,
default: false,
},
tags: {
type: [String],
default: [],
},
});

userSchema.statics.checkUser = async function (email, password) {
const foundUser = await this.findOne({ email: email });
let isValid = false;
if (foundUser) {
isValid = await bcrypt.compare(password, foundUser.password);
}

return isValid ? foundUser : false;
};

const User = model("User", userSchema);

module.exports = User;

+ 2857
- 0
package-lock.json
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 30
- 0
package.json Voir le fichier

@@ -0,0 +1,30 @@
{
"name": "baileys-whatsapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js",
"start": "node index.js",
"build": "npm install && npm install --prefix client && npm run build --prefix client"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@whiskeysockets/baileys": "github:WhiskeySockets/Baileys",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0",
"mongoose": "^8.0.3",
"nodemon": "^3.0.2",
"qrcode": "^1.5.3",
"qrcode-terminal": "^0.12.0",
"socket.io": "^4.7.2"
}
}

+ 12
- 0
validation/user.validity.js Voir le fichier

@@ -0,0 +1,12 @@
const Joi = require("joi");

exports.createSignUpValidation = Joi.object({
username: Joi.string().required().min(2),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});

exports.createSignInValidation = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});

Chargement…
Annuler
Enregistrer