-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: sanitise inputs * feat(question-service)!: add solution and template code in schema * chore: standardise API response * fix: Standardise API response * fix: add statusCode in response * fix: Change request body format * chore: Add test route for health checks * fix: empty templateCode or solutionCode * feat(question-service)!: add link attribute in question * fix: fetching of data after backend api standardisation * nit: fixed minor code usage issues * fix: create/update questions not working (missing field) * fix: compile error * feat: Implement Matching Service (#81) * feat(matching-service)!: Implement matching service backend - Created websocket server for matching service - Added client-sent events: - "connection": User connects to websocket server - "disconnect": User disconnects from websocket server - "create-match": User request for a match with another user with match criteria - "cancel-match": User cancels a current matching request - Added server-sent events: - "connect": Server successfully connected with user - "disconnect": Server disconnected with user - "finding-match": Server trying to find a match with other users - "found-match": Server found a match for this user, returns match object - "no-match": Server could not find a match within the timeout - "disconnect-while-match": User was disconnected with server while finding a match * fix: Handle errors and emit error event to frontend * fix: Reduce timeout duration, add TTL to user entries in DB * feat: Priority based queue from the number of categories selected * fix: Update docker-compose * chore: create .dockerignore files * Revert "chore: create .dockerignore files" This reverts commit f5309fb. * fix: fix wrong Dockerfile command --------- Co-authored-by: George Tay <georgetay71@gmail.com> * feat: Implement Matching flow and update Question CRUD (#82) * chore: Commit frontend mockup for matching service * feat(matching-service)!: Create matching service backend * fix: losing navigation state when refreshing * fix: navbar doesn't align * feat: match service up till failure * fix: add template code input + remove required for link Out of the three new added fields for question, only solution code is compulsory, template code and link are not compulsory. * nit: improve UI and componentization * feat: matching up till success state * nit: cleanup ui * feat: light/dark mode toggle * fix: code editor for templateCode and solutionCode * wip: questionlist * feat: view specific question page * chore: add rich text editor for question description * fix: add templateCode field for Question * fix: rich text editor for description + minor fix for code editor * fix: only open link if it exists * fix: submit html description on creating question * fix: rename description object variable names * nit: clean up navbar and add dark mode * feat: update homepage * nit: remove console.log * fix: fix categories displayed to be changed once backend updates the fetch categories endpoint to return a fixed set of categories * feat(matching-service)!: Implement matching service backend - Created websocket server for matching service - Added client-sent events: - "connection": User connects to websocket server - "disconnect": User disconnects from websocket server - "create-match": User request for a match with another user with match criteria - "cancel-match": User cancels a current matching request - Added server-sent events: - "connect": Server successfully connected with user - "disconnect": Server disconnected with user - "finding-match": Server trying to find a match with other users - "found-match": Server found a match for this user, returns match object - "no-match": Server could not find a match within the timeout - "disconnect-while-match": User was disconnected with server while finding a match * chore: create startup script * chore: update package * fix: Handle errors and emit error event to frontend * fix: Reduce timeout duration, add TTL to user entries in DB * feat: Priority based queue from the number of categories selected * fix: Update docker-compose * fix: forgot to merge * fix: rich text editor bug * fix: question CRUD minor changes ang bug fixes fixed the following issues: - cursor moving to last line when editing in the wysiwyg editor - undo button causing content in wysiwyg editor to be cleared - ensure dummy categories used instead of fetched categories (for now, to be replaced once backend sends over fixed categories with id) - change test code to code editor instead of text area - create question page by default show one test case (to be filled by user) * fix: make question link required * fix: build errors * chore: remove temp frontend for matching service --------- Co-authored-by: pzl111 <phuazailian@gmail.com> Co-authored-by: Lim Pei En <111886863+peienlim@users.noreply.github.com> --------- Co-authored-by: elaineshijie <lohelaineshijie.183@gmail.com> Co-authored-by: pzl111 <phuazailian@gmail.com> Co-authored-by: pzl111 <67725787+pzl111@users.noreply.github.com> Co-authored-by: elaineshijie <122242919+elaineshijie@users.noreply.github.com> Co-authored-by: Lim Pei En <111886863+peienlim@users.noreply.github.com> Co-authored-by: hollag <marcussoh38@gmail.com> Co-authored-by: Marcus Soh <31369072+HollaG@users.noreply.github.com>
- Loading branch information
1 parent
df69122
commit a6efaf0
Showing
65 changed files
with
5,464 additions
and
730 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/bin/bash | ||
|
||
# List of ports to clean up | ||
ports=(5173 8000 8001 8002 8003 8004) | ||
|
||
# Loop through each port and clean it up | ||
for port in "${ports[@]}"; do | ||
echo "Cleaning up port $port..." | ||
|
||
# Check if the port is in use and kill the processes using it | ||
fuser -n tcp -k "$port" 2>/dev/null | ||
|
||
if [ $? -eq 0 ]; then | ||
echo "Port $port cleaned successfully." | ||
else | ||
echo "No processes found on port $port, or failed to clean up." | ||
fi | ||
done | ||
|
||
echo "Service cleanup complete." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# PeerPrep Matching Service Dockerfile | ||
# | ||
# This file is used for creating a PRIVATE container using AWS's public Elastic Container | ||
# Registry. | ||
# | ||
# References: | ||
# https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/expressjs | ||
|
||
FROM public.ecr.aws/docker/library/node:20 | ||
|
||
# expose 8002 port for access to the container | ||
EXPOSE 8002 | ||
|
||
# copy all files into the Docker container | ||
COPY . . | ||
|
||
# install node libraries | ||
RUN npm install | ||
|
||
# execute the server | ||
CMD ["node", "server.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# PeerPrep Matching Service Dockerfile | ||
# | ||
# This file is used for creating a PUBLIC container using PUBLICY | ||
# available images. | ||
# | ||
# References: | ||
# https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/expressjs | ||
|
||
FROM node:20 | ||
|
||
# expose 8002 port for access to the container | ||
EXPOSE 8002 | ||
|
||
# Set work dir to prevent retarts | ||
WORKDIR /matching-service | ||
|
||
# copy all files into the Docker container | ||
COPY . . | ||
|
||
# install node libraries | ||
RUN npm install | ||
|
||
# execute the server | ||
CMD ["node", "server.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { | ||
ormFindPendingUserByCriteria, | ||
ormDeletePendingUserByEmail, | ||
ormCreatePendingUser, | ||
ormFindPendingUserByEmail, | ||
ormDeletePendingUserBySocketId, | ||
ormFindAllPendingUsers, | ||
ormDeletePendingUserByDocId | ||
} from "../models/orm.js"; | ||
|
||
export async function onDisconnect(socket) { | ||
try { | ||
console.log(`Socket disconnected: ${socket.id}`); | ||
|
||
// Delete pending user with socketId after disconnect, to prevent connecting with disconnected user | ||
const deletedUser = await ormDeletePendingUserBySocketId(socket.id); | ||
if (!deletedUser) { | ||
console.log(`No pending user of socket id ${socket.id} to delete when disconnected`); | ||
return; | ||
} | ||
|
||
console.log(`Deleted pending user ${deletedUser.email} after disconnect`); | ||
socket.emit('disconnect-while-match'); | ||
return; | ||
} catch (error) { | ||
console.log(`Error when disconnect: ${error.message}`); | ||
socket.emit('error', error.message); | ||
} | ||
} | ||
|
||
export async function onCancelMatch(socket) { | ||
try { | ||
console.log(`Cancelling match: ${socket.id}`); | ||
|
||
// Delete pending user with socketId | ||
const deletedUser = await ormDeletePendingUserBySocketId(socket.id); | ||
if (!deletedUser) { | ||
// Cancel match means a pending user was in the queue, so there should be a pending user to delete, but its cant be deleted | ||
throw new Error(`No pending user of socket id ${socket.id} to delete when cancelling match`); | ||
} | ||
|
||
console.log(`Deleted pending user ${deletedUser.email} after cancelling match`); | ||
return; | ||
} catch (error) { | ||
console.log(`Error when cancelling match: ${error.message}`); | ||
socket.emit('error', error.message); | ||
} | ||
}; | ||
|
||
export async function onCreateMatch(socket, data, io) { | ||
try { | ||
const { difficulties, categories, email, displayName } = data; | ||
const socketId = socket.id; | ||
const priority = categories.length; // Priority based on number of categories selected | ||
|
||
console.log(`Initiate create match by ${socket.id} (${displayName}), with data:`); | ||
console.log(data); | ||
|
||
// Get and log pending user queue | ||
const queue = await ormFindAllPendingUsers(); | ||
console.log(`Pending user queue before matching:`); | ||
console.log(queue); | ||
|
||
// Check if user is already in pending users | ||
const existingUser = await ormFindPendingUserByEmail(email); | ||
if (existingUser) { | ||
// User exists, so don't create new pending user entry and just return finding-match event | ||
console.log(`User already in pending users`); | ||
socket.emit('finding-match'); | ||
return; | ||
} | ||
|
||
// User does not exist | ||
console.log(`User not in pending users`); | ||
|
||
// Find if there is a match with a pending user | ||
const matchedUser = await ormFindPendingUserByCriteria({ difficulties, categories, email, displayName }); | ||
if (!matchedUser) { | ||
|
||
// No match found | ||
console.log(`No matching users with the criteria, create new match`); | ||
|
||
// Create pending user entry | ||
console.log({ email, displayName, socketId, difficulties, categories, priority }) | ||
const pendingUser = await ormCreatePendingUser({ email, displayName, socketId, difficulties, categories, priority }); | ||
if (!pendingUser) { | ||
throw new Error(`Could not create pending user entry for new match`); | ||
} else { | ||
console.log(`Created pending user with details:`); | ||
console.log(pendingUser); | ||
} | ||
|
||
// Get and log pending user queue | ||
const queue = await ormFindAllPendingUsers(); | ||
console.log(`Pending user queue after creating new pending user:`); | ||
console.log(queue); | ||
|
||
// Create timeout for deleting pending user | ||
setTimeout(async () => { | ||
console.log(`Timeout for pending user ${pendingUser.email}, try to delete pending user`); | ||
|
||
// Delete pending user after timeout based on docId | ||
const deletedUser = await ormDeletePendingUserByDocId(pendingUser._id); | ||
if (!deletedUser) { | ||
console.log(`No pending user by docId to delete for timeout`); | ||
return; | ||
} | ||
|
||
// Get and log pending user queue | ||
const queue = await ormFindAllPendingUsers(); | ||
console.log(`Pending user queue after no match:`); | ||
console.log(queue); | ||
|
||
console.log(`Deleted pending user ${deletedUser.email} after timeout`); | ||
socket.emit('no-match'); | ||
return; | ||
|
||
}, 50000); // 50 seconds (frontend has timer of 60secs, hence a 10 sec buffer fallback in case this service dies) | ||
|
||
// Emit finding-match event | ||
socket.emit('finding-match'); | ||
return; | ||
} else { | ||
|
||
// Match found | ||
console.log(`Match found with ${matchedUser.displayName}, with details:`); | ||
console.log(matchedUser); | ||
|
||
// Delete pending user from database which should be in queue | ||
const deletedUser = await ormDeletePendingUserByEmail(matchedUser.email); | ||
if (!deletedUser) { | ||
throw new Error(`Could not delete matched user by email after match found`); | ||
} | ||
|
||
// Get and log pending user queue | ||
const queue = await ormFindAllPendingUsers(); | ||
console.log(`Pending user queue after match:`); | ||
console.log(queue); | ||
|
||
// Find intersection of difficulties and categories in both users | ||
const commonDifficulties = difficulties.filter(d => matchedUser.difficulties.includes(d)); | ||
const commonCategories = categories.filter(c => matchedUser.categories.includes(c)); | ||
console.log(`Common difficulties: ${commonDifficulties}`); | ||
console.log(`Common categories: ${commonCategories}`); | ||
|
||
// Create match object | ||
const matchObject = { | ||
emails: [email, matchedUser.email], | ||
displayNames: [displayName, matchedUser.displayName], | ||
difficulties: commonDifficulties, | ||
categories: commonCategories, | ||
} | ||
console.log(`Match object:`); | ||
console.log(matchObject); | ||
|
||
// Emit found-match event to both users | ||
socket.emit("found-match", matchObject); | ||
io.to(matchedUser.socketId).emit("found-match", matchObject); | ||
} | ||
} catch (error) { | ||
console.log(`Error when creating match: ${error.message}`); | ||
socket.emit('error', error.message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { onDisconnect, onCancelMatch, onCreateMatch } from "./controller.js"; | ||
|
||
export async function socketHandler(io) { | ||
|
||
io.on("connection", (socket) => { | ||
console.log(`Socket connected: ${socket.id}`); | ||
|
||
// Disconnect event | ||
socket.on("disconnect", async () => onDisconnect(socket)); | ||
|
||
// Cancel match event | ||
socket.on("cancel-match", async () => onCancelMatch(socket)); | ||
|
||
// Create match event | ||
socket.on("create-match", async (data) => onCreateMatch(socket, data, io)); | ||
}); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import cors from "cors" | ||
|
||
const allowedOrigins = [ | ||
"http://localhost:5173", // frontend dev | ||
"http://peerprep.s3-website-ap-southeast-1.amazonaws.com", // frontend prod | ||
"http://peerprep-frontend-bucket.s3-website-ap-southeast-1.amazonaws.com", // frontend staging | ||
"http://localhost:8001", // user service | ||
"http://localhost:8002", // matching service | ||
"http://localhost:8003", // question service | ||
"http://localhost:8004", // collaboration service | ||
]; | ||
|
||
const corsOptions = { | ||
origin: allowedOrigins, | ||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], | ||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key' , 'X-Amz-Security-Token'], | ||
credentials: true, | ||
}; | ||
|
||
export default cors(corsOptions); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { | ||
_findPendingUserByCriteria, | ||
_deletePendingUserByEmail, | ||
_createPendingUser, | ||
_findPendingUserByEmail, | ||
_deletePendingUserBySocketId, | ||
_findAllPendingUsers, | ||
_deletePendingUserByDocId | ||
} from "./repository.js"; | ||
|
||
export async function ormFindPendingUserByCriteria(criteria) { | ||
return _findPendingUserByCriteria(criteria); | ||
} | ||
|
||
export async function ormDeletePendingUserByEmail(email) { | ||
return _deletePendingUserByEmail(email); | ||
} | ||
|
||
export async function ormFindPendingUserByEmail(email) { | ||
return _findPendingUserByEmail(email); | ||
} | ||
|
||
export async function ormCreatePendingUser(data) { | ||
return _createPendingUser(data); | ||
} | ||
|
||
export async function ormDeletePendingUserBySocketId(socketId) { | ||
return _deletePendingUserBySocketId(socketId); | ||
} | ||
|
||
export async function ormFindAllPendingUsers() { | ||
return _findAllPendingUsers(); | ||
} | ||
|
||
export async function ormDeletePendingUserByDocId(docId) { | ||
return _deletePendingUserByDocId(docId); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import mongoose from "mongoose"; | ||
|
||
const difficulties = ['EASY', 'MEDIUM', 'HARD'] | ||
const categories = ['STRINGS', 'ALGORITHMS', 'DATA STRUCTURES', 'BIT MANIPULATION', 'RECURSION', 'DATABASES', 'ARRAYS', 'BRAINTEASER'] | ||
|
||
const pendingUserSchema = mongoose.Schema( | ||
{ | ||
email: { | ||
type: String, | ||
required: true | ||
}, | ||
displayName: { | ||
type: String, | ||
required: true | ||
}, | ||
socketId: { | ||
type: String, | ||
required: true | ||
}, | ||
difficulties: { | ||
type: [String], | ||
enum: difficulties, | ||
required: true, | ||
validate: { | ||
validator: function (value) { | ||
return value.every(v => difficulties.includes(v)); | ||
}, | ||
message: props => `${props.value} contains invalid difficulty level` | ||
} | ||
}, | ||
categories: { | ||
type: [String], | ||
enum: categories, | ||
required: true, | ||
validate: { | ||
validator: function (value) { | ||
return value.every(v => categories.includes(v)); | ||
}, | ||
message: props => `${props.value} contains invalid difficulty level` | ||
} | ||
}, | ||
priority: { | ||
type: Number, | ||
required: true | ||
} | ||
}, | ||
{ | ||
timestamps: true | ||
} | ||
); | ||
pendingUserSchema.index({ "createdAt": 1 }, { expireAfterSeconds: 60 }); | ||
const PendingUserModel = mongoose.model("PendingUser", pendingUserSchema) | ||
export default PendingUserModel; |
Oops, something went wrong.