├── .gitignore
├── LICENSE
├── README.md
├── backend
├── dist
│ └── src
│ │ ├── controllers
│ │ ├── auth.controller.js
│ │ └── message.controller.js
│ │ ├── db
│ │ └── prisma.js
│ │ ├── index.js
│ │ ├── middleware
│ │ └── protectRoute.js
│ │ ├── routes
│ │ ├── auth.route.js
│ │ └── message.route.js
│ │ ├── socket
│ │ └── socket.js
│ │ └── utils
│ │ └── generateToken.js
├── prisma
│ └── schema.prisma
└── src
│ ├── controllers
│ ├── auth.controller.ts
│ └── message.controller.ts
│ ├── db
│ └── prisma.ts
│ ├── index.ts
│ ├── middleware
│ └── protectRoute.ts
│ ├── routes
│ ├── auth.route.ts
│ └── message.route.ts
│ ├── socket
│ └── socket.ts
│ └── utils
│ └── generateToken.ts
├── frontend
├── .eslintrc.cjs
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── bg.jpg
│ └── vite.svg
├── src
│ ├── App.tsx
│ ├── _ui_design
│ │ ├── assets
│ │ │ └── sounds
│ │ │ │ └── notification.mp3
│ │ ├── bg.jpg
│ │ ├── components
│ │ │ ├── GenderCheckbox.tsx
│ │ │ ├── messages
│ │ │ │ ├── Message.tsx
│ │ │ │ ├── MessageContainer.tsx
│ │ │ │ ├── MessageInput.tsx
│ │ │ │ └── Messages.tsx
│ │ │ ├── sidebar
│ │ │ │ ├── Conversation.tsx
│ │ │ │ ├── Conversations.tsx
│ │ │ │ ├── LogoutButton.tsx
│ │ │ │ ├── SearchInput.tsx
│ │ │ │ └── Sidebar.tsx
│ │ │ └── skeletons
│ │ │ │ └── MessageSkeleton.tsx
│ │ ├── dummy_data
│ │ │ └── dummy.ts
│ │ └── pages
│ │ │ ├── Home.tsx
│ │ │ ├── Login.tsx
│ │ │ └── SignUp.tsx
│ ├── assets
│ │ └── sounds
│ │ │ └── notification.mp3
│ ├── components
│ │ ├── GenderCheckbox.tsx
│ │ ├── messages
│ │ │ ├── Message.tsx
│ │ │ ├── MessageContainer.tsx
│ │ │ ├── MessageInput.tsx
│ │ │ └── Messages.tsx
│ │ ├── sidebar
│ │ │ ├── Conversation.tsx
│ │ │ ├── Conversations.tsx
│ │ │ ├── LogoutButton.tsx
│ │ │ ├── SearchInput.tsx
│ │ │ └── Sidebar.tsx
│ │ └── skeletons
│ │ │ └── MessageSkeleton.tsx
│ ├── context
│ │ ├── AuthContext.tsx
│ │ └── SocketContext.tsx
│ ├── dummy_data
│ │ └── dummy.ts
│ ├── hooks
│ │ ├── useChatScroll.ts
│ │ ├── useGetConversations.ts
│ │ ├── useGetMessages.ts
│ │ ├── useListenMessages.ts
│ │ ├── useLogin.tsx
│ │ ├── useLogout.tsx
│ │ ├── useSendMessage.tsx
│ │ └── useSignup.tsx
│ ├── index.css
│ ├── main.tsx
│ ├── pages
│ │ ├── Home.tsx
│ │ ├── Login.tsx
│ │ └── SignUp.tsx
│ ├── types
│ │ └── global.d.ts
│ ├── utils
│ │ ├── emojis.ts
│ │ └── extractTime.ts
│ ├── vite-env.d.ts
│ └── zustand
│ │ └── useConversation.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── package-lock.json
├── package.json
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # Keep environment variables out of version control
3 | .env
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Burak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PERN Stack Project: Build a Real Time Chat App | Postgres, TypeScript, Prisma
2 |
3 | 
4 |
5 | [Video Tutorial on Youtube](https://youtu.be/vL24eiwAG_g)
6 |
7 | Some Features:
8 |
9 | - 🌟 Tech stack: PERN + TypeScript + Socket.io + Prisma + TailwindCSS
10 | - 🎃 Authentication && Authorization with JWT
11 | - 👾 Real-time messaging with Socket.io
12 | - 🚀 Online user status (Socket.io and React Context)
13 | - 👌 Global state management with Zustand
14 | - 🐞 Error handling both on the server and on the client
15 | - 👻 TypeScript Tips and Tricks
16 | - ⭐ At the end Deployment like a pro for FREE!
17 | - ⏳ And much more!
18 |
19 | # Run Locally
20 |
21 | ### Setup .env file
22 |
23 | ```js
24 | DATABASE_URL=...
25 | JWT_SECRET=...
26 | NODE_ENV=...
27 | PORT=...
28 | ```
29 |
30 | ### Install dependencies
31 |
32 | ```shell
33 | npm install
34 | ```
35 |
36 | ### Build the app
37 |
38 | ```shell
39 | npm run build
40 | ```
41 |
42 | ### Start the app
43 |
44 | ```shell
45 | npm start
46 | ```
47 |
48 | ### Like and Subscribe 🌟
49 |
--------------------------------------------------------------------------------
/backend/dist/src/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import prisma from "../db/prisma.js";
2 | import bcryptjs from "bcryptjs";
3 | import generateToken from "../utils/generateToken.js";
4 | export const signup = async (req, res) => {
5 | try {
6 | const { fullName, username, password, confirmPassword, gender } = req.body;
7 | if (!fullName || !username || !password || !confirmPassword || !gender) {
8 | return res.status(400).json({ error: "Please fill in all fields" });
9 | }
10 | if (password !== confirmPassword) {
11 | return res.status(400).json({ error: "Passwords don't match" });
12 | }
13 | const user = await prisma.user.findUnique({ where: { username } });
14 | if (user) {
15 | return res.status(400).json({ error: "Username already exists" });
16 | }
17 | const salt = await bcryptjs.genSalt(10);
18 | const hashedPassword = await bcryptjs.hash(password, salt);
19 | // https://avatar-placeholder.iran.liara.run/
20 | const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`;
21 | const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${username}`;
22 | const newUser = await prisma.user.create({
23 | data: {
24 | fullName,
25 | username,
26 | password: hashedPassword,
27 | gender,
28 | profilePic: gender === "male" ? boyProfilePic : girlProfilePic,
29 | },
30 | });
31 | if (newUser) {
32 | // generate token in a sec
33 | generateToken(newUser.id, res);
34 | res.status(201).json({
35 | id: newUser.id,
36 | fullName: newUser.fullName,
37 | username: newUser.username,
38 | profilePic: newUser.profilePic,
39 | });
40 | }
41 | else {
42 | res.status(400).json({ error: "Invalid user data" });
43 | }
44 | }
45 | catch (error) {
46 | console.log("Error in signup controller", error.message);
47 | res.status(500).json({ error: "Internal Server Error" });
48 | }
49 | };
50 | export const login = async (req, res) => {
51 | try {
52 | const { username, password } = req.body;
53 | const user = await prisma.user.findUnique({ where: { username } });
54 | if (!user) {
55 | return res.status(400).json({ error: "Invalid credentials" });
56 | }
57 | const isPasswordCorrect = await bcryptjs.compare(password, user.password);
58 | if (!isPasswordCorrect) {
59 | return res.status(400).json({ error: "Invalid credentials" });
60 | }
61 | generateToken(user.id, res);
62 | res.status(200).json({
63 | id: user.id,
64 | fullName: user.fullName,
65 | username: user.username,
66 | profilePic: user.profilePic,
67 | });
68 | }
69 | catch (error) {
70 | console.log("Error in login controller", error.message);
71 | res.status(500).json({ error: "Internal Server Error" });
72 | }
73 | };
74 | export const logout = async (req, res) => {
75 | try {
76 | res.cookie("jwt", "", { maxAge: 0 });
77 | res.status(200).json({ message: "Logged out successfully" });
78 | }
79 | catch (error) {
80 | console.log("Error in logout controller", error.message);
81 | res.status(500).json({ error: "Internal Server Error" });
82 | }
83 | };
84 | export const getMe = async (req, res) => {
85 | try {
86 | const user = await prisma.user.findUnique({ where: { id: req.user.id } });
87 | if (!user) {
88 | return res.status(404).json({ error: "User not found" });
89 | }
90 | res.status(200).json({
91 | id: user.id,
92 | fullName: user.fullName,
93 | username: user.username,
94 | profilePic: user.profilePic,
95 | });
96 | }
97 | catch (error) {
98 | console.log("Error in getMe controller", error.message);
99 | res.status(500).json({ error: "Internal Server Error" });
100 | }
101 | };
102 |
--------------------------------------------------------------------------------
/backend/dist/src/controllers/message.controller.js:
--------------------------------------------------------------------------------
1 | import prisma from "../db/prisma.js";
2 | import { getReceiverSocketId, io } from "../socket/socket.js";
3 | export const sendMessage = async (req, res) => {
4 | try {
5 | const { message } = req.body;
6 | const { id: receiverId } = req.params;
7 | const senderId = req.user.id;
8 | let conversation = await prisma.conversation.findFirst({
9 | where: {
10 | participantIds: {
11 | hasEvery: [senderId, receiverId],
12 | },
13 | },
14 | });
15 | // the very first message is being sent, that's why we need to create a new conversation
16 | if (!conversation) {
17 | conversation = await prisma.conversation.create({
18 | data: {
19 | participantIds: {
20 | set: [senderId, receiverId],
21 | },
22 | },
23 | });
24 | }
25 | const newMessage = await prisma.message.create({
26 | data: {
27 | senderId,
28 | body: message,
29 | conversationId: conversation.id,
30 | },
31 | });
32 | if (newMessage) {
33 | conversation = await prisma.conversation.update({
34 | where: {
35 | id: conversation.id,
36 | },
37 | data: {
38 | messages: {
39 | connect: {
40 | id: newMessage.id,
41 | },
42 | },
43 | },
44 | });
45 | }
46 | // Socket io will go here
47 | const receiverSocketId = getReceiverSocketId(receiverId);
48 | if (receiverSocketId) {
49 | io.to(receiverSocketId).emit("newMessage", newMessage);
50 | }
51 | res.status(201).json(newMessage);
52 | }
53 | catch (error) {
54 | console.error("Error in sendMessage: ", error.message);
55 | res.status(500).json({ error: "Internal server error" });
56 | }
57 | };
58 | export const getMessages = async (req, res) => {
59 | try {
60 | const { id: userToChatId } = req.params;
61 | const senderId = req.user.id;
62 | const conversation = await prisma.conversation.findFirst({
63 | where: {
64 | participantIds: {
65 | hasEvery: [senderId, userToChatId],
66 | },
67 | },
68 | include: {
69 | messages: {
70 | orderBy: {
71 | createdAt: "asc",
72 | },
73 | },
74 | },
75 | });
76 | if (!conversation) {
77 | return res.status(200).json([]);
78 | }
79 | res.status(200).json(conversation.messages);
80 | }
81 | catch (error) {
82 | console.error("Error in getMessages: ", error.message);
83 | res.status(500).json({ error: "Internal server error" });
84 | }
85 | };
86 | export const getUsersForSidebar = async (req, res) => {
87 | try {
88 | const authUserId = req.user.id;
89 | const users = await prisma.user.findMany({
90 | where: {
91 | id: {
92 | not: authUserId,
93 | },
94 | },
95 | select: {
96 | id: true,
97 | fullName: true,
98 | profilePic: true,
99 | },
100 | });
101 | res.status(200).json(users);
102 | }
103 | catch (error) {
104 | console.error("Error in getUsersForSidebar: ", error.message);
105 | res.status(500).json({ error: "Internal server error" });
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/backend/dist/src/db/prisma.js:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | const prisma = new PrismaClient();
3 | export default prisma;
4 |
--------------------------------------------------------------------------------
/backend/dist/src/index.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cookieParser from "cookie-parser";
3 | import authRoutes from "./routes/auth.route.js";
4 | import messageRoutes from "./routes/message.route.js";
5 | import dotenv from "dotenv";
6 | import { app, server } from "./socket/socket.js";
7 | dotenv.config();
8 | const PORT = process.env.PORT || 5001;
9 | app.use(cookieParser()); // for parsing cookies
10 | app.use(express.json()); // for parsing application/json
11 | app.use("/api/auth", authRoutes);
12 | app.use("/api/messages", messageRoutes);
13 | server.listen(PORT, () => {
14 | console.log("Server is running on port " + PORT);
15 | });
16 | // Todo: Configure this server for the deployment
17 |
--------------------------------------------------------------------------------
/backend/dist/src/middleware/protectRoute.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import prisma from "../db/prisma.js";
3 | const protectRoute = async (req, res, next) => {
4 | try {
5 | const token = req.cookies.jwt;
6 | if (!token) {
7 | return res.status(401).json({ error: "Unauthorized - No token provided" });
8 | }
9 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
10 | if (!decoded) {
11 | return res.status(401).json({ error: "Unauthorized - Invalid Token" });
12 | }
13 | const user = await prisma.user.findUnique({
14 | where: { id: decoded.userId },
15 | select: { id: true, username: true, fullName: true, profilePic: true },
16 | });
17 | if (!user) {
18 | return res.status(404).json({ error: "User not found" });
19 | }
20 | req.user = user;
21 | next();
22 | }
23 | catch (error) {
24 | console.log("Error in protectRoute middleware", error.message);
25 | res.status(500).json({ error: "Internal Server Error" });
26 | }
27 | };
28 | export default protectRoute;
29 |
--------------------------------------------------------------------------------
/backend/dist/src/routes/auth.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { login, logout, signup, getMe } from "../controllers/auth.controller.js";
3 | import protectRoute from "../middleware/protectRoute.js";
4 | const router = express.Router();
5 | router.get("/me", protectRoute, getMe);
6 | router.post("/signup", signup);
7 | router.post("/login", login);
8 | router.post("/logout", logout);
9 | export default router;
10 |
--------------------------------------------------------------------------------
/backend/dist/src/routes/message.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import protectRoute from "../middleware/protectRoute.js";
3 | import { getMessages, getUsersForSidebar, sendMessage } from "../controllers/message.controller.js";
4 | const router = express.Router();
5 | router.get("/conversations", protectRoute, getUsersForSidebar);
6 | router.get("/:id", protectRoute, getMessages);
7 | router.post("/send/:id", protectRoute, sendMessage);
8 | export default router;
9 |
--------------------------------------------------------------------------------
/backend/dist/src/socket/socket.js:
--------------------------------------------------------------------------------
1 | import { Server } from "socket.io";
2 | import http from "http";
3 | import express from "express";
4 | const app = express();
5 | const server = http.createServer(app);
6 | const io = new Server(server, {
7 | cors: {
8 | origin: ["http://localhost:5173"],
9 | methods: ["GET", "POST"],
10 | },
11 | });
12 | export const getReceiverSocketId = (receiverId) => {
13 | return userSocketMap[receiverId];
14 | };
15 | const userSocketMap = {}; // {userId: socketId}
16 | io.on("connection", (socket) => {
17 | // console.log("a user connected", socket.id);
18 | const userId = socket.handshake.query.userId;
19 | if (userId)
20 | userSocketMap[userId] = socket.id;
21 | // io.emit() is used to send events to all the connected clients
22 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
23 | // socket.on() is used to listen to the events. can be used both on client and server side
24 | socket.on("disconnect", () => {
25 | // console.log("user disconnected", socket.id);
26 | delete userSocketMap[userId];
27 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
28 | });
29 | });
30 | export { app, io, server };
31 |
--------------------------------------------------------------------------------
/backend/dist/src/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | const generateToken = (userId, res) => {
3 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
4 | expiresIn: "15d",
5 | });
6 | res.cookie("jwt", token, {
7 | maxAge: 15 * 24 * 60 * 60 * 1000, // MS,
8 | httpOnly: true, // prevent XSS cross site scripting
9 | sameSite: "strict", // CSRF attack cross-site request forgery
10 | secure: process.env.NODE_ENV !== "development", // HTTPS
11 | });
12 | return token;
13 | };
14 | export default generateToken;
15 |
--------------------------------------------------------------------------------
/backend/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model User {
17 | id String @id @default(cuid())
18 | username String @unique
19 | fullName String
20 | password String
21 | gender Gender
22 | profilePic String
23 | createdAt DateTime @default(now())
24 | updatedAt DateTime @updatedAt
25 | conversationsIds String[]
26 |
27 | conversations Conversation[]
28 | messages Message[]
29 | }
30 |
31 | model Conversation {
32 | id String @id @default(cuid())
33 | createdAt DateTime @default(now())
34 | updatedAt DateTime @updatedAt
35 |
36 | participantIds String[]
37 | participants User[]
38 |
39 | messageIds String[]
40 | messages Message[]
41 | }
42 |
43 | model Message {
44 | id String @id @default(cuid())
45 |
46 | conversationId String
47 | conversation Conversation @relation(fields: [conversationId], references: [id])
48 |
49 | senderId String
50 | sender User @relation(fields: [senderId], references: [id])
51 |
52 | body String
53 | createdAt DateTime @default(now())
54 | updatedAt DateTime @updatedAt
55 | }
56 |
57 | enum Gender {
58 | male
59 | female
60 | }
61 |
--------------------------------------------------------------------------------
/backend/src/controllers/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import prisma from "../db/prisma.js";
3 | import bcryptjs from "bcryptjs";
4 | import generateToken from "../utils/generateToken.js";
5 |
6 | export const signup = async (req: Request, res: Response) => {
7 | try {
8 | const { fullName, username, password, confirmPassword, gender } = req.body;
9 |
10 | if (!fullName || !username || !password || !confirmPassword || !gender) {
11 | return res.status(400).json({ error: "Please fill in all fields" });
12 | }
13 |
14 | if (password !== confirmPassword) {
15 | return res.status(400).json({ error: "Passwords don't match" });
16 | }
17 |
18 | const user = await prisma.user.findUnique({ where: { username } });
19 |
20 | if (user) {
21 | return res.status(400).json({ error: "Username already exists" });
22 | }
23 |
24 | const salt = await bcryptjs.genSalt(10);
25 | const hashedPassword = await bcryptjs.hash(password, salt);
26 |
27 | // https://avatar-placeholder.iran.liara.run/
28 | const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`;
29 | const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${username}`;
30 |
31 | const newUser = await prisma.user.create({
32 | data: {
33 | fullName,
34 | username,
35 | password: hashedPassword,
36 | gender,
37 | profilePic: gender === "male" ? boyProfilePic : girlProfilePic,
38 | },
39 | });
40 |
41 | if (newUser) {
42 | // generate token in a sec
43 | generateToken(newUser.id, res);
44 |
45 | res.status(201).json({
46 | id: newUser.id,
47 | fullName: newUser.fullName,
48 | username: newUser.username,
49 | profilePic: newUser.profilePic,
50 | });
51 | } else {
52 | res.status(400).json({ error: "Invalid user data" });
53 | }
54 | } catch (error: any) {
55 | console.log("Error in signup controller", error.message);
56 | res.status(500).json({ error: "Internal Server Error" });
57 | }
58 | };
59 |
60 | export const login = async (req: Request, res: Response) => {
61 | try {
62 | const { username, password } = req.body;
63 | const user = await prisma.user.findUnique({ where: { username } });
64 |
65 | if (!user) {
66 | return res.status(400).json({ error: "Invalid credentials" });
67 | }
68 |
69 | const isPasswordCorrect = await bcryptjs.compare(password, user.password);
70 |
71 | if (!isPasswordCorrect) {
72 | return res.status(400).json({ error: "Invalid credentials" });
73 | }
74 |
75 | generateToken(user.id, res);
76 |
77 | res.status(200).json({
78 | id: user.id,
79 | fullName: user.fullName,
80 | username: user.username,
81 | profilePic: user.profilePic,
82 | });
83 | } catch (error: any) {
84 | console.log("Error in login controller", error.message);
85 | res.status(500).json({ error: "Internal Server Error" });
86 | }
87 | };
88 | export const logout = async (req: Request, res: Response) => {
89 | try {
90 | res.cookie("jwt", "", { maxAge: 0 });
91 | res.status(200).json({ message: "Logged out successfully" });
92 | } catch (error: any) {
93 | console.log("Error in logout controller", error.message);
94 | res.status(500).json({ error: "Internal Server Error" });
95 | }
96 | };
97 |
98 | export const getMe = async (req: Request, res: Response) => {
99 | try {
100 | const user = await prisma.user.findUnique({ where: { id: req.user.id } });
101 |
102 | if (!user) {
103 | return res.status(404).json({ error: "User not found" });
104 | }
105 |
106 | res.status(200).json({
107 | id: user.id,
108 | fullName: user.fullName,
109 | username: user.username,
110 | profilePic: user.profilePic,
111 | });
112 | } catch (error: any) {
113 | console.log("Error in getMe controller", error.message);
114 | res.status(500).json({ error: "Internal Server Error" });
115 | }
116 | };
117 |
--------------------------------------------------------------------------------
/backend/src/controllers/message.controller.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import prisma from "../db/prisma.js";
3 | import { getReceiverSocketId, io } from "../socket/socket.js";
4 |
5 | export const sendMessage = async (req: Request, res: Response) => {
6 | try {
7 | const { message } = req.body;
8 | const { id: receiverId } = req.params;
9 | const senderId = req.user.id;
10 |
11 | let conversation = await prisma.conversation.findFirst({
12 | where: {
13 | participantIds: {
14 | hasEvery: [senderId, receiverId],
15 | },
16 | },
17 | });
18 |
19 | // the very first message is being sent, that's why we need to create a new conversation
20 | if (!conversation) {
21 | conversation = await prisma.conversation.create({
22 | data: {
23 | participantIds: {
24 | set: [senderId, receiverId],
25 | },
26 | },
27 | });
28 | }
29 |
30 | const newMessage = await prisma.message.create({
31 | data: {
32 | senderId,
33 | body: message,
34 | conversationId: conversation.id,
35 | },
36 | });
37 |
38 | if (newMessage) {
39 | conversation = await prisma.conversation.update({
40 | where: {
41 | id: conversation.id,
42 | },
43 | data: {
44 | messages: {
45 | connect: {
46 | id: newMessage.id,
47 | },
48 | },
49 | },
50 | });
51 | }
52 |
53 | // Socket io will go here
54 | const receiverSocketId = getReceiverSocketId(receiverId);
55 |
56 | if (receiverSocketId) {
57 | io.to(receiverSocketId).emit("newMessage", newMessage);
58 | }
59 |
60 | res.status(201).json(newMessage);
61 | } catch (error: any) {
62 | console.error("Error in sendMessage: ", error.message);
63 | res.status(500).json({ error: "Internal server error" });
64 | }
65 | };
66 |
67 | export const getMessages = async (req: Request, res: Response) => {
68 | try {
69 | const { id: userToChatId } = req.params;
70 | const senderId = req.user.id;
71 |
72 | const conversation = await prisma.conversation.findFirst({
73 | where: {
74 | participantIds: {
75 | hasEvery: [senderId, userToChatId],
76 | },
77 | },
78 | include: {
79 | messages: {
80 | orderBy: {
81 | createdAt: "asc",
82 | },
83 | },
84 | },
85 | });
86 |
87 | if (!conversation) {
88 | return res.status(200).json([]);
89 | }
90 |
91 | res.status(200).json(conversation.messages);
92 | } catch (error: any) {
93 | console.error("Error in getMessages: ", error.message);
94 | res.status(500).json({ error: "Internal server error" });
95 | }
96 | };
97 |
98 | export const getUsersForSidebar = async (req: Request, res: Response) => {
99 | try {
100 | const authUserId = req.user.id;
101 |
102 | const users = await prisma.user.findMany({
103 | where: {
104 | id: {
105 | not: authUserId,
106 | },
107 | },
108 | select: {
109 | id: true,
110 | fullName: true,
111 | profilePic: true,
112 | },
113 | });
114 |
115 | res.status(200).json(users);
116 | } catch (error: any) {
117 | console.error("Error in getUsersForSidebar: ", error.message);
118 | res.status(500).json({ error: "Internal server error" });
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/backend/src/db/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prisma = new PrismaClient();
4 |
5 | export default prisma;
6 |
--------------------------------------------------------------------------------
/backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cookieParser from "cookie-parser";
3 | import path from "path";
4 |
5 | import authRoutes from "./routes/auth.route.js";
6 | import messageRoutes from "./routes/message.route.js";
7 |
8 | import dotenv from "dotenv";
9 | import { app, server } from "./socket/socket.js";
10 | dotenv.config();
11 |
12 | const PORT = process.env.PORT || 5001;
13 | const __dirname = path.resolve();
14 |
15 | app.use(cookieParser()); // for parsing cookies
16 | app.use(express.json()); // for parsing application/json
17 |
18 | app.use("/api/auth", authRoutes);
19 | app.use("/api/messages", messageRoutes);
20 |
21 | if (process.env.NODE_ENV !== "development") {
22 | app.use(express.static(path.join(__dirname, "/frontend/dist")));
23 | app.get("*", (req, res) => {
24 | res.sendFile(path.join(__dirname, "frontend", "dist", "index.html"));
25 | });
26 | }
27 |
28 | server.listen(PORT, () => {
29 | console.log("Server is running on port " + PORT);
30 | });
31 |
--------------------------------------------------------------------------------
/backend/src/middleware/protectRoute.ts:
--------------------------------------------------------------------------------
1 | import jwt, { JwtPayload } from "jsonwebtoken";
2 |
3 | import { Request, Response, NextFunction } from "express";
4 | import prisma from "../db/prisma.js";
5 |
6 | interface DecodedToken extends JwtPayload {
7 | userId: string;
8 | }
9 |
10 | declare global {
11 | namespace Express {
12 | export interface Request {
13 | user: {
14 | id: string;
15 | };
16 | }
17 | }
18 | }
19 |
20 | const protectRoute = async (req: Request, res: Response, next: NextFunction) => {
21 | try {
22 | const token = req.cookies.jwt;
23 |
24 | if (!token) {
25 | return res.status(401).json({ error: "Unauthorized - No token provided" });
26 | }
27 |
28 | const decoded = jwt.verify(token, process.env.JWT_SECRET!) as DecodedToken;
29 |
30 | if (!decoded) {
31 | return res.status(401).json({ error: "Unauthorized - Invalid Token" });
32 | }
33 |
34 | const user = await prisma.user.findUnique({
35 | where: { id: decoded.userId },
36 | select: { id: true, username: true, fullName: true, profilePic: true },
37 | });
38 |
39 | if (!user) {
40 | return res.status(404).json({ error: "User not found" });
41 | }
42 |
43 | req.user = user;
44 |
45 | next();
46 | } catch (error: any) {
47 | console.log("Error in protectRoute middleware", error.message);
48 | res.status(500).json({ error: "Internal Server Error" });
49 | }
50 | };
51 |
52 | export default protectRoute;
53 |
--------------------------------------------------------------------------------
/backend/src/routes/auth.route.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { login, logout, signup, getMe } from "../controllers/auth.controller.js";
3 | import protectRoute from "../middleware/protectRoute.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/me", protectRoute, getMe);
8 | router.post("/signup", signup);
9 | router.post("/login", login);
10 | router.post("/logout", logout);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/backend/src/routes/message.route.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import protectRoute from "../middleware/protectRoute.js";
3 | import { getMessages, getUsersForSidebar, sendMessage } from "../controllers/message.controller.js";
4 | const router = express.Router();
5 |
6 | router.get("/conversations", protectRoute, getUsersForSidebar);
7 | router.get("/:id", protectRoute, getMessages);
8 | router.post("/send/:id", protectRoute, sendMessage);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/backend/src/socket/socket.ts:
--------------------------------------------------------------------------------
1 | import { Server } from "socket.io";
2 | import http from "http";
3 | import express from "express";
4 |
5 | const app = express();
6 |
7 | const server = http.createServer(app);
8 | const io = new Server(server, {
9 | cors: {
10 | origin: ["http://localhost:5173"],
11 | methods: ["GET", "POST"],
12 | },
13 | });
14 |
15 | export const getReceiverSocketId = (receiverId: string) => {
16 | return userSocketMap[receiverId];
17 | };
18 |
19 | const userSocketMap: { [key: string]: string } = {}; // {userId: socketId}
20 |
21 | io.on("connection", (socket) => {
22 | // console.log("a user connected", socket.id);
23 |
24 | const userId = socket.handshake.query.userId as string;
25 |
26 | if (userId) userSocketMap[userId] = socket.id;
27 |
28 | // io.emit() is used to send events to all the connected clients
29 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
30 |
31 | // socket.on() is used to listen to the events. can be used both on client and server side
32 | socket.on("disconnect", () => {
33 | // console.log("user disconnected", socket.id);
34 | delete userSocketMap[userId];
35 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
36 | });
37 | });
38 |
39 | export { app, io, server };
40 |
--------------------------------------------------------------------------------
/backend/src/utils/generateToken.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { Response } from "express";
3 |
4 | const generateToken = (userId: string, res: Response) => {
5 | const token = jwt.sign({ userId }, process.env.JWT_SECRET!, {
6 | expiresIn: "15d",
7 | });
8 |
9 | res.cookie("jwt", token, {
10 | maxAge: 15 * 24 * 60 * 60 * 1000, // MS,
11 | httpOnly: true, // prevent XSS cross site scripting
12 | sameSite: "strict", // CSRF attack cross-site request forgery
13 | secure: process.env.NODE_ENV !== "development", // HTTPS
14 | });
15 |
16 | return token;
17 | };
18 |
19 | export default generateToken;
20 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
5 | ignorePatterns: ["dist", ".eslintrc.cjs"],
6 | parser: "@typescript-eslint/parser",
7 | plugins: ["react-refresh"],
8 | rules: {
9 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
10 | "@typescript-eslint/no-explicit-any": "off",
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | sourceType: 'module',
22 | project: ['./tsconfig.json', './tsconfig.node.json'],
23 | tsconfigRootDir: __dirname,
24 | },
25 | }
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "lucide-react": "^0.379.0",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-hot-toast": "^2.4.1",
17 | "react-router-dom": "^6.23.1",
18 | "socket.io-client": "^4.7.5",
19 | "zustand": "^4.5.2"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^18.2.66",
23 | "@types/react-dom": "^18.2.22",
24 | "@typescript-eslint/eslint-plugin": "^7.2.0",
25 | "@typescript-eslint/parser": "^7.2.0",
26 | "@vitejs/plugin-react": "^4.2.1",
27 | "autoprefixer": "^10.4.19",
28 | "daisyui": "^4.11.1",
29 | "eslint": "^8.57.0",
30 | "eslint-plugin-react-hooks": "^4.6.0",
31 | "eslint-plugin-react-refresh": "^0.4.6",
32 | "postcss": "^8.4.38",
33 | "tailwindcss": "^3.4.3",
34 | "typescript": "^5.2.2",
35 | "vite": "^5.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/pern-chat-app/fd77a7ad29bf121dea4c2bc7a1fb21daffd91913/frontend/public/bg.jpg
--------------------------------------------------------------------------------
/frontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 | import Home from "./pages/Home";
3 | import SignUp from "./pages/SignUp";
4 | import Login from "./pages/Login";
5 | import { useAuthContext } from "./context/AuthContext";
6 | import { Toaster } from "react-hot-toast";
7 |
8 | function App() {
9 | const { authUser, isLoading } = useAuthContext();
10 |
11 | if (isLoading) return null;
12 |
13 | return (
14 |
15 |
16 | : } />
17 | : } />
18 | : } />
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/assets/sounds/notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/pern-chat-app/fd77a7ad29bf121dea4c2bc7a1fb21daffd91913/frontend/src/_ui_design/assets/sounds/notification.mp3
--------------------------------------------------------------------------------
/frontend/src/_ui_design/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/pern-chat-app/fd77a7ad29bf121dea4c2bc7a1fb21daffd91913/frontend/src/_ui_design/bg.jpg
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/GenderCheckbox.tsx:
--------------------------------------------------------------------------------
1 | const GenderCheckbox = () => {
2 | return (
3 |
17 | );
18 | };
19 | export default GenderCheckbox;
20 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/messages/Message.tsx:
--------------------------------------------------------------------------------
1 | const Message = ({ message }: { message?: any }) => {
2 | const fromMe = message.fromMe;
3 | const chatClass = fromMe ? "chat-end" : "chat-start";
4 | const img = fromMe
5 | ? "https://avatar.iran.liara.run/public/boy?username=johndoe"
6 | : "https://avatar.iran.liara.run/public/boy?username=janedoe";
7 |
8 | const bubbleBg = fromMe ? "bg-blue-500" : "";
9 | return (
10 |
11 |
12 |
13 |

14 |
15 |
16 |
{message.body}
17 |
22:59
18 |
19 | );
20 | };
21 | export default Message;
22 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/messages/MessageContainer.tsx:
--------------------------------------------------------------------------------
1 | import MessageInput from "./MessageInput";
2 | import Messages from "./Messages";
3 |
4 | // import { MessageCircle } from "lucide-react";
5 |
6 | const MessageContainer = () => {
7 | return (
8 |
9 | <>
10 | {/* Header */}
11 |
12 | To: John doe
13 |
14 |
15 |
16 |
17 | >
18 |
19 | );
20 | };
21 | export default MessageContainer;
22 |
23 | // const NoChatSelected = () => {
24 | // return (
25 | //
26 | //
27 | //
Welcome 👋 John Doe ❄
28 | //
Select a chat to start messaging
29 | //
30 | //
31 | //
32 | // );
33 | // };
34 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/messages/MessageInput.tsx:
--------------------------------------------------------------------------------
1 | import { Send } from "lucide-react";
2 |
3 | const MessageInput = () => {
4 | return (
5 |
17 | );
18 | };
19 | export default MessageInput;
20 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/messages/Messages.tsx:
--------------------------------------------------------------------------------
1 | import { DUMMY_MESSAGES } from "../../dummy_data/dummy";
2 | import Message from "./Message";
3 |
4 | const Messages = () => {
5 | return (
6 |
7 | {DUMMY_MESSAGES.map((message) => (
8 |
9 | ))}
10 |
11 | );
12 | };
13 | export default Messages;
14 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/sidebar/Conversation.tsx:
--------------------------------------------------------------------------------
1 | const Conversation = ({ conversation }: { conversation: any }) => {
2 | return (
3 | <>
4 |
5 |
6 |
7 |

8 |
9 |
10 |
11 |
12 |
13 |
{conversation.fullName}
14 |
{conversation.emoji}
15 |
16 |
17 |
18 |
19 |
20 | >
21 | );
22 | };
23 | export default Conversation;
24 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/sidebar/Conversations.tsx:
--------------------------------------------------------------------------------
1 | import { DUMMY_CONVERSATIONS } from "../../dummy_data/dummy";
2 | import Conversation from "./Conversation";
3 |
4 | const Conversations = () => {
5 | return (
6 |
7 | {DUMMY_CONVERSATIONS.map((conversation) => (
8 |
9 | ))}
10 |
11 | );
12 | };
13 | export default Conversations;
14 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/sidebar/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | import { LogOut } from "lucide-react";
2 |
3 | const LogoutButton = () => {
4 | const logout = () => {
5 | alert("You are logged out");
6 | };
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 | export default LogoutButton;
15 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/sidebar/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import { Search } from "lucide-react";
2 |
3 | const SearchInput = () => {
4 | return (
5 |
15 | );
16 | };
17 | export default SearchInput;
18 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import Conversations from "./Conversations";
2 | import LogoutButton from "./LogoutButton";
3 | import SearchInput from "./SearchInput";
4 |
5 | const Sidebar = () => {
6 | return (
7 |
13 | );
14 | };
15 | export default Sidebar;
16 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/components/skeletons/MessageSkeleton.tsx:
--------------------------------------------------------------------------------
1 | const MessageSkeleton = () => {
2 | return (
3 | <>
4 |
11 |
17 | >
18 | );
19 | };
20 | export default MessageSkeleton;
21 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/dummy_data/dummy.ts:
--------------------------------------------------------------------------------
1 | export const DUMMY_CONVERSATIONS = [
2 | {
3 | id: 1,
4 | fullName: "John Doe",
5 | profilePic: "https://avatar.iran.liara.run/public/boy?username=johndoe",
6 | emoji: "🎃",
7 | },
8 | {
9 | id: 2,
10 | fullName: "Jane Doe",
11 | profilePic: "https://avatar.iran.liara.run/public/girl?username=janedoe",
12 | emoji: "👻",
13 | },
14 | {
15 | id: 3,
16 | fullName: "Alice",
17 | profilePic: "https://avatar.iran.liara.run/public/girl?username=alice",
18 | emoji: "🦇",
19 | },
20 | {
21 | id: 4,
22 | fullName: "Bob",
23 | profilePic: "https://avatar.iran.liara.run/public/boy?username=bob",
24 | emoji: "🧟♂️",
25 | },
26 | {
27 | id: 5,
28 | fullName: "Charlie",
29 | profilePic: "https://avatar.iran.liara.run/public/girl?username=charlie",
30 | emoji: "🧛",
31 | },
32 | ];
33 |
34 | export const DUMMY_MESSAGES = [
35 | {
36 | id: 1,
37 | fromMe: false,
38 | body: "Hello John!",
39 | },
40 | {
41 | id: 2,
42 | fromMe: true,
43 | body: "Hi! How's it going?",
44 | },
45 | {
46 | id: 3,
47 | fromMe: false,
48 | body: "I'm doing well, thanks for asking. How about you?",
49 | },
50 | {
51 | id: 4,
52 | fromMe: true,
53 | body: "I'm good too. Been busy with work.",
54 | },
55 | {
56 | id: 5,
57 | fromMe: false,
58 | body: "I can imagine. Have you had any time to relax?",
59 | },
60 | {
61 | id: 6,
62 | fromMe: true,
63 | body: "A little bit. I watched a movie last night.",
64 | },
65 | {
66 | id: 7,
67 | fromMe: false,
68 | body: "That's great! Which movie did you watch?",
69 | },
70 | ];
71 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import MessageContainer from "../components/messages/MessageContainer";
2 | import Sidebar from "../components/sidebar/Sidebar";
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 | export default Home;
13 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const Login = () => {
4 | return (
5 |
6 |
7 |
8 | Login
9 | ChatApp
10 |
11 |
12 |
41 |
42 |
43 | );
44 | };
45 | export default Login;
46 |
--------------------------------------------------------------------------------
/frontend/src/_ui_design/pages/SignUp.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import GenderCheckbox from "../components/GenderCheckbox";
3 |
4 | const SignUp = () => {
5 | return (
6 |
7 |
8 |
9 | Sign Up ChatApp
10 |
11 |
12 |
62 |
63 |
64 | );
65 | };
66 | export default SignUp;
67 |
--------------------------------------------------------------------------------
/frontend/src/assets/sounds/notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/pern-chat-app/fd77a7ad29bf121dea4c2bc7a1fb21daffd91913/frontend/src/assets/sounds/notification.mp3
--------------------------------------------------------------------------------
/frontend/src/components/GenderCheckbox.tsx:
--------------------------------------------------------------------------------
1 | const GenderCheckbox = ({
2 | selectedGender,
3 | onCheckboxChange,
4 | }: {
5 | selectedGender: string;
6 | onCheckboxChange: (gender: "male" | "female") => void;
7 | }) => {
8 | return (
9 |
33 | );
34 | };
35 | export default GenderCheckbox;
36 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/Message.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthContext } from "../../context/AuthContext";
2 | import { extractTime } from "../../utils/extractTime";
3 | import useConversation, { MessageType } from "../../zustand/useConversation";
4 |
5 | const Message = ({ message }: { message: MessageType }) => {
6 | const { authUser } = useAuthContext();
7 | const { selectedConversation } = useConversation();
8 |
9 | const fromMe = message?.senderId === authUser?.id;
10 | const img = fromMe ? authUser?.profilePic : selectedConversation?.profilePic;
11 | const chatClass = fromMe ? "chat-end" : "chat-start";
12 |
13 | const bubbleBg = fromMe ? "bg-blue-500" : "";
14 | const shakeClass = message.shouldShake ? "shake" : "";
15 |
16 | return (
17 |
18 |
19 |
20 |

21 |
22 |
23 |
{message.body}
24 |
25 | {extractTime(message.createdAt)}
26 |
27 |
28 | );
29 | };
30 | export default Message;
31 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/MessageContainer.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthContext } from "../../context/AuthContext";
2 | import useConversation from "../../zustand/useConversation";
3 | import MessageInput from "./MessageInput";
4 | import Messages from "./Messages";
5 |
6 | import { MessageCircle } from "lucide-react";
7 |
8 | const MessageContainer = () => {
9 | const { selectedConversation } = useConversation();
10 |
11 | return (
12 |
13 | {!selectedConversation ? (
14 |
15 | ) : (
16 | <>
17 | {/* Header */}
18 |
19 | To:{" "}
20 | {selectedConversation.fullName}
21 |
22 |
23 |
24 |
25 | >
26 | )}
27 |
28 | );
29 | };
30 | export default MessageContainer;
31 |
32 | const NoChatSelected = () => {
33 | const { authUser } = useAuthContext();
34 | return (
35 |
36 |
37 |
Welcome 👋 {authUser?.fullName} ❄
38 |
Select a chat to start messaging
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/MessageInput.tsx:
--------------------------------------------------------------------------------
1 | import { Send } from "lucide-react";
2 | import { useState } from "react";
3 | import useSendMessage from "../../hooks/useSendMessage";
4 |
5 | const MessageInput = () => {
6 | const [message, setMessage] = useState("");
7 |
8 | const { loading, sendMessage } = useSendMessage();
9 |
10 | const handleSubmit = async (e: React.FormEvent) => {
11 | e.preventDefault();
12 | if (!message.trim()) return;
13 | await sendMessage(message);
14 | setMessage("");
15 | };
16 | return (
17 |
31 | );
32 | };
33 | export default MessageInput;
34 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/Messages.tsx:
--------------------------------------------------------------------------------
1 | import useChatScroll from "../../hooks/useChatScroll";
2 | import useGetMessages from "../../hooks/useGetMessages";
3 | import useListenMessages from "../../hooks/useListenMessages";
4 | import MessageSkeleton from "../skeletons/MessageSkeleton";
5 | import Message from "./Message";
6 |
7 | const Messages = () => {
8 | const { loading, messages } = useGetMessages();
9 | useListenMessages();
10 |
11 | const ref = useChatScroll(messages) as React.MutableRefObject;
12 |
13 | return (
14 |
15 | {loading && [...Array(3)].map((_, idx) =>
)}
16 |
17 | {!loading && messages.map((message) =>
)}
18 |
19 | {!loading && messages.length === 0 && (
20 |
Send a message to start the conversation
21 | )}
22 |
23 | );
24 | };
25 | export default Messages;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Conversation.tsx:
--------------------------------------------------------------------------------
1 | import { useSocketContext } from "../../context/SocketContext";
2 | import useConversation from "../../zustand/useConversation";
3 |
4 | const Conversation = ({ conversation, emoji }: { conversation: ConversationType; emoji: string }) => {
5 | const { setSelectedConversation, selectedConversation } = useConversation();
6 | const isSelected = selectedConversation?.id === conversation.id;
7 |
8 | const { onlineUsers } = useSocketContext();
9 |
10 | const isOnline = onlineUsers.includes(conversation.id);
11 |
12 | return (
13 | <>
14 | setSelectedConversation(conversation)}
18 | >
19 |
20 |
21 |

22 |
23 |
24 |
25 |
26 |
27 |
{conversation.fullName}
28 |
{emoji}
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | };
37 | export default Conversation;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Conversations.tsx:
--------------------------------------------------------------------------------
1 | import useGetConversations from "../../hooks/useGetConversations";
2 | import { getRandomEmoji } from "../../utils/emojis";
3 | import Conversation from "./Conversation";
4 |
5 | const Conversations = () => {
6 | const { conversations, loading } = useGetConversations();
7 | return (
8 |
9 | {conversations.map((conversation) => (
10 |
11 | ))}
12 | {loading ? : null}
13 |
14 | );
15 | };
16 | export default Conversations;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | import { LogOut } from "lucide-react";
2 | import useLogout from "../../hooks/useLogout";
3 |
4 | const LogoutButton = () => {
5 | const { logout } = useLogout();
6 |
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 | export default LogoutButton;
14 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import { Search } from "lucide-react";
2 | import { useState } from "react";
3 | import toast from "react-hot-toast";
4 | import useConversation from "../../zustand/useConversation";
5 | import useGetConversations from "../../hooks/useGetConversations";
6 |
7 | const SearchInput = () => {
8 | const [search, setSearch] = useState("");
9 | const { setSelectedConversation } = useConversation();
10 | const { conversations } = useGetConversations();
11 |
12 | const handleSubmit = (e: React.FormEvent) => {
13 | e.preventDefault();
14 | if (!search) return;
15 | if (search.length < 3) {
16 | return toast.error("Search term must be at least 3 characters long");
17 | }
18 |
19 | const conversation = conversations.find((c: ConversationType) =>
20 | c.fullName.toLowerCase().includes(search.toLowerCase())
21 | );
22 |
23 | if (conversation) {
24 | setSelectedConversation(conversation);
25 | setSearch("");
26 | } else toast.error("No such user found!");
27 | };
28 |
29 | return (
30 |
42 | );
43 | };
44 | export default SearchInput;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import Conversations from "./Conversations";
2 | import LogoutButton from "./LogoutButton";
3 | import SearchInput from "./SearchInput";
4 |
5 | const Sidebar = () => {
6 | return (
7 |
13 | );
14 | };
15 | export default Sidebar;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/skeletons/MessageSkeleton.tsx:
--------------------------------------------------------------------------------
1 | const MessageSkeleton = () => {
2 | return (
3 | <>
4 |
11 |
17 | >
18 | );
19 | };
20 | export default MessageSkeleton;
21 |
--------------------------------------------------------------------------------
/frontend/src/context/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, ReactNode, SetStateAction, createContext, useContext, useEffect, useState } from "react";
2 | import toast from "react-hot-toast";
3 |
4 | type AuthUserType = {
5 | id: string;
6 | fullName: string;
7 | email: string;
8 | profilePic: string;
9 | gender: string;
10 | };
11 |
12 | const AuthContext = createContext<{
13 | authUser: AuthUserType | null;
14 | setAuthUser: Dispatch>;
15 | isLoading: boolean;
16 | }>({
17 | authUser: null,
18 | setAuthUser: () => {},
19 | isLoading: true,
20 | });
21 |
22 | // eslint-disable-next-line react-refresh/only-export-components
23 | export const useAuthContext = () => {
24 | return useContext(AuthContext);
25 | };
26 |
27 | export const AuthContextProvider = ({ children }: { children: ReactNode }) => {
28 | const [authUser, setAuthUser] = useState(null);
29 | const [isLoading, setIsLoading] = useState(true);
30 |
31 | // logic will go here
32 | useEffect(() => {
33 | const fetchAuthUser = async () => {
34 | try {
35 | const res = await fetch("/api/auth/me");
36 | const data = await res.json();
37 | if (!res.ok) {
38 | throw new Error(data.error);
39 | }
40 | setAuthUser(data);
41 | } catch (error: any) {
42 | console.error(error);
43 | toast.error(error.message);
44 | } finally {
45 | setIsLoading(false);
46 | }
47 | };
48 |
49 | fetchAuthUser();
50 | }, []);
51 |
52 | return (
53 |
60 | {children}
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/frontend/src/context/SocketContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect, useContext, ReactNode, useRef } from "react";
2 | import { useAuthContext } from "./AuthContext";
3 | import io, { Socket } from "socket.io-client";
4 |
5 | interface ISocketContext {
6 | socket: Socket | null;
7 | onlineUsers: string[];
8 | }
9 |
10 | const SocketContext = createContext(undefined);
11 |
12 | export const useSocketContext = (): ISocketContext => {
13 | const context = useContext(SocketContext);
14 | if (context === undefined) {
15 | throw new Error("useSocketContext must be used within a SocketContextProvider");
16 | }
17 | return context;
18 | };
19 |
20 | const socketURL = import.meta.env.MODE === "development" ? "http://localhost:5001" : "/";
21 |
22 | const SocketContextProvider = ({ children }: { children: ReactNode }) => {
23 | const socketRef = useRef(null);
24 |
25 | const [onlineUsers, setOnlineUsers] = useState([]);
26 | const { authUser, isLoading } = useAuthContext();
27 |
28 | useEffect(() => {
29 | if (authUser && !isLoading) {
30 | const socket = io(socketURL, {
31 | query: {
32 | userId: authUser.id,
33 | },
34 | });
35 | socketRef.current = socket;
36 |
37 | socket.on("getOnlineUsers", (users: string[]) => {
38 | setOnlineUsers(users);
39 | });
40 |
41 | return () => {
42 | socket.close();
43 | socketRef.current = null;
44 | };
45 | } else if (!authUser && !isLoading) {
46 | if (socketRef.current) {
47 | socketRef.current.close();
48 | socketRef.current = null;
49 | }
50 | }
51 | }, [authUser, isLoading]);
52 |
53 | return (
54 | {children}
55 | );
56 | };
57 |
58 | export default SocketContextProvider;
59 |
--------------------------------------------------------------------------------
/frontend/src/dummy_data/dummy.ts:
--------------------------------------------------------------------------------
1 | export const DUMMY_CONVERSATIONS = [
2 | {
3 | id: 1,
4 | fullName: "John Doe",
5 | profilePic: "https://avatar.iran.liara.run/public/boy?username=johndoe",
6 | emoji: "🎃",
7 | },
8 | {
9 | id: 2,
10 | fullName: "Jane Doe",
11 | profilePic: "https://avatar.iran.liara.run/public/girl?username=janedoe",
12 | emoji: "👻",
13 | },
14 | {
15 | id: 3,
16 | fullName: "Alice",
17 | profilePic: "https://avatar.iran.liara.run/public/girl?username=alice",
18 | emoji: "🦇",
19 | },
20 | {
21 | id: 4,
22 | fullName: "Bob",
23 | profilePic: "https://avatar.iran.liara.run/public/boy?username=bob",
24 | emoji: "🧟♂️",
25 | },
26 | {
27 | id: 5,
28 | fullName: "Charlie",
29 | profilePic: "https://avatar.iran.liara.run/public/girl?username=charlie",
30 | emoji: "🧛",
31 | },
32 | ];
33 |
34 | export const DUMMY_MESSAGES = [
35 | {
36 | id: 1,
37 | fromMe: false,
38 | body: "Hello John!",
39 | },
40 | {
41 | id: 2,
42 | fromMe: true,
43 | body: "Hi! How's it going?",
44 | },
45 | {
46 | id: 3,
47 | fromMe: false,
48 | body: "I'm doing well, thanks for asking. How about you?",
49 | },
50 | {
51 | id: 4,
52 | fromMe: true,
53 | body: "I'm good too. Been busy with work.",
54 | },
55 | {
56 | id: 5,
57 | fromMe: false,
58 | body: "I can imagine. Have you had any time to relax?",
59 | },
60 | {
61 | id: 6,
62 | fromMe: true,
63 | body: "A little bit. I watched a movie last night.",
64 | },
65 | {
66 | id: 7,
67 | fromMe: false,
68 | body: "That's great! Which movie did you watch?",
69 | },
70 | ];
71 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useChatScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | function useChatScroll(dep: any) {
4 | const ref = useRef();
5 |
6 | useEffect(() => {
7 | setTimeout(() => {
8 | if (ref.current) {
9 | ref.current.scrollTop = ref.current.scrollHeight;
10 | }
11 | }, 100);
12 | }, [dep]);
13 |
14 | return ref;
15 | }
16 |
17 | export default useChatScroll;
18 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useGetConversations.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import toast from "react-hot-toast";
3 |
4 | const useGetConversations = () => {
5 | const [loading, setLoading] = useState(false);
6 | const [conversations, setConversations] = useState([]);
7 |
8 | useEffect(() => {
9 | const getConversations = async () => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch("/api/messages/conversations");
13 | const data = await res.json();
14 | if (data.error) {
15 | throw new Error(data.error);
16 | }
17 | setConversations(data);
18 | } catch (error: any) {
19 | toast.error(error.message);
20 | } finally {
21 | setLoading(false);
22 | }
23 | };
24 |
25 | getConversations();
26 | }, []);
27 |
28 | return { loading, conversations };
29 | };
30 | export default useGetConversations;
31 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useGetMessages.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import useConversation from "../zustand/useConversation";
3 | import toast from "react-hot-toast";
4 |
5 | const useGetMessages = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { messages, setMessages, selectedConversation } = useConversation();
8 |
9 | useEffect(() => {
10 | const getMessages = async () => {
11 | if (!selectedConversation) return;
12 | setLoading(true);
13 | setMessages([]);
14 | try {
15 | const res = await fetch(`/api/messages/${selectedConversation.id}`);
16 | const data = await res.json();
17 | if (!res.ok) throw new Error(data.error || "An error occurred");
18 | setMessages(data);
19 | } catch (error: any) {
20 | toast.error(error.message);
21 | } finally {
22 | setLoading(false);
23 | }
24 | };
25 |
26 | getMessages();
27 | }, [selectedConversation, setMessages]);
28 |
29 | return { messages, loading };
30 | };
31 | export default useGetMessages;
32 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useListenMessages.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | import { useSocketContext } from "../context/SocketContext";
4 | import useConversation from "../zustand/useConversation";
5 |
6 | import notificationSound from "../assets/sounds/notification.mp3";
7 |
8 | const useListenMessages = () => {
9 | const { socket } = useSocketContext();
10 | const { messages, setMessages } = useConversation();
11 |
12 | useEffect(() => {
13 | socket?.on("newMessage", (newMessage) => {
14 | newMessage.shouldShake = true;
15 | const sound = new Audio(notificationSound);
16 | sound.play();
17 | setMessages([...messages, newMessage]);
18 | });
19 |
20 | return () => {
21 | socket?.off("newMessage");
22 | };
23 | }, [socket, messages, setMessages]);
24 | };
25 | export default useListenMessages;
26 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLogin.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAuthContext } from "../context/AuthContext";
3 | import toast from "react-hot-toast";
4 |
5 | const useLogin = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { setAuthUser } = useAuthContext();
8 |
9 | const login = async (username: string, password: string) => {
10 | try {
11 | setLoading(true);
12 | const res = await fetch("/api/auth/login", {
13 | method: "POST",
14 | headers: { "Content-Type": "application/json" },
15 | body: JSON.stringify({ username, password }),
16 | });
17 |
18 | const data = await res.json();
19 |
20 | if (!res.ok) throw new Error(data.error);
21 | setAuthUser(data);
22 | } catch (error: any) {
23 | toast.error(error.message);
24 | } finally {
25 | setLoading(false);
26 | }
27 | };
28 |
29 | return { loading, login };
30 | };
31 | export default useLogin;
32 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLogout.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAuthContext } from "../context/AuthContext";
3 | import toast from "react-hot-toast";
4 |
5 | const useLogout = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { setAuthUser } = useAuthContext();
8 |
9 | const logout = async () => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch("/api/auth/logout", {
13 | method: "POST",
14 | });
15 | const data = await res.json();
16 | if (!res.ok) {
17 | throw new Error(data.error);
18 | }
19 |
20 | setAuthUser(null);
21 | } catch (error: any) {
22 | console.error(error.message);
23 | toast.error(error.message);
24 | } finally {
25 | setLoading(false);
26 | }
27 | };
28 |
29 | return { loading, logout };
30 | };
31 | export default useLogout;
32 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useSendMessage.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import useConversation from "../zustand/useConversation";
3 | import toast from "react-hot-toast";
4 |
5 | const useSendMessage = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { messages, setMessages, selectedConversation } = useConversation();
8 |
9 | const sendMessage = async (message: string) => {
10 | if (!selectedConversation) return;
11 | setLoading(true);
12 | try {
13 | const res = await fetch(`/api/messages/send/${selectedConversation.id}`, {
14 | method: "POST",
15 | headers: {
16 | "Content-Type": "application/json",
17 | },
18 | body: JSON.stringify({ message }),
19 | });
20 | const data = await res.json();
21 | if (data.error) throw new Error(data.error);
22 |
23 | setMessages([...messages, data]);
24 | } catch (error: any) {
25 | toast.error(error.message);
26 | } finally {
27 | setLoading(false);
28 | }
29 | };
30 |
31 | return { sendMessage, loading };
32 | };
33 | export default useSendMessage;
34 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useSignup.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAuthContext } from "../context/AuthContext";
3 | import toast from "react-hot-toast";
4 |
5 | type SignupInputs = {
6 | fullName: string;
7 | username: string;
8 | password: string;
9 | confirmPassword: string;
10 | gender: string;
11 | };
12 |
13 | const useSignup = () => {
14 | const [loading, setLoading] = useState(false);
15 | const { setAuthUser } = useAuthContext();
16 |
17 | const signup = async (inputs: SignupInputs) => {
18 | try {
19 | setLoading(true);
20 | const res = await fetch("/api/auth/signup", {
21 | method: "POST",
22 | headers: {
23 | "Content-Type": "application/json",
24 | },
25 | body: JSON.stringify(inputs),
26 | });
27 | const data = await res.json();
28 |
29 | if (!res.ok) throw new Error(data.error);
30 | setAuthUser(data);
31 | } catch (error: any) {
32 | console.error(error.message);
33 | toast.error(error.message);
34 | } finally {
35 | setLoading(false);
36 | }
37 | };
38 |
39 | return { loading, signup };
40 | };
41 | export default useSignup;
42 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background-image: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url("/bg.jpg");
7 | background-size: cover;
8 | background-repeat: no-repeat;
9 | background-position: center;
10 | }
11 |
12 | .label-text {
13 | color: white;
14 | }
15 |
16 | /* dark mode looking scrollbar */
17 | ::-webkit-scrollbar {
18 | width: 8px;
19 | }
20 |
21 | ::-webkit-scrollbar-track {
22 | background: #555;
23 | }
24 |
25 | ::-webkit-scrollbar-thumb {
26 | background: #121212;
27 | border-radius: 5px;
28 | }
29 |
30 | ::-webkit-scrollbar-thumb:hover {
31 | background: #242424;
32 | }
33 |
34 | .shake {
35 | animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.2s both;
36 | transform: translate3d(0, 0, 0);
37 | backface-visibility: hidden;
38 | perspective: 1000px;
39 | }
40 |
41 | @keyframes shake {
42 | 10%,
43 | 90% {
44 | transform: translate3d(-1px, 0, 0);
45 | }
46 |
47 | 20%,
48 | 80% {
49 | transform: translate3d(2px, 0, 0);
50 | }
51 |
52 | 30%,
53 | 50%,
54 | 70% {
55 | transform: translate3d(-4px, 0, 0);
56 | }
57 |
58 | 40%,
59 | 60% {
60 | transform: translate3d(4px, 0, 0);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App.tsx";
3 | import "./index.css";
4 | import { BrowserRouter } from "react-router-dom";
5 | import { AuthContextProvider } from "./context/AuthContext.tsx";
6 | import SocketContextProvider from "./context/SocketContext.tsx";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")!).render(
9 | //
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | //
18 | );
19 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import MessageContainer from "../components/messages/MessageContainer";
2 | import Sidebar from "../components/sidebar/Sidebar";
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 | export default Home;
13 |
--------------------------------------------------------------------------------
/frontend/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import useLogin from "../hooks/useLogin";
4 |
5 | const Login = () => {
6 | const [inputs, setInputs] = useState({
7 | username: "",
8 | password: "",
9 | });
10 |
11 | const { loading, login } = useLogin();
12 |
13 | const handleSubmitForm = (e: React.FormEvent) => {
14 | e.preventDefault();
15 | login(inputs.username, inputs.password);
16 | };
17 |
18 | return (
19 |
20 |
21 |
22 | Login
23 | ChatApp
24 |
25 |
26 |
65 |
66 |
67 | );
68 | };
69 | export default Login;
70 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignUp.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import GenderCheckbox from "../components/GenderCheckbox";
3 | import { useState } from "react";
4 | import useSignup from "../hooks/useSignup";
5 |
6 | const SignUp = () => {
7 | const [inputs, setInputs] = useState({
8 | fullName: "",
9 | username: "",
10 | password: "",
11 | confirmPassword: "",
12 | gender: "",
13 | });
14 | const { loading, signup } = useSignup();
15 |
16 | const handleCheckboxChange = (gender: "male" | "female") => {
17 | setInputs({ ...inputs, gender });
18 | };
19 |
20 | const handleSubmitForm = (e: React.FormEvent) => {
21 | e.preventDefault();
22 | signup(inputs);
23 | };
24 |
25 | return (
26 |
27 |
28 |
29 | Sign Up ChatApp
30 |
31 |
32 |
100 |
101 |
102 | );
103 | };
104 | export default SignUp;
105 |
--------------------------------------------------------------------------------
/frontend/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | // global.d.ts is a special file. Types added here can be used globally in the project without importing them.
2 |
3 | type ConversationType = {
4 | id: string;
5 | fullName: string;
6 | profilePic: string;
7 | };
8 |
--------------------------------------------------------------------------------
/frontend/src/utils/emojis.ts:
--------------------------------------------------------------------------------
1 | export const funEmojis = [
2 | "👾",
3 | "⭐",
4 | "🌟",
5 | "🎉",
6 | "🎊",
7 | "🎈",
8 | "🎁",
9 | "🎂",
10 | "🎄",
11 | "🎃",
12 | "🎗",
13 | "🎟",
14 | "🎫",
15 | "🎖",
16 | "🏆",
17 | "🏅",
18 | "🥇",
19 | "🥈",
20 | "🥉",
21 | "⚽",
22 | "🏀",
23 | "🏈",
24 | "⚾",
25 | "🎾",
26 | "🏐",
27 | "🏉",
28 | "🎱",
29 | "🏓",
30 | "🏸",
31 | "🥅",
32 | "🏒",
33 | "🏑",
34 | "🏏",
35 | "⛳",
36 | "🏹",
37 | "🎣",
38 | "🥊",
39 | "🥋",
40 | "🎽",
41 | "⛸",
42 | "🥌",
43 | "🛷",
44 | "🎿",
45 | "⛷",
46 | "🏂",
47 | "🏋️",
48 | "🤼",
49 | "🤸",
50 | "🤺",
51 | "⛹️",
52 | "🤾",
53 | "🏌️",
54 | "🏇",
55 | "🧘",
56 | ];
57 |
58 | export const getRandomEmoji = () => {
59 | return funEmojis[Math.floor(Math.random() * funEmojis.length)];
60 | };
61 |
--------------------------------------------------------------------------------
/frontend/src/utils/extractTime.ts:
--------------------------------------------------------------------------------
1 | export function extractTime(dateString: string) {
2 | const date = new Date(dateString);
3 | const hours = padZero(date.getHours());
4 | const minutes = padZero(date.getMinutes());
5 | return `${hours}:${minutes}`;
6 | }
7 |
8 | // Helper function to pad single-digit numbers with a leading zero
9 | function padZero(number: number) {
10 | return number.toString().padStart(2, "0");
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/zustand/useConversation.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | // export type ConversationType = {
4 | // id: string;
5 | // fullName: string;
6 | // profilePic: string;
7 | // };
8 |
9 | export type MessageType = {
10 | id: string;
11 | body: string;
12 | senderId: string;
13 | createdAt: string;
14 | shouldShake?: boolean;
15 | };
16 |
17 | interface ConversationState {
18 | selectedConversation: ConversationType | null;
19 | messages: MessageType[];
20 | setSelectedConversation: (conversation: ConversationType | null) => void;
21 | setMessages: (messages: MessageType[]) => void;
22 | }
23 |
24 | const useConversation = create((set) => ({
25 | selectedConversation: null,
26 | setSelectedConversation: (conversation) => set({ selectedConversation: conversation }),
27 | messages: [],
28 | setMessages: (messages) => set({ messages: messages }),
29 | }));
30 |
31 | export default useConversation;
32 |
33 | // JS VERSION
34 | // import { create } from "zustand";
35 |
36 | // const useConversation = create((set) => ({
37 | // selectedConversation: null,
38 | // setSelectedConversation: (selectedConversation) => set({ selectedConversation }),
39 | // messages: [],
40 | // setMessages: (messages) => set({ messages }),
41 | // }));
42 |
43 | // export default useConversation;
44 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import daisyui from "daisyui";
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [daisyui],
10 | };
11 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | "/api": {
10 | target: "http://localhost:5001",
11 | },
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pern-chat-app",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "pern-chat-app",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@prisma/client": "^5.14.0",
13 | "bcryptjs": "^2.4.3",
14 | "cookie-parser": "^1.4.6",
15 | "dotenv": "^16.4.5",
16 | "express": "^4.19.2",
17 | "jsonwebtoken": "^9.0.2",
18 | "prisma": "^5.14.0",
19 | "socket.io": "^4.7.5"
20 | },
21 | "devDependencies": {
22 | "@types/bcryptjs": "^2.4.6",
23 | "@types/cookie-parser": "^1.4.7",
24 | "@types/express": "^4.17.21",
25 | "@types/jsonwebtoken": "^9.0.6",
26 | "nodemon": "^3.1.0",
27 | "ts-node": "^10.9.2",
28 | "typescript": "^5.4.5"
29 | }
30 | },
31 | "node_modules/@cspotcode/source-map-support": {
32 | "version": "0.8.1",
33 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
34 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
35 | "dev": true,
36 | "dependencies": {
37 | "@jridgewell/trace-mapping": "0.3.9"
38 | },
39 | "engines": {
40 | "node": ">=12"
41 | }
42 | },
43 | "node_modules/@jridgewell/resolve-uri": {
44 | "version": "3.1.2",
45 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
46 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
47 | "dev": true,
48 | "engines": {
49 | "node": ">=6.0.0"
50 | }
51 | },
52 | "node_modules/@jridgewell/sourcemap-codec": {
53 | "version": "1.4.15",
54 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
55 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
56 | "dev": true
57 | },
58 | "node_modules/@jridgewell/trace-mapping": {
59 | "version": "0.3.9",
60 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
61 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
62 | "dev": true,
63 | "dependencies": {
64 | "@jridgewell/resolve-uri": "^3.0.3",
65 | "@jridgewell/sourcemap-codec": "^1.4.10"
66 | }
67 | },
68 | "node_modules/@prisma/client": {
69 | "version": "5.14.0",
70 | "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz",
71 | "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==",
72 | "hasInstallScript": true,
73 | "engines": {
74 | "node": ">=16.13"
75 | },
76 | "peerDependencies": {
77 | "prisma": "*"
78 | },
79 | "peerDependenciesMeta": {
80 | "prisma": {
81 | "optional": true
82 | }
83 | }
84 | },
85 | "node_modules/@prisma/debug": {
86 | "version": "5.14.0",
87 | "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz",
88 | "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w=="
89 | },
90 | "node_modules/@prisma/engines": {
91 | "version": "5.14.0",
92 | "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz",
93 | "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==",
94 | "hasInstallScript": true,
95 | "dependencies": {
96 | "@prisma/debug": "5.14.0",
97 | "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48",
98 | "@prisma/fetch-engine": "5.14.0",
99 | "@prisma/get-platform": "5.14.0"
100 | }
101 | },
102 | "node_modules/@prisma/engines-version": {
103 | "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48",
104 | "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz",
105 | "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA=="
106 | },
107 | "node_modules/@prisma/fetch-engine": {
108 | "version": "5.14.0",
109 | "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz",
110 | "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==",
111 | "dependencies": {
112 | "@prisma/debug": "5.14.0",
113 | "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48",
114 | "@prisma/get-platform": "5.14.0"
115 | }
116 | },
117 | "node_modules/@prisma/get-platform": {
118 | "version": "5.14.0",
119 | "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz",
120 | "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==",
121 | "dependencies": {
122 | "@prisma/debug": "5.14.0"
123 | }
124 | },
125 | "node_modules/@socket.io/component-emitter": {
126 | "version": "3.1.2",
127 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
128 | "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
129 | },
130 | "node_modules/@tsconfig/node10": {
131 | "version": "1.0.11",
132 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
133 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
134 | "dev": true
135 | },
136 | "node_modules/@tsconfig/node12": {
137 | "version": "1.0.11",
138 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
139 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
140 | "dev": true
141 | },
142 | "node_modules/@tsconfig/node14": {
143 | "version": "1.0.3",
144 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
145 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
146 | "dev": true
147 | },
148 | "node_modules/@tsconfig/node16": {
149 | "version": "1.0.4",
150 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
151 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
152 | "dev": true
153 | },
154 | "node_modules/@types/bcryptjs": {
155 | "version": "2.4.6",
156 | "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
157 | "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
158 | "dev": true
159 | },
160 | "node_modules/@types/body-parser": {
161 | "version": "1.19.5",
162 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
163 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
164 | "dev": true,
165 | "dependencies": {
166 | "@types/connect": "*",
167 | "@types/node": "*"
168 | }
169 | },
170 | "node_modules/@types/connect": {
171 | "version": "3.4.38",
172 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
173 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
174 | "dev": true,
175 | "dependencies": {
176 | "@types/node": "*"
177 | }
178 | },
179 | "node_modules/@types/cookie": {
180 | "version": "0.4.1",
181 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
182 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
183 | },
184 | "node_modules/@types/cookie-parser": {
185 | "version": "1.4.7",
186 | "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
187 | "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
188 | "dev": true,
189 | "dependencies": {
190 | "@types/express": "*"
191 | }
192 | },
193 | "node_modules/@types/cors": {
194 | "version": "2.8.17",
195 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
196 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
197 | "dependencies": {
198 | "@types/node": "*"
199 | }
200 | },
201 | "node_modules/@types/express": {
202 | "version": "4.17.21",
203 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
204 | "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
205 | "dev": true,
206 | "dependencies": {
207 | "@types/body-parser": "*",
208 | "@types/express-serve-static-core": "^4.17.33",
209 | "@types/qs": "*",
210 | "@types/serve-static": "*"
211 | }
212 | },
213 | "node_modules/@types/express-serve-static-core": {
214 | "version": "4.19.1",
215 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz",
216 | "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==",
217 | "dev": true,
218 | "dependencies": {
219 | "@types/node": "*",
220 | "@types/qs": "*",
221 | "@types/range-parser": "*",
222 | "@types/send": "*"
223 | }
224 | },
225 | "node_modules/@types/http-errors": {
226 | "version": "2.0.4",
227 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
228 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
229 | "dev": true
230 | },
231 | "node_modules/@types/jsonwebtoken": {
232 | "version": "9.0.6",
233 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
234 | "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
235 | "dev": true,
236 | "dependencies": {
237 | "@types/node": "*"
238 | }
239 | },
240 | "node_modules/@types/mime": {
241 | "version": "1.3.5",
242 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
243 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
244 | "dev": true
245 | },
246 | "node_modules/@types/node": {
247 | "version": "20.12.12",
248 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
249 | "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
250 | "dependencies": {
251 | "undici-types": "~5.26.4"
252 | }
253 | },
254 | "node_modules/@types/qs": {
255 | "version": "6.9.15",
256 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
257 | "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
258 | "dev": true
259 | },
260 | "node_modules/@types/range-parser": {
261 | "version": "1.2.7",
262 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
263 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
264 | "dev": true
265 | },
266 | "node_modules/@types/send": {
267 | "version": "0.17.4",
268 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
269 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
270 | "dev": true,
271 | "dependencies": {
272 | "@types/mime": "^1",
273 | "@types/node": "*"
274 | }
275 | },
276 | "node_modules/@types/serve-static": {
277 | "version": "1.15.7",
278 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
279 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
280 | "dev": true,
281 | "dependencies": {
282 | "@types/http-errors": "*",
283 | "@types/node": "*",
284 | "@types/send": "*"
285 | }
286 | },
287 | "node_modules/accepts": {
288 | "version": "1.3.8",
289 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
290 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
291 | "dependencies": {
292 | "mime-types": "~2.1.34",
293 | "negotiator": "0.6.3"
294 | },
295 | "engines": {
296 | "node": ">= 0.6"
297 | }
298 | },
299 | "node_modules/acorn": {
300 | "version": "8.11.3",
301 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
302 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
303 | "dev": true,
304 | "bin": {
305 | "acorn": "bin/acorn"
306 | },
307 | "engines": {
308 | "node": ">=0.4.0"
309 | }
310 | },
311 | "node_modules/acorn-walk": {
312 | "version": "8.3.2",
313 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
314 | "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
315 | "dev": true,
316 | "engines": {
317 | "node": ">=0.4.0"
318 | }
319 | },
320 | "node_modules/anymatch": {
321 | "version": "3.1.3",
322 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
323 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
324 | "dev": true,
325 | "dependencies": {
326 | "normalize-path": "^3.0.0",
327 | "picomatch": "^2.0.4"
328 | },
329 | "engines": {
330 | "node": ">= 8"
331 | }
332 | },
333 | "node_modules/arg": {
334 | "version": "4.1.3",
335 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
336 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
337 | "dev": true
338 | },
339 | "node_modules/array-flatten": {
340 | "version": "1.1.1",
341 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
342 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
343 | },
344 | "node_modules/balanced-match": {
345 | "version": "1.0.2",
346 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
347 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
348 | "dev": true
349 | },
350 | "node_modules/base64id": {
351 | "version": "2.0.0",
352 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
353 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
354 | "engines": {
355 | "node": "^4.5.0 || >= 5.9"
356 | }
357 | },
358 | "node_modules/bcryptjs": {
359 | "version": "2.4.3",
360 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
361 | "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
362 | },
363 | "node_modules/binary-extensions": {
364 | "version": "2.3.0",
365 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
366 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
367 | "dev": true,
368 | "engines": {
369 | "node": ">=8"
370 | },
371 | "funding": {
372 | "url": "https://github.com/sponsors/sindresorhus"
373 | }
374 | },
375 | "node_modules/body-parser": {
376 | "version": "1.20.2",
377 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
378 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
379 | "dependencies": {
380 | "bytes": "3.1.2",
381 | "content-type": "~1.0.5",
382 | "debug": "2.6.9",
383 | "depd": "2.0.0",
384 | "destroy": "1.2.0",
385 | "http-errors": "2.0.0",
386 | "iconv-lite": "0.4.24",
387 | "on-finished": "2.4.1",
388 | "qs": "6.11.0",
389 | "raw-body": "2.5.2",
390 | "type-is": "~1.6.18",
391 | "unpipe": "1.0.0"
392 | },
393 | "engines": {
394 | "node": ">= 0.8",
395 | "npm": "1.2.8000 || >= 1.4.16"
396 | }
397 | },
398 | "node_modules/body-parser/node_modules/debug": {
399 | "version": "2.6.9",
400 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
401 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
402 | "dependencies": {
403 | "ms": "2.0.0"
404 | }
405 | },
406 | "node_modules/body-parser/node_modules/ms": {
407 | "version": "2.0.0",
408 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
409 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
410 | },
411 | "node_modules/brace-expansion": {
412 | "version": "1.1.11",
413 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
414 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
415 | "dev": true,
416 | "dependencies": {
417 | "balanced-match": "^1.0.0",
418 | "concat-map": "0.0.1"
419 | }
420 | },
421 | "node_modules/braces": {
422 | "version": "3.0.3",
423 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
424 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
425 | "dev": true,
426 | "dependencies": {
427 | "fill-range": "^7.1.1"
428 | },
429 | "engines": {
430 | "node": ">=8"
431 | }
432 | },
433 | "node_modules/buffer-equal-constant-time": {
434 | "version": "1.0.1",
435 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
436 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
437 | },
438 | "node_modules/bytes": {
439 | "version": "3.1.2",
440 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
441 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
442 | "engines": {
443 | "node": ">= 0.8"
444 | }
445 | },
446 | "node_modules/call-bind": {
447 | "version": "1.0.7",
448 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
449 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
450 | "dependencies": {
451 | "es-define-property": "^1.0.0",
452 | "es-errors": "^1.3.0",
453 | "function-bind": "^1.1.2",
454 | "get-intrinsic": "^1.2.4",
455 | "set-function-length": "^1.2.1"
456 | },
457 | "engines": {
458 | "node": ">= 0.4"
459 | },
460 | "funding": {
461 | "url": "https://github.com/sponsors/ljharb"
462 | }
463 | },
464 | "node_modules/chokidar": {
465 | "version": "3.6.0",
466 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
467 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
468 | "dev": true,
469 | "dependencies": {
470 | "anymatch": "~3.1.2",
471 | "braces": "~3.0.2",
472 | "glob-parent": "~5.1.2",
473 | "is-binary-path": "~2.1.0",
474 | "is-glob": "~4.0.1",
475 | "normalize-path": "~3.0.0",
476 | "readdirp": "~3.6.0"
477 | },
478 | "engines": {
479 | "node": ">= 8.10.0"
480 | },
481 | "funding": {
482 | "url": "https://paulmillr.com/funding/"
483 | },
484 | "optionalDependencies": {
485 | "fsevents": "~2.3.2"
486 | }
487 | },
488 | "node_modules/concat-map": {
489 | "version": "0.0.1",
490 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
491 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
492 | "dev": true
493 | },
494 | "node_modules/content-disposition": {
495 | "version": "0.5.4",
496 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
497 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
498 | "dependencies": {
499 | "safe-buffer": "5.2.1"
500 | },
501 | "engines": {
502 | "node": ">= 0.6"
503 | }
504 | },
505 | "node_modules/content-type": {
506 | "version": "1.0.5",
507 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
508 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
509 | "engines": {
510 | "node": ">= 0.6"
511 | }
512 | },
513 | "node_modules/cookie": {
514 | "version": "0.6.0",
515 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
516 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
517 | "engines": {
518 | "node": ">= 0.6"
519 | }
520 | },
521 | "node_modules/cookie-parser": {
522 | "version": "1.4.6",
523 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
524 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
525 | "dependencies": {
526 | "cookie": "0.4.1",
527 | "cookie-signature": "1.0.6"
528 | },
529 | "engines": {
530 | "node": ">= 0.8.0"
531 | }
532 | },
533 | "node_modules/cookie-parser/node_modules/cookie": {
534 | "version": "0.4.1",
535 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
536 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
537 | "engines": {
538 | "node": ">= 0.6"
539 | }
540 | },
541 | "node_modules/cookie-signature": {
542 | "version": "1.0.6",
543 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
544 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
545 | },
546 | "node_modules/cors": {
547 | "version": "2.8.5",
548 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
549 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
550 | "dependencies": {
551 | "object-assign": "^4",
552 | "vary": "^1"
553 | },
554 | "engines": {
555 | "node": ">= 0.10"
556 | }
557 | },
558 | "node_modules/create-require": {
559 | "version": "1.1.1",
560 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
561 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
562 | "dev": true
563 | },
564 | "node_modules/debug": {
565 | "version": "4.3.4",
566 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
567 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
568 | "dependencies": {
569 | "ms": "2.1.2"
570 | },
571 | "engines": {
572 | "node": ">=6.0"
573 | },
574 | "peerDependenciesMeta": {
575 | "supports-color": {
576 | "optional": true
577 | }
578 | }
579 | },
580 | "node_modules/define-data-property": {
581 | "version": "1.1.4",
582 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
583 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
584 | "dependencies": {
585 | "es-define-property": "^1.0.0",
586 | "es-errors": "^1.3.0",
587 | "gopd": "^1.0.1"
588 | },
589 | "engines": {
590 | "node": ">= 0.4"
591 | },
592 | "funding": {
593 | "url": "https://github.com/sponsors/ljharb"
594 | }
595 | },
596 | "node_modules/depd": {
597 | "version": "2.0.0",
598 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
599 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
600 | "engines": {
601 | "node": ">= 0.8"
602 | }
603 | },
604 | "node_modules/destroy": {
605 | "version": "1.2.0",
606 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
607 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
608 | "engines": {
609 | "node": ">= 0.8",
610 | "npm": "1.2.8000 || >= 1.4.16"
611 | }
612 | },
613 | "node_modules/diff": {
614 | "version": "4.0.2",
615 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
616 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
617 | "dev": true,
618 | "engines": {
619 | "node": ">=0.3.1"
620 | }
621 | },
622 | "node_modules/dotenv": {
623 | "version": "16.4.5",
624 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
625 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
626 | "engines": {
627 | "node": ">=12"
628 | },
629 | "funding": {
630 | "url": "https://dotenvx.com"
631 | }
632 | },
633 | "node_modules/ecdsa-sig-formatter": {
634 | "version": "1.0.11",
635 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
636 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
637 | "dependencies": {
638 | "safe-buffer": "^5.0.1"
639 | }
640 | },
641 | "node_modules/ee-first": {
642 | "version": "1.1.1",
643 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
644 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
645 | },
646 | "node_modules/encodeurl": {
647 | "version": "1.0.2",
648 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
649 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
650 | "engines": {
651 | "node": ">= 0.8"
652 | }
653 | },
654 | "node_modules/engine.io": {
655 | "version": "6.5.4",
656 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
657 | "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
658 | "dependencies": {
659 | "@types/cookie": "^0.4.1",
660 | "@types/cors": "^2.8.12",
661 | "@types/node": ">=10.0.0",
662 | "accepts": "~1.3.4",
663 | "base64id": "2.0.0",
664 | "cookie": "~0.4.1",
665 | "cors": "~2.8.5",
666 | "debug": "~4.3.1",
667 | "engine.io-parser": "~5.2.1",
668 | "ws": "~8.11.0"
669 | },
670 | "engines": {
671 | "node": ">=10.2.0"
672 | }
673 | },
674 | "node_modules/engine.io-parser": {
675 | "version": "5.2.2",
676 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
677 | "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
678 | "engines": {
679 | "node": ">=10.0.0"
680 | }
681 | },
682 | "node_modules/engine.io/node_modules/cookie": {
683 | "version": "0.4.2",
684 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
685 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
686 | "engines": {
687 | "node": ">= 0.6"
688 | }
689 | },
690 | "node_modules/es-define-property": {
691 | "version": "1.0.0",
692 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
693 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
694 | "dependencies": {
695 | "get-intrinsic": "^1.2.4"
696 | },
697 | "engines": {
698 | "node": ">= 0.4"
699 | }
700 | },
701 | "node_modules/es-errors": {
702 | "version": "1.3.0",
703 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
704 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
705 | "engines": {
706 | "node": ">= 0.4"
707 | }
708 | },
709 | "node_modules/escape-html": {
710 | "version": "1.0.3",
711 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
712 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
713 | },
714 | "node_modules/etag": {
715 | "version": "1.8.1",
716 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
717 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
718 | "engines": {
719 | "node": ">= 0.6"
720 | }
721 | },
722 | "node_modules/express": {
723 | "version": "4.19.2",
724 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
725 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
726 | "dependencies": {
727 | "accepts": "~1.3.8",
728 | "array-flatten": "1.1.1",
729 | "body-parser": "1.20.2",
730 | "content-disposition": "0.5.4",
731 | "content-type": "~1.0.4",
732 | "cookie": "0.6.0",
733 | "cookie-signature": "1.0.6",
734 | "debug": "2.6.9",
735 | "depd": "2.0.0",
736 | "encodeurl": "~1.0.2",
737 | "escape-html": "~1.0.3",
738 | "etag": "~1.8.1",
739 | "finalhandler": "1.2.0",
740 | "fresh": "0.5.2",
741 | "http-errors": "2.0.0",
742 | "merge-descriptors": "1.0.1",
743 | "methods": "~1.1.2",
744 | "on-finished": "2.4.1",
745 | "parseurl": "~1.3.3",
746 | "path-to-regexp": "0.1.7",
747 | "proxy-addr": "~2.0.7",
748 | "qs": "6.11.0",
749 | "range-parser": "~1.2.1",
750 | "safe-buffer": "5.2.1",
751 | "send": "0.18.0",
752 | "serve-static": "1.15.0",
753 | "setprototypeof": "1.2.0",
754 | "statuses": "2.0.1",
755 | "type-is": "~1.6.18",
756 | "utils-merge": "1.0.1",
757 | "vary": "~1.1.2"
758 | },
759 | "engines": {
760 | "node": ">= 0.10.0"
761 | }
762 | },
763 | "node_modules/express/node_modules/debug": {
764 | "version": "2.6.9",
765 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
766 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
767 | "dependencies": {
768 | "ms": "2.0.0"
769 | }
770 | },
771 | "node_modules/express/node_modules/ms": {
772 | "version": "2.0.0",
773 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
774 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
775 | },
776 | "node_modules/fill-range": {
777 | "version": "7.1.1",
778 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
779 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
780 | "dev": true,
781 | "dependencies": {
782 | "to-regex-range": "^5.0.1"
783 | },
784 | "engines": {
785 | "node": ">=8"
786 | }
787 | },
788 | "node_modules/finalhandler": {
789 | "version": "1.2.0",
790 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
791 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
792 | "dependencies": {
793 | "debug": "2.6.9",
794 | "encodeurl": "~1.0.2",
795 | "escape-html": "~1.0.3",
796 | "on-finished": "2.4.1",
797 | "parseurl": "~1.3.3",
798 | "statuses": "2.0.1",
799 | "unpipe": "~1.0.0"
800 | },
801 | "engines": {
802 | "node": ">= 0.8"
803 | }
804 | },
805 | "node_modules/finalhandler/node_modules/debug": {
806 | "version": "2.6.9",
807 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
808 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
809 | "dependencies": {
810 | "ms": "2.0.0"
811 | }
812 | },
813 | "node_modules/finalhandler/node_modules/ms": {
814 | "version": "2.0.0",
815 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
816 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
817 | },
818 | "node_modules/forwarded": {
819 | "version": "0.2.0",
820 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
821 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
822 | "engines": {
823 | "node": ">= 0.6"
824 | }
825 | },
826 | "node_modules/fresh": {
827 | "version": "0.5.2",
828 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
829 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
830 | "engines": {
831 | "node": ">= 0.6"
832 | }
833 | },
834 | "node_modules/fsevents": {
835 | "version": "2.3.3",
836 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
837 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
838 | "dev": true,
839 | "hasInstallScript": true,
840 | "optional": true,
841 | "os": [
842 | "darwin"
843 | ],
844 | "engines": {
845 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
846 | }
847 | },
848 | "node_modules/function-bind": {
849 | "version": "1.1.2",
850 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
851 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
852 | "funding": {
853 | "url": "https://github.com/sponsors/ljharb"
854 | }
855 | },
856 | "node_modules/get-intrinsic": {
857 | "version": "1.2.4",
858 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
859 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
860 | "dependencies": {
861 | "es-errors": "^1.3.0",
862 | "function-bind": "^1.1.2",
863 | "has-proto": "^1.0.1",
864 | "has-symbols": "^1.0.3",
865 | "hasown": "^2.0.0"
866 | },
867 | "engines": {
868 | "node": ">= 0.4"
869 | },
870 | "funding": {
871 | "url": "https://github.com/sponsors/ljharb"
872 | }
873 | },
874 | "node_modules/glob-parent": {
875 | "version": "5.1.2",
876 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
877 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
878 | "dev": true,
879 | "dependencies": {
880 | "is-glob": "^4.0.1"
881 | },
882 | "engines": {
883 | "node": ">= 6"
884 | }
885 | },
886 | "node_modules/gopd": {
887 | "version": "1.0.1",
888 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
889 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
890 | "dependencies": {
891 | "get-intrinsic": "^1.1.3"
892 | },
893 | "funding": {
894 | "url": "https://github.com/sponsors/ljharb"
895 | }
896 | },
897 | "node_modules/has-flag": {
898 | "version": "3.0.0",
899 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
900 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
901 | "dev": true,
902 | "engines": {
903 | "node": ">=4"
904 | }
905 | },
906 | "node_modules/has-property-descriptors": {
907 | "version": "1.0.2",
908 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
909 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
910 | "dependencies": {
911 | "es-define-property": "^1.0.0"
912 | },
913 | "funding": {
914 | "url": "https://github.com/sponsors/ljharb"
915 | }
916 | },
917 | "node_modules/has-proto": {
918 | "version": "1.0.3",
919 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
920 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
921 | "engines": {
922 | "node": ">= 0.4"
923 | },
924 | "funding": {
925 | "url": "https://github.com/sponsors/ljharb"
926 | }
927 | },
928 | "node_modules/has-symbols": {
929 | "version": "1.0.3",
930 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
931 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
932 | "engines": {
933 | "node": ">= 0.4"
934 | },
935 | "funding": {
936 | "url": "https://github.com/sponsors/ljharb"
937 | }
938 | },
939 | "node_modules/hasown": {
940 | "version": "2.0.2",
941 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
942 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
943 | "dependencies": {
944 | "function-bind": "^1.1.2"
945 | },
946 | "engines": {
947 | "node": ">= 0.4"
948 | }
949 | },
950 | "node_modules/http-errors": {
951 | "version": "2.0.0",
952 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
953 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
954 | "dependencies": {
955 | "depd": "2.0.0",
956 | "inherits": "2.0.4",
957 | "setprototypeof": "1.2.0",
958 | "statuses": "2.0.1",
959 | "toidentifier": "1.0.1"
960 | },
961 | "engines": {
962 | "node": ">= 0.8"
963 | }
964 | },
965 | "node_modules/iconv-lite": {
966 | "version": "0.4.24",
967 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
968 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
969 | "dependencies": {
970 | "safer-buffer": ">= 2.1.2 < 3"
971 | },
972 | "engines": {
973 | "node": ">=0.10.0"
974 | }
975 | },
976 | "node_modules/ignore-by-default": {
977 | "version": "1.0.1",
978 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
979 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
980 | "dev": true
981 | },
982 | "node_modules/inherits": {
983 | "version": "2.0.4",
984 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
985 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
986 | },
987 | "node_modules/ipaddr.js": {
988 | "version": "1.9.1",
989 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
990 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
991 | "engines": {
992 | "node": ">= 0.10"
993 | }
994 | },
995 | "node_modules/is-binary-path": {
996 | "version": "2.1.0",
997 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
998 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
999 | "dev": true,
1000 | "dependencies": {
1001 | "binary-extensions": "^2.0.0"
1002 | },
1003 | "engines": {
1004 | "node": ">=8"
1005 | }
1006 | },
1007 | "node_modules/is-extglob": {
1008 | "version": "2.1.1",
1009 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1010 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1011 | "dev": true,
1012 | "engines": {
1013 | "node": ">=0.10.0"
1014 | }
1015 | },
1016 | "node_modules/is-glob": {
1017 | "version": "4.0.3",
1018 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1019 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1020 | "dev": true,
1021 | "dependencies": {
1022 | "is-extglob": "^2.1.1"
1023 | },
1024 | "engines": {
1025 | "node": ">=0.10.0"
1026 | }
1027 | },
1028 | "node_modules/is-number": {
1029 | "version": "7.0.0",
1030 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1031 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1032 | "dev": true,
1033 | "engines": {
1034 | "node": ">=0.12.0"
1035 | }
1036 | },
1037 | "node_modules/jsonwebtoken": {
1038 | "version": "9.0.2",
1039 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
1040 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
1041 | "dependencies": {
1042 | "jws": "^3.2.2",
1043 | "lodash.includes": "^4.3.0",
1044 | "lodash.isboolean": "^3.0.3",
1045 | "lodash.isinteger": "^4.0.4",
1046 | "lodash.isnumber": "^3.0.3",
1047 | "lodash.isplainobject": "^4.0.6",
1048 | "lodash.isstring": "^4.0.1",
1049 | "lodash.once": "^4.0.0",
1050 | "ms": "^2.1.1",
1051 | "semver": "^7.5.4"
1052 | },
1053 | "engines": {
1054 | "node": ">=12",
1055 | "npm": ">=6"
1056 | }
1057 | },
1058 | "node_modules/jwa": {
1059 | "version": "1.4.1",
1060 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
1061 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
1062 | "dependencies": {
1063 | "buffer-equal-constant-time": "1.0.1",
1064 | "ecdsa-sig-formatter": "1.0.11",
1065 | "safe-buffer": "^5.0.1"
1066 | }
1067 | },
1068 | "node_modules/jws": {
1069 | "version": "3.2.2",
1070 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
1071 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
1072 | "dependencies": {
1073 | "jwa": "^1.4.1",
1074 | "safe-buffer": "^5.0.1"
1075 | }
1076 | },
1077 | "node_modules/lodash.includes": {
1078 | "version": "4.3.0",
1079 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1080 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
1081 | },
1082 | "node_modules/lodash.isboolean": {
1083 | "version": "3.0.3",
1084 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1085 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
1086 | },
1087 | "node_modules/lodash.isinteger": {
1088 | "version": "4.0.4",
1089 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1090 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
1091 | },
1092 | "node_modules/lodash.isnumber": {
1093 | "version": "3.0.3",
1094 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1095 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
1096 | },
1097 | "node_modules/lodash.isplainobject": {
1098 | "version": "4.0.6",
1099 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1100 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
1101 | },
1102 | "node_modules/lodash.isstring": {
1103 | "version": "4.0.1",
1104 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1105 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
1106 | },
1107 | "node_modules/lodash.once": {
1108 | "version": "4.1.1",
1109 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1110 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
1111 | },
1112 | "node_modules/make-error": {
1113 | "version": "1.3.6",
1114 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
1115 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
1116 | "dev": true
1117 | },
1118 | "node_modules/media-typer": {
1119 | "version": "0.3.0",
1120 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1121 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1122 | "engines": {
1123 | "node": ">= 0.6"
1124 | }
1125 | },
1126 | "node_modules/merge-descriptors": {
1127 | "version": "1.0.1",
1128 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
1129 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
1130 | },
1131 | "node_modules/methods": {
1132 | "version": "1.1.2",
1133 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1134 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1135 | "engines": {
1136 | "node": ">= 0.6"
1137 | }
1138 | },
1139 | "node_modules/mime": {
1140 | "version": "1.6.0",
1141 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1142 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1143 | "bin": {
1144 | "mime": "cli.js"
1145 | },
1146 | "engines": {
1147 | "node": ">=4"
1148 | }
1149 | },
1150 | "node_modules/mime-db": {
1151 | "version": "1.52.0",
1152 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1153 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1154 | "engines": {
1155 | "node": ">= 0.6"
1156 | }
1157 | },
1158 | "node_modules/mime-types": {
1159 | "version": "2.1.35",
1160 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1161 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1162 | "dependencies": {
1163 | "mime-db": "1.52.0"
1164 | },
1165 | "engines": {
1166 | "node": ">= 0.6"
1167 | }
1168 | },
1169 | "node_modules/minimatch": {
1170 | "version": "3.1.2",
1171 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1172 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1173 | "dev": true,
1174 | "dependencies": {
1175 | "brace-expansion": "^1.1.7"
1176 | },
1177 | "engines": {
1178 | "node": "*"
1179 | }
1180 | },
1181 | "node_modules/ms": {
1182 | "version": "2.1.2",
1183 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1184 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1185 | },
1186 | "node_modules/negotiator": {
1187 | "version": "0.6.3",
1188 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1189 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1190 | "engines": {
1191 | "node": ">= 0.6"
1192 | }
1193 | },
1194 | "node_modules/nodemon": {
1195 | "version": "3.1.0",
1196 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz",
1197 | "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==",
1198 | "dev": true,
1199 | "dependencies": {
1200 | "chokidar": "^3.5.2",
1201 | "debug": "^4",
1202 | "ignore-by-default": "^1.0.1",
1203 | "minimatch": "^3.1.2",
1204 | "pstree.remy": "^1.1.8",
1205 | "semver": "^7.5.3",
1206 | "simple-update-notifier": "^2.0.0",
1207 | "supports-color": "^5.5.0",
1208 | "touch": "^3.1.0",
1209 | "undefsafe": "^2.0.5"
1210 | },
1211 | "bin": {
1212 | "nodemon": "bin/nodemon.js"
1213 | },
1214 | "engines": {
1215 | "node": ">=10"
1216 | },
1217 | "funding": {
1218 | "type": "opencollective",
1219 | "url": "https://opencollective.com/nodemon"
1220 | }
1221 | },
1222 | "node_modules/normalize-path": {
1223 | "version": "3.0.0",
1224 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1225 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1226 | "dev": true,
1227 | "engines": {
1228 | "node": ">=0.10.0"
1229 | }
1230 | },
1231 | "node_modules/object-assign": {
1232 | "version": "4.1.1",
1233 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1234 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1235 | "engines": {
1236 | "node": ">=0.10.0"
1237 | }
1238 | },
1239 | "node_modules/object-inspect": {
1240 | "version": "1.13.1",
1241 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
1242 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
1243 | "funding": {
1244 | "url": "https://github.com/sponsors/ljharb"
1245 | }
1246 | },
1247 | "node_modules/on-finished": {
1248 | "version": "2.4.1",
1249 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1250 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1251 | "dependencies": {
1252 | "ee-first": "1.1.1"
1253 | },
1254 | "engines": {
1255 | "node": ">= 0.8"
1256 | }
1257 | },
1258 | "node_modules/parseurl": {
1259 | "version": "1.3.3",
1260 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1261 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1262 | "engines": {
1263 | "node": ">= 0.8"
1264 | }
1265 | },
1266 | "node_modules/path-to-regexp": {
1267 | "version": "0.1.7",
1268 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1269 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
1270 | },
1271 | "node_modules/picomatch": {
1272 | "version": "2.3.1",
1273 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1274 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1275 | "dev": true,
1276 | "engines": {
1277 | "node": ">=8.6"
1278 | },
1279 | "funding": {
1280 | "url": "https://github.com/sponsors/jonschlinkert"
1281 | }
1282 | },
1283 | "node_modules/prisma": {
1284 | "version": "5.14.0",
1285 | "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz",
1286 | "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==",
1287 | "hasInstallScript": true,
1288 | "dependencies": {
1289 | "@prisma/engines": "5.14.0"
1290 | },
1291 | "bin": {
1292 | "prisma": "build/index.js"
1293 | },
1294 | "engines": {
1295 | "node": ">=16.13"
1296 | }
1297 | },
1298 | "node_modules/proxy-addr": {
1299 | "version": "2.0.7",
1300 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1301 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1302 | "dependencies": {
1303 | "forwarded": "0.2.0",
1304 | "ipaddr.js": "1.9.1"
1305 | },
1306 | "engines": {
1307 | "node": ">= 0.10"
1308 | }
1309 | },
1310 | "node_modules/pstree.remy": {
1311 | "version": "1.1.8",
1312 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1313 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1314 | "dev": true
1315 | },
1316 | "node_modules/qs": {
1317 | "version": "6.11.0",
1318 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
1319 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
1320 | "dependencies": {
1321 | "side-channel": "^1.0.4"
1322 | },
1323 | "engines": {
1324 | "node": ">=0.6"
1325 | },
1326 | "funding": {
1327 | "url": "https://github.com/sponsors/ljharb"
1328 | }
1329 | },
1330 | "node_modules/range-parser": {
1331 | "version": "1.2.1",
1332 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1333 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1334 | "engines": {
1335 | "node": ">= 0.6"
1336 | }
1337 | },
1338 | "node_modules/raw-body": {
1339 | "version": "2.5.2",
1340 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1341 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1342 | "dependencies": {
1343 | "bytes": "3.1.2",
1344 | "http-errors": "2.0.0",
1345 | "iconv-lite": "0.4.24",
1346 | "unpipe": "1.0.0"
1347 | },
1348 | "engines": {
1349 | "node": ">= 0.8"
1350 | }
1351 | },
1352 | "node_modules/readdirp": {
1353 | "version": "3.6.0",
1354 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1355 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1356 | "dev": true,
1357 | "dependencies": {
1358 | "picomatch": "^2.2.1"
1359 | },
1360 | "engines": {
1361 | "node": ">=8.10.0"
1362 | }
1363 | },
1364 | "node_modules/safe-buffer": {
1365 | "version": "5.2.1",
1366 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1367 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1368 | "funding": [
1369 | {
1370 | "type": "github",
1371 | "url": "https://github.com/sponsors/feross"
1372 | },
1373 | {
1374 | "type": "patreon",
1375 | "url": "https://www.patreon.com/feross"
1376 | },
1377 | {
1378 | "type": "consulting",
1379 | "url": "https://feross.org/support"
1380 | }
1381 | ]
1382 | },
1383 | "node_modules/safer-buffer": {
1384 | "version": "2.1.2",
1385 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1386 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1387 | },
1388 | "node_modules/semver": {
1389 | "version": "7.6.2",
1390 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
1391 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
1392 | "bin": {
1393 | "semver": "bin/semver.js"
1394 | },
1395 | "engines": {
1396 | "node": ">=10"
1397 | }
1398 | },
1399 | "node_modules/send": {
1400 | "version": "0.18.0",
1401 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1402 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1403 | "dependencies": {
1404 | "debug": "2.6.9",
1405 | "depd": "2.0.0",
1406 | "destroy": "1.2.0",
1407 | "encodeurl": "~1.0.2",
1408 | "escape-html": "~1.0.3",
1409 | "etag": "~1.8.1",
1410 | "fresh": "0.5.2",
1411 | "http-errors": "2.0.0",
1412 | "mime": "1.6.0",
1413 | "ms": "2.1.3",
1414 | "on-finished": "2.4.1",
1415 | "range-parser": "~1.2.1",
1416 | "statuses": "2.0.1"
1417 | },
1418 | "engines": {
1419 | "node": ">= 0.8.0"
1420 | }
1421 | },
1422 | "node_modules/send/node_modules/debug": {
1423 | "version": "2.6.9",
1424 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1425 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1426 | "dependencies": {
1427 | "ms": "2.0.0"
1428 | }
1429 | },
1430 | "node_modules/send/node_modules/debug/node_modules/ms": {
1431 | "version": "2.0.0",
1432 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1433 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1434 | },
1435 | "node_modules/send/node_modules/ms": {
1436 | "version": "2.1.3",
1437 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1438 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1439 | },
1440 | "node_modules/serve-static": {
1441 | "version": "1.15.0",
1442 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1443 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1444 | "dependencies": {
1445 | "encodeurl": "~1.0.2",
1446 | "escape-html": "~1.0.3",
1447 | "parseurl": "~1.3.3",
1448 | "send": "0.18.0"
1449 | },
1450 | "engines": {
1451 | "node": ">= 0.8.0"
1452 | }
1453 | },
1454 | "node_modules/set-function-length": {
1455 | "version": "1.2.2",
1456 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1457 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1458 | "dependencies": {
1459 | "define-data-property": "^1.1.4",
1460 | "es-errors": "^1.3.0",
1461 | "function-bind": "^1.1.2",
1462 | "get-intrinsic": "^1.2.4",
1463 | "gopd": "^1.0.1",
1464 | "has-property-descriptors": "^1.0.2"
1465 | },
1466 | "engines": {
1467 | "node": ">= 0.4"
1468 | }
1469 | },
1470 | "node_modules/setprototypeof": {
1471 | "version": "1.2.0",
1472 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1473 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1474 | },
1475 | "node_modules/side-channel": {
1476 | "version": "1.0.6",
1477 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1478 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1479 | "dependencies": {
1480 | "call-bind": "^1.0.7",
1481 | "es-errors": "^1.3.0",
1482 | "get-intrinsic": "^1.2.4",
1483 | "object-inspect": "^1.13.1"
1484 | },
1485 | "engines": {
1486 | "node": ">= 0.4"
1487 | },
1488 | "funding": {
1489 | "url": "https://github.com/sponsors/ljharb"
1490 | }
1491 | },
1492 | "node_modules/simple-update-notifier": {
1493 | "version": "2.0.0",
1494 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1495 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1496 | "dev": true,
1497 | "dependencies": {
1498 | "semver": "^7.5.3"
1499 | },
1500 | "engines": {
1501 | "node": ">=10"
1502 | }
1503 | },
1504 | "node_modules/socket.io": {
1505 | "version": "4.7.5",
1506 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
1507 | "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
1508 | "dependencies": {
1509 | "accepts": "~1.3.4",
1510 | "base64id": "~2.0.0",
1511 | "cors": "~2.8.5",
1512 | "debug": "~4.3.2",
1513 | "engine.io": "~6.5.2",
1514 | "socket.io-adapter": "~2.5.2",
1515 | "socket.io-parser": "~4.2.4"
1516 | },
1517 | "engines": {
1518 | "node": ">=10.2.0"
1519 | }
1520 | },
1521 | "node_modules/socket.io-adapter": {
1522 | "version": "2.5.4",
1523 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz",
1524 | "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==",
1525 | "dependencies": {
1526 | "debug": "~4.3.4",
1527 | "ws": "~8.11.0"
1528 | }
1529 | },
1530 | "node_modules/socket.io-parser": {
1531 | "version": "4.2.4",
1532 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1533 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1534 | "dependencies": {
1535 | "@socket.io/component-emitter": "~3.1.0",
1536 | "debug": "~4.3.1"
1537 | },
1538 | "engines": {
1539 | "node": ">=10.0.0"
1540 | }
1541 | },
1542 | "node_modules/statuses": {
1543 | "version": "2.0.1",
1544 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1545 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1546 | "engines": {
1547 | "node": ">= 0.8"
1548 | }
1549 | },
1550 | "node_modules/supports-color": {
1551 | "version": "5.5.0",
1552 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1553 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1554 | "dev": true,
1555 | "dependencies": {
1556 | "has-flag": "^3.0.0"
1557 | },
1558 | "engines": {
1559 | "node": ">=4"
1560 | }
1561 | },
1562 | "node_modules/to-regex-range": {
1563 | "version": "5.0.1",
1564 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1565 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1566 | "dev": true,
1567 | "dependencies": {
1568 | "is-number": "^7.0.0"
1569 | },
1570 | "engines": {
1571 | "node": ">=8.0"
1572 | }
1573 | },
1574 | "node_modules/toidentifier": {
1575 | "version": "1.0.1",
1576 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1577 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1578 | "engines": {
1579 | "node": ">=0.6"
1580 | }
1581 | },
1582 | "node_modules/touch": {
1583 | "version": "3.1.1",
1584 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1585 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1586 | "dev": true,
1587 | "bin": {
1588 | "nodetouch": "bin/nodetouch.js"
1589 | }
1590 | },
1591 | "node_modules/ts-node": {
1592 | "version": "10.9.2",
1593 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1594 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1595 | "dev": true,
1596 | "dependencies": {
1597 | "@cspotcode/source-map-support": "^0.8.0",
1598 | "@tsconfig/node10": "^1.0.7",
1599 | "@tsconfig/node12": "^1.0.7",
1600 | "@tsconfig/node14": "^1.0.0",
1601 | "@tsconfig/node16": "^1.0.2",
1602 | "acorn": "^8.4.1",
1603 | "acorn-walk": "^8.1.1",
1604 | "arg": "^4.1.0",
1605 | "create-require": "^1.1.0",
1606 | "diff": "^4.0.1",
1607 | "make-error": "^1.1.1",
1608 | "v8-compile-cache-lib": "^3.0.1",
1609 | "yn": "3.1.1"
1610 | },
1611 | "bin": {
1612 | "ts-node": "dist/bin.js",
1613 | "ts-node-cwd": "dist/bin-cwd.js",
1614 | "ts-node-esm": "dist/bin-esm.js",
1615 | "ts-node-script": "dist/bin-script.js",
1616 | "ts-node-transpile-only": "dist/bin-transpile.js",
1617 | "ts-script": "dist/bin-script-deprecated.js"
1618 | },
1619 | "peerDependencies": {
1620 | "@swc/core": ">=1.2.50",
1621 | "@swc/wasm": ">=1.2.50",
1622 | "@types/node": "*",
1623 | "typescript": ">=2.7"
1624 | },
1625 | "peerDependenciesMeta": {
1626 | "@swc/core": {
1627 | "optional": true
1628 | },
1629 | "@swc/wasm": {
1630 | "optional": true
1631 | }
1632 | }
1633 | },
1634 | "node_modules/type-is": {
1635 | "version": "1.6.18",
1636 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1637 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1638 | "dependencies": {
1639 | "media-typer": "0.3.0",
1640 | "mime-types": "~2.1.24"
1641 | },
1642 | "engines": {
1643 | "node": ">= 0.6"
1644 | }
1645 | },
1646 | "node_modules/typescript": {
1647 | "version": "5.4.5",
1648 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
1649 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
1650 | "dev": true,
1651 | "bin": {
1652 | "tsc": "bin/tsc",
1653 | "tsserver": "bin/tsserver"
1654 | },
1655 | "engines": {
1656 | "node": ">=14.17"
1657 | }
1658 | },
1659 | "node_modules/undefsafe": {
1660 | "version": "2.0.5",
1661 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1662 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1663 | "dev": true
1664 | },
1665 | "node_modules/undici-types": {
1666 | "version": "5.26.5",
1667 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1668 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
1669 | },
1670 | "node_modules/unpipe": {
1671 | "version": "1.0.0",
1672 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1673 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1674 | "engines": {
1675 | "node": ">= 0.8"
1676 | }
1677 | },
1678 | "node_modules/utils-merge": {
1679 | "version": "1.0.1",
1680 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1681 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1682 | "engines": {
1683 | "node": ">= 0.4.0"
1684 | }
1685 | },
1686 | "node_modules/v8-compile-cache-lib": {
1687 | "version": "3.0.1",
1688 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
1689 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
1690 | "dev": true
1691 | },
1692 | "node_modules/vary": {
1693 | "version": "1.1.2",
1694 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1695 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1696 | "engines": {
1697 | "node": ">= 0.8"
1698 | }
1699 | },
1700 | "node_modules/ws": {
1701 | "version": "8.11.0",
1702 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
1703 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
1704 | "engines": {
1705 | "node": ">=10.0.0"
1706 | },
1707 | "peerDependencies": {
1708 | "bufferutil": "^4.0.1",
1709 | "utf-8-validate": "^5.0.2"
1710 | },
1711 | "peerDependenciesMeta": {
1712 | "bufferutil": {
1713 | "optional": true
1714 | },
1715 | "utf-8-validate": {
1716 | "optional": true
1717 | }
1718 | }
1719 | },
1720 | "node_modules/yn": {
1721 | "version": "3.1.1",
1722 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
1723 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
1724 | "dev": true,
1725 | "engines": {
1726 | "node": ">=6"
1727 | }
1728 | }
1729 | }
1730 | }
1731 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pern-chat-app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon --watch backend/src --exec ts-node backend/src/index.ts",
8 | "build": "tsc && npm install && prisma generate --schema=./backend/prisma/schema.prisma && npm install --prefix frontend && npm run build --prefix frontend",
9 | "start": "node backend/dist/src/index.js"
10 | },
11 | "type": "module",
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@types/bcryptjs": "^2.4.6",
17 | "@types/cookie-parser": "^1.4.7",
18 | "@types/express": "^4.17.21",
19 | "@types/jsonwebtoken": "^9.0.6",
20 | "nodemon": "^3.1.0",
21 | "ts-node": "^10.9.2",
22 | "typescript": "^5.4.5"
23 | },
24 | "dependencies": {
25 | "@prisma/client": "^5.14.0",
26 | "bcryptjs": "^2.4.3",
27 | "cookie-parser": "^1.4.6",
28 | "dotenv": "^16.4.5",
29 | "express": "^4.19.2",
30 | "jsonwebtoken": "^9.0.2",
31 | "prisma": "^5.14.0",
32 | "socket.io": "^4.7.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020", // Specifies what version of ECMAScript the code should be compiled to.
4 | "module": "NodeNext", // Specifies what module code should be generated.
5 | "moduleResolution": "NodeNext", // Specifies how TypeScript should resolve module imports.
6 | "outDir": "./backend/dist", // Redirect output structure to the directory.
7 | "rootDir": "./backend",
8 | "strict": true, // Enable all strict type-checking options.
9 | "esModuleInterop": true, // This will allow you to use default imports with CommonJS modules.
10 | "skipLibCheck": true // this option will skip type checking all .d.ts files.
11 | },
12 | "ts-node": {
13 | "esm": true
14 | },
15 | "include": ["backend/src/**/*"], // Specifies which files to include when compiling your project.
16 | "exclude": ["node_modules", "**/*.spec.ts", "frontend"] // Specifies which files to exclude when compiling your project.
17 | }
18 |
--------------------------------------------------------------------------------