diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0e851a7af2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +node_modules/ +.DS_Store \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000000..980fd575c9 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "macos-clang-arm64", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "/usr/bin/clang", + "cStandard": "${default}", + "cppStandard": "${default}", + "intelliSenseMode": "macos-clang-arm64", + "compilerArgs": [ + "" + ] + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..1148f042c3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "C/C++ Runner: Debug Session", + "type": "lldb", + "request": "launch", + "args": [], + "cwd": "/Users/simdinghao/Documents/Y3S1/CS3230/Assignments/PA1", + "program": "/Users/simdinghao/Documents/Y3S1/CS3230/Assignments/PA1/build/Debug/outDebug" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..b9c6ac8740 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ +{ + "C_Cpp_Runner.cCompilerPath": "clang", + "C_Cpp_Runner.cppCompilerPath": "clang++", + "C_Cpp_Runner.debuggerPath": "lldb", + "C_Cpp_Runner.cStandard": "", + "C_Cpp_Runner.cppStandard": "", + "C_Cpp_Runner.msvcBatchPath": "", + "C_Cpp_Runner.useMsvc": false, + "C_Cpp_Runner.warnings": [ + "-Wall", + "-Wextra", + "-Wpedantic", + "-Wshadow", + "-Wformat=2", + "-Wcast-align", + "-Wconversion", + "-Wsign-conversion", + "-Wnull-dereference" + ], + "C_Cpp_Runner.msvcWarnings": [ + "/W4", + "/permissive-", + "/w14242", + "/w14287", + "/w14296", + "/w14311", + "/w14826", + "/w44062", + "/w44242", + "/w14905", + "/w14906", + "/w14263", + "/w44265", + "/w14928" + ], + "C_Cpp_Runner.enableWarnings": true, + "C_Cpp_Runner.warningsAsError": false, + "C_Cpp_Runner.compilerArgs": [], + "C_Cpp_Runner.linkerArgs": [], + "C_Cpp_Runner.includePaths": [], + "C_Cpp_Runner.includeSearch": [ + "*", + "**/*" + ], + "C_Cpp_Runner.excludeSearch": [ + "**/build", + "**/build/**", + "**/.*", + "**/.*/**", + "**/.vscode", + "**/.vscode/**" + ], + "C_Cpp_Runner.useAddressSanitizer": false, + "C_Cpp_Runner.useUndefinedSanitizer": false, + "C_Cpp_Runner.useLeakSanitizer": false, + "C_Cpp_Runner.showCompilationTime": false, + "C_Cpp_Runner.useLinkTimeOptimization": false, + "C_Cpp_Runner.msvcSecureNoWarnings": false +} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 259f7bba2e..0000000000 --- a/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# CS3219 Project (PeerPrep) - AY2425S1 -## Group: Gxx - -### Note: -- You can choose to develop individual microservices within separate folders within this repository **OR** use individual repositories (all public) for each microservice. -- In the latter scenario, you should enable sub-modules on this GitHub classroom repository to manage the development/deployment **AND** add your mentor to the individual repositories as a collaborator. -- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements. diff --git a/backend/api-gateway/Dockerfile b/backend/api-gateway/Dockerfile new file mode 100644 index 0000000000..7e4d6f27b7 --- /dev/null +++ b/backend/api-gateway/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:alpine + +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/backend/api-gateway/nginx.conf b/backend/api-gateway/nginx.conf new file mode 100644 index 0000000000..f6959a39c9 --- /dev/null +++ b/backend/api-gateway/nginx.conf @@ -0,0 +1,109 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/json; + + sendfile on; + keepalive_timeout 65; + + # Upstream configuration for the question-service + upstream question-service { + server question-service:5050; # Docker service name, running on port 5050 + } + + # Upstream configuration for the user-service + upstream user-service { + server user-service:3001; # Docker service name, running on port 3001 + } + + # Upstream configuration for the matching-service + upstream matching-service { + server matching-service:3002; # Docker service name, running on port 3002 + } + + # Upstream configuration for the voice-service + upstream voice-service { + server voice-service:8085; + } + + # Upstream configuration for the collaboration-service + upstream collaboration-service { + server collaboration-service:8080; + } + + # Server configuration + server { + listen 80; + + # Pass requests to question-service + location /api/question-service/ { + proxy_pass http://question-service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Pass requests to user-service + location /api/user-service/ { + proxy_pass http://user-service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Forward Set-Cookie header from user service to the browser + proxy_pass_header Set-Cookie; + } + + # Pass requests to the matching-service + location /api/matching-service/ { + proxy_pass http://matching-service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Pass requests to the voice-service + location /api/voice-service/ { + proxy_pass http://voice-service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/collaboration-service/socket.io/ { + proxy_pass http://collaboration-service; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /api/collaboration-service/ { + proxy_pass http://collaboration-service/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom error page configuration + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} diff --git a/backend/collaboration-service/Dockerfile b/backend/collaboration-service/Dockerfile new file mode 100644 index 0000000000..b8f9156a21 --- /dev/null +++ b/backend/collaboration-service/Dockerfile @@ -0,0 +1,22 @@ +# Use Node.js base image +FROM node:18-alpine + +# Set working directory inside the container +WORKDIR /app + +# Copy package.json and package-lock.json to the container +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application files +COPY . . + +# Expose the port the app runs on +EXPOSE 8080 +EXPOSE 2501 + +# Run the app using ts-node +CMD ["npx", "ts-node", "app.ts"] + diff --git a/backend/collaboration-service/app.ts b/backend/collaboration-service/app.ts new file mode 100644 index 0000000000..ede8ff1f08 --- /dev/null +++ b/backend/collaboration-service/app.ts @@ -0,0 +1,185 @@ +import "dotenv/config"; +import express, { Request, Response } from "express"; +import { createServer } from "http"; +import { Server } from "socket.io"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import { startRabbitMQ } from "./consumer"; +import { authenticateAccessToken } from "./utils/jwt"; +import mongoose from "mongoose"; +import { checkAuthorisedUser, getInfoHandler, getHistoryHandler, saveCodeHandler, getSessionHandler, clearRoomIdCookie} from "./controllers/controller"; +import { verifyAccessToken } from "./middleware/middleware"; +import axios from "axios"; + +import { WebSocketServer } from "ws"; + +// set up y-server, y-server needs request parameter which socket.io does not offer +const setupWSConnection = require("y-websocket/bin/utils").setupWSConnection; + +const yServer = createServer((_request, response) => { + response.writeHead(200, { "Content-Type": "text/plain" }); + response.end("Binded"); +}); +const wss = new WebSocketServer({ server: yServer }); + +function onError(error: Error) { + console.log("error", error); +} + +function onListening() { + console.log(`Listening on port ${process.env.Y_SERVER_PORT_NUM}`); +} + +yServer.on("error", onError); +yServer.on("listening", onListening); + +// Handle code editor. +wss.on("connection", async (ws, req) => { + setupWSConnection(ws, req); + console.log("y-server-connected"); +}); + +yServer.listen(process.env.Y_SERVER_PORT_NUM, () => { + console.log(`y-server Started on port ${process.env.Y_SERVER_PORT_NUM}`) +}); + +const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; +const MONGO_URI_CS = process.env.MONGO_URI_CS; + +const app = express(); +app.use(cors({ origin: FRONTEND_URL, credentials: true })); +app.use(express.json()); +app.use(cookieParser()); // Add cookie-parser middleware + +mongoose + .connect(MONGO_URI_CS!) + .then(() => console.log("Connected to MongoDB")) + .catch((error) => console.error("Failed to connect to MongoDB:", error)); + +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { origin: FRONTEND_URL }, +}); + +app.get("/check-authorization", verifyAccessToken, checkAuthorisedUser); +app.get("/get-info", verifyAccessToken, getInfoHandler); +app.get("/get-history", verifyAccessToken, getHistoryHandler); +app.get("/get-session", verifyAccessToken, getSessionHandler); +app.post("/save-code", saveCodeHandler); +app.post("/clear-cookie", clearRoomIdCookie); + +// POST endpoint to submit code for execution +app.post("/code-execute", async (req: Request, res: Response) => { + try { + const { source_code, language_id } = req.body; + const url = `https://${process.env.REACT_APP_RAPID_API_HOST}/submissions`; + const response = await axios.post( + url, + { source_code, language_id }, + { + params: { base64_encoded: "false", fields: "*" }, + headers: { + "Content-Type": "application/json", + "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST!, + "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY!, + }, + } + ); + + const token = response.data.token; + res.json({ token }); + } catch (err) { + console.error("Error submitting code:", err); + res.status(500).json({ + errors: [{ msg: "Something went wrong while submitting code." }], + }); + } +}); + + +// GET endpoint to check code execution status +app.get("/code-execute/:token", async (req: Request, res: Response) => { + try { + const token = req.params.token; + const url = `https://${process.env.REACT_APP_RAPID_API_HOST}/submissions/${token}`; + const response = await axios.get(url, { + params: { base64_encoded: "false", fields: "*" }, + headers: { + "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST!, + "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY!, + }, + }); + res.send(response.data); + } catch (err) { + console.error("Error fetching code execution result:", err); + res.status(500).json({ + errors: [{ msg: "Something went wrong while fetching code execution result." }], + }); + } +}); + +interface UsersAgreedEnd { + [roomId: string]: Record; +} + +const usersAgreedEnd: UsersAgreedEnd = {}; + +io.on("connection", (socket) => { + console.log("New connection:", socket.id); + + // Retrieve accessToken from cookies in the handshake headers + const accessToken = socket.handshake.headers.cookie + ?.split("; ") + .find((cookie) => cookie.startsWith("accessToken=")) + ?.split("=")[1]; + + if (!accessToken) { + socket.emit("error", { errorMsg: "Not authorized, no access token" }); + socket.disconnect(); + return; + } + console.log("AccessToken received from cookie:", accessToken); + + authenticateAccessToken(accessToken) + .then((user) => { + socket.data.user = user; + + // Room joining + socket.on("join-room", (roomId: string, username: string) => { + socket.join(roomId); + socket.data.roomId = roomId; + socket.data.username = username; + + socket.emit("room-joined", roomId); + io.to(roomId).emit("user-join", username); + }); + + // User-agreed-end event + socket.on("user-end", (roomId: string, userId: string) => { + console.log(userId + " ended") + io.to(roomId).emit("other-user-end", roomId); + } + ); + + // Handle disconnect + socket.on("disconnect", () => { + console.log("User disconnected:", socket.id); + if (socket.data.roomId) { + io.to(socket.data.roomId).emit("user-disconnect", socket.data.username); + } + }); + }) + .catch((error) => { + console.log("Authentication failed:", error); + socket.emit("error", { errorMsg: "Not authorized, access token failed" }); + socket.disconnect(); + }); +}); + +// Starting RabbitMQ Consumer +startRabbitMQ(io); + +const PORT = process.env.PORT || 8080; +httpServer.listen(PORT, () => { + console.log(`Server started on port ${PORT}`); +}); \ No newline at end of file diff --git a/backend/collaboration-service/consumer.ts b/backend/collaboration-service/consumer.ts new file mode 100644 index 0000000000..cd063a2bd1 --- /dev/null +++ b/backend/collaboration-service/consumer.ts @@ -0,0 +1,93 @@ +import * as amqp from "amqplib/callback_api"; +import { MatchModel, Question } from "./models/match"; +import axios from "axios"; +import { Server } from "socket.io"; + +export function startRabbitMQ(io: Server) { + const rabbitMQUrl = process.env.RABBITMQ_URL || "amqp://localhost"; + const questionsAPIUrl = process.env.QUESTIONS_API_URL || "http://localhost/api/question-service/questions/get-question"; + + amqp.connect(rabbitMQUrl, (error0, connection) => { + if (error0) { + console.error("Failed to connect to RabbitMQ:", error0); + return; + } + connection.createChannel((error1, channel) => { + if (error1) { + console.error("Failed to open a channel:", error1); + return; + } + const queue = "match_queue"; + channel.assertQueue(queue, { durable: false }); + console.log(`[*] Waiting for messages in ${queue}. To exit press CTRL+C`); + + channel.consume( + queue, + async (msg) => { + if (!msg) return; + + const matchResult = JSON.parse(msg.content.toString()); + console.log(`Received message: ${JSON.stringify(matchResult)}`); + + // Build match info based on received message + const matchInfo = { + userOne: matchResult.UsernameOne, + userTwo: matchResult.UsernameTwo, + room_id: matchResult.RoomID, + complexity: matchResult.Complexity, + categories: matchResult.Categories, + programming_language: matchResult.ProgrammingLanguages, + question: {} as Question, + status: 'open', + }; + console.log(matchInfo.programming_language) + + try { + // Fetch a question matching the specified categories and complexity + const question = await fetchQuestion( + matchInfo.categories, + matchInfo.complexity, + questionsAPIUrl + ); + + matchInfo.question = question; + + // Save the match information to MongoDB + const newMatch = new MatchModel(matchInfo); + await newMatch.save(); + console.log(`Match saved to MongoDB: ${JSON.stringify(newMatch)}`); + } catch (error) { + console.error("Error processing message:", error); + } + }, + { noAck: true } + ); + }); + }); +} + +// Fetch a single question based on categories and complexity +async function fetchQuestion( + categories: string[], + complexities: string[], + apiURL: string +): Promise { + try { + // Set up the request with categories and complexity as separate query parameters + const params = new URLSearchParams(); + categories.forEach(category => params.append("categories", category)); + complexities.forEach(complexity => params.append("complexity", complexity)); + + const response = await axios.get(apiURL, { + params, + paramsSerializer: (params) => params.toString(), // Serialize to standard query string + }); + + const question = response.data; + if (!question) throw new Error("No matching question found"); + return question; + } catch (error) { + console.error("Failed to fetch question:", error); + throw error; + } +} diff --git a/backend/collaboration-service/controllers/controller.ts b/backend/collaboration-service/controllers/controller.ts new file mode 100644 index 0000000000..7c26322c06 --- /dev/null +++ b/backend/collaboration-service/controllers/controller.ts @@ -0,0 +1,195 @@ +import { Request, Response } from "express"; +import { MatchModel } from "../models/match"; +import { SessionModel } from "../models/session"; + +// AuthorisedUserHandler checks if a user is authorised to join the room +export const checkAuthorisedUser = async (req: Request, res: Response): Promise => { + res.setHeader("Content-Type", "application/json"); + const userId = req.query.userId as string; + const roomId = req.query.roomId as string; + + if (!userId || !roomId) { + res.status(400).json({ error: "Missing userId or roomId" }); + return; + } + + try { + const match = await MatchModel.findOne({ room_id: roomId }).exec(); + if (!match) { + res.status(404).json({ error: "Room not found" }); + return; + } + + const isAuthorised = match.userOne === userId || match.userTwo === userId; + res.json({ authorised: isAuthorised }); + } catch (error) { + console.error("Error finding room associated with user", error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; + +// GetInfoHandler retrieves the room info for a match based on the roomId +export const getInfoHandler = async (req: Request, res: Response): Promise => { + res.setHeader("Content-Type", "application/json"); + + const roomId = req.query.roomId as string; + if (!roomId) { + res.status(400).json({ error: "Missing roomId" }); + return; + } + + try { + // Find the match in the MongoDB collection + const match = await MatchModel.findOne({ room_id: roomId }).exec(); + if (!match) { + res.status(404).json({ error: "Match not found" }); + return; + } + + // Check if the question exists in the match + if (!match.question || !match.question.questionId) { + res.status(404).json({ error: "No question found for the given roomId" }); + return; + } + + if (match.status === "open") { + res.cookie('roomId', roomId, { + // httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 + }); + } + + // Send the match information as a JSON response + res.json(match); + } catch (error) { + console.error("Error finding match:", error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; + +// Get History Handler +export const getHistoryHandler = async (req: Request, res: Response): Promise => { + res.setHeader("Content-Type", "application/json"); + const username = req.query.username as string; + + if (!username) { + res.status(400).json({ error: "Missing username" }); + return; + } + + // Parse 'page' and 'limit' query parameters with default values + const page = parseInt(req.query.page as string, 10) || 1; + const limit = parseInt(req.query.limit as string, 10) || 10; + + try { + // Count total sessions matching the query + const totalSessions = await SessionModel.countDocuments({ + $or: [{ userOne: username }, { userTwo: username }] + }).exec(); + + // Calculate skip value for pagination + const skip = (page - 1) * limit; + + // Retrieve sessions with pagination and sorting by createdAt in descending order + const sessions = await SessionModel.find({ + $or: [{ userOne: username }, { userTwo: username }] + }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + + // Calculate total pages + const totalPages = Math.ceil(totalSessions / limit); + + // Send paginated response with sessions and pagination info + res.json({ + sessions, + totalPages, + currentPage: page, + totalSessions + }); + } catch (error) { + console.error("Error retrieving history:", error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; + +// Save Code Handler +export const saveCodeHandler = async (req: Request, res: Response): Promise => { + res.setHeader("Content-Type", "application/json"); + const { roomId, code, language } = req.body; + console.log(roomId + code + language); + + res.clearCookie("roomId", { + // httpOnly: true, + }); + + try { + const existingSession = await SessionModel.findOne({ room_id: roomId }).exec(); + if (existingSession) { + // If session exists, do nothing and return a success message + res.json({ message: "Session already exists", session: existingSession }); + return; + } + + const match = await MatchModel.findOne({ room_id: roomId }).exec(); + if (!match) { + res.status(404).json({ error: "Match not found" }); + return; + } + + const newSession = new SessionModel({ + userOne: match.userOne, + userTwo: match.userTwo, + room_id: roomId, + code: code, + programming_language: language, + question: match.question, + createdAt: new Date(), + }); + + await newSession.save(); + + // Update the match status to 'closed' + match.status = "closed"; + await match.save(); + + res.status(200).json({ message: "Code saved successfully, match closed", session: newSession }); + } catch (error) { + console.error("Error saving code:", error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; + +export const clearRoomIdCookie = async (req: Request, res: Response): Promise => { + res.clearCookie("roomId", { + // httpOnly: true, + }); + + res.json({ message: "RoomId cookie has been cleared" }); +}; + + +// GetSessionHandler retrieves a session based on the roomId +export const getSessionHandler = async (req: Request, res: Response): Promise => { + res.setHeader("Content-Type", "application/json"); + + const roomId = req.query.roomId as string; + if (!roomId) { + res.status(400).json({ error: "Missing roomId" }); + return; + } + + try { + const session = await SessionModel.findOne({ room_id: roomId }).exec(); + if (!session) { + res.status(404).json({ error: "Session not found" }); + return; + } + res.json(session); + } catch (error) { + console.error("Error finding session:", error); + res.status(500).json({ error: "Internal Server Error" }); + } +}; diff --git a/backend/collaboration-service/middleware/middleware.ts b/backend/collaboration-service/middleware/middleware.ts new file mode 100644 index 0000000000..f0892a8fcc --- /dev/null +++ b/backend/collaboration-service/middleware/middleware.ts @@ -0,0 +1,20 @@ +import jwt from "jsonwebtoken"; +import { Request, Response, NextFunction } from "express"; + +export function verifyAccessToken(req: Request, res: Response, next: NextFunction): void { + const token = req.cookies["accessToken"]; + + if (!token) { + res.status(401).json({ message: "Authentication failed" }); + return; + } + + jwt.verify(token, process.env.JWT_SECRET as string, (err: Error | null) => { + if (err) { + res.status(401).json({ message: "Authentication failed" }); + return; + } + + next(); + }); +} diff --git a/backend/collaboration-service/models/match.ts b/backend/collaboration-service/models/match.ts new file mode 100644 index 0000000000..b65f1b43ed --- /dev/null +++ b/backend/collaboration-service/models/match.ts @@ -0,0 +1,60 @@ +import { Schema, model } from "mongoose"; + + +// Interface for Question document +export interface Question { + questionId: string; + title: string; + description: string; + constraints: string; + examples: string; + category: string[]; + complexity: string; + imageUrl: string; + createdAt: Date; + updatedAt: Date; +} + +export interface Match { + userOne: string; + userTwo: string; + room_id: string; + programming_language: string; + complexity: string[]; + categories: string[]; + question: Question; // Single Question object + status: string; + createdAt: Date +} + +// Mongoose schema for the Question model +export const questionSchema = new Schema({ + questionId: { type: String, required: true }, + title: { type: String, required: true }, + description: { type: String, required: false }, + constraints: { type: String, required: false }, + examples: { type: String, required: false }, + category: [{ type: String, required: false }], + complexity: { type: String, required: false }, + imageUrl: { type: String, required: false }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +// Mongoose schema for the Match model, embedding a single Question +const matchSchema = new Schema({ + userOne: { type: String, required: true }, + userTwo: { type: String, required: true }, + room_id: { type: String, required: true }, + complexity: { type: [String], required: true }, + categories: { type: [String], required: true }, + programming_language: {type: String, required: true}, + question: { type: questionSchema, required: true }, // Embedded single Question document + status: {type: String, required:true}, + createdAt: { type: Date, default: Date.now }, +}); + +// Create models from schemas +export const QuestionModel = model("Question", questionSchema); +export const MatchModel = model("Match", matchSchema); + diff --git a/backend/collaboration-service/models/session.ts b/backend/collaboration-service/models/session.ts new file mode 100644 index 0000000000..6730bfe016 --- /dev/null +++ b/backend/collaboration-service/models/session.ts @@ -0,0 +1,27 @@ +import { Schema, model } from "mongoose"; +import { Question, questionSchema } from "./match"; // Import questionSchema from the Question file + +// Interface for Session document +export interface Session { + userOne: string; + userTwo: string; + room_id: string; + code: string; + programming_language: string; + question: Question; // Embedded Question object + createdAt: Date; +} + +// Mongoose schema for the Session model, embedding a single Question +const sessionSchema = new Schema({ + userOne: { type: String, required: true }, + userTwo: { type: String, required: true }, + room_id: { type: String, required: true, unique: true }, + code: { type: String }, + programming_language: { type: String, required: true }, + question: { type: questionSchema, required: true }, // Embedded single Question document + createdAt: { type: Date, default: Date.now }, +}); + +// Create the Session model from the schema +export const SessionModel = model("Session", sessionSchema); diff --git a/backend/collaboration-service/package-lock.json b/backend/collaboration-service/package-lock.json new file mode 100644 index 0000000000..a8c3e47c0d --- /dev/null +++ b/backend/collaboration-service/package-lock.json @@ -0,0 +1,2456 @@ +{ + "name": "collaboration-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "collaboration-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/ws": "^8.5.12", + "accepts": "^1.3.8", + "acorn": "^8.13.0", + "acorn-walk": "^8.3.4", + "amqplib": "^0.10.4", + "arg": "^4.1.3", + "array-flatten": "^1.1.1", + "asynckit": "^0.4.0", + "axios": "^1.7.7", + "base64id": "^2.0.0", + "body-parser": "^1.20.3", + "bson": "^6.9.0", + "buffer-equal-constant-time": "^1.0.1", + "bytes": "^3.1.2", + "call-bind": "^1.0.7", + "combined-stream": "^1.0.8", + "content-disposition": "^0.5.4", + "content-type": "^1.0.5", + "cookie": "^0.7.2", + "cookie-parser": "^1.4.7", + "cookie-signature": "^1.0.6", + "cors": "^2.8.5", + "create-require": "^1.1.1", + "debug": "^4.3.7", + "define-data-property": "^1.1.4", + "delayed-stream": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.2.0", + "diff": "^4.0.2", + "dotenv": "^16.4.5", + "ecdsa-sig-formatter": "^1.0.11", + "ee-first": "^1.1.1", + "encodeurl": "^2.0.0", + "engine.io": "^6.6.2", + "engine.io-parser": "^5.2.3", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "express": "^4.21.1", + "finalhandler": "^1.3.1", + "follow-redirects": "^1.15.9", + "form-data": "^4.0.1", + "forwarded": "^0.2.0", + "fresh": "^0.5.2", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "http-errors": "^2.0.0", + "iconv-lite": "^0.4.24", + "inherits": "^2.0.4", + "ipaddr.js": "^1.9.1", + "jsonwebtoken": "^9.0.2", + "jwa": "^1.4.1", + "jws": "^3.2.2", + "kareem": "^2.6.3", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.1.1", + "make-error": "^1.3.6", + "media-typer": "^0.3.0", + "memory-pager": "^1.5.0", + "merge-descriptors": "^1.0.3", + "methods": "^1.1.2", + "mime": "^1.6.0", + "mime-db": "^1.52.0", + "mime-types": "^2.1.35", + "mongodb": "^6.9.0", + "mongodb-connection-string-url": "^3.0.1", + "mongoose": "^8.7.3", + "mpath": "^0.9.0", + "mquery": "^5.0.0", + "ms": "^2.1.3", + "negotiator": "^0.6.3", + "object-assign": "^4.1.1", + "object-inspect": "^1.13.2", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "path-to-regexp": "^0.1.10", + "proxy-addr": "^2.0.7", + "proxy-from-env": "^1.1.0", + "punycode": "^2.3.1", + "qs": "^6.13.0", + "range-parser": "^1.2.1", + "raw-body": "^2.5.2", + "safe-buffer": "^5.2.1", + "safer-buffer": "^2.1.2", + "semver": "^7.6.3", + "send": "^0.19.0", + "serve-static": "^1.16.2", + "set-function-length": "^1.2.2", + "setprototypeof": "^1.2.0", + "side-channel": "^1.0.6", + "sift": "^17.1.3", + "socket.io": "^4.8.1", + "socket.io-adapter": "^2.5.5", + "socket.io-parser": "^4.2.4", + "sparse-bitfield": "^3.0.3", + "statuses": "^2.0.1", + "toidentifier": "^1.0.1", + "tr46": "^4.1.1", + "ts-node": "^10.9.2", + "type-is": "^1.6.18", + "typescript": "^5.6.3", + "undici-types": "^6.19.8", + "unpipe": "^1.0.0", + "utils-merge": "^1.0.1", + "v8-compile-cache-lib": "^3.0.1", + "vary": "^1.1.2", + "webidl-conversions": "^7.0.0", + "whatwg-url": "^13.0.0", + "ws": "^8.18.0", + "y-websocket": "^2.0.4", + "yn": "^3.1.1" + }, + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7" + } + }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "license": "MIT", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "optional": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bson": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", + "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "optional": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "optional": true, + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "optional": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "optional": true, + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/level-iterator-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "optional": true, + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "optional": true, + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "optional": true, + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "optional": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", + "integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", + "license": "MIT", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "optional": true, + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-websocket": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-2.0.4.tgz", + "integrity": "sha512-UbrkOU4GPNFFTDlJYAxAmzZhia8EPxHkngZ6qjrxgIYCN3gI2l+zzLzA9p4LQJ0IswzpioeIgmzekWe7HoBBjg==", + "dependencies": { + "lib0": "^0.2.52", + "lodash.debounce": "^4.0.8", + "y-protocols": "^1.0.5" + }, + "bin": { + "y-websocket": "bin/server.cjs", + "y-websocket-server": "bin/server.cjs" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1", + "y-leveldb": "^0.1.0" + }, + "peerDependencies": { + "yjs": "^13.5.6" + } + }, + "node_modules/y-websocket/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/yjs": { + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "peer": true, + "dependencies": { + "lib0": "^0.2.98" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json new file mode 100644 index 0000000000..b16bb1be45 --- /dev/null +++ b/backend/collaboration-service/package.json @@ -0,0 +1,143 @@ +{ + "name": "collaboration-service", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "@types/ws": "^8.5.12", + "accepts": "^1.3.8", + "acorn": "^8.13.0", + "acorn-walk": "^8.3.4", + "amqplib": "^0.10.4", + "arg": "^4.1.3", + "array-flatten": "^1.1.1", + "asynckit": "^0.4.0", + "axios": "^1.7.7", + "base64id": "^2.0.0", + "body-parser": "^1.20.3", + "bson": "^6.9.0", + "buffer-equal-constant-time": "^1.0.1", + "bytes": "^3.1.2", + "call-bind": "^1.0.7", + "combined-stream": "^1.0.8", + "content-disposition": "^0.5.4", + "content-type": "^1.0.5", + "cookie": "^0.7.2", + "cookie-parser": "^1.4.7", + "cookie-signature": "^1.0.6", + "cors": "^2.8.5", + "create-require": "^1.1.1", + "debug": "^4.3.7", + "define-data-property": "^1.1.4", + "delayed-stream": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.2.0", + "diff": "^4.0.2", + "dotenv": "^16.4.5", + "ecdsa-sig-formatter": "^1.0.11", + "ee-first": "^1.1.1", + "encodeurl": "^2.0.0", + "engine.io": "^6.6.2", + "engine.io-parser": "^5.2.3", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "express": "^4.21.1", + "finalhandler": "^1.3.1", + "follow-redirects": "^1.15.9", + "form-data": "^4.0.1", + "forwarded": "^0.2.0", + "fresh": "^0.5.2", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "http-errors": "^2.0.0", + "iconv-lite": "^0.4.24", + "inherits": "^2.0.4", + "ipaddr.js": "^1.9.1", + "jsonwebtoken": "^9.0.2", + "jwa": "^1.4.1", + "jws": "^3.2.2", + "kareem": "^2.6.3", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.1.1", + "make-error": "^1.3.6", + "media-typer": "^0.3.0", + "memory-pager": "^1.5.0", + "merge-descriptors": "^1.0.3", + "methods": "^1.1.2", + "mime": "^1.6.0", + "mime-db": "^1.52.0", + "mime-types": "^2.1.35", + "mongodb": "^6.9.0", + "mongodb-connection-string-url": "^3.0.1", + "mongoose": "^8.7.3", + "mpath": "^0.9.0", + "mquery": "^5.0.0", + "ms": "^2.1.3", + "negotiator": "^0.6.3", + "object-assign": "^4.1.1", + "object-inspect": "^1.13.2", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "path-to-regexp": "^0.1.10", + "proxy-addr": "^2.0.7", + "proxy-from-env": "^1.1.0", + "punycode": "^2.3.1", + "qs": "^6.13.0", + "range-parser": "^1.2.1", + "raw-body": "^2.5.2", + "safe-buffer": "^5.2.1", + "safer-buffer": "^2.1.2", + "semver": "^7.6.3", + "send": "^0.19.0", + "serve-static": "^1.16.2", + "set-function-length": "^1.2.2", + "setprototypeof": "^1.2.0", + "side-channel": "^1.0.6", + "sift": "^17.1.3", + "socket.io": "^4.8.1", + "socket.io-adapter": "^2.5.5", + "socket.io-parser": "^4.2.4", + "sparse-bitfield": "^3.0.3", + "statuses": "^2.0.1", + "toidentifier": "^1.0.1", + "tr46": "^4.1.1", + "ts-node": "^10.9.2", + "type-is": "^1.6.18", + "typescript": "^5.6.3", + "undici-types": "^6.19.8", + "unpipe": "^1.0.0", + "utils-merge": "^1.0.1", + "v8-compile-cache-lib": "^3.0.1", + "vary": "^1.1.2", + "webidl-conversions": "^7.0.0", + "whatwg-url": "^13.0.0", + "ws": "^8.18.0", + "y-websocket": "^2.0.4", + "yn": "^3.1.1" + }, + "scripts": { + "start": "ts-node app.ts", + "dev": "nodemon --exec ts-node app.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7" + } +} diff --git a/backend/collaboration-service/tsconfig.json b/backend/collaboration-service/tsconfig.json new file mode 100644 index 0000000000..56a8ab8109 --- /dev/null +++ b/backend/collaboration-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/backend/collaboration-service/utils/jwt.ts b/backend/collaboration-service/utils/jwt.ts new file mode 100644 index 0000000000..a7772f0534 --- /dev/null +++ b/backend/collaboration-service/utils/jwt.ts @@ -0,0 +1,21 @@ +import jwt from "jsonwebtoken"; + +const JWT_SECRET= process.env.JWT_SECRET as string; + +export async function authenticateAccessToken( + accessToken: string, +): Promise { + return new Promise((resolve, reject) => { + jwt.verify( + accessToken, + JWT_SECRET, + async (err: Error | null, decoded: Object | undefined) => { + if (err) { + reject(err); + } else { + resolve(decoded as Object); + } + }, + ); + }); +} \ No newline at end of file diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile new file mode 100644 index 0000000000..75488bd27c --- /dev/null +++ b/backend/matching-service/Dockerfile @@ -0,0 +1,34 @@ +# Build the Go application +FROM golang:1.20-alpine AS builder + + +# Set the working directory inside the container +WORKDIR /app + +# Copy go.mod and go.sum to download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire project +COPY . . + +# Build the Go service +RUN go build -o /matching-service ./cmd + +# Create the final image +FROM alpine:latest + +# Set the working directory +WORKDIR /root/ + +# Copy the built Go binary from the builder stage +COPY --from=builder /matching-service . + +# Copy the .env file from the backend folder +COPY --from=builder /app/.env . + +# Expose the port your application runs on +EXPOSE 3002 + +# Command to run the Go service +CMD ["./matching-service"] \ No newline at end of file diff --git a/backend/matching-service/cmd/main.go b/backend/matching-service/cmd/main.go new file mode 100644 index 0000000000..2677f61399 --- /dev/null +++ b/backend/matching-service/cmd/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "log" + "matching-service/internal/controllers" + "matching-service/internal/services" + "matching-service/internal/socket" + "matching-service/internal/middleware" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func main() { + // Connect to MongoDB + services.ConnectToMongo() + + // Connect to RabbitMQ + err := services.ConnectToRabbitMQ() + if err != nil { + log.Fatalf("Failed to connect to RabbitMQ: %v", err) + } + defer services.CloseRabbitMQ() + + // Run the WebSocket message handler in the background + go socket.HandleMessages() + + // Set up Gin router + router := gin.Default() + // Configure CORS middleware + config := cors.Config{ + AllowOrigins: []string{"http://localhost:3000", "http://localhost"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + } + + // Apply the CORS middleware to the router + router.Use(cors.New(config)) + // WebSocket route to handle connections + router.GET("/ws", socket.HandleConnections) + + // Apply authentication middleware to all routes that need protection + authRoutes := router.Group("/") + authRoutes.Use(middleware.VerifyAccessToken) // Apply authentication to all routes within this group + { + // Route for adding users (requires authentication) + authRoutes.POST("/addUser", controllers.AddUserHandler) + + // Route for cancelling a user (requires authentication) + authRoutes.POST("/cancel/:userID", controllers.CancelMatchHandler) + } + + + // Start the server + log.Println("Server started on :3002") + router.Run(":3002") +} diff --git a/backend/matching-service/go.mod b/backend/matching-service/go.mod new file mode 100644 index 0000000000..86344b76e8 --- /dev/null +++ b/backend/matching-service/go.mod @@ -0,0 +1,51 @@ +module matching-service + +go 1.20 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 + github.com/streadway/amqp v1.1.0 + go.mongodb.org/mongo-driver v1.17.1 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/cors v1.7.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/matching-service/go.sum b/backend/matching-service/go.sum new file mode 100644 index 0000000000..cc69c582cf --- /dev/null +++ b/backend/matching-service/go.sum @@ -0,0 +1,141 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= +github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/matching-service/internal/controllers/match_controller.go b/backend/matching-service/internal/controllers/match_controller.go new file mode 100644 index 0000000000..70ecd05c9a --- /dev/null +++ b/backend/matching-service/internal/controllers/match_controller.go @@ -0,0 +1,223 @@ +package controllers + +import ( + "log" + "matching-service/internal/models" + "matching-service/internal/services" + "matching-service/internal/socket" + "matching-service/internal/utils" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// AddUserHandler handles the request to add a user and start matching +func AddUserHandler(c *gin.Context) { + var matchingInfo models.MatchingInfo + if err := c.ShouldBindJSON(&matchingInfo); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + matchingInfo.Status = models.Pending + matchingInfo.RoomID = uuid.New().String() + + services.DeleteUnusedFromDB() + + // Insert matching info into MongoDB + _, err := services.InsertMatching(matchingInfo) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + go startMatchingProcess(matchingInfo) + + c.JSON(200, gin.H{"message": "User added", "user_id": matchingInfo.UserID, "socket_id": matchingInfo.SocketID}) +} + +// CancelMatchHandler handles the request to cancel a user's match search +func CancelMatchHandler(c *gin.Context) { + userID := c.Param("userID") + + // update the user's status to 'Cancelled' in MongoDB + err := services.CancelUserMatch(userID) + if err != nil { + log.Printf("Error canceling match for user_id: %s, error: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cancel match"}) + return + } + + log.Printf("User %s has successfully canceled their match", userID) + c.JSON(http.StatusOK, gin.H{"message": "Match search canceled"}) +} + +// findIntersection returns the intersection of two slices of strings +func findIntersection(a, b []string) []string { + set := make(map[string]bool) + intersection := []string{} + + for _, item := range a { + set[item] = true + } + for _, item := range b { + if set[item] { + intersection = append(intersection, item) + } + } + + return intersection +} + +// findComplexityIntersection returns the intersection of two slices of QuestionComplexityEnum +func findComplexityIntersection(a, b []models.QuestionComplexityEnum) []models.QuestionComplexityEnum { + set := make(map[models.QuestionComplexityEnum]bool) + intersection := []models.QuestionComplexityEnum{} + + for _, item := range a { + set[item] = true + } + + for _, item := range b { + if set[item] { + intersection = append(intersection, item) + } + } + + return intersection +} + +func convertEnumToStringSlice(enums []models.ProgrammingLanguageEnum) []string { + strSlice := make([]string, len(enums)) + for i, e := range enums { + strSlice[i] = string(e) // Assuming ProgrammingLanguageEnum can be cast to string + } + return strSlice +} + +// startMatchingProcess starts the matching logic with a timeout +func startMatchingProcess(matchingInfo models.MatchingInfo) { + matchChan := make(chan *models.MatchingInfo) + + // start a goroutine to attempt matching the user + go func() { + result, err := services.FindMatch(matchingInfo) + if err != nil || result == nil { + matchChan <- nil + return + } + + if len(result.ProgrammingLanguages) == 0 && len(matchingInfo.ProgrammingLanguages) == 0 { + // default language JavaScript + result.ProgrammingLanguages = []models.ProgrammingLanguageEnum{models.JavaScript} + matchingInfo.ProgrammingLanguages = []models.ProgrammingLanguageEnum{models.JavaScript} + log.Println("both set to js") + matchChan <- result + return + } + + // If generalization is allowed or languages match, proceed with the match + matchChan <- result + + }() + + // set up a 30-second timeout + select { + case matchedUser := <-matchChan: + if matchedUser != nil { + log.Printf("Found a match for user_id: %s", matchingInfo.UserID) + + // check if both users are still Pending before proceeding + user1Status, err := services.GetUserStatus(matchingInfo.UserID) + if err != nil { + log.Printf("Error retrieving status for user_id: %s", matchingInfo.UserID) + return + } + + user2Status, err := services.GetUserStatus(matchedUser.UserID) + if err != nil { + log.Printf("Error retrieving status for user_id: %s", matchedUser.UserID) + return + } + + if user1Status == models.Cancelled || user2Status == models.Cancelled { + log.Printf("User %s or %s has cancelled, match discarded", matchingInfo.UserID, matchedUser.UserID) + return + } + + // cancel the timeout for both users + if timer, ok := utils.Store[matchingInfo.UserID]; ok { + timer.Stop() + delete(utils.Store, matchingInfo.UserID) + } + + if timer, ok := utils.Store[matchedUser.UserID]; ok { + timer.Stop() + delete(utils.Store, matchedUser.UserID) + } + + roomID := matchingInfo.RoomID + + // i[date the status and room_id of both users in MongoDB (only after the match is confirmed) + err = services.UpdateMatchStatusAndRoomID(matchingInfo.UserID, matchedUser.UserID, roomID) + if err != nil { + log.Printf("Error updating status for user_id: %s and user_id: %s", matchingInfo.UserID, matchedUser.UserID) + } + + // dind the intersection of complexities and categories + complexityIntersection := findComplexityIntersection(matchingInfo.DifficultyLevel, matchedUser.DifficultyLevel) + categoriesIntersection := findIntersection(matchingInfo.Categories, matchedUser.Categories) + programmingLanguageIntersection := findIntersection(convertEnumToStringSlice(matchedUser.ProgrammingLanguages), convertEnumToStringSlice(matchingInfo.ProgrammingLanguages)) + + var selectedLanguage models.ProgrammingLanguageEnum + if len(programmingLanguageIntersection) > 0 { + selectedLanguage = models.ProgrammingLanguageEnum(programmingLanguageIntersection[0]) + } + + matchResult := models.MatchResult{ + UserOneSocketID: matchingInfo.SocketID, + UserTwoSocketID: matchedUser.SocketID, + UserOne: matchingInfo.UserID, + UsernameOne: matchingInfo.Username, + UserTwo: matchedUser.UserID, + UsernameTwo: matchedUser.Username, + RoomID: roomID, + ProgrammingLanguages: selectedLanguage, + Complexity: complexityIntersection, + Categories: categoriesIntersection, + Question: models.Question{}, + } + + // Publish the match result to RabbitMQ + err = services.PublishMatch(matchResult) + if err != nil { + log.Printf("Error publishing match result to RabbitMQ: %v", err) + } + + // Send match result to WebSocket clients + socket.BroadcastMatch(socket.MatchMessage{ + User1: matchingInfo.SocketID, + User2: matchedUser.SocketID, + RoomId: matchResult.RoomID, + State: "Matched", + }) + + log.Printf("User %s and User %s have been matched and published to RabbitMQ", matchingInfo.UserID, matchedUser.UserID) + + } else { + // no match was found within the matchChan logic + log.Printf("No match found for user_id: %s within matchChan logic", matchingInfo.UserID) + + time.Sleep(30 * time.Second) // Give the match process time to continue + services.MarkAsTimeout(matchingInfo) + log.Printf("User %s has been marked as Timeout", matchingInfo.UserID) + } + case <-time.After(30 * time.Second): + // timeout after 30 seconds, no match was found + log.Printf("Timeout occurred for user_id: %s, no match found", matchingInfo.UserID) + services.MarkAsTimeout(matchingInfo) + log.Printf("User %s has been marked as Timeout (Timeout elapsed)", matchingInfo.UserID) + } +} diff --git a/backend/matching-service/internal/middleware/authMiddleware.go b/backend/matching-service/internal/middleware/authMiddleware.go new file mode 100644 index 0000000000..4c332493fc --- /dev/null +++ b/backend/matching-service/internal/middleware/authMiddleware.go @@ -0,0 +1,79 @@ +package middleware + +import ( + "errors" + "log" + "net/http" + "os" + "time" + + "matching-service/internal/models" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" +) + +// VerifyAccessToken checks the validity of the JWT token for Gin context +func VerifyAccessToken(c *gin.Context) { + // Extract the access token from the 'accessToken' cookie + accessToken, err := c.Cookie("accessToken") + if err != nil { + log.Println("Access token cookie missing or invalid") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized, no access token"}) + c.Abort() // Stop further processing + return + } + + // Authenticate the access token from the cookie value + token, err := authenticateAccessToken(accessToken) + if err != nil { + log.Printf("Failed to authenticate token: %v", err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + c.Abort() // Stop further processing + return + } + + // Store the user information in the Gin context if the token is valid + if claims, ok := token.Claims.(*models.JwtPayload); ok && token.Valid { + log.Printf("Token is valid for user: %s (ID: %s)", claims.User.Username, claims.User.ID.Hex()) + // Store the user in the Gin context for access in subsequent handlers + c.Set("user", claims.User) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() // Stop further processing + return + } + + // Continue to the next handler in the chain + c.Next() +} + +// authenticateAccessToken verifies the JWT token and returns the parsed token +func authenticateAccessToken(tokenString string) (*jwt.Token, error) { + // Parse the token with the expected claims + token, err := jwt.ParseWithClaims(tokenString, &models.JwtPayload{}, func(token *jwt.Token) (interface{}, error) { + // Ensure the signing method is HMAC (e.g., HS256) + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + // Return the JWT secret to validate the token signature + return []byte(os.Getenv("JWT_SECRET")), nil + }) + + // Return any errors that occurred during parsing + if err != nil { + return nil, err + } + + // Ensure the token is still valid (i.e., not expired) + if claims, ok := token.Claims.(*models.JwtPayload); ok && token.Valid { + if time.Now().Unix() > claims.Exp { + log.Printf("Token has expired. Current time: %d, Expiration time: %d", time.Now().Unix(), claims.Exp) + return nil, errors.New("token has expired") + } + // Return the valid token + return token, nil + } + + // Return an error if the token is invalid + return nil, errors.New("invalid token") +} diff --git a/backend/matching-service/internal/models/matching_model.go b/backend/matching-service/internal/models/matching_model.go new file mode 100644 index 0000000000..d2b9f94c5b --- /dev/null +++ b/backend/matching-service/internal/models/matching_model.go @@ -0,0 +1,75 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type MatchStatusEnum string + +const ( + Matched MatchStatusEnum = "Matched" + Pending MatchStatusEnum = "Pending" + Timeout MatchStatusEnum = "Timeout" + Cancelled MatchStatusEnum = "Cancelled" +) + +type QuestionComplexityEnum string + +const ( + Easy QuestionComplexityEnum = "Easy" + Medium QuestionComplexityEnum = "Medium" + Hard QuestionComplexityEnum = "Hard" +) + +// ProgrammingLanguageEnum contains 8 common programming languages +type ProgrammingLanguageEnum string + +const ( + Go ProgrammingLanguageEnum = "go" + Python ProgrammingLanguageEnum = "python" + Java ProgrammingLanguageEnum = "java" + Cpp ProgrammingLanguageEnum = "c++" + JavaScript ProgrammingLanguageEnum = "js" + Ruby ProgrammingLanguageEnum = "ruby" + CSharp ProgrammingLanguageEnum = "c#" +) + +// MatchingInfo struct includes user matching criteria +type MatchingInfo struct { + UserID string `json:"user_id" bson:"user_id"` + Username string `json: "username" bson "username"` + SocketID string `json:"socket_id" bson:"socket_id"` + DifficultyLevel []QuestionComplexityEnum `json:"difficulty_levels" bson:"difficulty_levels"` + Categories []string `json:"categories" bson:"categories"` + ProgrammingLanguages []ProgrammingLanguageEnum `json:"programming_languages" bson:"programming_languages"` + GeneralizeLanguages bool `json:"generalize_languages" bson:"generalize_languages"` + Status MatchStatusEnum `json:"status" bson:"status"` + RoomID string `json:"room_id" bson:"room_id"` +} + +type Question struct { + QuestionID string `json:"questionId" bson:"questionId"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Constraints string `json:"constraints" bson:"constraints"` + Examples string `json:"examples" bson:"examples"` + Category []string `json:"category" bson:"category"` + Complexity string `json:"complexity" bson:"complexity"` + ImageURL string `json:"imageUrl" bson:"imageUrl"` + CreatedAt primitive.DateTime `json:"createdAt" bson:"createdAt"` + UpdatedAt primitive.DateTime `json:"updatedAt" bson:"updatedAt"` +} + +type MatchResult struct { + UserOneSocketID string `json:"user_one_socket_id"` + UserTwoSocketID string `json:"user_two_socket_id"` + UserOne string `bson:"userOne"` + UsernameOne string `bson:"usernameOne` + UserTwo string `bson:"userTwo"` + UsernameTwo string `bson:"usernameTwo` + RoomID string `bson:"room_id"` + ProgrammingLanguages ProgrammingLanguageEnum `bson:"programming_languages"` + Complexity []QuestionComplexityEnum`bson:"complexity"` + Categories []string `bson:"categories"` + Question Question `bson:"question"` +} diff --git a/backend/matching-service/internal/models/userModel.go b/backend/matching-service/internal/models/userModel.go new file mode 100644 index 0000000000..3bbb7fc1c2 --- /dev/null +++ b/backend/matching-service/internal/models/userModel.go @@ -0,0 +1,33 @@ +package models + +import ( + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// UserWithoutPassword struct to match the payload (excluding password) +type UserWithoutPassword struct { + ID primitive.ObjectID `json:"id" bson:"id"` // MongoDB ObjectID + Username string `json:"username" bson:"username"` + Email string `json:"email" bson:"email"` + IsAdmin bool `json:"isAdmin" bson:"isAdmin"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +// JwtPayload struct to match the JWT payload +type JwtPayload struct { + User UserWithoutPassword `json:"user"` + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` +} + +// Valid function to check if the token is expired +func (p JwtPayload) Valid() error { + if p.Exp < time.Now().Unix() { + return errors.New("token has expired") + } + return nil +} + diff --git a/backend/matching-service/internal/services/mongo_service.go b/backend/matching-service/internal/services/mongo_service.go new file mode 100644 index 0000000000..54ede49395 --- /dev/null +++ b/backend/matching-service/internal/services/mongo_service.go @@ -0,0 +1,300 @@ +package services + +import ( + "context" + "log" + "matching-service/internal/models" + "os" + "time" + + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var MongoClient *mongo.Client +var MatchingCollection *mongo.Collection + +// ConnectToMongo initializes the MongoDB connection and collection +func ConnectToMongo() { + err := godotenv.Load(".env") + if err != nil { + log.Fatalf("Error loading .env file: %v", err) + } + + // Get MongoDB URI and database name from environment variables + mongoURI := os.Getenv("MONGO_URI_MS") + dbName := os.Getenv("MONGO_DBNAME") + + if mongoURI == "" || dbName == "" { + log.Fatalf("MongoDB URI or DB name not set in .env file") + } + + // Set MongoDB client options + clientOptions := options.Client().ApplyURI(mongoURI) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Connect to MongoDB + MongoClient, err = mongo.Connect(ctx, clientOptions) + if err != nil { + log.Fatalf("Failed to connect to MongoDB: %v", err) + } + + // Ping MongoDB to verify connection + err = MongoClient.Ping(ctx, nil) + if err != nil { + log.Fatalf("Error pinging MongoDB: %v", err) + } + + // Initialize the collection + MatchingCollection = MongoClient.Database(dbName).Collection("matching") + log.Printf("Connected to database: %s, collection: %s", dbName, MatchingCollection.Name()) + log.Println("Connected to MongoDB") +} + +// InsertMatching inserts a matching entry into the MongoDB collection +func InsertMatching(matchingInfo models.MatchingInfo) (*models.MatchingInfo, error) { + log.Printf("Inserting document: %+v", matchingInfo) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Insert the matchingInfo into the MongoDB collection + result, err := MatchingCollection.InsertOne(ctx, matchingInfo) + if err != nil { + log.Printf("Error inserting matching info: %v", err) + return nil, err + } + + log.Printf("MongoDB Insert Result: %+v", result) + log.Printf("Inserted matching info for user_id: %s", matchingInfo.UserID) + return &matchingInfo, nil +} + +// FindMatch finds a pending match for a user based on difficulty and topics +func FindMatch(matchingInfo models.MatchingInfo) (*models.MatchingInfo, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Base filter criteria for finding a match + filter := bson.M{ + "status": models.Pending, + "user_id": bson.M{ + "$ne": matchingInfo.UserID, + }, + "difficulty_levels": bson.M{ + "$in": matchingInfo.DifficultyLevel, + }, + "categories": bson.M{ + "$in": matchingInfo.Categories, + }, + } + + if !matchingInfo.GeneralizeLanguages { + filter["$or"] = []bson.M{ + {"programming_languages": bson.M{"$in": matchingInfo.ProgrammingLanguages}}, + {"generalize_languages": true}, + } + } + + // Try to find a matching user + var potentialMatch models.MatchingInfo + err := MatchingCollection.FindOne(ctx, filter).Decode(&potentialMatch) + if err != nil { + log.Printf("No matching user found for user_id: %s", matchingInfo.UserID) + return nil, nil + } + + // Mark the potential match as matched + _, err = MatchingCollection.UpdateOne(ctx, bson.M{"user_id": potentialMatch.UserID}, bson.M{ + "$set": bson.M{"status": models.Matched}, + }) + + if err != nil { + log.Printf("Error updating match status for user_id %s: %v", potentialMatch.UserID, err) + return nil, err + } + + log.Printf("Found match for user_id: %s", matchingInfo.UserID) + return &potentialMatch, nil +} + +func MarkAsTimeout(matchingInfo models.MatchingInfo) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Recheck the user's status to ensure they are still Pending + currentStatus, err := GetUserStatus(matchingInfo.UserID) + if err != nil { + return err + } + + // Only mark as Timeout if the user is still Pending + if currentStatus != models.Pending { + log.Printf("User %s is not Pending, skipping Timeout marking", matchingInfo.UserID) + return nil + } + + // Update the user's status to Timeout in MongoDB + _, err = MatchingCollection.UpdateOne(ctx, bson.M{ + "user_id": matchingInfo.UserID, + "status": models.Pending, + }, bson.M{ + "$set": bson.M{"status": models.Timeout}, + }) + + if err != nil { + log.Printf("Error marking user_id %s as Timeout: %v", matchingInfo.UserID, err) + return err + } + + log.Printf("User %s has been marked as Timeout", matchingInfo.UserID) + // Delete the user from MongoDB after status change + err = deleteUserFromDB(matchingInfo.UserID) + if err != nil { + log.Printf("Error deleting user with user_id %s: %v", matchingInfo.UserID, err) + return err + } + + log.Printf("User with user_id %s has been deleted from the database", matchingInfo.UserID) + return nil +} + +// UpdateMatchStatusAndRoomID updates the room_id of both users in MongoDB and deletes them after matching +func UpdateMatchStatusAndRoomID(userID1, userID2, roomID string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + users := []string{userID1, userID2} + for _, userID := range users { + // Set the filter and update instructions for each user + filter := bson.M{"user_id": userID, "status": models.Pending} // Ensure user is Pending + update := bson.M{ + "$set": bson.M{ + "status": models.Matched, // Set status to Matched + "room_id": roomID, // Update the room_id + }, + } + + // Attempt to update the user's status and room ID + _, err := MatchingCollection.UpdateOne(ctx, filter, update) + if err != nil { + log.Printf("Error updating match status and room_id for user_id %s: %v", userID, err) + return err + } + log.Printf("Updated user_id %s status to 'Matched' and room_id to %s", userID, roomID) + + // Delete the user after successfully updating their match status and room ID + err = deleteUserFromDB(userID) + if err != nil { + log.Printf("Error deleting user with user_id %s: %v", userID, err) + return err + } + log.Printf("User with user_id %s has been deleted from the database", userID) + } + + return nil +} + +// GetUserStatus retrieves the current status of the user from MongoDB +func GetUserStatus(userID string) (models.MatchStatusEnum, error) { + // Set up the MongoDB collection (assuming you have a users collection) + collection := MatchingCollection + // Create a context with a timeout for the query + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Filter for the userID + filter := bson.M{"user_id": userID} + + // Define a struct to hold the result + var result struct { + Status models.MatchStatusEnum `bson:"status"` + } + + // Query MongoDB for the user's status + err := collection.FindOne(ctx, filter).Decode(&result) + if err != nil { + if err == mongo.ErrNoDocuments { + log.Printf("No user found with user_id: %s", userID) + return "", nil + } + log.Printf("Error retrieving status for user_id: %s, error: %v", userID, err) + return "", err + } + + // Return the status + return result.Status, nil +} + +// CancelUserMatch updates the user's status to 'Cancelled' in MongoDB +func CancelUserMatch(userID string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Update the user's status to 'Cancelled' if they are currently 'Pending' + filter := bson.M{ + "user_id": userID, + "status": models.Pending, + } + update := bson.M{ + "$set": bson.M{"status": models.Cancelled}, + } + + // perform the update in MongoDB + result, err := MatchingCollection.UpdateOne(ctx, filter, update) + if err != nil { + log.Printf("Error updating status to 'Cancelled' for user_id: %s, error: %v", userID, err) + return err + } + + // check if a document was actually updated (i.e., if the user was in 'Pending' status) + if result.ModifiedCount == 0 { + log.Printf("No pending match found for user_id: %s, unable to cancel", userID) + return nil + } + + log.Printf("User %s has been successfully marked as 'Cancelled'", userID) + // delete the user from MongoDB after status change + err = deleteUserFromDB(userID) + if err != nil { + log.Printf("Error deleting user with user_id %s: %v", userID, err) + return err + } + + log.Printf("User with user_id %s has been deleted from the database", userID) + return nil +} + +// deleteUserFromDB deletes a user from MongoDB by user_id +func deleteUserFromDB(userID string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + filter := bson.M{"user_id": userID} + _, err := MatchingCollection.DeleteOne(ctx, filter) + if err != nil { + log.Printf("Error deleting user_id %s from the database: %v", userID, err) + return err + } + + log.Printf("Successfully deleted user_id %s from the database", userID) + return nil +} + +func DeleteUnusedFromDB() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + filter := bson.M{"status": bson.M{"$ne": "Pending"}} + + result, err := MatchingCollection.DeleteMany(ctx, filter) + if err != nil { + log.Printf("Error deleting unused entries from the database: %v", err) + return err + } + + log.Printf("Successfully deleted %d unused entries from the database", result.DeletedCount) + return nil +} diff --git a/backend/matching-service/internal/services/rabbitmq_service.go b/backend/matching-service/internal/services/rabbitmq_service.go new file mode 100644 index 0000000000..1564e744d1 --- /dev/null +++ b/backend/matching-service/internal/services/rabbitmq_service.go @@ -0,0 +1,86 @@ +package services + +import ( + "encoding/json" + "fmt" + "log" + "matching-service/internal/models" + "os" + + "github.com/joho/godotenv" + "github.com/streadway/amqp" +) + +var RabbitMQConn *amqp.Connection +var RabbitMQChannel *amqp.Channel + +// ConnectToRabbitMQ initializes RabbitMQ connection and channel +func ConnectToRabbitMQ() error { + // Load environment variables + err := godotenv.Load(".env") + if err != nil { + return fmt.Errorf("error loading .env file: %v", err) + } + + rabbitMQURL := os.Getenv("RABBITMQ_URL") + if rabbitMQURL == "" { + return fmt.Errorf("RABBITMQ_URL not set in environment") + } + + // Establish connection to RabbitMQ + RabbitMQConn, err = amqp.Dial(rabbitMQURL) + if err != nil { + return fmt.Errorf("failed to connect to RabbitMQ: %v", err) + } + + // Open a channel + RabbitMQChannel, err = RabbitMQConn.Channel() + if err != nil { + return fmt.Errorf("failed to open a RabbitMQ channel: %v", err) + } + + log.Println("Connected to RabbitMQ") + return nil +} + +// PublishMatch publishes a match result to RabbitMQ +func PublishMatch(matchResult models.MatchResult) error { + if RabbitMQChannel == nil { + return fmt.Errorf("RabbitMQ channel is not initialized") + } + + // Serialize the matchResult to JSON + body, err := json.Marshal(matchResult) + if err != nil { + return fmt.Errorf("failed to marshal match result: %v", err) + } + + // Publish the message to RabbitMQ + err = RabbitMQChannel.Publish( + "", // exchange + "match_queue", // routing key (queue name) + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: body, + }, + ) + if err != nil { + return fmt.Errorf("failed to publish match result: %v", err) + } + + log.Printf("Published match result to RabbitMQ: %+v", matchResult) + return nil +} + +// CloseRabbitMQ closes the RabbitMQ connection and channel +func CloseRabbitMQ() { + if RabbitMQChannel != nil { + RabbitMQChannel.Close() + } + if RabbitMQConn != nil { + RabbitMQConn.Close() + } + log.Println("RabbitMQ connection closed") +} diff --git a/backend/matching-service/internal/socket/socket.go b/backend/matching-service/internal/socket/socket.go new file mode 100644 index 0000000000..f37cc04070 --- /dev/null +++ b/backend/matching-service/internal/socket/socket.go @@ -0,0 +1,89 @@ +package socket + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +var clients = make(map[string]*websocket.Conn) // Map to hold WebSocket clients by their socket_id +var broadcast = make(chan MatchMessage) // Channel for broadcasting messages + +type MatchMessage struct { + User1 string `json:"user1"` + User2 string `json:"user2"` + RoomId string `json:"roomId"` + State string `json:"state"` +} + +// WebSocket upgrader (to upgrade HTTP connections to WebSockets) +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// HandleConnections manages incoming WebSocket requests and upgrades them to WebSocket connections +func HandleConnections(c *gin.Context) { + // Extract socket_id from the query parameters + socketID := c.Query("socket_id") + if socketID == "" { + log.Println("No socket_id provided") + c.JSON(http.StatusBadRequest, gin.H{"error": "socket_id is required"}) + return + } + + // Upgrade the HTTP connection to a WebSocket connection + ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Println("Failed to upgrade WebSocket connection:", err) + return + } + defer ws.Close() + + // Register the client by socket_id + clients[socketID] = ws + log.Printf("New WebSocket connection established for socket_id: %s", socketID) + + // Listen for messages from the client (in this case, we just wait for the client to disconnect) + for { + _, _, err := ws.ReadMessage() + if err != nil { + log.Printf("WebSocket connection closed for socket_id: %s", socketID) + delete(clients, socketID) + break + } + } +} + +// BroadcastMatch sends a match result to all connected WebSocket clients +func BroadcastMatch(msg MatchMessage) { + broadcast <- msg +} + +// HandleMessages listens for new messages and broadcasts them to all clients +func HandleMessages() { + for { + msg := <-broadcast + + // Send the message to the two matched clients + if conn, ok := clients[msg.User1]; ok { + err := conn.WriteJSON(msg) + if err != nil { + log.Printf("Error sending WebSocket message to user %s: %v", msg.User1, err) + conn.Close() + delete(clients, msg.User1) + } + } + if conn, ok := clients[msg.User2]; ok { + err := conn.WriteJSON(msg) + if err != nil { + log.Printf("Error sending WebSocket message to user %s: %v", msg.User2, err) + conn.Close() + delete(clients, msg.User2) + } + } + } +} diff --git a/backend/matching-service/internal/utils/store.go b/backend/matching-service/internal/utils/store.go new file mode 100644 index 0000000000..257c5046e6 --- /dev/null +++ b/backend/matching-service/internal/utils/store.go @@ -0,0 +1,6 @@ +package utils + +import "time" + +// Store maps user IDs to their associated timers +var Store = make(map[string]*time.Timer) diff --git a/backend/matching-service/matching-service b/backend/matching-service/matching-service new file mode 100755 index 0000000000..b67426a9a4 Binary files /dev/null and b/backend/matching-service/matching-service differ diff --git a/backend/peer-server/Dockerfile b/backend/peer-server/Dockerfile new file mode 100644 index 0000000000..b88f264111 --- /dev/null +++ b/backend/peer-server/Dockerfile @@ -0,0 +1,13 @@ +FROM node:14 + +# Install PeerServer +RUN npm install -g peer + +# Copy configuration file +COPY config.json /config.json + +# Expose PeerServer port +EXPOSE 9000 + +# Start PeerServer with custom configuration +CMD ["peerjs", "--port", "9000", "--path", "/", "--config", "/config.json"] diff --git a/backend/peer-server/config.json b/backend/peer-server/config.json new file mode 100644 index 0000000000..b776bebe65 --- /dev/null +++ b/backend/peer-server/config.json @@ -0,0 +1,8 @@ +{ + "secure": false, + "port": 9000, + "path": "/", + "key": "peerjs", + "allow_discovery": true + } + \ No newline at end of file diff --git a/backend/question-service/.env.example b/backend/question-service/.env.example new file mode 100644 index 0000000000..5d14be96e1 --- /dev/null +++ b/backend/question-service/.env.example @@ -0,0 +1,6 @@ +# Replace MONGO_URI with your MongoDB connection string. Otherwise: +# Replace [username:password@] with your MongoDB Atlas username and password +# Replace host1[:port1][,...hostN[:portN]] with your MongoDB Atlas cluster URI +# Replace [defaultauthdb] with your MongoDB Atlas database name +# Replace [?options] with your MongoDB Atlas connection options +MONGO_URI=mongodb://[username:password]@host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]] \ No newline at end of file diff --git a/backend/question-service/.gitignore b/backend/question-service/.gitignore new file mode 100644 index 0000000000..86dc54edc1 --- /dev/null +++ b/backend/question-service/.gitignore @@ -0,0 +1,5 @@ +# dependencies +/node_modules + +# local env files +.env*.local \ No newline at end of file diff --git a/backend/question-service/Dockerfile b/backend/question-service/Dockerfile new file mode 100644 index 0000000000..f8c650508b --- /dev/null +++ b/backend/question-service/Dockerfile @@ -0,0 +1,33 @@ +# Build the Go application +FROM golang:1.18-alpine AS builder + +# Set the working directory inside the container +WORKDIR /app + +# Copy go.mod and go.sum to download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire project +COPY . . + +# Build the Go service +RUN go build -o /question-service ./cmd + +# Create the final image +FROM alpine:latest + +# Set the working directory +WORKDIR /root/ + +# Copy the built Go binary from the builder stage +COPY --from=builder /question-service . + +# Copy the .env file from the backend folder +COPY --from=builder /app/.env . + +# Expose the port your application runs on +EXPOSE 5050 + +# Command to run the Go service +CMD ["./question-service"] diff --git a/LICENSE b/backend/question-service/LICENSE similarity index 100% rename from LICENSE rename to backend/question-service/LICENSE diff --git a/backend/question-service/README.md b/backend/question-service/README.md new file mode 100644 index 0000000000..3e20e8bc70 --- /dev/null +++ b/backend/question-service/README.md @@ -0,0 +1 @@ +Backend services \ No newline at end of file diff --git a/backend/question-service/cmd/main.go b/backend/question-service/cmd/main.go new file mode 100644 index 0000000000..73dc5ac971 --- /dev/null +++ b/backend/question-service/cmd/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "time" + + "question-service/internal/controllers" + "question-service/internal/routes" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var questionCollection *mongo.Collection + +func main() { + log.Println("Starting the Go application...") + + // Load environment variables from the .env file + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file:", err) + } + + // Get the MongoDB URI from the environment variable + mongoURI := os.Getenv("MONGO_URI") + if mongoURI == "" { + log.Fatal("MONGO_URI not set in .env file") + } + + // Set up MongoDB client options + clientOptions := options.Client().ApplyURI(mongoURI) + + // Connect to MongoDB + client, err := mongo.Connect(context.TODO(), clientOptions) + if err != nil { + log.Fatal("Error connecting to MongoDB:", err) + } + + // Ping MongoDB to ensure the connection is established + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err = client.Ping(ctx, nil) + if err != nil { + log.Fatal("Error pinging MongoDB:", err) + } + + // Initialize the MongoDB collection (questions) + questionCollection = client.Database("questiondb").Collection("questions") + if questionCollection == nil { + log.Fatal("Failed to initialize questionCollection") + } + log.Println("questionCollection initialized successfully") + + // Set the collection in the controller + controllers.SetCollection(questionCollection) + + // Initialize the router + r := mux.NewRouter() + + // Enable CORS + headers := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}) + methods := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}) + origins := handlers.AllowedOrigins([]string{"http://localhost:3000", "http://localhost:8080"}) // Allow all origins or specify your frontend's origin + credentials := handlers.AllowCredentials() // Enable credentials + + // Register the routes for the API + routes.RegisterQuestionRoutes(r) + + // Start the HTTP server + port := os.Getenv("PORT") + if port == "" { + port = "5050" + } + log.Println("Starting the server on port", port) + log.Fatal(http.ListenAndServe(":5050", handlers.CORS(headers, methods, origins, credentials)(r))) +} diff --git a/backend/question-service/config/db.go b/backend/question-service/config/db.go new file mode 100644 index 0000000000..259fba79ea --- /dev/null +++ b/backend/question-service/config/db.go @@ -0,0 +1,47 @@ +package config + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// DB holds the MongoDB client instance +var DB *mongo.Client = ConnectDB() + +// ConnectDB connects to MongoDB and returns the client +func ConnectDB() *mongo.Client { + mongoURI := os.Getenv("MONGO_URI") + + clientOptions := options.Client().ApplyURI(mongoURI) + client, err := mongo.NewClient(clientOptions) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err = client.Connect(ctx) + if err != nil { + log.Fatal(err) + } + + err = client.Ping(ctx, nil) + if err != nil { + log.Fatal(err) + } + + fmt.Println("Connected to MongoDB!") + return client +} + +// GetCollection returns a MongoDB collection +func GetCollection(client *mongo.Client, collectionName string) *mongo.Collection { + return client.Database("questiondb").Collection(collectionName) +} diff --git a/backend/question-service/go.mod b/backend/question-service/go.mod new file mode 100644 index 0000000000..a4773b1db6 --- /dev/null +++ b/backend/question-service/go.mod @@ -0,0 +1,29 @@ +module question-service + +go 1.23 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 // direct + github.com/joho/godotenv v1.5.1 // direct +) + +require ( + github.com/gorilla/handlers v1.5.1 + go.mongodb.org/mongo-driver v1.16.1 +) + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/backend/question-service/go.sum b/backend/question-service/go.sum new file mode 100644 index 0000000000..85b89be9a2 --- /dev/null +++ b/backend/question-service/go.sum @@ -0,0 +1,61 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= +go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/backend/question-service/internal/controllers/createQuestion.go b/backend/question-service/internal/controllers/createQuestion.go new file mode 100644 index 0000000000..ed8585e583 --- /dev/null +++ b/backend/question-service/internal/controllers/createQuestion.go @@ -0,0 +1,83 @@ +package controllers + +import ( + "context" + "encoding/json" + "log" + "net/http" + "strings" + "time" + + "question-service/internal/models" + + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var questionCollection *mongo.Collection + +// SetCollection sets the MongoDB collection in the controller +func SetCollection(collection *mongo.Collection) { + questionCollection = collection +} + +// CreateQuestion handles the POST request to create a new question +func CreateQuestion(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + var question models.Question + + log.Println("Received a request to create a question...") + + // Decode the request body into the Question struct + err := json.NewDecoder(r.Body).Decode(&question) + if err != nil { + log.Println("Error decoding request body:", err) + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + log.Println("Decoded request body successfully...") + + // Convert the title to lowercase for the check + lowercaseTitle := strings.ToLower(question.Title) + + existingQuestion := bson.M{} + filter := bson.M{"$expr": bson.M{ + "$eq": []interface{}{ + bson.M{"$toLower": "$title"}, // Convert the title in the database to lowercase + lowercaseTitle, // Compare with the lowercase title + }, + }} + err = questionCollection.FindOne(context.TODO(), filter).Decode(&existingQuestion) + if err == nil { + // If we find a document, the title is already in use + log.Println("A question with this title already exists") + http.Error(w, "A question with this title already exists", http.StatusBadRequest) + return + } else if err != mongo.ErrNoDocuments { + // If there is an error other than no documents found + log.Println("Error checking for existing question:", err) + http.Error(w, "Failed to check existing question", http.StatusInternalServerError) + return + } + + // Set creation and update timestamps + question.CreatedAt = time.Now() + question.UpdatedAt = time.Now() + + // Generate a UUID for the question + question.QuestionID = uuid.New().String() + + // Insert the question into MongoDB + result, err := questionCollection.InsertOne(context.TODO(), question) + if err != nil { + log.Println("Error inserting question into MongoDB:", err) + http.Error(w, "Failed to create question", http.StatusInternalServerError) + return + } + + log.Println("Question inserted with ID:", result.InsertedID) + json.NewEncoder(w).Encode(result.InsertedID) +} diff --git a/backend/question-service/internal/controllers/deleteQuestion.go b/backend/question-service/internal/controllers/deleteQuestion.go new file mode 100644 index 0000000000..33a02adcde --- /dev/null +++ b/backend/question-service/internal/controllers/deleteQuestion.go @@ -0,0 +1,48 @@ +package controllers + +import ( + "context" + "log" + "net/http" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson" +) + +// DeleteQuestion deletes a question from the MongoDB collection by ID +func DeleteQuestion(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Get the question ID from the URL parameters + params := mux.Vars(r) + questionID := params["id"] + + // Validate the question ID to ensure it is a valid UUID + _, err := uuid.Parse(questionID) + if err != nil { + log.Println("Invalid UUID format:", err) + http.Error(w, "Invalid question ID format", http.StatusBadRequest) + return + } + + // Prepare the filter to find the question by UUID + filter := bson.M{"questionid": questionID} + + // Delete the question from MongoDB + result, err := questionCollection.DeleteOne(context.TODO(), filter) + if err != nil { + log.Println("Error deleting question:", err) + http.Error(w, "Failed to delete question", http.StatusInternalServerError) + return + } + + if result.DeletedCount == 0 { + http.Error(w, "Question not found", http.StatusNotFound) + return + } + + log.Println("Question deleted successfully") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Question deleted successfully")) +} diff --git a/backend/question-service/internal/controllers/getAllQuestion.go b/backend/question-service/internal/controllers/getAllQuestion.go new file mode 100644 index 0000000000..f8dc46f517 --- /dev/null +++ b/backend/question-service/internal/controllers/getAllQuestion.go @@ -0,0 +1,74 @@ +package controllers + +import ( + "context" + "encoding/json" + "log" + "net/http" + "strconv" + "question-service/internal/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetAllQuestions handles the GET request to fetch all questions with pagination +func GetAllQuestions(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Get 'page' and 'limit' query parameters + pageStr := r.URL.Query().Get("page") + limitStr := r.URL.Query().Get("limit") + + // Set default values for page and limit + page, err := strconv.Atoi(pageStr) + if err != nil || page < 1 { + page = 1 // Default to page 1 + } + + limit, err := strconv.Atoi(limitStr) + if err != nil || limit < 1 { + limit = 10 // Default to 10 questions per page + } + + totalQuestions, err := questionCollection.CountDocuments(context.TODO(), bson.M{}) + if err != nil { + log.Println("Error counting documents:", err) + http.Error(w, "Failed to count questions", http.StatusInternalServerError) + return + } + + // Calculate skip for MongoDB + skip := (page - 1) * limit + + // Set MongoDB options to limit and skip the results + findOptions := options.Find() + findOptions.SetLimit(int64(limit)) + findOptions.SetSkip(int64(skip)) + + // Fetch questions from the database + cursor, err := questionCollection.Find(context.TODO(), bson.M{}, findOptions) + if err != nil { + log.Println("Error fetching questions:", err) + http.Error(w, "Failed to fetch questions", http.StatusInternalServerError) + return + } + + // Convert the MongoDB cursor into a slice of questions + var questions []models.Question + if err = cursor.All(context.TODO(), &questions); err != nil { + log.Println("Error decoding questions:", err) + http.Error(w, "Failed to decode questions", http.StatusInternalServerError) + return + } + + totalPages := int((totalQuestions + int64(limit) - 1) / int64(limit)) + + response := map[string]interface{}{ + "questions": questions, + "totalPages": totalPages, + } + + // Return the questions as JSON + json.NewEncoder(w).Encode(response) +} diff --git a/backend/question-service/internal/controllers/getQuestionById.go b/backend/question-service/internal/controllers/getQuestionById.go new file mode 100644 index 0000000000..e568f8c677 --- /dev/null +++ b/backend/question-service/internal/controllers/getQuestionById.go @@ -0,0 +1,57 @@ +package controllers + +import ( + "context" + "encoding/json" + "log" + "net/http" + "question-service/internal/models" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +func GetQuestionByID(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Get the QuestionID from the URL parameters + vars := mux.Vars(r) + questionID := vars["id"] + + // Validate the question ID to ensure it is a valid UUID + _, err := uuid.Parse(questionID) + if err != nil { + log.Println("Invalid UUID format:", err) + http.Error(w, "Invalid question ID format", http.StatusBadRequest) + return + } + + log.Println("Received a request to fetch question by ID:", questionID) + + // Create a filter to search for the question by its QuestionID + filter := bson.M{"questionid": questionID} + + // Find the question in the collection + var question models.Question + err = questionCollection.FindOne(context.TODO(), filter).Decode(&question) + if err != nil { + if err == mongo.ErrNoDocuments { + // If the question is not found + log.Println("Question not found with ID:", questionID) + http.Error(w, "Question not found", http.StatusNotFound) + return + } + + // If there's an error in fetching the question + log.Println("Error fetching question by ID:", err) + http.Error(w, "Failed to fetch question", http.StatusInternalServerError) + return + } + + // Return the question as JSON + log.Println("Returning question with ID:", questionID) + json.NewEncoder(w).Encode(question) + return +} diff --git a/backend/question-service/internal/controllers/getQuestionForMatching.go b/backend/question-service/internal/controllers/getQuestionForMatching.go new file mode 100644 index 0000000000..412a42e4e4 --- /dev/null +++ b/backend/question-service/internal/controllers/getQuestionForMatching.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "context" + "encoding/json" + "log" + "math/rand" + "net/http" + "question-service/internal/models" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetQuestion retrieves a random question based on complexity and category (topics) +func GetQuestion(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Extract query parameters from the URL + complexity := r.URL.Query().Get("complexity") + categories := r.URL.Query()["categories"] // Get the array of categories + + // Log the parameters for debugging + log.Printf("Received complexity: %s, categories: %v", complexity, categories) + + // Validate the parameters (ensure complexity and at least one category are provided) + if complexity == "" || len(categories) == 0 { + http.Error(w, "Invalid query parameters", http.StatusBadRequest) + return + } + + // Create filter for MongoDB based on complexity and categories + filter := bson.M{ + "complexity": primitive.Regex{Pattern: complexity, Options: "i"}, + "category": bson.M{"$in": regexArray(categories)}, + } + + // Log the filter being used for MongoDB + log.Printf("MongoDB query filter: %v", filter) + + // Query MongoDB to find all matching questions + cursor, err := questionCollection.Find(context.TODO(), filter) + if err != nil { + log.Printf("Error finding questions: %v", err) + http.Error(w, "Error retrieving questions", http.StatusInternalServerError) + return + } + defer cursor.Close(context.TODO()) + + // Collect the matching questions into a slice + var questions []models.Question + for cursor.Next(context.TODO()) { + var question models.Question + if err := cursor.Decode(&question); err != nil { + log.Printf("Error decoding question: %v", err) + continue + } + questions = append(questions, question) + } + + // If no questions are found, return a 404 response + if len(questions) == 0 { + log.Println("No suitable questions found, fetching a random question.") + + // Fetch all questions without any filters + randomCursor, err := questionCollection.Aggregate(context.TODO(), bson.A{bson.M{"$sample": bson.M{"size": 1}}}) + if err != nil { + log.Printf("Error finding random question: %v", err) + http.Error(w, "Error retrieving random question", http.StatusInternalServerError) + return + } + defer randomCursor.Close(context.TODO()) + + var randomQuestions []models.Question + for randomCursor.Next(context.TODO()) { + var question models.Question + if err := randomCursor.Decode(&question); err != nil { + log.Printf("Error decoding random question: %v", err) + continue + } + randomQuestions = append(randomQuestions, question) + } + + // If no random question is found (highly unlikely), return a 404 + if len(randomQuestions) == 0 { + http.Error(w, "No questions found", http.StatusNotFound) + return + } + + // Return the random question + json.NewEncoder(w).Encode(randomQuestions[0]) + return + } + + + // Create a new random number generator with a source based on the current time + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Select a random question from the list + selectedQuestion := questions[rng.Intn(len(questions))] + + // Send the selected question as a JSON response + json.NewEncoder(w).Encode(selectedQuestion) +} + +func regexArray(arr []string) []primitive.Regex { + regexes := make([]primitive.Regex, len(arr)) + for i, item := range arr { + regexes[i] = primitive.Regex{Pattern: item, Options: "i"} + } + return regexes +} \ No newline at end of file diff --git a/backend/question-service/internal/controllers/updateQuestion.go b/backend/question-service/internal/controllers/updateQuestion.go new file mode 100644 index 0000000000..b906edabc4 --- /dev/null +++ b/backend/question-service/internal/controllers/updateQuestion.go @@ -0,0 +1,97 @@ +package controllers + +import ( + "context" + "encoding/json" + "log" + "net/http" + "strings" + "time" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +// UpdateQuestion updates a question in the MongoDB collection by UUID +func UpdateQuestion(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Get the question ID from the URL parameters + params := mux.Vars(r) + questionID := params["id"] + + // Validate that the question ID is a valid UUID + if _, err := uuid.Parse(questionID); err != nil { + log.Println("Invalid UUID format:", err) + http.Error(w, "Invalid question ID format", http.StatusBadRequest) + return + } + + // Prepare the update data + var updateData map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil { + log.Println("Error decoding update data:", err) + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + // Check if the "title" field is being updated + if title, ok := updateData["title"]; ok { + // Convert the title to lowercase for the check + lowercaseTitle := strings.ToLower(title.(string)) + + // Query the database to check if a question with the same title already exists + existingQuestion := bson.M{} + filter := bson.M{ + "$expr": bson.M{ + "$and": []interface{}{ + bson.M{"$eq": []interface{}{bson.M{"$toLower": "$title"}, lowercaseTitle}}, // Check title case-insensitively + bson.M{"$ne": []interface{}{"$questionid", questionID}}, // Ensure it's not the same question being updated + }, + }, + } + + err := questionCollection.FindOne(context.TODO(), filter).Decode(&existingQuestion) + if err == nil { + // If we find a document, the title is already in use + log.Println("Duplicate title found") + http.Error(w, "A question with this title already exists", http.StatusBadRequest) + return + } else if err != mongo.ErrNoDocuments { + // If there is an error other than no documents found + log.Println("Error checking for existing question:", err) + http.Error(w, "Failed to check existing question", http.StatusInternalServerError) + return + } + } + + // Ensure that the "UpdatedAt" field is updated with the current time + updateData["updatedAt"] = time.Now() + + // Prepare the filter to find the question by UUID + filter := bson.M{"questionid": questionID} + + // Create the update object using MongoDB's $set operator + update := bson.M{ + "$set": updateData, + } + + // Perform the update operation + result, err := questionCollection.UpdateOne(context.TODO(), filter, update) + if err != nil { + log.Println("Error updating question:", err) + http.Error(w, "Failed to update question", http.StatusInternalServerError) + return + } + + if result.MatchedCount == 0 { + http.Error(w, "Question not found", http.StatusNotFound) + return + } + + // Respond with success + log.Println("Question updated successfully") + json.NewEncoder(w).Encode("Question updated successfully") +} diff --git a/backend/question-service/internal/middleware/authMiddleware.go b/backend/question-service/internal/middleware/authMiddleware.go new file mode 100644 index 0000000000..25d9a7ec52 --- /dev/null +++ b/backend/question-service/internal/middleware/authMiddleware.go @@ -0,0 +1,105 @@ +package middleware + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "time" + + "question-service/internal/models" + "github.com/dgrijalva/jwt-go" +) + +type key int + +const ( + // UserKey for storing user in the context + UserKey key = iota +) + +// VerifyAccessToken middleware for JWT authentication, extracting token from the cookie +func VerifyAccessToken(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Extract the access token from the 'accessToken' cookie + cookie, err := r.Cookie("accessToken") + if err != nil { + log.Println("Access token cookie missing or invalid") + http.Error(w, "Unauthorized, no access token", http.StatusUnauthorized) + return + } + + // Authenticate the access token from the cookie value + token, err := authenticateAccessToken(cookie.Value) + if err != nil { + log.Printf("Failed to authenticate token: %v", err) + http.Error(w, "Invalid or expired token", http.StatusUnauthorized) + return + } + + // Check if the token is valid and print out the claims + if claims, ok := token.Claims.(*models.JwtPayload); ok && token.Valid { + // Add the entire JwtPayload object to the request context + ctx := context.WithValue(r.Context(), UserKey, claims) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + http.Error(w, "Invalid token", http.StatusUnauthorized) + } + }) +} + +// ProtectAdmin middleware ensures that only admin users can access the route +func ProtectAdmin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Retrieve the JwtPayload directly from the context (must have passed VerifyAccessToken first) + claims, ok := r.Context().Value(UserKey).(*models.JwtPayload) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Debug: Print the whole JwtPayload object for debugging + log.Printf("JWT Payload: %+v", claims) + + // Check if the user is an admin + if !claims.IsAdmin { + log.Printf("User %s (ID: %s) is not an admin", claims.Username, claims.ID.Hex()) + http.Error(w, "Forbidden: Admin access required", http.StatusForbidden) + return + } + + // User is an admin, allow access + next.ServeHTTP(w, r) + }) +} + +// authenticateAccessToken verifies the JWT token and returns the parsed token +func authenticateAccessToken(tokenString string) (*jwt.Token, error) { + // Parse the token with the correct claims + token, err := jwt.ParseWithClaims(tokenString, &models.JwtPayload{}, func(token *jwt.Token) (interface{}, error) { + // Check signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + // Return the JWT_SECRET from environment variable + return []byte(os.Getenv("JWT_SECRET")), nil + }) + + // Return any parsing errors + if err != nil { + return nil, err + } + + // Ensure the token is valid + if claims, ok := token.Claims.(*models.JwtPayload); ok && token.Valid { + // Check if the token has expired + if time.Now().Unix() > claims.Exp { + log.Printf("Token has expired. Current time: %d, Expiration time: %d", time.Now().Unix(), claims.Exp) + return nil, errors.New("token has expired") + } + return token, nil + } + + return nil, errors.New("invalid token") +} diff --git a/backend/question-service/internal/models/questionModel.go b/backend/question-service/internal/models/questionModel.go new file mode 100644 index 0000000000..1b5d9b7406 --- /dev/null +++ b/backend/question-service/internal/models/questionModel.go @@ -0,0 +1,16 @@ +package models + +import "time" + +type Question struct { + QuestionID string `json:"questionId"` + Title string `json:"title"` + Description string `json:"description"` + Constraints string `json:"constraints"` + Examples string `json:"examples"` + Category []string `json:"category"` + Complexity string `json:"complexity"` + ImageURL string `json:"imageUrl"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/backend/question-service/internal/models/userModel.go b/backend/question-service/internal/models/userModel.go new file mode 100644 index 0000000000..36d80fbe71 --- /dev/null +++ b/backend/question-service/internal/models/userModel.go @@ -0,0 +1,27 @@ +package models + +import ( + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type JwtPayload struct { + ID primitive.ObjectID `json:"id" bson:"id"` + Username string `json:"username" bson:"username"` + Email string `json:"email" bson:"email"` + IsAdmin bool `json:"isAdmin" bson:"isAdmin"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` +} + +// Valid function to check if the token is expired +func (p JwtPayload) Valid() error { + if p.Exp < time.Now().Unix() { + return errors.New("token has expired") + } + return nil +} + diff --git a/backend/question-service/internal/routes/questionRoutes.go b/backend/question-service/internal/routes/questionRoutes.go new file mode 100644 index 0000000000..4cc8d83428 --- /dev/null +++ b/backend/question-service/internal/routes/questionRoutes.go @@ -0,0 +1,26 @@ +package routes + +import ( + "net/http" + + "question-service/internal/controllers" + "question-service/internal/middleware" + + "github.com/gorilla/mux" +) + +// RegisterQuestionRoutes defines the API routes for managing questions +func RegisterQuestionRoutes(router *mux.Router) { + // Public routes (GET requests) - Require authentication + router.Handle("/questions", http.HandlerFunc(controllers.GetAllQuestions)).Methods("GET") + + // Route for collaboration service to get a matching question based on complexity and category + router.Handle("/questions/get-question", http.HandlerFunc(controllers.GetQuestion)).Methods("GET") + + router.Handle("/questions/{id}", http.HandlerFunc(controllers.GetQuestionByID)).Methods("GET") + + // Admin-only routes (POST, PUT, DELETE requests) - Require both authentication and admin privileges + router.Handle("/questions", middleware.VerifyAccessToken(middleware.ProtectAdmin(http.HandlerFunc(controllers.CreateQuestion)))).Methods("POST") + router.Handle("/questions/{id}", middleware.VerifyAccessToken(middleware.ProtectAdmin(http.HandlerFunc(controllers.UpdateQuestion)))).Methods("PUT") + router.Handle("/questions/{id}", middleware.VerifyAccessToken(middleware.ProtectAdmin(http.HandlerFunc(controllers.DeleteQuestion)))).Methods("DELETE") +} diff --git a/backend/question-service/package-lock.json b/backend/question-service/package-lock.json new file mode 100644 index 0000000000..af2a0d12c2 --- /dev/null +++ b/backend/question-service/package-lock.json @@ -0,0 +1,159 @@ +{ + "name": "question-service", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "dotenv": "^16.4.5", + "mongodb": "^6.9.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + } + } +} diff --git a/backend/question-service/package.json b/backend/question-service/package.json new file mode 100644 index 0000000000..db1583b928 --- /dev/null +++ b/backend/question-service/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "dotenv": "^16.4.5", + "mongodb": "^6.9.0" + } +} diff --git a/backend/user-service/.dockerignore b/backend/user-service/.dockerignore new file mode 100644 index 0000000000..b512c09d47 --- /dev/null +++ b/backend/user-service/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample new file mode 100644 index 0000000000..b3518e0224 --- /dev/null +++ b/backend/user-service/.env.sample @@ -0,0 +1,9 @@ +DB_CLOUD_URI= +DB_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB +PORT=3001 + +# Will use cloud MongoDB Atlas database +ENV=PROD + +# Secret for creating JWT signature +JWT_SECRET=you-can-replace-this-with-your-own-secret diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile new file mode 100644 index 0000000000..b2afe64421 --- /dev/null +++ b/backend/user-service/Dockerfile @@ -0,0 +1,14 @@ +# offcial node image as parent image +FROM node:20-alpine +# set working directory to user-servce, all subsequent commands will run in this dire +WORKDIR /app +# copy all package json files to current-directory +COPY package*.json ./ +# install dependencies +RUN npm install +# copy all the files in the current dir +COPY . . +# user-service uses port 3001 +EXPOSE 3001 +# start service with npm run start +CMD ["npm", "start"] diff --git a/backend/user-service/GuideAssets/AddIPAddress.png b/backend/user-service/GuideAssets/AddIPAddress.png new file mode 100644 index 0000000000..9d79550ca4 Binary files /dev/null and b/backend/user-service/GuideAssets/AddIPAddress.png differ diff --git a/backend/user-service/GuideAssets/ConnectCluster.png b/backend/user-service/GuideAssets/ConnectCluster.png new file mode 100644 index 0000000000..1f045f0e04 Binary files /dev/null and b/backend/user-service/GuideAssets/ConnectCluster.png differ diff --git a/backend/user-service/GuideAssets/ConnectionString.png b/backend/user-service/GuideAssets/ConnectionString.png new file mode 100644 index 0000000000..073d3ed6a9 Binary files /dev/null and b/backend/user-service/GuideAssets/ConnectionString.png differ diff --git a/backend/user-service/GuideAssets/Creation.png b/backend/user-service/GuideAssets/Creation.png new file mode 100644 index 0000000000..055ed783af Binary files /dev/null and b/backend/user-service/GuideAssets/Creation.png differ diff --git a/backend/user-service/GuideAssets/DriverSelection.png b/backend/user-service/GuideAssets/DriverSelection.png new file mode 100644 index 0000000000..585087811c Binary files /dev/null and b/backend/user-service/GuideAssets/DriverSelection.png differ diff --git a/backend/user-service/GuideAssets/IPWhitelisting.png b/backend/user-service/GuideAssets/IPWhitelisting.png new file mode 100644 index 0000000000..5718234553 Binary files /dev/null and b/backend/user-service/GuideAssets/IPWhitelisting.png differ diff --git a/backend/user-service/GuideAssets/Network.png b/backend/user-service/GuideAssets/Network.png new file mode 100644 index 0000000000..57bad4e677 Binary files /dev/null and b/backend/user-service/GuideAssets/Network.png differ diff --git a/backend/user-service/GuideAssets/Security.png b/backend/user-service/GuideAssets/Security.png new file mode 100644 index 0000000000..f328764083 Binary files /dev/null and b/backend/user-service/GuideAssets/Security.png differ diff --git a/backend/user-service/GuideAssets/Selection.png b/backend/user-service/GuideAssets/Selection.png new file mode 100644 index 0000000000..5029373ea1 Binary files /dev/null and b/backend/user-service/GuideAssets/Selection.png differ diff --git a/backend/user-service/GuideAssets/Selection1.png b/backend/user-service/GuideAssets/Selection1.png new file mode 100644 index 0000000000..211c69a156 Binary files /dev/null and b/backend/user-service/GuideAssets/Selection1.png differ diff --git a/backend/user-service/GuideAssets/Selection2.png b/backend/user-service/GuideAssets/Selection2.png new file mode 100644 index 0000000000..17ff928855 Binary files /dev/null and b/backend/user-service/GuideAssets/Selection2.png differ diff --git a/backend/user-service/GuideAssets/Selection3.png b/backend/user-service/GuideAssets/Selection3.png new file mode 100644 index 0000000000..948ed1026d Binary files /dev/null and b/backend/user-service/GuideAssets/Selection3.png differ diff --git a/backend/user-service/GuideAssets/Selection4.png b/backend/user-service/GuideAssets/Selection4.png new file mode 100644 index 0000000000..abf60bbe21 Binary files /dev/null and b/backend/user-service/GuideAssets/Selection4.png differ diff --git a/backend/user-service/GuideAssets/SidePane.png b/backend/user-service/GuideAssets/SidePane.png new file mode 100644 index 0000000000..95f7e2866f Binary files /dev/null and b/backend/user-service/GuideAssets/SidePane.png differ diff --git a/backend/user-service/MongoDBSetup.md b/backend/user-service/MongoDBSetup.md new file mode 100644 index 0000000000..276161ee4e --- /dev/null +++ b/backend/user-service/MongoDBSetup.md @@ -0,0 +1,60 @@ +# Setting up MongoDB Instance for User Service + +1. Visit the MongoDB Atlas Site [https://www.mongodb.com/atlas](https://www.mongodb.com/atlas) and click on "Try Free" + +2. Sign Up/Sign In with your preferred method. + +3. You will be greeted with welcome screens. Feel free to skip them till you reach the Dashboard page. + +4. Create a Database Deployment by clicking on the green `+ Create` Button: + +![alt text](./GuideAssets/Creation.png) + +5. Make selections as followings: + +- Select Shared Cluster +- Select `aws` as Provider + +![alt text](./GuideAssets/Selection1.png) + +- Select `Singapore` for Region + +![alt text](./GuideAssets/Selection2.png) + +- Select `M0 Sandbox` Cluster (Free Forever - No Card Required) + +> Ensure to select M0 Sandbox, else you may be prompted to enter card details and may be charged! + +![alt text](./GuideAssets/Selection3.png) + +- Leave `Additional Settings` as it is + +- Provide a suitable name to the Cluster + +![alt text](./GuideAssets/Selection4.png) + +6. You will be prompted to set up Security for the database by providing `Username and Password`. Select that option and enter `Username` and `Password`. Please keep this safe as it will be used in User Service later on. + +![alt text](./GuideAssets/Security.png) + +7. Next, click on `Add my Current IP Address`. This will whiteliste your IP address and allow you to connect to the MongoDB Database. + +![alt text](./GuideAssets/Network.png) + +8. Click `Finish and Close` and the MongoDB Instance should be up and running. + +## Whitelisting All IP's + +1. Select `Network Access` from the left side pane on Dashboard. + +![alt text](./GuideAssets/SidePane.png) + +2. Click on the `Add IP Address` Button + +![alt text](./GuideAssets/AddIPAddress.png) + +3. Select the `ALLOW ACCESS FROM ANYWHERE` Button and Click `Confirm` + +![alt text](./GuideAssets/IPWhitelisting.png) + +Now, any IP Address can access this Database. diff --git a/backend/user-service/README.md b/backend/user-service/README.md new file mode 100644 index 0000000000..be27594dbc --- /dev/null +++ b/backend/user-service/README.md @@ -0,0 +1,272 @@ +# User Service Guide + +## Setting-up + +> :notebook: If you are familiar to MongoDB and wish to use a local instance, please feel free to do so. This guide utilizes MongoDB Cloud Services. + +1. Set up a MongoDB Shared Cluster by following the steps in this [Guide](./MongoDBSetup.md). + +2. After setting up, go to the Database Deployment Page. You would see a list of the Databases you have set up. Select `Connect` on the cluster you just created earlier on for User Service. + + ![alt text](./GuideAssets/ConnectCluster.png) + +3. Select the `Drivers` option, as we have to link to a Node.js App (User Service). + + ![alt text](./GuideAssets/DriverSelection.png) + +4. Select `Node.js` in the `Driver` pull-down menu, and copy the connection string. + + Notice, you may see `` in this connection string. We will be replacing this with the admin account password that we created earlier on when setting up the Shared Cluster. + + ![alt text](./GuideAssets/ConnectionString.png) + +5. In the `user-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +6. Update the `DB_CLOUD_URI` of the `.env` file, and paste the string we copied earlier in step 4. Also remember to replace the `` placeholder with the actual password. + +## Running User Service + +1. Open Command Line/Terminal and navigate into the `user-service` directory. + +2. Run the command: `npm install`. This will install all the necessary dependencies. + +3. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +4. Using applications like Postman, you can interact with the User Service on port 3001. If you wish to change this, please update the `.env` file. + +## User Service API Guide + +### Create User + +- This endpoint allows adding a new user to the database (i.e., user registration). + +- HTTP Method: `POST` + +- Endpoint: http://localhost:3001/users + +- Body + - Required: `username` (string), `email` (string), `password` (string) + + ```json + { + "username": "SampleUserName", + "email": "sample@gmail.com", + "password": "SecurePassword" + } + ``` + +- Responses: + + | Response Code | Explanation | + |-----------------------------|-------------------------------------------------------| + | 201 (Created) | User created successfully, created user data returned | + | 400 (Bad Request) | Missing fields | + | 409 (Conflict) | Duplicate username or email encountered | + | 500 (Internal Server Error) | Database or server error | + +### Get User + +- This endpoint allows retrieval of a single user's data from the database using the user's ID. + + > :bulb: The user ID refers to the MongoDB Object ID, a unique identifier automatically generated by MongoDB for each document in a collection. + +- HTTP Method: `GET` + +- Endpoint: http://localhost:3001/users/{userId} + +- Parameters + - Required: `userId` path parameter + - Example: `http://localhost:3001/users/60c72b2f9b1d4c3a2e5f8b4c` + +- Headers + + - Required: `Authorization: Bearer ` + + - Explanation: This endpoint requires the client to include a JWT (JSON Web Token) in the HTTP request header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. + + - Auth Rules: + + - Admin users: Can retrieve any user's data. The server verifies the user associated with the JWT token is an admin user and allows access to the requested user's data. + + - Non-admin users: Can only retrieve their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server returns the user's own data. + +- Responses: + + | Response Code | Explanation | + |-----------------------------|----------------------------------------------------------| + | 200 (OK) | Success, user data returned | + | 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | + | 403 (Forbidden) | Access denied for non-admin users accessing others' data | + | 404 (Not Found) | User with the specified ID not found | + | 500 (Internal Server Error) | Database or server error | + +### Get All Users + +- This endpoint allows retrieval of all users' data from the database. +- HTTP Method: `GET` +- Endpoint: http://localhost:3001/users +- Headers + - Required: `Authorization: Bearer ` + - Auth Rules: + + - Admin users: Can retrieve all users' data. The server verifies the user associated with the JWT token is an admin user and allows access to all users' data. + + - Non-admin users: Not allowed access. + +- Responses: + + | Response Code | Explanation | + |-----------------------------|--------------------------------------------------| + | 200 (OK) | Success, all user data returned | + | 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | + | 403 (Forbidden) | Access denied for non-admin users | + | 500 (Internal Server Error) | Database or server error | + +### Update User + +- This endpoint allows updating a user and their related data in the database using the user's ID. + +- HTTP Method: `PATCH` + +- Endpoint: http://localhost:3001/users/{userId} + +- Parameters + - Required: `userId` path parameter + +- Body + - At least one of the following fields is required: `username` (string), `email` (string), `password` (string) + + ```json + { + "username": "SampleUserName", + "email": "sample@gmail.com", + "password": "SecurePassword" + } + ``` + +- Headers + - Required: `Authorization: Bearer ` + - Auth Rules: + + - Admin users: Can update any user's data. The server verifies the user associated with the JWT token is an admin user and allows the update of requested user's data. + + - Non-admin users: Can only update their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server updates the user's own data. + +- Responses: + + | Response Code | Explanation | + |-----------------------------|---------------------------------------------------------| + | 200 (OK) | User updated successfully, updated user data returned | + | 400 (Bad Request) | Missing fields | + | 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | + | 403 (Forbidden) | Access denied for non-admin users updating others' data | + | 404 (Not Found) | User with the specified ID not found | + | 409 (Conflict) | Duplicate username or email encountered | + | 500 (Internal Server Error) | Database or server error | + +### Update User Privilege + +- This endpoint allows updating a user’s privilege, i.e., promoting or demoting them from admin status. + +- HTTP Method: `PATCH` + +- Endpoint: http://localhost:3001/users/{userId} + +- Parameters + - Required: `userId` path parameter + +- Body + - Required: `isAdmin` (boolean) + + ```json + { + "isAdmin": true + } + ``` + +- Headers + - Required: `Authorization: Bearer ` + - Auth Rules: + + - Admin users: Can update any user's privilege. The server verifies the user associated with the JWT token is an admin user and allows the privilege update. + - Non-admin users: Not allowed access. + +> :bulb: You may need to manually assign admin status to the first user by directly editing the database document before using this endpoint. + +- Responses: + + | Response Code | Explanation | + |-----------------------------|-----------------------------------------------------------------| + | 200 (OK) | User privilege updated successfully, updated user data returned | + | 400 (Bad Request) | Missing fields | + | 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | + | 403 (Forbidden) | Access denied for non-admin users | + | 404 (Not Found) | User with the specified ID not found | + | 500 (Internal Server Error) | Database or server error | + +### Delete User + +- This endpoint allows deletion of a user and their related data from the database using the user's ID. +- HTTP Method: `DELETE` +- Endpoint: http://localhost:3001/users/{userId} +- Parameters + + - Required: `userId` path parameter +- Headers + + - Required: `Authorization: Bearer ` + + - Auth Rules: + + - Admin users: Can delete any user's data. The server verifies the user associated with the JWT token is an admin user and allows the deletion of requested user's data. + + - Non-admin users: Can only delete their own data. The server checks if the user ID in the request URL matches the ID of the user associated with the JWT token. If it matches, the server deletes the user's own data. +- Responses: + + | Response Code | Explanation | + |-----------------------------|---------------------------------------------------------| + | 200 (OK) | User deleted successfully | + | 401 (Unauthorized) | Access denied due to missing/invalid/expired JWT | + | 403 (Forbidden) | Access denied for non-admin users deleting others' data | + | 404 (Not Found) | User with the specified ID not found | + | 500 (Internal Server Error) | Database or server error | + +### Login + +- This endpoint allows a user to authenticate with an email and password and returns a JWT access token. The token is valid for 1 day and can be used subsequently to access protected resources. For example usage, refer to the [Authorization header section in the Get User endpoint](#auth-header). +- HTTP Method: `POST` +- Endpoint: http://localhost:3001/auth/login +- Body + - Required: `email` (string), `password` (string) + + ```json + { + "email": "sample@gmail.com", + "password": "SecurePassword" + } + ``` + +- Responses: + + | Response Code | Explanation | + |-----------------------------|----------------------------------------------------| + | 200 (OK) | Login successful, JWT token and user data returned | + | 400 (Bad Request) | Missing fields | + | 401 (Unauthorized) | Incorrect email or password | + | 500 (Internal Server Error) | Database or server error | + +### Verify Token + +- This endpoint allows one to verify a JWT access token to authenticate and retrieve the user's data associated with the token. +- HTTP Method: `GET` +- Endpoint: http://localhost:3001/auth/verify-token +- Headers + - Required: `Authorization: Bearer ` + +- Responses: + + | Response Code | Explanation | + |-----------------------------|----------------------------------------------------| + | 200 (OK) | Token verified, authenticated user's data returned | + | 401 (Unauthorized) | Missing/invalid/expired JWT | + | 500 (Internal Server Error) | Database or server error | \ No newline at end of file diff --git a/backend/user-service/controller/auth-controller.js b/backend/user-service/controller/auth-controller.js new file mode 100644 index 0000000000..3f88b5201d --- /dev/null +++ b/backend/user-service/controller/auth-controller.js @@ -0,0 +1,131 @@ +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { findUserByEmail as _findUserByEmail } from "../model/repository.js"; +import { formatUserResponse } from "./user-controller.js"; +import {transporter} from "../utils/nodemailer.js"; + +export async function handleLogin(req, res) { + const { email, password } = req.body; + if (email && password) { + try { + const user = await _findUserByEmail(email); + if (!user) { + return res.status(401).json({ message: "Wrong email and/or password" }); + } + + const match = await bcrypt.compare(password, user.password); + if (!match) { + return res.status(401).json({ message: "Wrong email and/or password" }); + } + + // Generate access token + const accessToken = jwt.sign( + { + id: user.id, + username: user.username, + email: user.email, + isAdmin: user.isAdmin, + createdAt: user.createdAt + }, + process.env.JWT_SECRET, + { expiresIn: "12h" } + ); + + // Set access token as an HTTP-only cookie + res.cookie("accessToken", accessToken, { + httpOnly: true, // Ensure it's not accessible via JavaScript (XSS protection) + secure: true, + sameSite: "none", // Mitigate CSRF attacks + maxAge: 12 * 60 * 60 * 1000, // 12 hour + }); + + // Send access token and user data in the response body + return res.status(200).json({ + message: "User logged in", + data: { + accessToken, + ...formatUserResponse(user), + }, + }); + } catch (err) { + return res.status(500).json({ message: err.message }); + } + } else { + return res.status(400).json({ message: "Missing email and/or password" }); + } +} + +export function handleLogout(req, res) { + const accessToken = req.cookies["accessToken"]; + + if (!accessToken) { + return res.status(401).json({ message: "No access token provided" }); + } + + try { + // Verify the access token + jwt.verify(accessToken, process.env.JWT_SECRET); + + // Clear both accessToken and refreshToken cookies + res.clearCookie("accessToken", { + httpOnly: true, + }); + + return res.status(200).json({ message: "User logged out successfully" }); + } catch (err) { + return res.status(403).json({ message: "Invalid access token" }); + } +} + +export async function handleForgotPassword(req, res) { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ message: "Email is required" }); + } + + try { + // Check if the user exists + const user = await _findUserByEmail(email); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + // Generate a reset token (JWT token valid for 1 hour) + const resetToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { + expiresIn: "1h", + }); + + // Create reset URL + const resetURL = `http://localhost:3000/reset-password?token=${resetToken}`; + + // Email options + const mailOptions = { + from: process.env.GMAIL_USER, + to: email, + subject: "Password Reset Request", + html: ` +

Password Reset

+

Please click the link below to reset your password:

+ Reset Password +

This link will expire in 1 hour.

+ `, + }; + + // Send the email + await transporter.sendMail(mailOptions); + + return res.status(200).json({ message: "Password reset email sent" }); + } catch (err) { + return res.status(500).json({ message: err.message }); + } +} + +export async function handleVerifyToken(req, res) { + try { + const verifiedUser = req.user; + return res.status(200).json({ message: "Token verified", data: verifiedUser }); + } catch (err) { + return res.status(500).json({ message: err.message }); + } +} diff --git a/backend/user-service/controller/user-controller.js b/backend/user-service/controller/user-controller.js new file mode 100644 index 0000000000..985a83384f --- /dev/null +++ b/backend/user-service/controller/user-controller.js @@ -0,0 +1,167 @@ +import bcrypt from "bcrypt"; +import { isValidObjectId } from "mongoose"; +import { + createUser as _createUser, + deleteUserById as _deleteUserById, + findAllUsers as _findAllUsers, + findUserByEmail as _findUserByEmail, + findUserById as _findUserById, + findUserByUsername as _findUserByUsername, + findUserByUsernameOrEmail as _findUserByUsernameOrEmail, + updateUserById as _updateUserById, + updateUserPrivilegeById as _updateUserPrivilegeById, +} from "../model/repository.js"; + +export async function createUser(req, res) { + try { + const { username, email, password } = req.body; + if (username && email && password) { + const existingUser = await _findUserByUsernameOrEmail(username, email); + if (existingUser) { + return res.status(409).json({ message: "username or email already exists" }); + } + + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + const createdUser = await _createUser(username, email, hashedPassword); + return res.status(201).json({ + message: `Created new user ${username} successfully`, + data: formatUserResponse(createdUser), + }); + } else { + return res.status(400).json({ message: "username and/or email and/or password are missing" }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when creating new user!" }); + } +} + +export async function getUser(req, res) { + try { + const userId = req.params.id; + if (!isValidObjectId(userId)) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + + const user = await _findUserById(userId); + if (!user) { + return res.status(404).json({ message: `User ${userId} not found` }); + } else { + return res.status(200).json({ message: `Found user`, data: formatUserResponse(user) }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when getting user!" }); + } +} + +export async function getAllUsers(req, res) { + try { + const users = await _findAllUsers(); + + return res.status(200).json({ message: `Found users`, data: users.map(formatUserResponse) }); + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when getting all users!" }); + } +} + +export async function updateUser(req, res) { + try { + const { username, email, password } = req.body; + if (username || email || password) { + const userId = req.params.id; + if (!isValidObjectId(userId)) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + const user = await _findUserById(userId); + if (!user) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + if (username || email) { + let existingUser = await _findUserByUsername(username); + if (existingUser && existingUser.id !== userId) { + return res.status(409).json({ message: "username already exists" }); + } + existingUser = await _findUserByEmail(email); + if (existingUser && existingUser.id !== userId) { + return res.status(409).json({ message: "email already exists" }); + } + } + + let hashedPassword; + if (password) { + const salt = bcrypt.genSaltSync(10); + hashedPassword = bcrypt.hashSync(password, salt); + } + const updatedUser = await _updateUserById(userId, username, email, hashedPassword); + return res.status(200).json({ + message: `Updated data for user ${userId}`, + data: formatUserResponse(updatedUser), + }); + } else { + return res.status(400).json({ message: "No field to update: username and email and password are all missing!" }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when updating user!" }); + } +} + +export async function updateUserPrivilege(req, res) { + try { + const { isAdmin } = req.body; + + if (isAdmin !== undefined) { // isAdmin can have boolean value true or false + const userId = req.params.id; + if (!isValidObjectId(userId)) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + const user = await _findUserById(userId); + if (!user) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + + const updatedUser = await _updateUserPrivilegeById(userId, isAdmin === true); + return res.status(200).json({ + message: `Updated privilege for user ${userId}`, + data: formatUserResponse(updatedUser), + }); + } else { + return res.status(400).json({ message: "isAdmin is missing!" }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when updating user privilege!" }); + } +} + +export async function deleteUser(req, res) { + try { + const userId = req.params.id; + if (!isValidObjectId(userId)) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + const user = await _findUserById(userId); + if (!user) { + return res.status(404).json({ message: `User ${userId} not found` }); + } + + await _deleteUserById(userId); + return res.status(200).json({ message: `Deleted user ${userId} successfully` }); + } catch (err) { + console.error(err); + return res.status(500).json({ message: "Unknown error when deleting user!" }); + } +} + +export function formatUserResponse(user) { + return { + id: user.id, + username: user.username, + email: user.email, + isAdmin: user.isAdmin, + createdAt: user.createdAt, + }; +} diff --git a/backend/user-service/index.js b/backend/user-service/index.js new file mode 100644 index 0000000000..3c55d1328b --- /dev/null +++ b/backend/user-service/index.js @@ -0,0 +1,65 @@ +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; + +import userRoutes from "./routes/user-routes.js"; +import authRoutes from "./routes/auth-routes.js"; + +const app = express(); + +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); +app.use(cookieParser()); + +app.use( + cors({ origin: "http://localhost:3000", optionsSuccessStatus: 200, credentials: true }), +); // Handles cookies from frontend + +// To handle CORS Errors +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', "http://localhost:3000"); + res.header('Access-Control-Allow-Credentials', 'true'); // Allow credentials (cookies) + + + res.header( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept, Authorization", + ); + + // Browsers usually send this before PUT or POST Requests + if (req.method === "OPTIONS") { + res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH"); + return res.status(200).json({}); + } + + // Continue Route Processing + next(); +}); + +app.use("/users", userRoutes); +app.use("/auth", authRoutes); + +app.get("/", (req, res, next) => { + console.log("Sending Greetings!"); + res.json({ + message: "Hello World from user-service", + }); +}); + +// Handle When No Route Match Is Found +app.use((req, res, next) => { + const error = new Error("Route Not Found"); + error.status = 404; + next(error); +}); + +app.use((error, req, res, next) => { + res.status(error.status || 500); + res.json({ + error: { + message: error.message, + }, + }); +}); + +export default app; diff --git a/backend/user-service/middleware/basic-access-control.js b/backend/user-service/middleware/basic-access-control.js new file mode 100644 index 0000000000..00ed14c93b --- /dev/null +++ b/backend/user-service/middleware/basic-access-control.js @@ -0,0 +1,59 @@ +import jwt from "jsonwebtoken"; +import { findUserById as _findUserById } from "../model/repository.js"; + +export function verifyAccessToken(req, res, next) { + // Check if token exists in cookies + let token = req.cookies["accessToken"]; + + // If not in cookies, check the Authorization header + if (!token && req.headers.authorization) { + const authHeader = req.headers.authorization; + if (authHeader.startsWith("Bearer ")) { + token = authHeader.split(" ")[1]; // Extract the token from "Bearer " + } + } + + // If token is still not found, send an authentication error + if (!token) { + return res.status(401).json({ message: "Authentication failed" }); + } + + // Verify the token + jwt.verify(token, process.env.JWT_SECRET, async (err, user) => { + if (err) { + return res.status(401).json({ message: "Authentication failed" }); + } + + // Load the latest user info from the database + const dbUser = await _findUserById(user.id); + if (!dbUser) { + return res.status(401).json({ message: "Authentication failed" }); + } + + // Attach the user info to the request object + req.user = { id: dbUser.id, username: dbUser.username, email: dbUser.email, isAdmin: dbUser.isAdmin }; + next(); + }); +} + +export function verifyIsAdmin(req, res, next) { + if (req.user.isAdmin) { + next(); + } else { + return res.status(403).json({ message: "Not authorized to access this resource" }); + } +} + +export function verifyIsOwnerOrAdmin(req, res, next) { + if (req.user.isAdmin) { + return next(); + } + + const userIdFromReqParams = req.params.id; + const userIdFromToken = req.user.id; + if (userIdFromReqParams === userIdFromToken) { + return next(); + } + + return res.status(403).json({ message: "Not authorized to access this resource" }); +} diff --git a/backend/user-service/model/repository.js b/backend/user-service/model/repository.js new file mode 100644 index 0000000000..5d56b91e71 --- /dev/null +++ b/backend/user-service/model/repository.js @@ -0,0 +1,71 @@ +import UserModel from "./user-model.js"; +import "dotenv/config"; +import { connect } from "mongoose"; + +export async function connectToDB() { + let mongoDBUri = + process.env.ENV === "PROD" + ? process.env.DB_CLOUD_URI + : process.env.DB_LOCAL_URI; + + await connect(mongoDBUri); +} + +export async function createUser(username, email, password) { + return new UserModel({ username, email, password }).save(); +} + +export async function findUserByEmail(email) { + return UserModel.findOne({ email }); +} + +export async function findUserById(userId) { + return UserModel.findById(userId); +} + +export async function findUserByUsername(username) { + return UserModel.findOne({ username }); +} + +export async function findUserByUsernameOrEmail(username, email) { + return UserModel.findOne({ + $or: [ + { username }, + { email }, + ], + }); +} + +export async function findAllUsers() { + return UserModel.find(); +} + +export async function updateUserById(userId, username, email, password) { + return UserModel.findByIdAndUpdate( + userId, + { + $set: { + username, + email, + password, + }, + }, + { new: true }, // return the updated user + ); +} + +export async function updateUserPrivilegeById(userId, isAdmin) { + return UserModel.findByIdAndUpdate( + userId, + { + $set: { + isAdmin, + }, + }, + { new: true }, // return the updated user + ); +} + +export async function deleteUserById(userId) { + return UserModel.findByIdAndDelete(userId); +} diff --git a/backend/user-service/model/user-model.js b/backend/user-service/model/user-model.js new file mode 100644 index 0000000000..df37491d09 --- /dev/null +++ b/backend/user-service/model/user-model.js @@ -0,0 +1,31 @@ +import mongoose from "mongoose"; + +const Schema = mongoose.Schema; + +const UserModelSchema = new Schema({ + username: { + type: String, + required: true, + unique: true, + }, + email: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, // Setting default to the current date/time + }, + isAdmin: { + type: Boolean, + required: true, + default: false, + }, +}); + +export default mongoose.model("UserModel", UserModelSchema); diff --git a/backend/user-service/package-lock.json b/backend/user-service/package-lock.json new file mode 100644 index 0000000000..4229ac6343 --- /dev/null +++ b/backend/user-service/package-lock.json @@ -0,0 +1,1929 @@ +{ + "name": "user-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "user-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.5.4", + "nodemailer": "^6.9.15" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongodb": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", + "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.5.4.tgz", + "integrity": "sha512-nG3eehhWf9l1q80WuHvp5DV+4xDNFpDWLE5ZgcFD5tslUV2USJ56ogun8gaZ62MKAocJnoStjAdno08b8U57hg==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.7.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/backend/user-service/package.json b/backend/user-service/package.json new file mode 100644 index 0000000000..41c5380c08 --- /dev/null +++ b/backend/user-service/package.json @@ -0,0 +1,28 @@ +{ + "name": "user-service", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "dev": "nodemon server.js", + "start": "node server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "nodemon": "^3.1.4" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.5.4", + "nodemailer": "^6.9.15" + } +} diff --git a/backend/user-service/routes/auth-routes.js b/backend/user-service/routes/auth-routes.js new file mode 100644 index 0000000000..f171ebec63 --- /dev/null +++ b/backend/user-service/routes/auth-routes.js @@ -0,0 +1,16 @@ +import express from "express"; + +import { handleLogin, handleLogout, handleVerifyToken, handleForgotPassword } from "../controller/auth-controller.js"; +import { verifyAccessToken } from "../middleware/basic-access-control.js"; + +const router = express.Router(); + +router.post("/login", handleLogin); + +router.post("/logout", handleLogout); + +router.get("/verify-token", verifyAccessToken, handleVerifyToken); + +router.post("/forgot-password", handleForgotPassword); + +export default router; diff --git a/backend/user-service/routes/user-routes.js b/backend/user-service/routes/user-routes.js new file mode 100644 index 0000000000..51c2fb64a8 --- /dev/null +++ b/backend/user-service/routes/user-routes.js @@ -0,0 +1,27 @@ +import express from "express"; + +import { + createUser, + deleteUser, + getAllUsers, + getUser, + updateUser, + updateUserPrivilege, +} from "../controller/user-controller.js"; +import { verifyAccessToken, verifyIsAdmin, verifyIsOwnerOrAdmin } from "../middleware/basic-access-control.js"; + +const router = express.Router(); + +router.get("/", verifyAccessToken, verifyIsAdmin, getAllUsers); + +router.patch("/:id/privilege", verifyAccessToken, verifyIsAdmin, updateUserPrivilege); + +router.post("/", createUser); + +router.get("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, getUser); + +router.patch("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, updateUser); + +router.delete("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, deleteUser); + +export default router; diff --git a/backend/user-service/server.js b/backend/user-service/server.js new file mode 100644 index 0000000000..f59ed938ac --- /dev/null +++ b/backend/user-service/server.js @@ -0,0 +1,19 @@ +import http from "http"; +import index from "./index.js"; +import "dotenv/config"; +import { connectToDB } from "./model/repository.js"; + +const port = process.env.PORT || 3001; + +const server = http.createServer(index); + +await connectToDB().then(() => { + console.log("MongoDB Connected!"); + + server.listen(port); + console.log("User service server listening on http://localhost:" + port); +}).catch((err) => { + console.error("Failed to connect to DB"); + console.error(err); +}); + diff --git a/backend/user-service/utils/nodemailer.js b/backend/user-service/utils/nodemailer.js new file mode 100644 index 0000000000..eb84adee1e --- /dev/null +++ b/backend/user-service/utils/nodemailer.js @@ -0,0 +1,12 @@ +import nodemailer from 'nodemailer'; + +export const transporter = nodemailer.createTransport({ + service: "gmail", + host: "smtp.gmail.com", + secure: false, + auth: { + user: process.env.GMAIL_USER, + pass: process.env.GMAIL_PASSWORD, + }, +}); + diff --git a/backend/voice-service/Dockerfile b/backend/voice-service/Dockerfile new file mode 100644 index 0000000000..4e7d51bb63 --- /dev/null +++ b/backend/voice-service/Dockerfile @@ -0,0 +1,31 @@ +# Stage 1: Build the Go binary +FROM golang:1.19 AS builder + +# Set the working directory inside the container +WORKDIR /app + +# Copy Go module files and install dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the application source code +COPY . . + +# Build the Go application +RUN CGO_ENABLED=0 GOOS=linux go build -o voice-service main.go + +# Stage 2: Create a smaller image to run the binary +FROM alpine:latest + +# Install CA certificates for HTTPS support +RUN apk --no-cache add ca-certificates + +# Set working directory and copy the compiled binary from the builder stage +WORKDIR /root/ +COPY --from=builder /app/voice-service . + +# Expose the application port (update this if your Go app uses a different port) +EXPOSE 8085 + +# Run the application +CMD ["./voice-service"] \ No newline at end of file diff --git a/backend/voice-service/controller/room_controller.go b/backend/voice-service/controller/room_controller.go new file mode 100644 index 0000000000..339e78b18f --- /dev/null +++ b/backend/voice-service/controller/room_controller.go @@ -0,0 +1,99 @@ +package controller + +import ( + "log" + "math/rand" + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" +) + +// Room represents a voice chat room with two peers +type Room struct { + Peer1ID string + Peer2ID string +} + +var ( + rooms = make(map[string]*Room) + roomsMutex = sync.Mutex{} +) + +// JoinRoom handles requests to join or reconnect to a room. +func JoinRoom(c *gin.Context) { + roomID := c.Param("roomID") + providedPeerID := c.Query("peerID") // Get the peerID from the query parameter for reconnection + log.Printf("Attempting to join RoomID: %s with Provided PeerID: %s", roomID, providedPeerID) + + roomsMutex.Lock() + defer roomsMutex.Unlock() + + // Ensure room entry exists in the map + if rooms[roomID] == nil { + rooms[roomID] = &Room{} + log.Printf("Created new room for RoomID: %s", roomID) + } + room := rooms[roomID] + + // Reconnection logic: Check if providedPeerID matches an existing peer in the room + if providedPeerID != "" { + if providedPeerID == room.Peer1ID || providedPeerID == room.Peer2ID { + log.Printf("Reconnecting with existing PeerID: %s", providedPeerID) + c.JSON(http.StatusOK, gin.H{ + "peerID": providedPeerID, + "connectionPeerID": getConnectionPeerID(room, providedPeerID), + }) + return + } else { + log.Printf("Provided PeerID: %s does not match any existing PeerID in RoomID: %s", providedPeerID, roomID) + } + } + + // If it's a new join (no valid providedPeerID for reconnection), generate a new peerID + newPeerID := generatePeerID() + if room.Peer1ID == "" { + room.Peer1ID = newPeerID + log.Printf("Assigned new Peer1ID: %s for RoomID: %s", room.Peer1ID, roomID) + c.JSON(http.StatusOK, gin.H{ + "peerID": room.Peer1ID, + "connectionPeerID": room.Peer2ID, + }) + return + } else if room.Peer2ID == "" { + room.Peer2ID = newPeerID + log.Printf("Assigned new Peer2ID: %s for RoomID: %s", room.Peer2ID, roomID) + c.JSON(http.StatusOK, gin.H{ + "peerID": room.Peer2ID, + "connectionPeerID": room.Peer1ID, + }) + return + } + + // Room is full, and reconnection failed + log.Printf("Room %s is full and reconnection failed. Provided PeerID: %s", roomID, providedPeerID) + c.JSON(http.StatusBadRequest, gin.H{"error": "Room is full and no reconnection match found"}) +} + +// getConnectionPeerID returns the connection peer ID for the other peer in the room. +func getConnectionPeerID(room *Room, peerID string) string { + if room.Peer1ID == peerID { + return room.Peer2ID + } else if room.Peer2ID == peerID { + return room.Peer1ID + } + return "" +} + +// generatePeerID creates a random unique peer ID with a prefix +func generatePeerID() string { + rand.Seed(time.Now().UnixNano()) + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + length := 10 + result := make([]byte, length) + for i := range result { + result[i] = charset[rand.Intn(len(charset))] + } + return "peer-" + string(result) +} diff --git a/backend/voice-service/go.mod b/backend/voice-service/go.mod new file mode 100644 index 0000000000..45eef3e829 --- /dev/null +++ b/backend/voice-service/go.mod @@ -0,0 +1,35 @@ +module voice-service + +go 1.19 + +require github.com/gin-gonic/gin v1.10.0 + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/cors v1.7.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/voice-service/go.sum b/backend/voice-service/go.sum new file mode 100644 index 0000000000..ba5071e3cc --- /dev/null +++ b/backend/voice-service/go.sum @@ -0,0 +1,89 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/voice-service/main.go b/backend/voice-service/main.go new file mode 100644 index 0000000000..9d50b739c6 --- /dev/null +++ b/backend/voice-service/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-contrib/cors" + "voice-service/controller" +) + +type Room struct { + Peer1ID string + Peer2ID string +} + +var ( + rooms = make(map[string]*Room) + roomsMutex = sync.Mutex{} +) + +func main() { + router := gin.Default() + + // Add CORS middleware to allow requests from localhost:3000 + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"http://localhost:3000"}, + AllowMethods: []string{"GET", "POST", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + // Endpoint to join a room + router.GET("/join/:roomID", controller.JoinRoom) + + log.Println("Starting server on port 8085") + if err := router.Run(":8085"); err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000..fba599c887 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,127 @@ +version: '3.8' +services: + frontend: + build: + context: ./frontend + ports: + - "3000:3000" + env_file: + - ./frontend/.env # Load frontend environment variables + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=http://nginx + depends_on: + - user-service + - question-service + - matching-service + - collaboration-service + - voice-service + networks: + - app-network + + user-service: + build: + context: ./backend/user-service + ports: + - "3001:3001" + environment: + - NODE_ENV=production + networks: + - app-network + + question-service: + build: + context: ./backend/question-service + ports: + - "5050:5050" + environment: + - NODE_ENV=production + networks: + - app-network + + matching-service: + build: + context: ./backend/matching-service + ports: + - "3002:3002" + environment: + - NODE_ENV=production + - RABBITMQ_HOST=rabbitmq + - RABBITMQ_PORT=5672 + - RABBITMQ_URL=amqp://ihate:cs3219@rabbitmq:5672/ + depends_on: + rabbitmq: + condition: service_healthy + networks: + - app-network + + collaboration-service: + build: + context: ./backend/collaboration-service + ports: + - "8080:8080" + - "2501:2501" + environment: + - NODE_ENV=production + depends_on: + rabbitmq: + condition: service_healthy + networks: + - app-network + + voice-service: + build: ./backend/voice-service + ports: + - "8085:8085" # Exposes the backend on localhost:8085 + environment: + - PORT=8085 + networks: + - app-network + + peer-server: + build: ./backend/peer-server + ports: + - "9000:9000" # Exposes PeerJS server on localhost:9000 + networks: + - app-network + environment: + - PEER_PORT=9000 + - PEER_PATH=/ + - PEER_KEY=peerjs + - PEER_CONFIG_FILE=/config.json + volumes: + - ./backend/peer-server/config.json:/config.json + + rabbitmq: + image: rabbitmq:3-management + ports: + - "5672:5672" # RabbitMQ port for messaging + - "15672:15672" # RabbitMQ management UI + environment: + - RABBITMQ_DEFAULT_USER=ihate + - RABBITMQ_DEFAULT_PASS=cs3219 + - RABBITMQ_ERLANG_COOKIE=1234 + networks: + - app-network + healthcheck: + test: ["CMD", "rabbitmqctl", "status"] + interval: 30s + timeout: 10s + retries: 5 + + nginx: + build: + context: ./backend/api-gateway + ports: + - "80:80" + depends_on: + - user-service + - question-service + - matching-service + - collaboration-service + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 0000000000..af6ab76f80 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,20 @@ +.now/* +*.css +.changeset +dist +esm/* +public/* +tests/* +scripts/* +*.config.js +.DS_Store +node_modules +coverage +.next +build +!.commitlintrc.cjs +!.lintstagedrc.cjs +!jest.config.js +!plopfile.js +!react-shim.js +!tsup.config.ts \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000000..62113a4ce6 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc.json", + "env": { + "browser": false, + "es2021": true, + "node": true + }, + "extends": [ + "plugin:react/recommended", + "plugin:prettier/recommended", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended" + ], + "plugins": [ + "react", + "unused-imports", + "import", + "@typescript-eslint", + "jsx-a11y", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "no-console": "off", + "react/prop-types": "off", + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "react-hooks/exhaustive-deps": "off", + "jsx-a11y/click-events-have-key-events": "warn", + "jsx-a11y/interactive-supports-focus": "warn", + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto" + } + ], + "no-unused-vars": "off", + "unused-imports/no-unused-vars": "off", + "unused-imports/no-unused-imports": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "after-used", + "ignoreRestSiblings": false, + "argsIgnorePattern": "^_.*?$" + } + ], + "import/order": [ + "warn", + { + "groups": [ + "type", + "builtin", + "object", + "external", + "internal", + "parent", + "sibling", + "index" + ], + "pathGroups": [ + { + "pattern": "~/**", + "group": "external", + "position": "after" + } + ], + "newlines-between": "always" + } + ], + "react/self-closing-comp": "warn", + "react/jsx-sort-props": [ + "warn", + { + "callbacksLast": true, + "shorthandFirst": true, + "noSortAlphabetically": false, + "reservedFirst": true + } + ], + "padding-line-between-statements": [ + "warn", + { "blankLine": "always", "prev": "*", "next": "return" }, + { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, + { + "blankLine": "any", + "prev": ["const", "let", "var"], + "next": ["const", "let", "var"] + } + ] + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..56818511eb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env.docker + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000..910a8107d5 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +# Use an official Node.js image +FROM node:18-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Build the Next.js app +RUN npm run build + +# Expose the port where the app will run +EXPOSE 3000 + +# Start the Next.js app +CMD ["npm", "start"] diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100644 index 0000000000..7f91f8483f --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Next UI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000..cd661f7d3a --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +# PeerPrep G47 Frontend + +## How to set up frontend after cloning the repository + +### Install dependencies + +Using `npm`: + +```bash +cd frontend +npm install +``` + +### Run the development server + +```bash +npm run dev +``` + +## Acknowledgements + +This application was bootstrapped with [NextUI](https://nextui.org/docs/guide/installation)'s "Next.js & NextUI Template". + +## Technologies Used + +- [Next.js 14](https://nextjs.org/docs/getting-started) +- [NextUI](https://nextui.org) +- [Tailwind CSS](https://tailwindcss.com) +- [Tailwind Variants](https://tailwind-variants.org) +- [TypeScript](https://www.typescriptlang.org) +- [Framer Motion](https://www.framer.com/motion) +- [next-themes](https://github.com/pacocoursey/next-themes) + +## License + +Licensed under the [MIT license](https://github.com/nextui-org/next-pages-template/blob/main/LICENSE). diff --git a/frontend/app/(collaboration)/collaboration/[roomId]/layout.tsx b/frontend/app/(collaboration)/collaboration/[roomId]/layout.tsx new file mode 100644 index 0000000000..77427e45da --- /dev/null +++ b/frontend/app/(collaboration)/collaboration/[roomId]/layout.tsx @@ -0,0 +1,47 @@ +import "react-toastify/dist/ReactToastify.css"; + +import type { Metadata, Viewport } from "next"; + +import { SocketProvider } from "@/context/SockerIOContext"; +import { siteConfig } from "@/config/site"; +import "@/styles/globals.css"; +import { Navbar } from "@/components/navbar"; + +const config = siteConfig(false); + +export const metadata: Metadata = { + title: config.name, + description: config.description, + openGraph: { + title: config.name, + description: config.description, + }, + icons: { + icon: "/favicon.ico", + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "black" }, + ], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
+ {children} +
+
+

PeerPrep built by Group 47

+
+
+ ); +} diff --git a/frontend/app/(collaboration)/collaboration/[roomId]/page.tsx b/frontend/app/(collaboration)/collaboration/[roomId]/page.tsx new file mode 100644 index 0000000000..c17b14df9c --- /dev/null +++ b/frontend/app/(collaboration)/collaboration/[roomId]/page.tsx @@ -0,0 +1,383 @@ +"use client"; + +import { useState, useContext, useEffect, useRef } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + useDisclosure, +} from "@nextui-org/modal"; +import { Button } from "@nextui-org/button"; +import { Avatar } from "@nextui-org/avatar"; +import { toast } from "react-toastify"; +import Cookies from "js-cookie"; +import { Spinner } from "@nextui-org/spinner"; + +import { Question } from "@/types/questions"; +import QuestionDescription from "@/components/questions/QuestionDescription"; +import { SocketContext } from "@/context/SockerIOContext"; +import { useUser } from "@/hooks/users"; +import CodeEditor from "@/components/collaboration/CodeEditor"; +import VoiceChat from "@/components/collaboration/VoiceChat"; +import { + useClearCookie, + useGetInfo, + useGetIsAuthorisedUser, + useSaveCode, +} from "@/hooks/api/collaboration"; +import { SaveCodeVariables } from "@/utils/collaboration"; + +export default function Page() { + const [output, setOutput] = useState("Your output will appear here..."); + const [isLoading, setIsLoading] = useState(false); + const code = useRef(""); + const language = useRef(""); + const router = useRouter(); + const params = useParams(); + const roomId = params?.roomId || ""; + + const socket = useContext(SocketContext); + const { user } = useUser(); + const { isOpen, onOpen, onOpenChange } = useDisclosure(); + const [otherUserEnd, setOtherUserEnd] = useState(false); + const [otherUser, setOtherUser] = useState(""); + const [question, setQuestion] = useState({ + title: "", + complexity: "", + category: [], + description: "", + examples: "", + constraints: "", + }); + const [roomClosed, setRoomClosed] = useState(false); + + const { data: roomInfo, isPending: isQuestionPending } = useGetInfo( + roomId as string, + ); + + const { + data: isAuthorisedUser, + isPending: isAuthorisationPending, + isError, + } = useGetIsAuthorisedUser(roomId as string, user?.username as string); + + const { mutate: saveCode } = useSaveCode(); + + const { mutate: clearCookie } = useClearCookie(); + + const handleCodeChange = (newCode: string) => { + code.current = newCode; + }; + + const saveCodeAndEndSession = (savedCode: SaveCodeVariables) => { + saveCode( + { ...savedCode }, + { + onSuccess: () => { + socket?.emit("user-end", roomId, user?.username); + router.push("/match"); + }, + onError: (error) => { + console.error("Error saving code:", error); + socket?.emit("user-end", roomId, user?.username); + router.push("/match"); + }, + }, + ); + }; + + const handleEndSession = (): void => { + socket?.off("user-join"); + socket?.off("other-user-end"); + + saveCodeAndEndSession({ + roomId: roomId as string, + code: code.current, + language: language.current, + }); + }; + + useEffect(() => { + if (!isQuestionPending && roomInfo) { + if (roomInfo.status === "closed") { + setRoomClosed(true); + setOtherUser(""); + } + } + }, [roomInfo]); + + useEffect(() => { + if (!isAuthorisationPending && !isAuthorisedUser?.authorised) { + router.push("/403"); + } + }, [isAuthorisationPending, isAuthorisedUser, router]); + + useEffect(() => { + if (!isQuestionPending && roomInfo) { + const matchedQuestion = roomInfo?.question; + + if (roomInfo.userOne !== user?.username) { + setOtherUser(roomInfo.userOne); + } else { + setOtherUser(roomInfo.userTwo); + } + + setQuestion({ + title: matchedQuestion?.title || "", + complexity: matchedQuestion?.complexity || "", + category: matchedQuestion?.category || [], + description: matchedQuestion?.description || "", + examples: matchedQuestion?.examples || "", + constraints: matchedQuestion?.constraints || "", + }); + + language.current = roomInfo["programming_language"] || ""; + } + }, [isQuestionPending, roomInfo, user]); + + useEffect(() => { + const handleBeforeUnload = () => { + const peerID = Cookies.get("peerID"); + + if (peerID) { + fetch(`http://localhost:8085/leave/${roomId}?peerID=${peerID}`, { + method: "POST", + }).catch((err) => console.error("Error leaving room:", err)); + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + + return () => window.removeEventListener("beforeunload", handleBeforeUnload); + }, [roomId]); + + useEffect(() => { + if (socket && roomId && user) { + // Check for existing peerID in localStorage for reconnection + const storedPeerID = localStorage.getItem("peerID"); + + if (storedPeerID) { + // Attempt reconnection + socket.emit("join-room", roomId, user.username, storedPeerID); + } else { + // Join as a new connection + socket.emit("join-room", roomId, user.username); + + // Store peerID received from server after initial join + socket.on("joined-room", (peerID) => { + localStorage.setItem("peerID", peerID); + }); + } + + socket.on("user-join", (otherUser) => { + if (otherUser !== user?.username) { + toast.info(`User ${otherUser} has joined the room`, { + position: "top-right", + autoClose: 1000, + hideProgressBar: false, + }); + + setOtherUser(otherUser); + } + }); + + // Setup socket listeners + const handleUserDisconnect = (username: string) => { + if (!otherUserEnd) { + toast.info( + `User ${username} disconnected. You can exit the session if the other user does not rejoin.`, + { + position: "top-left", + autoClose: 3000, + hideProgressBar: false, + }, + ); + setOtherUser(""); + } + }; + + const handleOtherUserEnd = () => { + setOtherUserEnd(true); + }; + + socket.on("user-disconnect", handleUserDisconnect); + socket.on("other-user-end", handleOtherUserEnd); + + // Cleanup function to remove listeners on unmount or when dependencies change + return () => { + socket.off("user-disconnect", handleUserDisconnect); + socket.off("other-user-end", handleOtherUserEnd); + }; + } + }, [socket, roomId, user, otherUserEnd]); + + const isAvatarActive = (otherUser: string) => { + return otherUser ? "success" : "default"; + }; + + const handleCodeOutput = (output: string) => { + setOutput(output); + setIsLoading(false); + }; + + return ( + <> + {isAuthorisationPending ? ( +

Loading...

+ ) : isError ? ( +

Had trouble authorising user!

+ ) : ( +
+
+ {/* Question Section */} +
+ +
+ + {/* Editor Section */} +
+ setIsLoading(true)} + handleCodeOutput={handleCodeOutput} + language={language.current || "js"} + roomId={roomId as string} + userEmail={user?.email || "unknown user"} + userId={user?.id || "unknown user"} + userName={user?.username || "unknown user"} + onCodeChange={handleCodeChange} + /> +
+ + {/* Output Section */} +
+
+
+ + +
+ +
+ +
+

Output:

+
+
+                    {isLoading ?  : output}
+                  
+
+
+
+
+ + {/* Modals */} + + + {(onClose) => ( + <> + + Exit Session + + +

+ Are you sure you want to exit the session? This action + cannot be undone and the room will be closed. +

+
+ + + + + + )} +
+
+ + + + {() => ( + <> + + The other user exited the session. + + +

The session ended and this room is no longer open.

+
+ + + + + )} +
+
+ + + + {() => ( + <> + + The room is already closed. + + +

+ The session ended as the other user exited and this room + is no longer open. Please match again! +

+
+ + + + + )} +
+
+
+ )} + + ); +} diff --git a/frontend/app/(main)/403/page.tsx b/frontend/app/(main)/403/page.tsx new file mode 100644 index 0000000000..d18e50c4dc --- /dev/null +++ b/frontend/app/(main)/403/page.tsx @@ -0,0 +1,9 @@ +export default function Page() { + return ( + <> +
+

403 Forbidden Access

+
+ + ); +} diff --git a/frontend/app/(main)/admin/questions/add/page.tsx b/frontend/app/(main)/admin/questions/add/page.tsx new file mode 100644 index 0000000000..3c90fd9a82 --- /dev/null +++ b/frontend/app/(main)/admin/questions/add/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import QuestionForm from "@/components/forms/QuestionForm"; +import { Question } from "@/types/questions"; +import { useAddQuestions } from "@/hooks/api/questions"; + +export default function Page() { + const router = useRouter(); + const [formData, setFormData] = useState({ + title: "", + complexity: "", + category: [], + description: "", + examples: "", + constraints: "", + }); + + const { mutate: addQuestion, isError, error } = useAddQuestions(); + + const handleOnSubmit = (updatedData: Question) => { + addQuestion( + { ...updatedData }, + { + onSuccess: () => { + alert("Question successfully added!"); + router.push("/admin/questions"); // Redirect to admin questions list on success + }, + }, + ); + }; + + return ( +
+
+ + + {isError && ( +

{`${error?.message}. Please try again later.`}

+ )} +
+
+ ); +} diff --git a/frontend/app/(main)/admin/questions/edit/[id]/page.tsx b/frontend/app/(main)/admin/questions/edit/[id]/page.tsx new file mode 100644 index 0000000000..e96271719b --- /dev/null +++ b/frontend/app/(main)/admin/questions/edit/[id]/page.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useRouter, useParams } from "next/navigation"; +import { useState, useEffect } from "react"; + +import QuestionForm from "@/components/forms/QuestionForm"; +import { useGetQuestion } from "@/hooks/api/questions"; +import { useUpdateQuestions } from "@/hooks/api/questions"; +import { Question } from "@/types/questions"; + +export default function Page() { + const router = useRouter(); + const params = useParams(); + const id = params?.id; + + // Fetch the question using useGetQuestion hook + const { data: question, isLoading, error } = useGetQuestion(id as string); + + const [formData, setFormData] = useState({ + title: "", + complexity: "", + category: [], + description: "", + examples: "", + constraints: "", + }); + + // Update formData when question is fetched + useEffect(() => { + if (question) { + setFormData({ + title: question.title || "", + complexity: question.complexity || "", + category: question.category || [], + description: question.description || "", + examples: question.examples || "", + constraints: question.constraints || "", + }); + } + }, [question]); + + // Mutation hook to update the question + const { + mutate: updateQuestion, + isError: isUpdateError, + error: updateError, + } = useUpdateQuestions(); + + // Handle the form submission for updating the question + const handleOnSubmit = (updatedData: Question) => { + updateQuestion( + { ...updatedData, questionId: id as string }, // Pass the updated question with ID + { + onSuccess: () => { + alert("Question successfully updated!"); + router.push("/admin/questions"); // Redirect to questions list on success + }, + }, + ); + }; + + return ( +
+
+ {isLoading ? ( +

Loading...

+ ) : error ? ( +

Error loading question. Please try again later.

+ ) : !question ? ( +

Question does not exist!

+ ) : ( + <> + + {isUpdateError && ( +

{`${updateError?.message}. Please try again later.`}

+ )} + + )} +
+
+ ); +} diff --git a/frontend/app/(main)/admin/questions/page.tsx b/frontend/app/(main)/admin/questions/page.tsx new file mode 100644 index 0000000000..99d66475c9 --- /dev/null +++ b/frontend/app/(main)/admin/questions/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useState } from "react"; + +import { useQuestions } from "@/hooks/api/questions"; +import QuestionTable from "@/components/questions/QuestionTable"; + +export default function Page() { + const [pageNumber, setPageNumber] = useState(1); + const { data: questionList, isLoading, isError } = useQuestions(pageNumber); + const handleOnPageClick = (page: number) => { + setPageNumber(page); + }; + + return ( + <> + {isLoading ? ( +

Fetching Questions...

+ ) : isError ? ( +

Had Trouble Fetching Questions!

+ ) : ( +
+ +
+ )} + + ); +} diff --git a/frontend/app/(main)/api/ws.ts b/frontend/app/(main)/api/ws.ts new file mode 100644 index 0000000000..76be05598a --- /dev/null +++ b/frontend/app/(main)/api/ws.ts @@ -0,0 +1,39 @@ +import { NextApiRequest } from "next"; +import { Server } from "ws"; + +export default function websocketHandler(req: NextApiRequest, res: any) { + if (res.socket.server.ws) { + console.log("WebSocket server already running"); + res.end(); + + return; + } + + // Create WebSocket server + const wss = new Server({ noServer: true }); + + res.socket.server.ws = wss; + + // Handle WebSocket connections + wss.on("connection", (ws) => { + ws.on("message", (message) => { + const parsedMessage = JSON.parse(message.toString()); + + // Broadcast the message to other connected clients + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === client.OPEN) { + client.send(JSON.stringify(parsedMessage)); + } + }); + }); + }); + + console.log("WebSocket server started"); + res.end(); +} + +export const config = { + api: { + bodyParser: false, // Disables body parsing for WebSocket handling + }, +}; diff --git a/frontend/app/(main)/forget-password/page.tsx b/frontend/app/(main)/forget-password/page.tsx new file mode 100644 index 0000000000..b12545f6b1 --- /dev/null +++ b/frontend/app/(main)/forget-password/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@nextui-org/button"; +import { Card } from "@nextui-org/card"; + +import ForgetPasswordForm from "@/components/forms/ForgetPasswordForm"; +import { useForgetPassword } from "@/hooks/api/auth"; + +export default function Page() { + const router = useRouter(); + const { mutate: forgetPassword, isPending } = useForgetPassword(); + const [statusMessage, setStatusMessage] = React.useState(null); + + const handleForgetPassword = (email: string) => { + forgetPassword( + { email }, + { + onSuccess: () => { + // Show the success message in place + setStatusMessage("Reset email sent!"); + }, + onError: (err) => { + console.error("Forgot password failed:", err); + if (err?.response?.status === 401) { + setStatusMessage("Please enter a valid registered email."); + } else { + setStatusMessage("An unexpected error occurred. Please try again."); + } + }, + }, + ); + }; + + const handleLogin = () => { + router.push("/login"); + }; + + return ( +
+ +

+ Forgot Your Password? +

+ + {/* ForgetPasswordForm component, with the correct onSubmit handler */} + + + {statusMessage &&

{statusMessage}

} + {isPending && !statusMessage && ( +

Sending reset email...

+ )} +
+ +
+
+
+ ); +} + diff --git a/frontend/app/(main)/history/page.tsx b/frontend/app/(main)/history/page.tsx new file mode 100644 index 0000000000..b82c9fe1e8 --- /dev/null +++ b/frontend/app/(main)/history/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useState } from "react"; + +import { useHistory } from "@/hooks/api/history"; +import HistoryTable from "@/components/history/HistoryTable"; +import { useUser } from "@/hooks/users"; + +export default function Page() { + const [pageNumber, setPageNumber] = useState(1); + const { user } = useUser(); + const { + data: historyList, + isLoading, + isError, + } = useHistory(user?.username || "", pageNumber); + const handleOnPageClick = (page: number) => { + setPageNumber(page); + }; + + return ( + <> + {isLoading ? ( +

Fetching History...

+ ) : isError ? ( +

Had Trouble Fetching History!

+ ) : ( +
+ +
+ )} + + ); +} diff --git a/frontend/app/(main)/history/session-details/page.tsx b/frontend/app/(main)/history/session-details/page.tsx new file mode 100644 index 0000000000..52eaa216d1 --- /dev/null +++ b/frontend/app/(main)/history/session-details/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; + +import { useGetHistory } from "@/hooks/api/history"; +import HistoryDescription from "@/components/history/HistoryDescription"; +import { useUser } from "@/hooks/users"; + +function HistoryContent() { + const searchParams = useSearchParams(); + const historyId = searchParams?.get("id"); + const idString: string = ( + Array.isArray(historyId) ? historyId[0] : historyId + ) as string; + const { user } = useUser(); + const { data: history, isLoading, isError } = useGetHistory(idString); + + return isLoading ? ( +

fetching history...

+ ) : isError || !history ? ( +

Error fetching History

+ ) : ( + + ); +} + +export default function Page() { + return ( + Loading history...}> + + + ); +} diff --git a/frontend/app/(main)/layout.tsx b/frontend/app/(main)/layout.tsx new file mode 100644 index 0000000000..4fdb3dd7ed --- /dev/null +++ b/frontend/app/(main)/layout.tsx @@ -0,0 +1,44 @@ +import type { Metadata, Viewport } from "next"; + +import { siteConfig } from "@/config/site"; +import "@/styles/globals.css"; +import { Navbar } from "@/components/navbar"; + +const config = siteConfig(false); + +export const metadata: Metadata = { + title: config.name, + description: config.description, + openGraph: { + title: config.name, + description: config.description, + }, + icons: { + icon: "/favicon.ico", + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "black" }, + ], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
+ {children} +
+
+

PeerPrep built by Group 47

+
+
+ ); +} diff --git a/frontend/app/(main)/login/page.tsx b/frontend/app/(main)/login/page.tsx new file mode 100644 index 0000000000..d123f964c9 --- /dev/null +++ b/frontend/app/(main)/login/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@nextui-org/button"; +import { Card } from "@nextui-org/card"; + +import LoginForm from "@/components/forms/LoginForm"; +import { useLogin } from "@/hooks/api/auth"; +import { useUser } from "@/hooks/users"; +import { User } from "@/types/user"; + +export default function Page() { + const router = useRouter(); + const { setUser } = useUser(); + const { mutate: login, isPending } = useLogin(); + const [errorMessage, setErrorMessage] = React.useState(null); + + const handleLogin = (email: string, password: string) => { + login( + { email, password }, + { + onSuccess: (data) => { + console.log("Login successful!"); + const user: User = { + id: data.id, + username: data.username, + email: data.email, + isAdmin: data.isAdmin, + }; + + setUser(user); + router.push("/match"); + }, + onError: (err) => { + console.error("Login failed:", err); + + if (err?.response?.status === 401) { + setErrorMessage("Incorrect email or password."); + } else { + setErrorMessage("An unexpected error occurred. Please try again."); + } + }, + }, + ); + }; + + const handleRegister = () => { + router.push("/register"); + }; + + const handleForgetPassword = () => { + router.push("/forget-password"); + }; + + return ( +
+ +

+ Welcome to PeerPrep! +

+ + + + {errorMessage &&

{errorMessage}

} + {isPending &&

Logging in...

} + +
+

Don't have an account?

+ +
+ +
+

Forgot your password?

+ +
+
+
+ ); +} diff --git a/frontend/app/(main)/match/page.tsx b/frontend/app/(main)/match/page.tsx new file mode 100644 index 0000000000..b99e2968a3 --- /dev/null +++ b/frontend/app/(main)/match/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from "@nextui-org/modal"; +import { Button } from "@nextui-org/button"; + +import MatchingPageBody from "@/components/matching/MatchingPageBody"; + +export default function Page() { + const router = useRouter(); + const [roomId, setRoomId] = useState(""); + + useEffect(() => { + // Read the roomId cookie + const cookies = document.cookie.split("; "); + const roomIdCookie = cookies.find((cookie) => cookie.startsWith("roomId=")); + + if (roomIdCookie) { + const roomIdValue = roomIdCookie.split("=")[1]; + + setRoomId(roomIdValue); + } + }, []); + + const isOpen = roomId !== ""; + + return ( + <> + + + + + {() => ( + <> + + Existing Session + + +

You have an existing session, please rejoin the room.

+
+ + + + + )} +
+
+ + ); +} diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx new file mode 100644 index 0000000000..25cdc0d5e7 --- /dev/null +++ b/frontend/app/(main)/page.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Link } from "@nextui-org/link"; +import { button as buttonStyles } from "@nextui-org/theme"; + +import { siteConfig } from "@/config/site"; +import { title, subtitle } from "@/components/primitives"; +import { GithubIcon } from "@/components/icons"; + +const config = siteConfig(false); + +export default function Page() { + return ( +
+
+ Start finding a  + match  +
+ now on PeerPrep! +
+ Interactive interview prep site. +
+
+ +
+ + Login here! + + + + GitHub + +
+
+ ); +} diff --git a/frontend/app/(main)/questions/page.tsx b/frontend/app/(main)/questions/page.tsx new file mode 100644 index 0000000000..e76e9c5b98 --- /dev/null +++ b/frontend/app/(main)/questions/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useState } from "react"; + +import { useQuestions } from "@/hooks/api/questions"; +import QuestionTable from "@/components/questions/QuestionTable"; + +export default function Page() { + const [pageNumber, setPageNumber] = useState(1); + const { data: questionList, isLoading, isError } = useQuestions(pageNumber); + const handleOnPageClick = (page: number) => { + setPageNumber(page); + }; + + return ( + <> + {isLoading ? ( +

Fetching Questions...

+ ) : isError ? ( +

Had Trouble Fetching Questions!

+ ) : ( +
+ +
+ )} + + ); +} diff --git a/frontend/app/(main)/questions/question-description/page.tsx b/frontend/app/(main)/questions/question-description/page.tsx new file mode 100644 index 0000000000..fd27273a7c --- /dev/null +++ b/frontend/app/(main)/questions/question-description/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; + +import { useGetQuestion } from "@/hooks/api/questions"; +import QuestionDescription from "@/components/questions/QuestionDescription"; + +function QuestionContent() { + const searchParams = useSearchParams(); + const questionId = searchParams?.get("id"); + const idString: string = ( + Array.isArray(questionId) ? questionId[0] : questionId + ) as string; + const { data: question, isLoading, isError } = useGetQuestion(idString); + + return isLoading ? ( +

fetching question

+ ) : isError || !question ? ( +

Error fetching Question

+ ) : ( + + ); +} + +export default function Page() { + return ( + Loading question...}> + + + ); +} diff --git a/frontend/app/(main)/register/page.tsx b/frontend/app/(main)/register/page.tsx new file mode 100644 index 0000000000..45b65ed01a --- /dev/null +++ b/frontend/app/(main)/register/page.tsx @@ -0,0 +1,61 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@nextui-org/button"; +import { Card } from "@nextui-org/card"; + +import RegistrationForm from "@/components/forms/RegistrationForm"; +import { useRegister } from "@/hooks/api/auth"; + +const RegisterPage = () => { + const router = useRouter(); + const { mutate: register, isPending } = useRegister(); + const [errorMessage, setErrorMessage] = React.useState(null); + + const handleRegister = ( + username: string, + email: string, + password: string, + ) => { + register( + { username, email, password }, + { + onSuccess: () => { + router.push("/login"); + }, + onError: (err) => { + console.error("Registration failed:", err); + setErrorMessage("An unexpected error occurred. Please try again."); + }, + }, + ); + }; + const handleLogin = () => { + router.push("/login"); + }; + + return ( +
+ +

+ Welcome to PeerPrep! +

+ + + + {errorMessage &&

{errorMessage}

} + {isPending &&

Registering...

} + +
+

Have an account?

+ +
+
+
+ ); +}; + +export default RegisterPage; diff --git a/frontend/app/(main)/reset-password/page.tsx b/frontend/app/(main)/reset-password/page.tsx new file mode 100644 index 0000000000..243f0ab847 --- /dev/null +++ b/frontend/app/(main)/reset-password/page.tsx @@ -0,0 +1,71 @@ +// src/app/reset-password/page.tsx + +"use client"; + +import { Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Button } from "@nextui-org/button"; +import { Card } from "@nextui-org/card"; + +import ResetPasswordForm from "@/components/forms/ResetPasswordForm"; +import { useResetPassword } from "@/hooks/api/auth"; + +function ResetPasswordContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams?.get("token"); + const tokenString: string = ( + Array.isArray(token) ? token[0] : token + ) as string; + const { mutate: resetPassword } = useResetPassword(); + + const handleResetPassword = (newPassword: string) => { + if (!tokenString) { + console.error("Token is missing from the URL"); + + return; + } + + // Call mutation to reset password with token and new password + resetPassword( + { tokenString, newPassword }, + { + onSuccess: () => { + console.log("Password reset successfully!"); + router.push("/login"); // Redirect to login on success + }, + onError: (err) => { + console.error("Password reset failed:", err); + }, + }, + ); + }; + + const handleLogin = () => { + router.push("/login"); + }; + + return ( +
+ +

+ Reset Your Password +

+ +
+ +
+
+
+ ); +} + +export default function Page() { + return ( + Loading...}> + + + ); +} diff --git a/frontend/app/(main)/user-profile/[id]/page.tsx b/frontend/app/(main)/user-profile/[id]/page.tsx new file mode 100644 index 0000000000..31d980266c --- /dev/null +++ b/frontend/app/(main)/user-profile/[id]/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; + +import NavigationColumn from "@/components/users/NavigationColumn"; +import UserProfileForm from "@/components/forms/UserProfileForm"; +import { useUser } from "@/hooks/users"; +import { User, UserProfile } from "@/types/user"; +import { useUpdateUser } from "@/hooks/api/users"; + +export default function Page() { + const { user, setUser } = useUser(); + const { theme } = useTheme(); + const params = useParams(); + + const id = params?.id; + + const [mounted, setMounted] = useState(false); + const [userProfileFormData, setUserProfileFormData] = useState({ + username: "", + email: "", + password: "", + }); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const userProfile = { + username: user?.username || "", + email: user?.email || "", + password: "", + }; + + setUserProfileFormData(userProfile); + }, [user]); + + const { mutate: updateUser, isError, error } = useUpdateUser(); + + const handleOnSubmit = (userProfileData: UserProfile) => { + updateUser( + { user: userProfileData, userId: id as string }, + { + onSuccess: (data) => { + const newUser: User = { + id: data.id, + username: data.username, + email: data.email, + isAdmin: data.isAdmin, + }; + + setUser(newUser); + alert("Profile updated!"); + }, + }, + ); + }; + + if (!mounted) { + return null; // Avoid rendering until the component is fully mounted + } + + return ( + <> +
+
+ +
+
+ + {isError && ( +

{`${error?.message}. Please try again later.`}

+ )} +
+
+ + ); +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000000..47aaf4a347 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,27 @@ +import clsx from "clsx"; + +import Providers from "@/app/providers"; +import { fontSans } from "@/config/fonts"; +import "@/styles/globals.css"; + +export default function BaseLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + {children} + + + + ); +} diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx new file mode 100644 index 0000000000..53f17577f9 --- /dev/null +++ b/frontend/app/providers.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { NextUIProvider } from "@nextui-org/system"; +import { useRouter } from "next/navigation"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { ThemeProviderProps } from "next-themes/dist/types"; + +import { UserProvider } from "@/context/UserContext"; + +const queryClient = new QueryClient(); + +export interface ProvidersProps { + children: React.ReactNode; + themeProps?: ThemeProviderProps; +} + +export default function Providers({ children, themeProps }: ProvidersProps) { + const router = useRouter(); + + return ( + + + + {children} + + + + ); +} diff --git a/frontend/components/collaboration/CodeEditor.tsx b/frontend/components/collaboration/CodeEditor.tsx new file mode 100644 index 0000000000..40a9959d3f --- /dev/null +++ b/frontend/components/collaboration/CodeEditor.tsx @@ -0,0 +1,236 @@ +import { useRef, useState } from "react"; +import Editor from "@monaco-editor/react"; +import { type editor } from "monaco-editor"; +import * as Y from "yjs"; +import { MonacoBinding } from "y-monaco"; +import { WebsocketProvider } from "y-websocket"; +import { useTheme } from "next-themes"; + +import axios from "@/utils/axios"; + +var randomColor = require("randomcolor"); // import the script + +interface CodeEditorProps { + handleCodeOutput: (output: string) => void; + handleCodeExecuteStart: () => void; + roomId: string; + language: string; + onCodeChange: (code: string) => void; + userName: string; + userId: string; + userEmail: string; +} + +const LANGUAGE_MAP: Record = { + cpp: 52, + csharp: 51, + python: 71, + javascript: 63, + java: 62, + ruby: 72, + go: 95, + php: 98, + typescript: 94, +}; + +const CODE_EDITOR_LANGUAGE_MAP: { [language: string]: string } = { + "c++": "cpp", + "c#": "csharp", + python: "python", + js: "javascript", + java: "java", + ruby: "ruby", + go: "go", + php: "php", + typescript: "typescript", +}; + +const CODE_EDITOR_LANGUAGE_DISPLAY_MAP: { [language: string]: string } = { + "c++": "C++", + "c#": "C#", + python: "Python", + js: "JavaScript", + java: "Java", + ruby: "Ruby", + go: "Go", + php: "PHP", + typescript: "TypeScript", +}; + +export default function CodeEditor({ + handleCodeExecuteStart, + handleCodeOutput, + roomId, + language, + userName, + userEmail, + userId, + onCodeChange, +}: CodeEditorProps) { + const codeEditorRef = useRef(); + const { theme } = useTheme(); + const [userInput, setUserInput] = useState(""); + const executeCode = async () => { + if (!codeEditorRef.current) return; + const code = codeEditorRef.current.getValue(); + + // Get the language from the Monaco editor + const currentLanguage = codeEditorRef.current.getModel()?.getLanguageId(); + const languageId = + currentLanguage && LANGUAGE_MAP[currentLanguage] + ? LANGUAGE_MAP[currentLanguage] + : 63; + + try { + const response = await axios.post(`/collaboration-service/code-execute`, { + source_code: code, + language_id: languageId, + }); + const token = response.data.token; + const intervalId = setInterval(async () => { + try { + const { data } = await axios.get( + `/collaboration-service/code-execute/${token}`, + { + params: { base64_encoded: "false", fields: "*" }, + }, + ); + + if (data.status.id >= 3) { + clearInterval(intervalId); + if (data.status.id === 3) { + handleCodeOutput(data.stdout || data.stderr || "No Output"); + } else if (data.status.id > 3) { + handleCodeOutput( + data.stderr || "An Error Occured in Code Execution", + ); + } + } + } catch (error) { + clearInterval(intervalId); + console.error("Error fetching code execution result:", error); + handleCodeOutput( + "Something went wrong while sending code execution request.", + ); + } + }, 1000); + + handleCodeExecuteStart(); + } catch (error) { + console.error("Error executing code:", error); + handleCodeOutput("Something went wrong during code execution."); + } + }; + + return ( +
+
+

+ {CODE_EDITOR_LANGUAGE_DISPLAY_MAP[language]} +

+ +
+ { + setUserInput(value || ""); + onCodeChange(value || ""); + }} // Update userInput in real-time + onMount={(editor, monaco) => { + codeEditorRef.current = editor; + if (typeof window !== "undefined" && monaco) { + const ydoc = new Y.Doc(); + const provider = new WebsocketProvider( + process.env.NEXT_PUBLIC_COLLAB_SERVICE_Y_SERVER_PATH || + "ws://localhost:2501", + roomId, + ydoc, + ); + const yAwareness = provider.awareness; + const yDocTextMonaco = ydoc.getText("monaco"); + + const editor = monaco.editor.getEditors()[0]; + const userColor = randomColor(); + + yAwareness.setLocalStateField("user", { + name: userName, + userId: userId, + email: userEmail, + color: userColor, + }); + + yAwareness.on( + "change", + (changes: { + added: number[]; + updated: number[]; + removed: number[]; + }) => { + const awarenessStates = yAwareness.getStates(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + changes.added.forEach((clientId) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const state = awarenessStates.get(clientId)?.user; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + const color = state?.color; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + const username = state?.name; + const cursorStyleElem = document.head.appendChild( + document.createElement("style"), + ); + + cursorStyleElem.innerHTML = `.yRemoteSelectionHead-${clientId} { border-left: ${color} solid 2px;}`; + const highlightStyleElem = document.head.appendChild( + document.createElement("style"), + ); + + highlightStyleElem.innerHTML = `.yRemoteSelection-${clientId} { background-color: ${color}9A;}`; + const styleElem = document.head.appendChild( + document.createElement("style"), + ); + + styleElem.innerHTML = `.yRemoteSelectionHead-${clientId}::after { transform: translateY(5); margin-left: 5px; border-radius: 5px; opacity: 80%; background-color: ${color}; color: black; content: '${username}'}`; + }); + }, + ); + + // create the monaco binding to the yjs doc + new MonacoBinding( + yDocTextMonaco, + editor.getModel()!, + // @ts-expect-error TODO: fix this + new Set([editor]), + yAwareness, + ); + } + }} + /> +
+ ); +} diff --git a/frontend/components/collaboration/VoiceChat.tsx b/frontend/components/collaboration/VoiceChat.tsx new file mode 100644 index 0000000000..7422562047 --- /dev/null +++ b/frontend/components/collaboration/VoiceChat.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect, useRef } from "react"; +import Peer, { MediaConnection } from "peerjs"; +import { useParams } from "next/navigation"; +import { Card, CardBody, CardFooter } from "@nextui-org/card"; +import Cookies from "js-cookie"; +import { Button } from "@nextui-org/react"; +import { MicrophoneIcon } from "../icons"; +import axios from "@/utils/axios"; + + +interface VoiceChatProps { + className?: string; +} + +export default function VoiceChat({ + className: cardClassName, +}: VoiceChatProps) { + const params = useParams(); + const roomID = params?.roomId as string; + + const [peer, setPeer] = useState(null); + const [call, setCall] = useState(null); + const [remoteStream, setRemoteStream] = useState(null); + const [peerID, setPeerID] = useState(""); + const [connectionPeerID, setConnectionPeerID] = useState(""); + const [isMuted, setIsMuted] = useState(false); + + const audioRef = useRef(null); + + useEffect(() => { + const storedPeerID = Cookies.get("peerID"); + + if (roomID) { + console.log("Fetching peer IDs for room:", roomID); + + axios + .get(`/voice-service/join/${roomID}`, { + params: { peerID: storedPeerID || "" }, + }) + .then((response) => { + const data = response.data; + setPeerID(data.peerID); + setConnectionPeerID(data.connectionPeerID || ""); + console.log("Received peer IDs:", data); + + // Update the cookie with the new peerID if it's not already stored + if (data.peerID !== storedPeerID) { + Cookies.set("peerID", data.peerID, { expires: 1 }); + console.log("peerID saved to cookie:", Cookies.get("peerID")); + } + }) + .catch((err) => console.error("Error fetching peer IDs:", err)); + } + }, [roomID]); + + useEffect(() => { + // Only initialize PeerJS if peerID exists and peer is not set + if (peerID && !peer) { + console.log("Initializing PeerJS instance with peerID:", peerID); + const peerInstance = new Peer(peerID, { + host: "localhost", + port: 9000, + path: "/", + }); + + setPeer(peerInstance); + + // Listen for incoming calls + peerInstance.on("call", (incomingCall) => { + console.log("Incoming call received"); + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + incomingCall.answer(stream); + incomingCall.on("stream", (remoteStream) => { + console.log("Remote stream received from caller"); + setRemoteStream(remoteStream); + }); + }) + .catch((err) => console.error("Error accessing local media:", err)); + }); + + peerInstance.on("open", (id) => { + console.log("Peer connection open with ID:", id); + }); + + peerInstance.on("error", (err) => { + console.error("PeerJS error:", err); + }); + + // Reconnect on disconnection + peerInstance.on("disconnected", () => { + console.log("Peer disconnected, attempting to reconnect..."); + peerInstance.reconnect(); + }); + + peerInstance.on("close", () => { + console.log("Peer connection closed"); + }); + + // Cleanup on component unmount + return () => { + console.log("Destroying PeerJS instance"); + peerInstance.destroy(); + setPeer(null); + }; + } + }, [peerID]); + + useEffect(() => { + if (peer && connectionPeerID) { + console.log("Attempting to call connectionPeerID:", connectionPeerID); + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + const outgoingCall: MediaConnection = peer.call( + connectionPeerID, + stream, + ); + + setCall(outgoingCall); + + outgoingCall.on("stream", (remoteStream) => { + console.log("Remote stream received from outgoing call"); + setRemoteStream(remoteStream); + }); + + outgoingCall.on("error", (err) => { + console.error("Error in outgoing call:", err); + }); + }) + .catch((err) => console.error("Error accessing local media:", err)); + } + }, [peer, connectionPeerID]); + + // Set `srcObject` on the audio element directly through the ref + useEffect(() => { + if (audioRef.current && remoteStream) { + console.log("Setting remote stream to audio element"); + audioRef.current.srcObject = remoteStream; + } + }, [remoteStream]); + const handleMute = () => setIsMuted((prev) => !prev); + + return ( + + +

PeerJS Voice Chat

+

Room: {roomID}

+
+ +
+ {/* Disable the specific ESLint rule for the audio element */} + {/* eslint-disable-next-line jsx-a11y/media-has-caption */} +
+
+
+ ); +} diff --git a/frontend/components/forms/ForgetPasswordForm.tsx b/frontend/components/forms/ForgetPasswordForm.tsx new file mode 100644 index 0000000000..38a11571ba --- /dev/null +++ b/frontend/components/forms/ForgetPasswordForm.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { Input } from "@nextui-org/input"; +import { Button } from "@nextui-org/button"; + +interface ForgetPasswordFormProps { + onSubmit: (email: string) => void; +} + +const ForgetPasswordForm: React.FC = ({ + onSubmit, +}) => { + const [formData, setFormData] = useState({ + email: "", + }); + + const [errors, setErrors] = useState<{ [key: string]: boolean }>({}); + + const handleEmailOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + email: e.target.value, + }); + + // Reset the email error if user starts typing after form submission + setErrors((prevErrors) => ({ + ...prevErrors, + email: false, + })); + }; + + const isValid = () => { + const newErrors: { [key: string]: boolean } = {}; + + if (!formData.email) { + newErrors.email = true; + } + + setErrors(newErrors); + + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isValid()) { + onSubmit(formData.email); + } + }; + + return ( +
+
+

+ Enter your registered email +

+
+ +
+ + +
+ + +
+ ); +}; + +export default ForgetPasswordForm; diff --git a/frontend/components/forms/LoginForm.tsx b/frontend/components/forms/LoginForm.tsx new file mode 100644 index 0000000000..d27a0f2f4c --- /dev/null +++ b/frontend/components/forms/LoginForm.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { Input } from "@nextui-org/input"; +import { Button } from "@nextui-org/button"; + +interface LoginFormProps { + onSubmit: (email: string, password: string) => void; +} + +const LoginForm: React.FC = ({ onSubmit }) => { + const [formData, setFormData] = useState({ + email: "", + password: "", + }); + + const [errors, setErrors] = useState<{ [key: string]: boolean }>({}); + + // Reset email error when user starts typing + const handleEmailOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + email: e.target.value, + }); + + // Reset the email error if user starts typing after form submission + setErrors((prevErrors) => ({ + ...prevErrors, + email: false, + })); + }; + + // Reset password error when user starts typing + const handlePasswordOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + password: e.target.value, + }); + + // Reset the password error if user starts typing after form submission + setErrors((prevErrors) => ({ + ...prevErrors, + password: false, + })); + }; + + const isValid = () => { + const newErrors: { [key: string]: boolean } = {}; + + if (!formData.email) { + newErrors.email = true; + } + + if (!formData.password) { + newErrors.password = true; + } + + setErrors(newErrors); + + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isValid()) { + onSubmit(formData.email, formData.password); + } + }; + + return ( +
+
+

Login

+
+ +
+ + +
+ +
+ + +
+ + +
+ ); +}; + +export default LoginForm; diff --git a/frontend/components/forms/MatchingForm.tsx b/frontend/components/forms/MatchingForm.tsx new file mode 100644 index 0000000000..7bf5b42b32 --- /dev/null +++ b/frontend/components/forms/MatchingForm.tsx @@ -0,0 +1,140 @@ +import { useRef, useState, useContext } from "react"; +import { Button } from "@nextui-org/button"; +import { Card } from "@nextui-org/card"; +import { v4 } from "uuid"; + +import ProgrammingLanguageSelectDropdown from "../matching/ProgrammingLanguageSelectDropdown"; +import QuestionDifficultyDropDown from "../matching/DifficultyDropdown"; +import TopicSelection from "../matching/TopicSelection"; + +import { UserMatchRequest, UserMatchResponse } from "@/types/match"; +import { useAddUserToMatch } from "@/hooks/api/matching"; +import { UserContext } from "@/context/UserContext"; +import { User } from "@/types/user"; + +const CARD_STYLES = "w-9/12 gap-y-7 flex mx-auto flex-col justify-center p-16"; + +interface MatchingFormProps { + onSuccess: (data: UserMatchResponse) => void; + onCancel: () => void; +} +export default function MatchingForm({ + onSuccess, + onCancel, +}: MatchingFormProps) { + const questionDifficultyRef = useRef([]); + const programmingLanguagesRef = useRef([]); + const topicsRef = useRef([]); + const [invalidFields, setInvalidFields] = useState>(new Set()); + const userProps = useContext(UserContext); + const onSuccessMatch = (responseData: UserMatchResponse) => { + questionDifficultyRef.current = []; + programmingLanguagesRef.current = []; + topicsRef.current = []; + onSuccess(responseData); + }; + + // initialise mutate hook to add user to match-service + const { + mutate, + isPending: isPendingMatch, + isError, + } = useAddUserToMatch(onSuccessMatch); + + if (!userProps || userProps.user === null) { + return

Invalid User

; + } + const user: User = userProps.user; + // onSelect Handlers + const onSelectQuestionDifficulty = (difficulties: string[]) => { + questionDifficultyRef.current = difficulties; + }; + + const onSelectProgrammingLanguages = (languages: string[]) => { + programmingLanguagesRef.current = languages; + }; + + const onSelectTopics = (topics: string[]) => { + topicsRef.current = topics; + }; + + // validates and addUser + const onSubmit = () => { + const invalidSet: Set = new Set(); + + if (questionDifficultyRef.current.length < 1) { + invalidSet.add(1); + } + if (programmingLanguagesRef.current.length < 1) { + invalidSet.add(2); + } + if (topicsRef.current.length < 1) { + invalidSet.add(3); + } + setInvalidFields(invalidSet); + if (invalidSet.size !== 0) { + return; + } + const socketId = v4(); + let generalize = false; + let languages = programmingLanguagesRef.current; + + if (programmingLanguagesRef.current.includes("generalize")) { + generalize = true; + languages = []; + } + const userMatchRequest: UserMatchRequest = { + user_id: user.id, + username: user.username, + socket_id: socketId, + difficulty_levels: questionDifficultyRef.current, + categories: topicsRef.current, + programming_languages: languages, + generalize_languages: generalize, + }; + console.log(userMatchRequest); + mutate(userMatchRequest); + }; + + return ( + +

Session Details

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ + +
+ {isError && ( +

+ Error Occurred While Adding User to Queue, Try Again +

+ )} +
+ ); +} diff --git a/frontend/components/forms/QuestionForm.tsx b/frontend/components/forms/QuestionForm.tsx new file mode 100644 index 0000000000..1e874e6dc0 --- /dev/null +++ b/frontend/components/forms/QuestionForm.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Input, Textarea } from "@nextui-org/input"; +import { Button } from "@nextui-org/button"; +import { Select, SelectItem } from "@nextui-org/select"; + +import { Question } from "@/types/questions"; +import { getAllQuestionCategories } from "@/utils/questions"; + +interface QuestionFormProps { + formType: string; + formData: Question; + setFormData: (formData: Question) => void; + onSubmit: (data: Question) => void; +} + +const QuestionForm: React.FC = ({ + formType, + formData, + setFormData, + onSubmit, +}) => { + const router = useRouter(); + const [errors, setErrors] = useState<{ [key: string]: boolean }>({}); + + const allCategories = getAllQuestionCategories(); + + const handleTitleOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + title: e.target.value, + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + title: false, + })); + }; + + const handleComplexityOnChange = ( + e: React.ChangeEvent, + ) => { + setFormData({ + ...formData, + complexity: e.target.value, + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + complexity: false, + })); + }; + + const handleCategoryOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + category: e.target.value.split(",").filter(Boolean), + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + category: false, + })); + }; + + const handleDescriptionOnChange = ( + e: React.ChangeEvent, + ) => { + setFormData({ + ...formData, + description: e.target.value, + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + description: false, + })); + }; + + const handleExamplesOnChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + examples: e.target.value, + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + examples: false, + })); + }; + + const handleConstraintsOnChange = ( + e: React.ChangeEvent, + ) => { + setFormData({ + ...formData, + constraints: e.target.value, + }); + + setErrors((prevErrors) => ({ + ...prevErrors, + constraints: false, + })); + }; + + const isValid = () => { + const newErrors: { [key: string]: boolean } = {}; + + if (!formData.title) { + newErrors.title = true; + } + + if (!formData.complexity) { + newErrors.complexity = true; + } + + if (!formData.category || formData.category.length === 0) { + newErrors.category = true; + } + + if (!formData.description) { + newErrors.description = true; + } + + setErrors(newErrors); + + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!isValid()) { + return; + } + + onSubmit(formData); + }; + + return ( + <> +

{formType} Question

+
+

Question Title

+ +
+
+

Question Complexity

+ +
+
+

Question Category

+ +
+
+