Skip to content

Commit

Permalink
Milestone D4 Submission (#83)
Browse files Browse the repository at this point in the history
* 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
8 people authored Oct 20, 2024
1 parent df69122 commit a6efaf0
Show file tree
Hide file tree
Showing 65 changed files with 5,464 additions and 730 deletions.
20 changes: 20 additions & 0 deletions cleanup-dev.sh
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."
9 changes: 8 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ services:
ports:
- '8001:8001'
network_mode: bridge
matching-service:
build:
context: ./matching-service
dockerfile: Dockerfile.public
ports:
- '8002:8002'
network_mode: bridge
question-service:
build:
context: ./question-service
Expand All @@ -19,4 +26,4 @@ services:
dockerfile: Dockerfile.public
ports:
- '5173:5173'
network_mode: bridge
network_mode: bridge
21 changes: 21 additions & 0 deletions matching-service/Dockerfile
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"]
24 changes: 24 additions & 0 deletions matching-service/Dockerfile.public
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"]
164 changes: 164 additions & 0 deletions matching-service/controllers/controller.js
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);
}
}
18 changes: 18 additions & 0 deletions matching-service/controllers/socketHandler.js
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));
});

}
20 changes: 20 additions & 0 deletions matching-service/middlewares/cors.js
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);
37 changes: 37 additions & 0 deletions matching-service/models/orm.js
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);
}
53 changes: 53 additions & 0 deletions matching-service/models/pendingUser-model.js
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;
Loading

0 comments on commit a6efaf0

Please sign in to comment.