From 7773a2ba22a270a843e1e2b746661a57b36afaaa Mon Sep 17 00:00:00 2001 From: pedram karimi Date: Fri, 27 Jun 2025 12:18:42 -0400 Subject: [PATCH 1/5] init --- middlewareNode/routes/auth.js | 4 +- middlewareNode/routes/lessons.js | 497 +++--- middlewareNode/routes/users.js | 88 +- middlewareNode/server.js | 4 +- .../src/Pages/SignUp/SignUp.tsx | 1428 +++++++++-------- .../src/Pages/SignUp/signupController.ts | 22 +- 6 files changed, 1123 insertions(+), 920 deletions(-) diff --git a/middlewareNode/routes/auth.js b/middlewareNode/routes/auth.js index c66587e3..18537ea2 100644 --- a/middlewareNode/routes/auth.js +++ b/middlewareNode/routes/auth.js @@ -73,7 +73,7 @@ router.post( function (err, token) { if (err) throw err; res.json({ token }); - }, + } ); return jwt; //Return the encrypted jwt @@ -81,7 +81,7 @@ router.post( console.error(error.message); res.status(500).send("Server error"); } - }, + } ); module.exports = router; diff --git a/middlewareNode/routes/lessons.js b/middlewareNode/routes/lessons.js index c5338ba2..247b5a1f 100644 --- a/middlewareNode/routes/lessons.js +++ b/middlewareNode/routes/lessons.js @@ -1,16 +1,17 @@ const config = require("config"); -const express = require('express'); +const express = require("express"); const passport = require("passport"); const router = express.Router(); -const jwt = require('jsonwebtoken'); -const { MongoClient } = require('mongodb'); -require('dotenv').config(); +const jwt = require("jsonwebtoken"); +const { MongoClient } = require("mongodb"); +require("dotenv").config(); let cachedClient = null; // cache db client to prevent repeated connections // get db client async function getDb() { - if (!cachedClient) { // if not cached, connect + if (!cachedClient) { + // if not cached, connect cachedClient = new MongoClient(config.get("mongoURI")); await cachedClient.connect(); } @@ -22,10 +23,14 @@ async function getDb() { router.get( "/getCompletedLessonCount", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - req.user = user || null; - next(); - })(req, res, next) // authenticate jwt + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + req.user = user || null; + next(); + } + )(req, res, next); // authenticate jwt }, async (req, res) => { const piece = decodeURIComponent(req.query.piece); @@ -41,18 +46,21 @@ router.get( if (req.user) { const { username } = req.user; // get username from jwt - const userDoc = await users.findOne( // get the userDoc according to username + const userDoc = await users.findOne( + // get the userDoc according to username { username: username }, { projection: { lessonsCompleted: 1 } } ); if (!userDoc) throw new Error("User does not exist"); - if (!userDoc.lessonsCompleted) throw new Error("User does not have lesson record"); + if (!userDoc.lessonsCompleted) + throw new Error("User does not have lesson record"); // the number of lessons completed for the piece let lessonNum = 0; for (const chessPiece of userDoc.lessonsCompleted) { - if (chessPiece.piece === piece) { // find the piece - lessonNum = chessPiece.lessonNumber; + if (chessPiece.piece === piece) { + // find the piece + lessonNum = chessPiece.lessonNumber; break; } } @@ -70,90 +78,107 @@ router.get( updatedAt: new Date(), // update date expiresAt, // update expiration date }, - $setOnInsert: { // if this ip is not in db, add lessonsCompelted field + $setOnInsert: { + // if this ip is not in db, add lessonsCompelted field lessonsCompleted: [ { - piece: 'Piece Checkmate 1 Basic checkmates', - lessonNumber: 0 + piece: "Piece Checkmate 1 Basic checkmates", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 1 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 2 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 3 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 4 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Piece checkmates 2 Challenging checkmates", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 1 Recognize the patterns', - lessonNumber: 0 + piece: "Knight and Bishop Mate interactive lesson", + lessonNumber: 0, }, + { piece: "The Pin Pin it to win it", lessonNumber: 0 }, + { piece: "The Skewer Yum - Skewers!", lessonNumber: 0 }, + { piece: "The Fork Use the fork, Luke", lessonNumber: 0 }, { - piece: 'Checkmate Pattern 2 Recognize the patterns', - lessonNumber: 0 + piece: "Discovered Attacks Including discovered checks", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 3 Recognize the patterns', - lessonNumber: 0 + piece: "Double Check A very powerfull tactic", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 4 Recognize the patterns', - lessonNumber: 0 + piece: "Overloaded Pieces They have too much work", + lessonNumber: 0, }, + { piece: "Zwischenzug In-between moves", lessonNumber: 0 }, { - piece: 'Piece checkmates 2 Challenging checkmates', - lessonNumber: 0 + piece: "X-Ray Attacking through an enemy piece", + lessonNumber: 0, }, + { piece: "Zugzwang Being forced to move", lessonNumber: 0 }, { - piece: 'Knight and Bishop Mate interactive lesson', - lessonNumber: 0 + piece: "Interference Interpose a piece to great effect", + lessonNumber: 0, }, - { piece: 'The Pin Pin it to win it', lessonNumber: 0 }, - { piece: 'The Skewer Yum - Skewers!', lessonNumber: 0 }, - { piece: 'The Fork Use the fork, Luke', lessonNumber: 0 }, { - piece: 'Discovered Attacks Including discovered checks', - lessonNumber: 0 + piece: "Greek Gift Study the greek gift scrifice", + lessonNumber: 0, }, - { piece: 'Double Check A very powerfull tactic', lessonNumber: 0 }, + { piece: "Deflection Distracting a defender", lessonNumber: 0 }, { - piece: 'Overloaded Pieces They have too much work', - lessonNumber: 0 + piece: "Attraction Lure a piece to bad square", + lessonNumber: 0, }, - { piece: 'Zwischenzug In-between moves', lessonNumber: 0 }, - { piece: 'X-Ray Attacking through an enemy piece', lessonNumber: 0 }, - { piece: 'Zugzwang Being forced to move', lessonNumber: 0 }, { - piece: 'Interference Interpose a piece to great effect', - lessonNumber: 0 + piece: "Underpromotion Promote - but not to a queen!", + lessonNumber: 0, }, { - piece: 'Greek Gift Study the greek gift scrifice', - lessonNumber: 0 + piece: "Desperado A piece is lost, but it can still help", + lessonNumber: 0, }, - { piece: 'Deflection Distracting a defender', lessonNumber: 0 }, - { piece: 'Attraction Lure a piece to bad square', lessonNumber: 0 }, { - piece: 'Underpromotion Promote - but not to a queen!', - lessonNumber: 0 + piece: "Counter Check Respond to a check with a check", + lessonNumber: 0, }, { - piece: 'Desperado A piece is lost, but it can still help', - lessonNumber: 0 + piece: "Undermining Remove the defending piece", + lessonNumber: 0, }, + { piece: "Clearance Get out of the way!", lessonNumber: 0 }, + { piece: "Key Squares Reach the key square", lessonNumber: 0 }, + { piece: "Opposition take the opposition", lessonNumber: 0 }, + { piece: "7th-Rank Rook Pawn Versus a Queen", lessonNumber: 0 }, { - piece: 'Counter Check Respond to a check with a check', - lessonNumber: 0 + piece: "7th-Rank Rook Pawn And Passive Rook vs Rook", + lessonNumber: 0, }, - { piece: 'Undermining Remove the defending piece', lessonNumber: 0 }, - { piece: 'Clearance Get out of the way!', lessonNumber: 0 }, - { piece: 'Key Squares Reach the key square', lessonNumber: 0 }, - { piece: 'Opposition take the opposition', lessonNumber: 0 }, - { piece: '7th-Rank Rook Pawn Versus a Queen', lessonNumber: 0 }, { - piece: '7th-Rank Rook Pawn And Passive Rook vs Rook', - lessonNumber: 0 + piece: "Basic Rook Endgames Lucena and Philidor", + lessonNumber: 0, }, - { piece: 'Basic Rook Endgames Lucena and Philidor', lessonNumber: 0 } ], }, }, { upsert: true } ); - const guestDoc = await guests.findOne( // get guestDoc by ip + const guestDoc = await guests.findOne( + // get guestDoc by ip { ip: clientIp } ); if (!guestDoc) throw new Error("Guest does not exist"); @@ -165,8 +190,8 @@ router.get( break; } } - if(lessonNum == -1) return res.status(400).json("Error: 400. Invalid piece."); - + if (lessonNum == -1) + return res.status(400).json("Error: 400. Invalid piece."); else res.json(lessonNum); } } catch (err) { @@ -181,10 +206,14 @@ router.get( router.get( "/getTotalPieceLesson", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - req.user = user || null; - next(); - })(req, res, next) // authenticate jwt + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + req.user = user || null; + next(); + } + )(req, res, next); // authenticate jwt }, async (req, res) => { const piece = decodeURIComponent(req.query.piece); // get the chess piece @@ -194,7 +223,7 @@ router.get( try { const db = await getDb(); - const lessons= db.collection("newLessons"); // get lessons collection + const lessons = db.collection("newLessons"); // get lessons collection const lessonDoc = await lessons.findOne({ piece: piece }); // all lessons for that piece res.json(lessonDoc.lessons.length); // respond with length of lessons @@ -210,10 +239,14 @@ router.get( router.get( "/getLesson", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - req.user = user || null; - next(); - })(req, res, next) // authenticate jwt + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + req.user = user || null; + next(); + } + )(req, res, next); // authenticate jwt }, async (req, res) => { // get piece & lessonNum from query @@ -227,7 +260,7 @@ router.get( const db = await getDb(); const lessons = db.collection("newLessons"); // get lessons collection const lessonDoc = await lessons.findOne({ piece: piece }); // get doc for the piece - if (!lessonDoc) return res.status(400).json("Error: 400. Invalid piece.");; + if (!lessonDoc) return res.status(400).json("Error: 400. Invalid piece."); if (lessonNum <= 0 || lessonNum > lessonDoc.lessons.length) { return res.status(404).json("Lesson index out of range"); @@ -247,10 +280,14 @@ router.get( router.get( "/updateLessonCompletion", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - req.user = user || null; - next(); - })(req, res, next) // authenticate jwt + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + req.user = user || null; + next(); + } + )(req, res, next); // authenticate jwt }, async (req, res) => { // get parameters from query @@ -265,34 +302,44 @@ router.get( const users = db.collection("users"); // get users collection const guests = db.collection("guest"); // get users collection - if (req.user){ + if (req.user) { const { username } = req.user; // get username - const userDoc = await users.findOne( // get userDoc by username + const userDoc = await users.findOne( + // get userDoc by username { username: username }, { projection: { lessonsCompleted: 1 } } ); if (!userDoc) throw new Error("User does not exist"); - if (!userDoc.lessonsCompleted) throw new Error("User does not have lesson record"); + if (!userDoc.lessonsCompleted) + throw new Error("User does not have lesson record"); let index = -1; // index for that piece - userDoc.lessonsCompleted.forEach((lesson, i) => { // try finding user's progress for that piece + userDoc.lessonsCompleted.forEach((lesson, i) => { + // try finding user's progress for that piece if (lesson.piece === piece) { index = i; } }); - if (index === -1) { // piece progress not found + if (index === -1) { + // piece progress not found return res.status(404).json("Piece not found in lessonsCompleted"); } - const updateResult = await users.updateOne( + const updateResult = await users.updateOne( { username, $or: [ - { [`lessonsCompleted.${index}.lessonNumber`]: { $lt: lessonNum + 1 } }, - { [`lessonsCompleted.${index}.lessonNumber`]: { $exists: false } } - ] + { + [`lessonsCompleted.${index}.lessonNumber`]: { + $lt: lessonNum + 1, + }, + }, + { + [`lessonsCompleted.${index}.lessonNumber`]: { $exists: false }, + }, + ], }, { $set: { @@ -311,7 +358,7 @@ router.get( res.status(304).json("No changes made"); } } else { - const clientIp = getClientIp(req); + const clientIp = getClientIp(req); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); await guests.updateOne( @@ -322,111 +369,136 @@ router.get( updatedAt: new Date(), // update date expiresAt, // update new expiration date }, - $setOnInsert: { // add lessonsCompleted field if ip is not stored before + $setOnInsert: { + // add lessonsCompleted field if ip is not stored before lessonsCompleted: [ { - piece: 'Piece Checkmate 1 Basic checkmates', - lessonNumber: 0 + piece: "Piece Checkmate 1 Basic checkmates", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 1 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 1 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 2 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 2 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 3 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 3 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 4 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 4 Recognize the patterns', - lessonNumber: 0 + piece: "Piece checkmates 2 Challenging checkmates", + lessonNumber: 0, }, { - piece: 'Piece checkmates 2 Challenging checkmates', - lessonNumber: 0 + piece: "Knight and Bishop Mate interactive lesson", + lessonNumber: 0, }, + { piece: "The Pin Pin it to win it", lessonNumber: 0 }, + { piece: "The Skewer Yum - Skewers!", lessonNumber: 0 }, + { piece: "The Fork Use the fork, Luke", lessonNumber: 0 }, { - piece: 'Knight and Bishop Mate interactive lesson', - lessonNumber: 0 + piece: "Discovered Attacks Including discovered checks", + lessonNumber: 0, }, - { piece: 'The Pin Pin it to win it', lessonNumber: 0 }, - { piece: 'The Skewer Yum - Skewers!', lessonNumber: 0 }, - { piece: 'The Fork Use the fork, Luke', lessonNumber: 0 }, { - piece: 'Discovered Attacks Including discovered checks', - lessonNumber: 0 + piece: "Double Check A very powerfull tactic", + lessonNumber: 0, }, - { piece: 'Double Check A very powerfull tactic', lessonNumber: 0 }, { - piece: 'Overloaded Pieces They have too much work', - lessonNumber: 0 + piece: "Overloaded Pieces They have too much work", + lessonNumber: 0, }, - { piece: 'Zwischenzug In-between moves', lessonNumber: 0 }, - { piece: 'X-Ray Attacking through an enemy piece', lessonNumber: 0 }, - { piece: 'Zugzwang Being forced to move', lessonNumber: 0 }, + { piece: "Zwischenzug In-between moves", lessonNumber: 0 }, { - piece: 'Interference Interpose a piece to great effect', - lessonNumber: 0 + piece: "X-Ray Attacking through an enemy piece", + lessonNumber: 0, }, + { piece: "Zugzwang Being forced to move", lessonNumber: 0 }, { - piece: 'Greek Gift Study the greek gift scrifice', - lessonNumber: 0 + piece: "Interference Interpose a piece to great effect", + lessonNumber: 0, }, - { piece: 'Deflection Distracting a defender', lessonNumber: 0 }, - { piece: 'Attraction Lure a piece to bad square', lessonNumber: 0 }, { - piece: 'Underpromotion Promote - but not to a queen!', - lessonNumber: 0 + piece: "Greek Gift Study the greek gift scrifice", + lessonNumber: 0, }, + { piece: "Deflection Distracting a defender", lessonNumber: 0 }, { - piece: 'Desperado A piece is lost, but it can still help', - lessonNumber: 0 + piece: "Attraction Lure a piece to bad square", + lessonNumber: 0, }, { - piece: 'Counter Check Respond to a check with a check', - lessonNumber: 0 + piece: "Underpromotion Promote - but not to a queen!", + lessonNumber: 0, }, - { piece: 'Undermining Remove the defending piece', lessonNumber: 0 }, - { piece: 'Clearance Get out of the way!', lessonNumber: 0 }, - { piece: 'Key Squares Reach the key square', lessonNumber: 0 }, - { piece: 'Opposition take the opposition', lessonNumber: 0 }, - { piece: '7th-Rank Rook Pawn Versus a Queen', lessonNumber: 0 }, { - piece: '7th-Rank Rook Pawn And Passive Rook vs Rook', - lessonNumber: 0 + piece: "Desperado A piece is lost, but it can still help", + lessonNumber: 0, + }, + { + piece: "Counter Check Respond to a check with a check", + lessonNumber: 0, + }, + { + piece: "Undermining Remove the defending piece", + lessonNumber: 0, + }, + { piece: "Clearance Get out of the way!", lessonNumber: 0 }, + { piece: "Key Squares Reach the key square", lessonNumber: 0 }, + { piece: "Opposition take the opposition", lessonNumber: 0 }, + { piece: "7th-Rank Rook Pawn Versus a Queen", lessonNumber: 0 }, + { + piece: "7th-Rank Rook Pawn And Passive Rook vs Rook", + lessonNumber: 0, + }, + { + piece: "Basic Rook Endgames Lucena and Philidor", + lessonNumber: 0, }, - { piece: 'Basic Rook Endgames Lucena and Philidor', lessonNumber: 0 } ], }, }, { upsert: true } ); - const guestDoc = await guests.findOne( // get userDoc by ip + const guestDoc = await guests.findOne( + // get userDoc by ip { ip: clientIp } ); let index = -1; // index for that piece - guestDoc.lessonsCompleted.forEach((lesson, i) => { // try finding user's progress for that piece + guestDoc.lessonsCompleted.forEach((lesson, i) => { + // try finding user's progress for that piece if (lesson.piece === piece) { index = i; } }); - if (index === -1) { // piece progress not found + if (index === -1) { + // piece progress not found return res.status(404).json("Piece not found in lessonsCompleted"); } - const updateResult = await guests.updateOne( + const updateResult = await guests.updateOne( { ip: clientIp, $or: [ - { [`lessonsCompleted.${index}.lessonNumber`]: { $lt: lessonNum + 1 } }, - { [`lessonsCompleted.${index}.lessonNumber`]: { $exists: false } } - ] + { + [`lessonsCompleted.${index}.lessonNumber`]: { + $lt: lessonNum + 1, + }, + }, + { + [`lessonsCompleted.${index}.lessonNumber`]: { $exists: false }, + }, + ], }, { $set: { @@ -457,10 +529,14 @@ router.get( router.get( "/getAllLessonsProgress", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - req.user = user || null; - next(); - })(req, res, next) // authenticate jwt + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + req.user = user || null; + next(); + } + )(req, res, next); // authenticate jwt }, async (req, res) => { try { @@ -468,22 +544,26 @@ router.get( const users = db.collection("users"); // get users collection const guests = db.collection("guest"); // get users collection - if (req.user){ + if (req.user) { const { username } = req.user; - const userDoc = await users.findOne( // get userDoc by username + const userDoc = await users.findOne( + // get userDoc by username { username: username }, { projection: { lessonsCompleted: 1 } } ); if (!userDoc) throw new Error("User does not exist"); - if (!userDoc.lessonsCompleted) throw new Error("User does not have lesson record"); + if (!userDoc.lessonsCompleted) + throw new Error("User does not have lesson record"); // create the map const lessonsMap = Object.fromEntries( - userDoc.lessonsCompleted.map(({ piece, lessonNumber }) => [piece, lessonNumber]) + userDoc.lessonsCompleted.map(({ piece, lessonNumber }) => [ + piece, + lessonNumber, + ]) ); res.json(lessonsMap); - } else { const clientIp = getClientIp(req); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); @@ -496,98 +576,119 @@ router.get( updatedAt: new Date(), // update date expiresAt, // update new expiration date }, - $setOnInsert: { // add lessonsCompleted field if ip is not stored before + $setOnInsert: { + // add lessonsCompleted field if ip is not stored before lessonsCompleted: [ { - piece: 'Piece Checkmate 1 Basic checkmates', - lessonNumber: 0 + piece: "Piece Checkmate 1 Basic checkmates", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 1 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 2 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 3 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Checkmate Pattern 4 Recognize the patterns", + lessonNumber: 0, + }, + { + piece: "Piece checkmates 2 Challenging checkmates", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 1 Recognize the patterns', - lessonNumber: 0 + piece: "Knight and Bishop Mate interactive lesson", + lessonNumber: 0, }, + { piece: "The Pin Pin it to win it", lessonNumber: 0 }, + { piece: "The Skewer Yum - Skewers!", lessonNumber: 0 }, + { piece: "The Fork Use the fork, Luke", lessonNumber: 0 }, { - piece: 'Checkmate Pattern 2 Recognize the patterns', - lessonNumber: 0 + piece: "Discovered Attacks Including discovered checks", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 3 Recognize the patterns', - lessonNumber: 0 + piece: "Double Check A very powerfull tactic", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 4 Recognize the patterns', - lessonNumber: 0 + piece: "Overloaded Pieces They have too much work", + lessonNumber: 0, }, + { piece: "Zwischenzug In-between moves", lessonNumber: 0 }, { - piece: 'Piece checkmates 2 Challenging checkmates', - lessonNumber: 0 + piece: "X-Ray Attacking through an enemy piece", + lessonNumber: 0, }, + { piece: "Zugzwang Being forced to move", lessonNumber: 0 }, { - piece: 'Knight and Bishop Mate interactive lesson', - lessonNumber: 0 + piece: "Interference Interpose a piece to great effect", + lessonNumber: 0, }, - { piece: 'The Pin Pin it to win it', lessonNumber: 0 }, - { piece: 'The Skewer Yum - Skewers!', lessonNumber: 0 }, - { piece: 'The Fork Use the fork, Luke', lessonNumber: 0 }, { - piece: 'Discovered Attacks Including discovered checks', - lessonNumber: 0 + piece: "Greek Gift Study the greek gift scrifice", + lessonNumber: 0, }, - { piece: 'Double Check A very powerfull tactic', lessonNumber: 0 }, + { piece: "Deflection Distracting a defender", lessonNumber: 0 }, { - piece: 'Overloaded Pieces They have too much work', - lessonNumber: 0 + piece: "Attraction Lure a piece to bad square", + lessonNumber: 0, }, - { piece: 'Zwischenzug In-between moves', lessonNumber: 0 }, - { piece: 'X-Ray Attacking through an enemy piece', lessonNumber: 0 }, - { piece: 'Zugzwang Being forced to move', lessonNumber: 0 }, { - piece: 'Interference Interpose a piece to great effect', - lessonNumber: 0 + piece: "Underpromotion Promote - but not to a queen!", + lessonNumber: 0, }, { - piece: 'Greek Gift Study the greek gift scrifice', - lessonNumber: 0 + piece: "Desperado A piece is lost, but it can still help", + lessonNumber: 0, }, - { piece: 'Deflection Distracting a defender', lessonNumber: 0 }, - { piece: 'Attraction Lure a piece to bad square', lessonNumber: 0 }, { - piece: 'Underpromotion Promote - but not to a queen!', - lessonNumber: 0 + piece: "Counter Check Respond to a check with a check", + lessonNumber: 0, }, { - piece: 'Desperado A piece is lost, but it can still help', - lessonNumber: 0 + piece: "Undermining Remove the defending piece", + lessonNumber: 0, }, + { piece: "Clearance Get out of the way!", lessonNumber: 0 }, + { piece: "Key Squares Reach the key square", lessonNumber: 0 }, + { piece: "Opposition take the opposition", lessonNumber: 0 }, + { piece: "7th-Rank Rook Pawn Versus a Queen", lessonNumber: 0 }, { - piece: 'Counter Check Respond to a check with a check', - lessonNumber: 0 + piece: "7th-Rank Rook Pawn And Passive Rook vs Rook", + lessonNumber: 0, }, - { piece: 'Undermining Remove the defending piece', lessonNumber: 0 }, - { piece: 'Clearance Get out of the way!', lessonNumber: 0 }, - { piece: 'Key Squares Reach the key square', lessonNumber: 0 }, - { piece: 'Opposition take the opposition', lessonNumber: 0 }, - { piece: '7th-Rank Rook Pawn Versus a Queen', lessonNumber: 0 }, { - piece: '7th-Rank Rook Pawn And Passive Rook vs Rook', - lessonNumber: 0 + piece: "Basic Rook Endgames Lucena and Philidor", + lessonNumber: 0, }, - { piece: 'Basic Rook Endgames Lucena and Philidor', lessonNumber: 0 } ], }, }, { upsert: true } ); - const guestDoc = await guests.findOne( // get userDoc by ip + const guestDoc = await guests.findOne( + // get userDoc by ip { ip: clientIp } ); if (!guestDoc) throw new Error("Guest does not exist"); - if (!guestDoc.lessonsCompleted) throw new Error("Guest does not have lesson record"); + if (!guestDoc.lessonsCompleted) + throw new Error("Guest does not have lesson record"); // create the map const lessonsMap = Object.fromEntries( - guestDoc.lessonsCompleted.map(({ piece, lessonNumber }) => [piece, lessonNumber]) + guestDoc.lessonsCompleted.map(({ piece, lessonNumber }) => [ + piece, + lessonNumber, + ]) ); res.json(lessonsMap); } @@ -602,4 +703,4 @@ function getClientIp(req) { return req.headers["x-forwarded-for"] || req.connection.remoteAddress; } -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/middlewareNode/routes/users.js b/middlewareNode/routes/users.js index 705e4269..443044eb 100644 --- a/middlewareNode/routes/users.js +++ b/middlewareNode/routes/users.js @@ -9,14 +9,15 @@ const { } = require("../template/changePasswordTemplate"); const { sendMail } = require("../utils/nodemailer"); const { validator } = require("../utils/middleware"); -const { MongoClient } = require('mongodb'); +const { MongoClient } = require("mongodb"); const config = require("config"); let cachedClient = null; // cache db client to prevent repeated connections // get db client async function getDb() { - if (!cachedClient) { // if not cached, connect + if (!cachedClient) { + // if not cached, connect cachedClient = new MongoClient(config.get("mongoURI")); await cachedClient.connect(); } @@ -84,7 +85,6 @@ router.post( //Set the account created date for the new user const currDate = new Date(); - //Switch statement for functionality depending on role if (role === "parent") { let studentsArray = JSON.parse(students); @@ -118,7 +118,7 @@ router.post( timePlayed: 0, }); await newStudent.save(); - }), + }) ); } } @@ -138,7 +138,7 @@ router.post( console.error(error.message); res.status(500).json("Server error"); } - }, + } ); // @route POST /user/children @@ -194,7 +194,7 @@ router.post( console.error(error.message); res.status(500).json("Server error"); } - }, + } ); // @route POST /user/sendMail // @desc sending the mail based on username and email @@ -247,7 +247,7 @@ const updatePassword = async (body) => { const result = await users.findOneAndUpdate( { username: body.username, email: body.email }, { password: body.password }, - { new: true }, + { new: true } ); return result; }; @@ -255,17 +255,19 @@ const updatePassword = async (body) => { // @route GET /user/mentorless/:keyword // @desc for getting the mentorless students whose username matches keyword. router.get("/mentorless", async (req, res) => { - const keyword = req.query.keyword || ''; // get the keyword + const keyword = req.query.keyword || ""; // get the keyword try { const db = await getDb(); const users = db.collection("users"); - const userList = await users.find({ - role: 'student',// get student - username: { $regex: keyword, $options: 'i' }, // case-insensitive match for username - mentorshipUsername: "" // students who don't have mentors - }).toArray();; - res.json(userList.map(user => user.username)); // return a list of the usernames + const userList = await users + .find({ + role: "student", // get student + username: { $regex: keyword, $options: "i" }, // case-insensitive match for username + mentorshipUsername: "", // students who don't have mentors + }) + .toArray(); + res.json(userList.map((user) => user.username)); // return a list of the usernames } catch (err) { res.status(500).json({ error: err.message }); // error } @@ -276,21 +278,28 @@ router.get("/mentorless", async (req, res) => { // @desc if user is mentor, update its student username to the mentorship= query // @access Public with jwt Authentication router.put( - "/updateMentorship", + "/updateMentorship", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - if (!user) { - return res.status(401).json({ message: "Unauthorized" }); + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + if (!user) { + return res.status(401).json({ message: "Unauthorized" }); + } + req.user = user; + next(); } - req.user = user; - next(); - })(req, res, next) // authenticate jwt - }, + )(req, res, next); // authenticate jwt + }, async (req, res) => { // get the student/mentor username const mentorship = req.query.mentorship; - if (!mentorship) { // mentorship query is required - return res.status(400).json({ message: "Missing mentorship username in query" }); + if (!mentorship) { + // mentorship query is required + return res + .status(400) + .json({ message: "Missing mentorship username in query" }); } try { @@ -310,30 +319,41 @@ router.put( // if mentorship field is not modified if (result.modifiedCount === 0) { - return res.status(404).json({ message: "User not found or already has that mentorshipUsername, username: " + req.user.username }); + return res + .status(404) + .json({ + message: + "User not found or already has that mentorshipUsername, username: " + + req.user.username, + }); } res.json({ message: "Mentorship updated successfully" }); } catch (err) { res.status(500).json({ error: err.message }); // error } - } + } ); // @route GET /user/getMentorship // @desc if user is a student, responds with its mentor's object {username, firstName, lastName} // @desc if user is a mentor, responds with its student's object {username, firstName, lastName} // @access Public with jwt Authentication -router.get("/getMentorship", +router.get( + "/getMentorship", async (req, res, next) => { - passport.authenticate("jwt", { session: false }, async (err, user, info) => { - if (!user) { - return res.status(401).json({ message: "Unauthorized" }); + passport.authenticate( + "jwt", + { session: false }, + async (err, user, info) => { + if (!user) { + return res.status(401).json({ message: "Unauthorized" }); + } + req.user = user; + next(); } - req.user = user; - next(); - })(req, res, next) // authenticate jwt - }, + )(req, res, next); // authenticate jwt + }, async (req, res) => { try { const db = await getDb(); diff --git a/middlewareNode/server.js b/middlewareNode/server.js index 07b718fa..21351084 100644 --- a/middlewareNode/server.js +++ b/middlewareNode/server.js @@ -19,9 +19,9 @@ app.use(express.json({ extended: false })); // Configure express-session app.use( session({ - secret: 'your_secret_key_here', // Use a long and random string for better security + secret: "your_secret_key_here", // Use a long and random string for better security resave: false, - saveUninitialized: false + saveUninitialized: false, }) ); diff --git a/react-ystemandchess/src/Pages/SignUp/SignUp.tsx b/react-ystemandchess/src/Pages/SignUp/SignUp.tsx index b70708a4..18d12861 100644 --- a/react-ystemandchess/src/Pages/SignUp/SignUp.tsx +++ b/react-ystemandchess/src/Pages/SignUp/SignUp.tsx @@ -1,704 +1,786 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; // import { Cookie } from 'react-router'; // 'react-router' does not export 'Cookie'. This line should probably be removed or corrected. -import './SignUp.scss'; // Imports the stylesheet for this component. -import { environment } from '../../environments/environment'; // Imports environment variables, likely containing API URLs. +import "./SignUp.scss"; // Imports the stylesheet for this component. +import { environment } from "../../environments/environment"; // Imports environment variables, likely containing API URLs. // import StudentProfile from '../StudentProfile/Student-Profile'; // Only import if actively used in this component // Define the interface for the props of the StudentTemplate component interface StudentTemplateProps { - studentUsername: string; // The student prop is a string (username) - onClick: (username: string) => void; // onClick takes the username as an argument + studentUsername: string; // The student prop is a string (username) + onClick: (username: string) => void; // onClick takes the username as an argument } // Renamed to start with a capital letter and typed its props -const StudentTemplate: React.FC = ({ studentUsername, onClick }) => { - return ( -
onClick(studentUsername)}> -
{studentUsername}
-
- ); +const StudentTemplate: React.FC = ({ + studentUsername, + onClick, +}) => { + return ( +
onClick(studentUsername)}> +
{studentUsername}
+
+ ); }; const Signup = () => { - // State to manage the form data for the user. - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - username: '', - password: '', - retypedPassword: '', - accountType: 'mentor', // Default account type is set to 'mentor'. - }); - - // State flags to track the validity of individual input fields. - const [firstNameFlag, setFirstNameFlag] = useState(false); - const [lastNameFlag, setLastNameFlag] = useState(false); - const [emailFlag, setEmailFlag] = useState(false); - const [userNameFlag, setUserNameFlag] = useState(false); - const [passwordFlag, setPasswordFlag] = useState(false); - const [retypeFlag, setRetypeFlag] = useState(false); - const [termsFlag, setTermsFlag] = useState(false); - - // States for the student search dropdown - const [matchingStudents, setMatchingStudents] = useState([]); // Array of strings (usernames) - const [usernameToSearch, setUserToSearch] = useState(''); // Text in the "Find a student" input - const [activeDropdown, setActiveDropdown] = useState(false); // Controls dropdown visibility - const [dropdownLoading, setDropdownLoading] = useState(false); // Loading state for dropdown search - - // State to store any validation errors for the form fields. - const [errors, setErrors] = useState({ - firstName: '', - lastName: '', - email: '', - username: '', - password: '', - retypePassword: '', - terms: '', - general: '', // Added for general signup errors - }); - - // State to manage the parent account specific UI and data. - const [parentAccountFlag, setParentAccountFlag] = useState(false); // Flag to indicate if the selected account type is 'parent'. - const [showStudentForm, setShowStudentForm] = useState(false); // Flag to control the visibility of the student creation form. - const [students, setStudents] = useState([]); // State to store data for student accounts under a parent. - const [assignedMenteeUsername, setAssignedMenteeUsername] = useState(null); // To store the selected mentee's username - - // Handles changes to input fields in the main form. - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - // Updates the formData state with the new value for the changed field. - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - - // Performs specific validation based on the input field that changed. - switch (name) { - case 'firstName': - firstNameVerification(value); - break; - case 'lastName': - lastNameVerification(value); - break; - case 'email': - emailVerification(value); - break; - case 'username': - usernameVerification(value); - break; - case 'password': - passwordVerification(value); - break; - case 'retypedPassword': - retypePasswordVerification(value, formData.password); - break; - default: - break; - } - }; - - // Verifies the format of the first name. - const firstNameVerification = (firstName: string) => { - const isValid = /^[A-Za-z ]{2,15}$/.test(firstName); - setFirstNameFlag(isValid); - setErrors((prev) => ({ - ...prev, - firstName: isValid ? '' : 'Invalid First Name', - })); - return isValid; - }; - - // Verifies the format of the last name. - const lastNameVerification = (lastName: string) => { - const isValid = /^[A-Za-z]{2,15}$/.test(lastName); - setLastNameFlag(isValid); - setErrors((prev) => ({ - ...prev, - lastName: isValid ? '' : 'Invalid Last Name', - })); - return isValid; - }; - - // Verifies the format of the email address. - const emailVerification = (email: string) => { - const isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}/.test(email); - setEmailFlag(isValid); - setErrors((prev) => ({ - ...prev, - email: isValid ? '' : 'Invalid Email', - })); - return isValid; + // State to manage the form data for the user. + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + username: "", + password: "", + retypedPassword: "", + accountType: "mentor", // Default account type is set to 'mentor'. + }); + + // State flags to track the validity of individual input fields. + const [firstNameFlag, setFirstNameFlag] = useState(false); + const [lastNameFlag, setLastNameFlag] = useState(false); + const [emailFlag, setEmailFlag] = useState(false); + const [userNameFlag, setUserNameFlag] = useState(false); + const [passwordFlag, setPasswordFlag] = useState(false); + const [retypeFlag, setRetypeFlag] = useState(false); + const [termsFlag, setTermsFlag] = useState(false); + + // States for the student search dropdown + const [matchingStudents, setMatchingStudents] = useState([]); // Array of strings (usernames) + const [usernameToSearch, setUserToSearch] = useState(""); // Text in the "Find a student" input + const [activeDropdown, setActiveDropdown] = useState(false); // Controls dropdown visibility + const [dropdownLoading, setDropdownLoading] = useState(false); // Loading state for dropdown search + + // State to store any validation errors for the form fields. + const [errors, setErrors] = useState({ + firstName: "", + lastName: "", + email: "", + username: "", + password: "", + retypePassword: "", + terms: "", + general: "", // Added for general signup errors + }); + + // State to manage the parent account specific UI and data. + const [parentAccountFlag, setParentAccountFlag] = useState(false); // Flag to indicate if the selected account type is 'parent'. + const [showStudentForm, setShowStudentForm] = useState(false); // Flag to control the visibility of the student creation form. + const [students, setStudents] = useState([]); // State to store data for student accounts under a parent. + const [assignedMenteeUsername, setAssignedMenteeUsername] = useState< + string | null + >(null); // To store the selected mentee's username + + // Handles changes to input fields in the main form. + const handleInputChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + // Updates the formData state with the new value for the changed field. + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + + // Performs specific validation based on the input field that changed. + switch (name) { + case "firstName": + firstNameVerification(value); + break; + case "lastName": + lastNameVerification(value); + break; + case "email": + emailVerification(value); + break; + case "username": + usernameVerification(value); + break; + case "password": + passwordVerification(value); + break; + case "retypedPassword": + retypePasswordVerification(value, formData.password); + break; + default: + break; + } + }; + + // Verifies the format of the first name. + const firstNameVerification = (firstName: string) => { + const isValid = /^[A-Za-z ]{2,15}$/.test(firstName); + setFirstNameFlag(isValid); + setErrors((prev) => ({ + ...prev, + firstName: isValid ? "" : "Invalid First Name", + })); + return isValid; + }; + + // Verifies the format of the last name. + const lastNameVerification = (lastName: string) => { + const isValid = /^[A-Za-z]{2,15}$/.test(lastName); + setLastNameFlag(isValid); + setErrors((prev) => ({ + ...prev, + lastName: isValid ? "" : "Invalid Last Name", + })); + return isValid; + }; + + // Verifies the format of the email address. + const emailVerification = (email: string) => { + const isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}/.test(email); + setEmailFlag(isValid); + setErrors((prev) => ({ + ...prev, + email: isValid ? "" : "Invalid Email", + })); + return isValid; + }; + + // Verifies the format of the username. + const usernameVerification = (username: string) => { + const isValid = /^[a-zA-Z](\S){1,14}$/.test(username); + setUserNameFlag(isValid); + setErrors((prev) => ({ + ...prev, + username: isValid ? "" : "Invalid Username", + })); + return isValid; + }; + + // Verifies the length of the password. + const passwordVerification = (password: string) => { + const isValid = password.length >= 8; + setPasswordFlag(isValid); + setErrors((prev) => ({ + ...prev, + password: isValid ? "" : "Password must be at least 8 characters", + })); + return isValid; + }; + + // Verifies if the retyped password matches the original password. + const retypePasswordVerification = ( + retypedPassword: string, + password: string + ) => { + const isValid = retypedPassword === password; + setRetypeFlag(isValid); + setErrors((prev) => ({ + ...prev, + retypePassword: isValid ? "" : "Passwords do not match", + })); + return isValid; + }; + + // Verifies if the retyped password matches the original password. + const termsCheckChange = (e: React.ChangeEvent) => { + setTermsFlag(e.target.checked); + }; + + // Handles changes to the account type dropdown. + const handleAccountTypeChange = (e: React.ChangeEvent) => { + const isParent = e.target.value === "parent"; + setParentAccountFlag(isParent); + // Updates the accountType in the form data. + setFormData((prev) => ({ + ...prev, + accountType: e.target.value, + })); + // Reset mentee search/selection when switching account type + setUserToSearch(""); + setAssignedMenteeUsername(null); + setActiveDropdown(false); + setMatchingStudents([]); + }; + + // Adds a new student form to the UI for parent accounts. + const handleAddStudent = () => { + const newStudent = { + id: Date.now(), + firstName: "", + lastName: "", + username: "", + email: "", + password: "", + retypedPassword: "", + errors: {}, }; + setStudents((prev: any) => [...prev, newStudent]); + setShowStudentForm(true); // Makes the student form visible. + }; + + // Handles changes to input fields within a student's form. + const handleStudentInputChange = ( + studentId: any, + field: string, + value: string + ) => { + // Performs specific validation based on the input field that changed. + switch (field) { + case "firstName": + firstNameVerification(value); + break; + case "lastName": + lastNameVerification(value); + break; + case "email": + emailVerification(value); + break; + case "username": + usernameVerification(value); + break; + case "password": + passwordVerification(value); + break; + case "retypedPassword": + const student = students.find((s) => s.id === studentId); // ge the student from id + const password = student?.password; // get its password + retypePasswordVerification(value, password); // verify if the retype is the same + break; + default: + break; + } + + // Updates the specific student's data in the students array. + setStudents((prev: any) => + prev.map((student: any) => + student.id === studentId ? { ...student, [field]: value } : student + ) + ); + }; - // Verifies the format of the username. - const usernameVerification = (username: string) => { - const isValid = /^[a-zA-Z](\S){1,14}$/.test(username); - setUserNameFlag(isValid); - setErrors((prev) => ({ - ...prev, - username: isValid ? '' : 'Invalid Username', - })); - return isValid; - }; - - // Verifies the length of the password. - const passwordVerification = (password: string) => { - const isValid = password.length >= 8; - setPasswordFlag(isValid); - setErrors((prev) => ({ - ...prev, - password: isValid ? '' : 'Password must be at least 8 characters', - })); - return isValid; - }; - - // Verifies if the retyped password matches the original password. - const retypePasswordVerification = (retypedPassword: string, password: string) => { - const isValid = retypedPassword === password; - setRetypeFlag(isValid); - setErrors((prev) => ({ - ...prev, - retypePassword: isValid ? '' : 'Passwords do not match', - })); - return isValid; - }; - - // Verifies if the retyped password matches the original password. - const termsCheckChange = (e: React.ChangeEvent) => { - setTermsFlag(e.target.checked) - }; - - // Handles changes to the account type dropdown. - const handleAccountTypeChange = (e: React.ChangeEvent) => { - const isParent = e.target.value === 'parent'; - setParentAccountFlag(isParent); - // Updates the accountType in the form data. - setFormData((prev) => ({ - ...prev, - accountType: e.target.value, - })); - // Reset mentee search/selection when switching account type - setUserToSearch(''); - setAssignedMenteeUsername(null); - setActiveDropdown(false); - setMatchingStudents([]); - }; - - - // Adds a new student form to the UI for parent accounts. - const handleAddStudent = () => { - const newStudent = { - id: Date.now(), - firstName: '', - lastName: '', - username: '', - email: '', - password: '', - retypedPassword: '', - errors: {}, - }; - setStudents((prev: any) => [...prev, newStudent]); - setShowStudentForm(true); // Makes the student form visible. - }; - - // Handles changes to input fields within a student's form. - const handleStudentInputChange = (studentId: any, field: string, value: string) => { - // Performs specific validation based on the input field that changed. - switch (field) { - case 'firstName': - firstNameVerification(value); - break; - case 'lastName': - lastNameVerification(value); - break; - case 'email': - emailVerification(value); - break; - case 'username': - usernameVerification(value); - break; - case 'password': - passwordVerification(value); - break; - case 'retypedPassword': - const student = students.find((s) => s.id === studentId); // ge the student from id - const password = student?.password; // get its password - retypePasswordVerification(value, password); // verify if the retype is the same - break; - default: - break; - } - - // Updates the specific student's data in the students array. - setStudents((prev: any) => - prev.map((student: any) => - student.id === studentId ? { ...student, [field]: value } : student - ) - ); - }; - - // Removes a student form from the UI. - const handleRemoveStudent = (studentId: any) => { - // Filters out the student with the given ID. - setStudents((prev: any) => prev.filter((student: any) => student.id !== studentId)); - // Hides the student form if no students are present. - if (students.length === 1) { // If only one student left, and it's removed, hide the form - setShowStudentForm(false); - } - }; - - // Handles changes in the "Find a student" input and triggers API call - const handleMenteeSearchChange = async (searchText: string) => { - setUserToSearch(searchText); // Update the controlled input's value - setAssignedMenteeUsername(null); // Clear any previously assigned mentee - - if (searchText.trim() === "") { - setActiveDropdown(false); - setMatchingStudents([]); - setDropdownLoading(false); - return; + // Removes a student form from the UI. + const handleRemoveStudent = (studentId: any) => { + // Filters out the student with the given ID. + setStudents((prev: any) => + prev.filter((student: any) => student.id !== studentId) + ); + // Hides the student form if no students are present. + if (students.length === 1) { + // If only one student left, and it's removed, hide the form + setShowStudentForm(false); + } + }; + + // Handles changes in the "Find a student" input and triggers API call + const handleMenteeSearchChange = async (searchText: string) => { + setUserToSearch(searchText); // Update the controlled input's value + setAssignedMenteeUsername(null); // Clear any previously assigned mentee + + if (searchText.trim() === "") { + setActiveDropdown(false); + setMatchingStudents([]); + setDropdownLoading(false); + return; + } + + setActiveDropdown(true); + setDropdownLoading(true); + try { + // Using query parameter for keyword + const response = await fetch( + `${environment.urls.middlewareURL}/user/mentorless?keyword=${searchText}`, + { + method: "GET", } - - setActiveDropdown(true); - setDropdownLoading(true); + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const usernames = await response.json(); + + // Slice the array to limit results to top 10 + const top10Usernames = usernames.slice(0, 10); + setMatchingStudents(top10Usernames); + + setDropdownLoading(false); + } catch (error) { + console.error("Error fetching mentorless students:", error); + setDropdownLoading(false); + setMatchingStudents([]); // Clear matches on error + setErrors((prev) => ({ + ...prev, + general: "Failed to fetch student list.", + })); + } + }; + + // Handles selecting a mentee from the dropdown + const handleSelectMentee = (selectedUsername: string) => { + setAssignedMenteeUsername(selectedUsername); + setUserToSearch(selectedUsername); // Set the input field to the selected username + setActiveDropdown(false); // Hide the dropdown + setMatchingStudents([]); // Clear the matching students + }; + + // Handles the submission of the signup form. + const handleSubmit = async () => { + console.log("Submit clicked", formData); + + // check if the terms and conditions are checked + if (!termsFlag) { + setErrors((prev) => ({ + ...prev, + general: "Please accept the terms and conditions.", + })); + return; + } + + // Checks if all main form fields are valid based on their flags. + const isValid = + firstNameFlag && + lastNameFlag && + emailFlag && + userNameFlag && + passwordFlag && + retypeFlag; + + // If the main form is not valid, prevents submission. + if (!isValid) { + console.log("Form validation failed"); + setErrors((prev) => ({ + ...prev, + general: "Please correct the form errors.", + })); + return; + } + + // if user is a mentor but has not selected mentee + if (formData.accountType === "mentor" && !assignedMenteeUsername) { + setErrors((prev) => ({ ...prev, general: "Please select your mentee" })); + return; + } + + let signupUrl = `${environment.urls.middlewareURL}/user/`; + let signupParams: URLSearchParams; + + if (parentAccountFlag) { + // Maps student data into the required format for the API. + const studentsData = students.map((student: any) => ({ + first: student.firstName, + last: student.lastName, + email: student.email, + username: student.username, + password: student.password, + })); + + // Prepare params for parent signup, stringifying the students array + signupParams = new URLSearchParams({ + first: formData.firstName, + last: formData.lastName, + email: formData.email, + password: formData.password, + username: formData.username, + role: formData.accountType, + students: JSON.stringify(studentsData), // Students array sent as a stringified JSON in query + }); + } else { + // Prepare params for non-parent accounts. + signupParams = new URLSearchParams({ + first: formData.firstName, + last: formData.lastName, + email: formData.email, + password: formData.password, + username: formData.username, + role: formData.accountType, + }); + } + + // Append query parameters to the URL for the signup request + signupUrl = `${signupUrl}?${signupParams.toString()}`; + + console.log("Signup Request URL:", signupUrl); // Log the full URL for debugging + + try { + // --- STEP 1: Perform User Signup (POST request with ALL data in query params) --- + const signupResponse = await fetch(signupUrl, { + method: "POST", + // IMPORTANT: Removed headers and body here, as data is now in query params. + // This prevents sending an empty body with 'Content-Type: application/json' header. + }); + + console.log("Signup Response status:", signupResponse.status); + + if (!signupResponse.ok) { + let errorContent: any; try { - // Using query parameter for keyword - const response = await fetch( - `${environment.urls.middlewareURL}/user/mentorless?keyword=${searchText}`, - { - method: 'GET', - } - ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const usernames = await response.json(); - - // Slice the array to limit results to top 10 - const top10Usernames = usernames.slice(0, 10); - setMatchingStudents(top10Usernames); - - setDropdownLoading(false); - } catch (error) { - console.error('Error fetching mentorless students:', error); - setDropdownLoading(false); - setMatchingStudents([]); // Clear matches on error - setErrors((prev) => ({ - ...prev, - general: 'Failed to fetch student list.', - })); + errorContent = await signupResponse.json(); + } catch (jsonError) { + errorContent = await signupResponse.text(); } - }; - - // Handles selecting a mentee from the dropdown - const handleSelectMentee = (selectedUsername: string) => { - setAssignedMenteeUsername(selectedUsername); - setUserToSearch(selectedUsername); // Set the input field to the selected username - setActiveDropdown(false); // Hide the dropdown - setMatchingStudents([]); // Clear the matching students - }; - - // Handles the submission of the signup form. - const handleSubmit = async () => { - console.log('Submit clicked', formData); - - // check if the terms and conditions are checked - if (!termsFlag) { - setErrors((prev) => ({ ...prev, general: 'Please accept the terms and conditions.' })); - return; + if ( + typeof errorContent === "string" && + errorContent === + "This username has been taken. Please choose another." + ) { + setErrors((prev) => ({ + ...prev, + username: "Username already taken", + general: "", + })); + } else { + const errorMessage = + typeof errorContent === "object" && + errorContent !== null && + "message" in errorContent && + typeof errorContent.message === "string" + ? errorContent.message + : typeof errorContent === "string" && errorContent.length > 0 + ? errorContent + : "Unknown error during signup"; + throw new Error( + `HTTP error! status: ${signupResponse.status}, message: ${errorMessage}` + ); } - - // Checks if all main form fields are valid based on their flags. - const isValid = - firstNameFlag && - lastNameFlag && - emailFlag && - userNameFlag && - passwordFlag && - retypeFlag; - - // If the main form is not valid, prevents submission. - if (!isValid) { - console.log('Form validation failed'); - setErrors((prev) => ({ ...prev, general: 'Please correct the form errors.' })); - return; + return; + } + + console.log("Signup successful, proceeding to login to get token..."); + + let jwtToken: string | null = null; + + // --- STEP 2: Perform Login to get JWT Token (POST request with query params) --- + try { + // Construct Login URL with username and password as query parameters + const loginUrl = `${ + environment.urls.middlewareURL + }/auth/login?username=${encodeURIComponent( + formData.username + )}&password=${encodeURIComponent(formData.password)}`; + console.log("Login URL for token acquisition:", loginUrl); // Log the full URL for debugging + + const loginResponse = await fetch(loginUrl, { + method: "POST", + // No 'Content-Type' header or body needed as per backend expecting query params + }); + + console.log("Login Response status:", loginResponse.status); + + if (!loginResponse.ok) { + const loginErrorData = await loginResponse.json(); + throw new Error( + `Login failed after signup: ${ + loginErrorData.message || "Unknown login error" + }` + ); } - // if user is a mentor but has not selected mentee - if (formData.accountType === 'mentor' && !assignedMenteeUsername) { - setErrors((prev) => ({ ...prev, general: 'Please select your mentee' })); - return; - } + const loginData = await loginResponse.json(); + jwtToken = loginData.token; + console.log("Login successful, JWT token obtained."); + } catch (loginError: any) { + console.error("Error during login after signup:", loginError); + setErrors((prev) => ({ + ...prev, + general: `Signup successful, but failed to log in to assign mentee: ${ + loginError.message || "Login network error" + }`, + })); + window.location.href = "/"; + return; + } + + // --- STEP 3: Conditionally Perform Mentee Assignment (PUT request for mentors with query params) --- + if ( + formData.accountType === "mentor" && + assignedMenteeUsername && + jwtToken + ) { + // Construct URL with 'mentorship' query parameter + const updateMentorshipUrl = `${ + environment.urls.middlewareURL + }/user/updateMentorship?mentorship=${encodeURIComponent( + assignedMenteeUsername + )}`; - let signupUrl = `${environment.urls.middlewareURL}/user/`; - let signupParams: URLSearchParams; - - if (parentAccountFlag) { - // Maps student data into the required format for the API. - const studentsData = students.map((student: any) => ({ - first: student.firstName, - last: student.lastName, - email: student.email, - username: student.username, - password: student.password, + try { + const updateMentorshipResponse = await fetch(updateMentorshipUrl, { + method: "PUT", + headers: { + Authorization: `Bearer ${jwtToken}`, // Bearer token is still in headers + }, + // No body needed as per your backend's current PUT route definition expecting query param + }); + + console.log( + "Update Mentorship Response status:", + updateMentorshipResponse.status + ); + + if (!updateMentorshipResponse.ok) { + const updateErrorData = await updateMentorshipResponse.json(); + console.error("Mentorship update failed:", updateErrorData); + setErrors((prev) => ({ + ...prev, + general: `Signup successful, but mentee assignment failed: ${ + updateErrorData.message || "Unknown error" + }`, })); - - // Prepare params for parent signup, stringifying the students array - signupParams = new URLSearchParams({ - first: formData.firstName, - last: formData.lastName, - email: formData.email, - password: formData.password, - username: formData.username, - role: formData.accountType, - students: JSON.stringify(studentsData), // Students array sent as a stringified JSON in query - }); - } else { - // Prepare params for non-parent accounts. - signupParams = new URLSearchParams({ - first: formData.firstName, - last: formData.lastName, - email: formData.email, - password: formData.password, - username: formData.username, - role: formData.accountType, - }); + } else { + const updateSuccessData = await updateMentorshipResponse.json(); + console.log("Mentorship update successful:", updateSuccessData); + } + } catch (updateError: any) { + console.error("Error during mentorship update:", updateError); + setErrors((prev) => ({ + ...prev, + general: `Signup successful, but mentee assignment network error: ${ + updateError.message || "Network error" + }`, + })); } - - // Append query parameters to the URL for the signup request - signupUrl = `${signupUrl}?${signupParams.toString()}`; - - console.log('Signup Request URL:', signupUrl); // Log the full URL for debugging - - try { - // --- STEP 1: Perform User Signup (POST request with ALL data in query params) --- - const signupResponse = await fetch(signupUrl, { - method: 'POST', - // IMPORTANT: Removed headers and body here, as data is now in query params. - // This prevents sending an empty body with 'Content-Type: application/json' header. - }); - - console.log('Signup Response status:', signupResponse.status); - - if (!signupResponse.ok) { - let errorContent: any; - try { - errorContent = await signupResponse.json(); - } catch (jsonError) { - errorContent = await signupResponse.text(); + } + + // --- Final Step: Redirect to homepage after all operations --- + window.location.href = "/"; + } catch (error: any) { + console.error("Signup error:", error); + setErrors((prev) => ({ + ...prev, + general: error.message || "Signup failed. Please try again.", + })); + } + }; + + return ( +
e.preventDefault()}> +

+ Sign up +

+ +
+ {/* Maps through the errors object and displays any error messages. */} + {Object.values(errors).map((error, index) => + error ?

{error}

: null + )} +
+ +
+ + + + + + +
+ +
+

Select Account Type

+ +
+ + {!parentAccountFlag && ( + <> + {" "} + {/* Using a React Fragment to wrap without an extra div */} + handleMenteeSearchChange(e.target.value)} + autoComplete="off" // Prevent browser's default autocomplete + /> + {activeDropdown && ( +
+ {" "} + {/* Keep ID for click outside handler */} + {dropdownLoading ? ( +
Loading...
+ ) : matchingStudents.length > 0 ? ( + matchingStudents.map((username) => ( + + )) + ) : ( +
No matching students found.
+ )} +
+ )} + + )} + + {/* Conditional rendering of the student section for parent accounts. */} + {parentAccountFlag && ( +
+ {/* Button to add a new student form if the student form is not currently shown. */} + {!showStudentForm && ( + + )} + + {/* Maps through the students array and renders a form for each student. */} + {students.map((student: any) => ( +
+ {/* Button to remove a student form. */} + + + + handleStudentInputChange( + student.id, + "firstName", + e.target.value + ) } - - if (typeof errorContent === 'string' && errorContent === 'This username has been taken. Please choose another.') { - setErrors((prev) => ({ - ...prev, - username: 'Username already taken', - general: '', - })); - } else { - const errorMessage = (typeof errorContent === 'object' && errorContent !== null && 'message' in errorContent && typeof errorContent.message === 'string') - ? errorContent.message - : (typeof errorContent === 'string' && errorContent.length > 0) - ? errorContent - : 'Unknown error during signup'; - throw new Error(`HTTP error! status: ${signupResponse.status}, message: ${errorMessage}`); + /> + + handleStudentInputChange( + student.id, + "lastName", + e.target.value + ) } - return; - } - - console.log('Signup successful, proceeding to login to get token...'); - - let jwtToken: string | null = null; - - // --- STEP 2: Perform Login to get JWT Token (POST request with query params) --- - try { - // Construct Login URL with username and password as query parameters - const loginUrl = `${environment.urls.middlewareURL}/auth/login?username=${encodeURIComponent(formData.username)}&password=${encodeURIComponent(formData.password)}`; - console.log('Login URL for token acquisition:', loginUrl); // Log the full URL for debugging - - const loginResponse = await fetch(loginUrl, { - method: 'POST', - // No 'Content-Type' header or body needed as per backend expecting query params - }); - - console.log('Login Response status:', loginResponse.status); - - if (!loginResponse.ok) { - const loginErrorData = await loginResponse.json(); - throw new Error(`Login failed after signup: ${loginErrorData.message || 'Unknown login error'}`); + /> + + handleStudentInputChange( + student.id, + "username", + e.target.value + ) } - - const loginData = await loginResponse.json(); - jwtToken = loginData.token; - console.log('Login successful, JWT token obtained.'); - - } catch (loginError: any) { - console.error('Error during login after signup:', loginError); - setErrors((prev) => ({ - ...prev, - general: `Signup successful, but failed to log in to assign mentee: ${loginError.message || 'Login network error'}`, - })); - window.location.href = '/'; - return; - } - - // --- STEP 3: Conditionally Perform Mentee Assignment (PUT request for mentors with query params) --- - if (formData.accountType === 'mentor' && assignedMenteeUsername && jwtToken) { - // Construct URL with 'mentorship' query parameter - const updateMentorshipUrl = `${environment.urls.middlewareURL}/user/updateMentorship?mentorship=${encodeURIComponent(assignedMenteeUsername)}`; - - try { - const updateMentorshipResponse = await fetch(updateMentorshipUrl, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${jwtToken}`, // Bearer token is still in headers - }, - // No body needed as per your backend's current PUT route definition expecting query param - }); - - console.log('Update Mentorship Response status:', updateMentorshipResponse.status); - - if (!updateMentorshipResponse.ok) { - const updateErrorData = await updateMentorshipResponse.json(); - console.error('Mentorship update failed:', updateErrorData); - setErrors((prev) => ({ - ...prev, - general: `Signup successful, but mentee assignment failed: ${updateErrorData.message || 'Unknown error'}`, - })); - } else { - const updateSuccessData = await updateMentorshipResponse.json(); - console.log('Mentorship update successful:', updateSuccessData); - } - } catch (updateError: any) { - console.error('Error during mentorship update:', updateError); - setErrors((prev) => ({ - ...prev, - general: `Signup successful, but mentee assignment network error: ${updateError.message || 'Network error'}`, - })); + /> + + handleStudentInputChange(student.id, "email", e.target.value) } - } - - // --- Final Step: Redirect to homepage after all operations --- - window.location.href = '/'; - - } catch (error: any) { - console.error('Signup error:', error); - setErrors((prev) => ({ - ...prev, - general: error.message || 'Signup failed. Please try again.', - })); - } - }; - - return ( - e.preventDefault()}> -

Sign up

- -
- {/* Maps through the errors object and displays any error messages. */} - {Object.values(errors).map((error, index) => - error ?

{error}

: null - )} -
- -
- - - - - - -
- -
-

Select Account Type

- -
- - {!parentAccountFlag && ( - <> {/* Using a React Fragment to wrap without an extra div */} - handleMenteeSearchChange(e.target.value)} - autoComplete="off" // Prevent browser's default autocomplete - /> - {activeDropdown && ( -
{/* Keep ID for click outside handler */} - {dropdownLoading ? ( -
Loading...
- ) : ( - matchingStudents.length > 0 ? ( - matchingStudents.map((username) => ( - - )) - ) : ( -
No matching students found.
- ) - )} -
- )} - - )} - - {/* Conditional rendering of the student section for parent accounts. */} - {parentAccountFlag && ( -
- {/* Button to add a new student form if the student form is not currently shown. */} - {!showStudentForm && ( - - )} - - {/* Maps through the students array and renders a form for each student. */} - {students.map((student: any) => ( -
- {/* Button to remove a student form. */} - - - - handleStudentInputChange( - student.id, - 'firstName', - e.target.value - ) - } - /> - - handleStudentInputChange( - student.id, - 'lastName', - e.target.value - ) - } - /> - - handleStudentInputChange( - student.id, - 'username', - e.target.value - ) - } - /> - - handleStudentInputChange(student.id, 'email', e.target.value) - } - /> - - handleStudentInputChange( - student.id, - 'password', - e.target.value - ) - } - /> - - handleStudentInputChange( - student.id, - 'retypedPassword', - e.target.value - ) - } - /> -
- ))} -
- )} - -
- - + /> + + handleStudentInputChange( + student.id, + "password", + e.target.value + ) + } + /> + + handleStudentInputChange( + student.id, + "retypedPassword", + e.target.value + ) + } + />
- - {/* Submit button for the signup form. */} - - - ); + ))} +
+ )} + +
+ + +
+ + {/* Submit button for the signup form. */} + + + ); }; export default Signup; diff --git a/react-ystemandchess/src/Pages/SignUp/signupController.ts b/react-ystemandchess/src/Pages/SignUp/signupController.ts index 781c7c9f..72891402 100644 --- a/react-ystemandchess/src/Pages/SignUp/signupController.ts +++ b/react-ystemandchess/src/Pages/SignUp/signupController.ts @@ -1,24 +1,24 @@ -const { createUser, createStudents } = require('./signupService'); -const User = require('../models/User'); -import * as crypto from 'node:crypto'; +const { createUser, createStudents } = require("./signupService"); +const User = require("../models/User"); +import * as crypto from "node:crypto"; const signup = async (req: any, res: any) => { const { username, password, first, last, email, role, students } = req.query; try { - const sha384 = crypto.createHash('sha384'); - const hashedPassword = sha384.update(password).digest('hex'); + const sha384 = crypto.createHash("sha384"); + const hashedPassword = sha384.update(password).digest("hex"); const existingUser = await User.findOne({ username }); if (existingUser) { return res .status(400) - .json('This username has been taken. Please choose another.'); + .json("This username has been taken. Please choose another."); } const currDate = new Date(); - if (role === 'parent' && students) { + if (role === "parent" && students) { const studentsArray = JSON.parse(students); if (studentsArray.length > 0) { @@ -29,7 +29,7 @@ const signup = async (req: any, res: any) => { if (studentExists) { return res .status(400) - .json('This username has been taken. Please choose another.'); + .json("This username has been taken. Please choose another."); } } @@ -47,10 +47,10 @@ const signup = async (req: any, res: any) => { accountCreatedAt: currDate.toLocaleString(), }); - res.status(200).json('Added users'); + res.status(200).json("Added users"); } catch (error) { - console.error('Signup error:', error); - res.status(500).json('Server error'); + console.error("Signup error:", error); + res.status(500).json("Server error"); } }; From 3ed4e51163c4860bf9dca329db5ad87c12251bcc Mon Sep 17 00:00:00 2001 From: pedram karimi Date: Tue, 1 Jul 2025 03:38:16 -0400 Subject: [PATCH 2/5] migrate S3 pre signed URL and Agora recording logic from PHP to Node.js --- middlewareNode/models/users.js | 92 +++++++-------- middlewareNode/routes/recordings.js | 168 ++++++++++++++++++++++++++++ middlewareNode/routes/s3.js | 48 ++++++++ middlewareNode/routes/users.js | 12 +- middlewareNode/utils/verifyJWT.js | 13 +++ 5 files changed, 280 insertions(+), 53 deletions(-) create mode 100644 middlewareNode/routes/recordings.js create mode 100644 middlewareNode/routes/s3.js create mode 100644 middlewareNode/utils/verifyJWT.js diff --git a/middlewareNode/models/users.js b/middlewareNode/models/users.js index 644ac685..f7509b60 100644 --- a/middlewareNode/models/users.js +++ b/middlewareNode/models/users.js @@ -55,84 +55,84 @@ const usersSchema = new mongoose.Schema( ], default: () => [ { - piece: 'Piece Checkmate 1 Basic checkmates', - lessonNumber: 0 + piece: "Piece Checkmate 1 Basic checkmates", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 1 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 1 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 2 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 2 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 3 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 3 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Checkmate Pattern 4 Recognize the patterns', - lessonNumber: 0 + piece: "Checkmate Pattern 4 Recognize the patterns", + lessonNumber: 0, }, { - piece: 'Piece checkmates 2 Challenging checkmates', - lessonNumber: 0 + piece: "Piece checkmates 2 Challenging checkmates", + lessonNumber: 0, }, { - piece: 'Knight and Bishop Mate interactive lesson', - lessonNumber: 0 + piece: "Knight and Bishop Mate interactive lesson", + lessonNumber: 0, }, - { piece: 'The Pin Pin it to win it', lessonNumber: 0 }, - { piece: 'The Skewer Yum - Skewers!', lessonNumber: 0 }, - { piece: 'The Fork Use the fork, Luke', lessonNumber: 0 }, + { piece: "The Pin Pin it to win it", lessonNumber: 0 }, + { piece: "The Skewer Yum - Skewers!", lessonNumber: 0 }, + { piece: "The Fork Use the fork, Luke", lessonNumber: 0 }, { - piece: 'Discovered Attacks Including discovered checks', - lessonNumber: 0 + piece: "Discovered Attacks Including discovered checks", + lessonNumber: 0, }, - { piece: 'Double Check A very powerfull tactic', lessonNumber: 0 }, + { piece: "Double Check A very powerfull tactic", lessonNumber: 0 }, { - piece: 'Overloaded Pieces They have too much work', - lessonNumber: 0 + piece: "Overloaded Pieces They have too much work", + lessonNumber: 0, }, - { piece: 'Zwischenzug In-between moves', lessonNumber: 0 }, - { piece: 'X-Ray Attacking through an enemy piece', lessonNumber: 0 }, - { piece: 'Zugzwang Being forced to move', lessonNumber: 0 }, + { piece: "Zwischenzug In-between moves", lessonNumber: 0 }, + { piece: "X-Ray Attacking through an enemy piece", lessonNumber: 0 }, + { piece: "Zugzwang Being forced to move", lessonNumber: 0 }, { - piece: 'Interference Interpose a piece to great effect', - lessonNumber: 0 + piece: "Interference Interpose a piece to great effect", + lessonNumber: 0, }, { - piece: 'Greek Gift Study the greek gift scrifice', - lessonNumber: 0 + piece: "Greek Gift Study the greek gift scrifice", + lessonNumber: 0, }, - { piece: 'Deflection Distracting a defender', lessonNumber: 0 }, - { piece: 'Attraction Lure a piece to bad square', lessonNumber: 0 }, + { piece: "Deflection Distracting a defender", lessonNumber: 0 }, + { piece: "Attraction Lure a piece to bad square", lessonNumber: 0 }, { - piece: 'Underpromotion Promote - but not to a queen!', - lessonNumber: 0 + piece: "Underpromotion Promote - but not to a queen!", + lessonNumber: 0, }, { - piece: 'Desperado A piece is lost, but it can still help', - lessonNumber: 0 + piece: "Desperado A piece is lost, but it can still help", + lessonNumber: 0, }, { - piece: 'Counter Check Respond to a check with a check', - lessonNumber: 0 + piece: "Counter Check Respond to a check with a check", + lessonNumber: 0, }, - { piece: 'Undermining Remove the defending piece', lessonNumber: 0 }, - { piece: 'Clearance Get out of the way!', lessonNumber: 0 }, - { piece: 'Key Squares Reach the key square', lessonNumber: 0 }, - { piece: 'Opposition take the opposition', lessonNumber: 0 }, - { piece: '7th-Rank Rook Pawn Versus a Queen', lessonNumber: 0 }, + { piece: "Undermining Remove the defending piece", lessonNumber: 0 }, + { piece: "Clearance Get out of the way!", lessonNumber: 0 }, + { piece: "Key Squares Reach the key square", lessonNumber: 0 }, + { piece: "Opposition take the opposition", lessonNumber: 0 }, + { piece: "7th-Rank Rook Pawn Versus a Queen", lessonNumber: 0 }, { - piece: '7th-Rank Rook Pawn And Passive Rook vs Rook', - lessonNumber: 0 + piece: "7th-Rank Rook Pawn And Passive Rook vs Rook", + lessonNumber: 0, }, - { piece: 'Basic Rook Endgames Lucena and Philidor', lessonNumber: 0 } + { piece: "Basic Rook Endgames Lucena and Philidor", lessonNumber: 0 }, ], }, }, - { versionKey: false }, + { versionKey: false } ); module.exports = users = model("users", usersSchema); diff --git a/middlewareNode/routes/recordings.js b/middlewareNode/routes/recordings.js new file mode 100644 index 00000000..d058be12 --- /dev/null +++ b/middlewareNode/routes/recordings.js @@ -0,0 +1,168 @@ +const express = require("express"); +const router = express.Router(); +const axios = require("axios"); +const config = require("config"); + +// Load env/config values +const appID = config.get("appID"); +const auth = config.get("auth"); // Agora Basic Auth header +const channelName = config.get("channel"); +const uid = config.get("uid"); +const queryURL = `https://api.agora.io/v1/apps/${appID}/cloud_recording/`; + +// AWS Info (used in request body) +const awsAccessKey = config.get("awsAccessKey"); +const awsSecretKey = config.get("awsSecretKey"); +const bucketName = "ystemandchess-meeting-recordings"; +const region = 1; +const vendor = 1; + +// Acquire Resource ID +async function acquireRecording() { + const response = await axios.post( + `${queryURL}acquire`, + { + cname: channelName, + uid: uid, + clientRequest: { resourceExpiredHour: 24 }, + }, + { + headers: { + "Content-Type": "application/json", + Authorization: auth, + }, + } + ); + + return response.data.resourceId; +} + +// Start Recording +async function startRecording(resourceId) { + const startURL = `${queryURL}resourceid/${resourceId}/mode/mix/start`; + + const startBody = { + uid, + cname: channelName, + clientRequest: { + storageConfig: { + vendor, + region, + bucket: bucketName, + accessKey: awsAccessKey, + secretKey: awsSecretKey, + }, + recordingConfig: { + maxIdleTime: 30, + audioProfile: 0, + channelType: 0, + transcodingConfig: { + width: 1280, + height: 720, + fps: 15, + bitrate: 600, + mixedVideoLayout: 3, + backgroundColor: "#000000", + layoutConfig: [ + { + x_axis: 0, + y_axis: 0, + width: 0.5, + height: 1, + alpha: 1, + render_mode: 1, + }, + { + x_axis: 0.5, + y_axis: 0, + width: 0.5, + height: 1, + alpha: 1, + render_mode: 1, + }, + ], + }, + }, + }, + }; + + const response = await axios.post(startURL, startBody, { + headers: { + "Content-Type": "application/json", + Authorization: auth, + }, + }); + + return { + sid: response.data.sid, + resourceId: resourceId, + }; +} + +// Stop Recording +async function stopRecording(resourceId, sid) { + const stopURL = `${queryURL}resourceid/${resourceId}/sid/${sid}/mode/mix/stop`; + + const stopBody = { + uid, + cname: channelName, + clientRequest: {}, + }; + + const response = await axios.post(stopURL, stopBody, { + headers: { + "Content-Type": "application/json", + Authorization: auth, + }, + }); + + return response.data; +} + +// @route POST /recordings/start +// @desc Starts Agora recording and stores in S3 +// @access Public (auth should be added in production) +router.post("/start", async (req, res) => { + try { + const resourceId = await acquireRecording(); + const result = await startRecording(resourceId); + res.status(200).json({ + message: "Recording started", + sid: result.sid, + resourceId: result.resourceId, + }); + } catch (error) { + console.error( + "Start Recording Error:", + error.response?.data || error.message + ); + res.status(500).json({ error: "Failed to start recording" }); + } +}); + +// @route POST /recordings/stop +// @desc Stops Agora recording session +// @access Public (auth should be added in production) +router.post("/stop", async (req, res) => { + const { resourceId, sid } = req.body; + + if (!resourceId || !sid) { + return res.status(400).json({ error: "Missing resourceId or sid" }); + } + + try { + const result = await stopRecording(resourceId, sid); + res.status(200).json({ + message: "Recording stopped", + fileList: result.serverResponse?.fileList || [], + }); + } catch (error) { + console.error( + "Stop Recording Error:", + error.response?.data || error.message + ); + res.status(500).json({ error: "Failed to stop recording" }); + } +}); + +module.exports = router; diff --git a/middlewareNode/routes/s3.js b/middlewareNode/routes/s3.js new file mode 100644 index 00000000..44bb2134 --- /dev/null +++ b/middlewareNode/routes/s3.js @@ -0,0 +1,48 @@ +const express = require("express"); +const router = express.Router(); +const AWS = require("aws-sdk"); +const verifyToken = require("../utils/verifyJWT"); +const config = require("config"); + +dotenv.config(); + +// @route GET /s3/getPresignedUrl +// @desc Generates a pre-signed URL for a file in S3 +// @access Public with jwt Authentication +router.get("/getPresignedUrl", async (req, res) => { + try { + const { jwt, vid: filename } = req.query; + + if (!jwt || !filename) { + return res.status(400).json("Missing JWT or filename in query"); + } + + const credentials = verifyToken(jwt); + if (typeof credentials === "string" && credentials.startsWith("Error")) { + return res.status(401).json(credentials); + } + + const s3 = new AWS.S3({ + region: "us-east-2", + credentials: { + accessKeyId: config.get("awsAccessKey"), + secretAccessKey: config.get("awsSecretKey"), + }, + }); + + const params = { + Bucket: "ystemandchess-meeting-recordings", + Key: filename, + Expires: 60 * 20, // 20 minutes + }; + + const signedUrl = s3.getSignedUrl("getObject", params); + + return res.status(200).send(signedUrl); + } catch (error) { + console.error("Error generating presigned URL:", error.message); + return res.status(500).json("Server error"); + } +}); + +module.exports = router; diff --git a/middlewareNode/routes/users.js b/middlewareNode/routes/users.js index 443044eb..64785f71 100644 --- a/middlewareNode/routes/users.js +++ b/middlewareNode/routes/users.js @@ -319,13 +319,11 @@ router.put( // if mentorship field is not modified if (result.modifiedCount === 0) { - return res - .status(404) - .json({ - message: - "User not found or already has that mentorshipUsername, username: " + - req.user.username, - }); + return res.status(404).json({ + message: + "User not found or already has that mentorshipUsername, username: " + + req.user.username, + }); } res.json({ message: "Mentorship updated successfully" }); diff --git a/middlewareNode/utils/verifyJWT.js b/middlewareNode/utils/verifyJWT.js new file mode 100644 index 00000000..6c727edf --- /dev/null +++ b/middlewareNode/utils/verifyJWT.js @@ -0,0 +1,13 @@ +const jwt = require("jsonwebtoken"); +require("dotenv").config(); + +module.exports = (token) => { + if (!token) return "Error: 406. Please Provide a JSON Web Token."; + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + return decoded; + } catch (err) { + return "Error: 405. This key has been tampered with or is out of date."; + } +}; From de9f43743f916ae2eaed2867b5de122b3dcf782e Mon Sep 17 00:00:00 2001 From: pedram karimi Date: Mon, 7 Jul 2025 02:43:16 -0400 Subject: [PATCH 3/5] migrated newGame.php to NodeJS --- middlewareNode/routes/newGame.js | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 middlewareNode/routes/newGame.js diff --git a/middlewareNode/routes/newGame.js b/middlewareNode/routes/newGame.js new file mode 100644 index 00000000..bae6a3d1 --- /dev/null +++ b/middlewareNode/routes/newGame.js @@ -0,0 +1,57 @@ +const express = require("express"); +const router = express.Router(); +const { MongoClient } = require("mongodb"); +const config = require("config"); + +const verifyJWT = require("../utils/verifyJWT"); + +const mongoURI = config.get("mongoURI"); + +// @route GET /waiting/join +// @desc Add mentor or student to waiting list if not already waiting +// @access Public via JWT +router.get("/join", async (req, res) => { + const token = decodeURIComponent(req.query.jwt || ""); + const credentials = verifyJWT(token); + + if (typeof credentials === "string") { + return res.status(400).send(credentials); // error message returned by verifyJWT + } + + const { role, username, firstName, lastName } = credentials; + + if (role !== "mentor" && role !== "student") { + return res.status(400).send("Please be either a student or a mentor."); + } + + const client = new MongoClient(mongoURI); + try { + await client.connect(); + const db = client.db("ystem"); + const collection = + role === "mentor" + ? db.collection("waitingMentors") + : db.collection("waitingStudents"); + + const existing = await collection.findOne({ username }); + if (existing) { + return res.status(409).send("Person already waiting for game."); + } + + await collection.insertOne({ + username, + firstName, + lastName, + requestedGameAt: Math.floor(Date.now() / 1000), + }); + + return res.status(200).send("Person Added Sucessfully."); + } catch (err) { + console.error("Error adding person to waitlist:", err); + return res.status(500).send("Internal Server Error"); + } finally { + await client.close(); + } +}); + +module.exports = router; From 0eba3080644663d40a07ff99a1141e08fe189e8b Mon Sep 17 00:00:00 2001 From: pedram karimi Date: Fri, 11 Jul 2025 17:33:40 -0400 Subject: [PATCH 4/5] Migrate game waiting-list removal and user deletion routes from PHP to Node.js with JWT authentication. --- middlewareNode/routes/newGame.js | 40 ++++++++++++++++++++++++++ middlewareNode/routes/users.js | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/middlewareNode/routes/newGame.js b/middlewareNode/routes/newGame.js index bae6a3d1..68e93c50 100644 --- a/middlewareNode/routes/newGame.js +++ b/middlewareNode/routes/newGame.js @@ -54,4 +54,44 @@ router.get("/join", async (req, res) => { } }); +router.delete("/leave", async (req, res) => { + const token = decodeURIComponent(req.query.jwt || ""); + const credentials = verifyJWT(token); + + if (typeof credentials === "string") { + return res.status(400).send(credentials); // "Error: 405..." or "406..." + } + + const { username, role } = credentials; + + if (role !== "mentor" && role !== "student") { + return res.status(400).send("Please be either a student or a mentor."); + } + + const client = new MongoClient(mongoURI); + try { + await client.connect(); + const db = client.db("ystem"); + const collection = + role === "mentor" + ? db.collection("waitingMentors") + : db.collection("waitingStudents"); + + const existing = await collection.findOne({ username }); + + if (!existing) { + return res.status(404).send("Person is not waiting for a match."); + } + + await collection.deleteOne({ username }); + + return res.status(200).send("Person Removed Successfully."); + } catch (error) { + console.error("Error removing from waiting list:", error); + return res.status(500).send("Internal Server Error"); + } finally { + await client.close(); + } +}); + module.exports = router; diff --git a/middlewareNode/routes/users.js b/middlewareNode/routes/users.js index 64785f71..2551494c 100644 --- a/middlewareNode/routes/users.js +++ b/middlewareNode/routes/users.js @@ -4,6 +4,7 @@ const router = express.Router(); const crypto = require("crypto"); const { check, validationResult } = require("express-validator"); const users = require("../models/users"); +const verifyJWT = require("../utils/verifyJWT"); const { ChangePasswordTemplateForUser, } = require("../template/changePasswordTemplate"); @@ -377,5 +378,52 @@ router.get( } } ); +router.delete("/delete", async (req, res) => { + const token = decodeURIComponent(req.query.jwt || ""); + const credentials = verifyJWT(token); + if (typeof credentials === "string") { + return res.status(400).send(credentials); // error string from verifyJWT + } + + const { username, role, parentUsername } = credentials; + + const client = new MongoClient(mongoURI); + try { + await client.connect(); + const db = client.db("ystem"); + const usersCollection = db.collection("users"); + + const user = await usersCollection.findOne({ username }); + if (!user) { + return res.status(404).send("Failure. Document does not exist."); + } + + // Delete the user + await usersCollection.deleteOne({ username }); + + // If student: remove from parent's children array + if (role === "student" && parentUsername) { + await usersCollection.updateOne( + { username: parentUsername }, + { $pull: { children: username } } + ); + } + + // If parent: update all their children to remove parentUsername reference + if (role === "parent") { + await usersCollection.updateMany( + { parentUsername: username }, + { $set: { parentUsername: null } } + ); + } + + return res.status(200).send("Success"); + } catch (error) { + console.error("Error deleting user:", error); + return res.status(500).send("Internal Server Error"); + } finally { + await client.close(); + } +}); module.exports = router; From 1d5ff9d98244242970e10479d2a2377a6ded654b Mon Sep 17 00:00:00 2001 From: Onkar Apte Date: Tue, 12 Aug 2025 22:08:41 -0600 Subject: [PATCH 5/5] Add meeting controller and port PHP meeting logic to Node --- middlewareNode/controllers/meetings.js | 271 +++++++++ middlewareNode/routes/meetings.js | 726 +++++++------------------ 2 files changed, 453 insertions(+), 544 deletions(-) diff --git a/middlewareNode/controllers/meetings.js b/middlewareNode/controllers/meetings.js index e69de29b..ebd91b47 100644 --- a/middlewareNode/controllers/meetings.js +++ b/middlewareNode/controllers/meetings.js @@ -0,0 +1,271 @@ +const { v4: uuidv4 } = require("uuid"); +const meetings = require("../models/meetings"); +const { waitingStudents, waitingMentors } = require("../models/waiting"); +const movesList = require("../models/moves"); +const undoPermission = require("../models/undoPermission"); +const users = require("../models/users"); + +const { startRecording, stopRecording } = require("../utils/recordings"); + +/** + * inMeeting(role, username) + * - returns: array of meeting docs if found (otherwise the exact string) + */ +const inMeeting = async (role, username) => { + let filters = { CurrentlyOngoing: true }; + if (role === "student") { + filters.studentUsername = username; + } else if (role === "mentor") { + filters.mentorUsername = username; + } else { + return "Please be either a student or a mentor."; + } + + const foundMeeting = await meetings.find(filters); + if (foundMeeting.length !== 0) { + await deleteUser(role, username); + return foundMeeting; // keep array return for compatibility + } + return "There are no current meetings with this user."; +}; + +/** + * deleteUser(role, username) + * - parity with routes/meetings.js + */ +const deleteUser = async (role, username) => { + if (role === "student") { + const user = await waitingStudents.findOne({ username }); + if (user != null) await user.delete(); + } else if (role === "mentor") { + const mentor = await waitingMentors.findOne({ username }); + if (mentor != null) await mentor.delete(); + } + return true; +}; + +/** + * endMeetingForUser(userCtx) + * - Returns: + * { ok: true, meetingId, minutesPlayed } on success + * or throws Error("...") with messages compatible with the route usage + * + */ +const endMeetingForUser = async (userCtx) => { + const { role, username, firstName, lastName } = userCtx; + + let filters = { CurrentlyOngoing: true }; + if (role === "student") { + filters.studentUsername = username; + filters.studentFirstName = firstName; + filters.studentLastName = lastName; + } else if (role === "mentor") { + filters.mentorUsername = username; + filters.mentorFirstName = firstName; + filters.mentorLastName = lastName; + } else { + throw new Error("You must be a student or mentor to end the meeting!"); + } + + const currMeeting = await meetings.findOne(filters); + if (!currMeeting) { + throw new Error("You are currently not in a meeting!"); + } + + // Stop the recording + let filesList = []; + try { + const stopResponse = await stopRecording( + currMeeting.meetingId, + currMeeting.resourceId, + currMeeting.sid, + ); + + // Collect only .mp4 names + if ( + stopResponse && + stopResponse?.fileList && + Array.isArray(stopResponse.fileList) + ) { + for (const file of stopResponse.fileList) { + if (file?.fileName?.indexOf(".mp4") !== -1) { + filesList.push(file.fileName); + } + } + } + } catch (e) { + + } + + // Mark meeting inactive and persist file list + end time + currMeeting.CurrentlyOngoing = false; + currMeeting.meetingEndTime = new Date(); + currMeeting.filesList = filesList; + await currMeeting.save(); + + // Compute minutes played (route parity) + const minutesPlayed = Math.floor( + (currMeeting.meetingEndTime.getTime() - + currMeeting.meetingStartTime.getTime()) / + 1000 / + 60, + ); + + // Update student's total minutes (route parity) + const msg = await updateTimePlayed( + currMeeting.studentUsername, + currMeeting.studentFirstName, + currMeeting.studentLastName, + minutesPlayed, + ); + if (msg !== "Saved") { + // keep compatibility with route error handling + throw new Error(msg || "Failed to update time played"); + } + + return { + ok: true, + meetingId: currMeeting.meetingId, + minutesPlayed, + }; +}; + +/** + * updateTimePlayed(username, firstName, lastName, timePlayed) + * - returns "Saved" on success, string error on failure + */ +const updateTimePlayed = async (username, firstName, lastName, timePlayed) => { + const user = await users.findOne({ username, firstName, lastName }); + if (!user) return "Could not find user"; + + // Ensure numeric + const current = typeof user.timePlayed === "number" ? user.timePlayed : 0; + user.timePlayed = current + Number(timePlayed || 0); + await user.save(); + return "Saved"; +}; + + + +const getMoves = async (meetingId) => { + const doc = await meetings.findOne({ meetingId, CurrentlyOngoing: true }); + return doc; +}; + +const updateMoves = async (meetingId, oldMovesArr) => { + const updated = await meetings.findOneAndUpdate( + { meetingId }, + { moves: oldMovesArr }, + { new: true }, + ); + return updated; +}; + +const updateUndoPermission = async (meetingId, value) => { + if (value == "student") { + const existing = await undoPermission.findOne({ meetingId }); + if (!existing) { + await undoPermission.create({ meetingId, permission: true }); + } else { + await undoPermission.findOneAndUpdate( + { meetingId }, + { permission: true }, + ); + } + } else { + await undoPermission.findOneAndUpdate( + { meetingId }, + { permission: false }, + ); + } +}; + + + +const getMovesByGameId = async (gameId) => { + const doc = await movesList.findOne({ gameId }); + return doc; +}; + +const updateMoveByGameId = async (gameId, oldMovesArr) => { + const updated = await movesList.findOneAndUpdate( + { gameId }, + { moves: oldMovesArr }, + { new: true }, + ); + return updated; +}; + +const deleteMovesByMeetingId = async (meetingId, deletedData) => { + const updated = await meetings.findOneAndUpdate( + { meetingId }, + { moves: deletedData }, + { new: true }, + ); + return updated; +}; + +const deleteMovesByGameId = async (gameId, deletedData) => { + const updated = await movesList.findOneAndUpdate( + { gameId }, + { moves: deletedData }, + { new: true }, + ); + return updated; +}; + +/** + * createMeetingPair(studentInfo, mentorInfo) + * - returns { meetingId, password, resourceId, sid } + */ +const createMeetingPair = async (studentInfo, mentorInfo) => { + const meetingId = uuidv4(); + const recordingInfo = await startRecording(meetingId); + if (recordingInfo === "Could not start recording. Server error.") { + throw new Error(recordingInfo); + } + + const uniquePassword = uuidv4(); + await meetings.create({ + meetingId, + password: uniquePassword, + studentUsername: studentInfo.username, + studentFirstName: studentInfo.firstName, + studentLastName: studentInfo.lastName, + mentorUsername: mentorInfo.username, + mentorFirstName: mentorInfo.firstName, + mentorLastName: mentorInfo.lastName, + CurrentlyOngoing: true, + resourceId: recordingInfo.resourceId, + sid: recordingInfo.sid, + meetingStartTime: new Date(), + }); + + return { + meetingId, + password: uniquePassword, + resourceId: recordingInfo.resourceId, + sid: recordingInfo.sid, + }; +}; + +module.exports = { + // Core functions + inMeeting, + deleteUser, + endMeetingForUser, + updateTimePlayed, + + // Moves => meetingId + getMoves, + updateMoves, + updateUndoPermission, + deleteMovesByMeetingId, + + // Store moves => gameId + getMovesByGameId, + updateMoveByGameId, + deleteMovesByGameId, + + createMeetingPair, +}; diff --git a/middlewareNode/routes/meetings.js b/middlewareNode/routes/meetings.js index b5b2a6d7..c167835b 100644 --- a/middlewareNode/routes/meetings.js +++ b/middlewareNode/routes/meetings.js @@ -1,60 +1,52 @@ const express = require("express"); const passport = require("passport"); -const crypto = require("crypto"); -const jwt = require("jsonwebtoken"); const AWS = require("aws-sdk"); -const axios = require("axios"); const config = require("config"); const requestIp = require("request-ip"); const { v4: uuidv4 } = require("uuid"); +const { check, validationResult } = require("express-validator"); + const router = express.Router(); -const { check, validationResult, query } = require("express-validator"); -const { waitingStudents, waitingMentors } = require("../models/waiting"); + +const users = require("../models/users"); const meetings = require("../models/meetings"); +const { waitingStudents, waitingMentors } = require("../models/waiting"); const movesList = require("../models/moves"); -const undoPermission = require("../models/undoPermission"); -const { startRecording, stopRecording } = require("../utils/recordings"); -var isBusy = false; //State variable to see if a query is already running. +const meetingCtrl = require("../controllers/meeting"); + +let isBusy = false; -// @route GET /meetings/singleRecording -// @desc GET a presigned URL from AWS S3 -// @access Public with jwt Authentication router.get( "/singleRecording", [check("filename", "The filename is required").not().isEmpty()], passport.authenticate("jwt"), async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } try { - console.log(config.get("awsSecretKey")); - const s3Config = { + const s3 = new AWS.S3({ apiVersion: "latest", region: "us-east-2", accessKeyId: config.get("awsAccessKey"), secretAccessKey: config.get("awsSecretKey"), - }; - - var s3 = new AWS.S3(s3Config); - + }); const params = { Bucket: "ystemandchess-meeting-recordings", Key: req.query.filename, Expires: 604800, }; - const url = s3.getSignedUrl("getObject", params); - console.log(url); - res.status(200).json(url); + return res.status(200).json(url); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }, ); -// @route GET /meetings/recordings -// @desc GET all recordings available for the student or mentor -// @access Public with jwt Authentication router.get("/recordings", passport.authenticate("jwt"), async (req, res) => { try { const { role, username, firstName, lastName } = req.user; @@ -68,142 +60,86 @@ router.get("/recordings", passport.authenticate("jwt"), async (req, res) => { filters.mentorFirstName = firstName; filters.mentorLastName = lastName; } else { - return res - .status(404) - .json("Must be a student or mentor to get your own recordings"); + return res.status(404).json("Must be a student or mentor to get your own recordings"); } + const recs = await meetings.find(filters); + if (!recs) return res.status(400).json("User did not have any recordings available"); + return res.send(recs); + } catch (error) { + console.error(error.message); + return res.status(500).json("Server error"); + } +}); - const recordings = await meetings.find(filters); //Find all meetings with the listed filters above - - //Error handling for query - if (!recordings) { - res.status(400).json("User did not have any recordings available"); - } else { - res.send(recordings); +router.get("/usersRecordings", passport.authenticate("jwt"), async (req, res) => { + try { + const { role, username } = req.user; + const filters = role === "student" + ? { studentUsername: username } + : role === "mentor" + ? { mentorUsername: username } + : null; + if (!filters) { + return res.status(404).json("Must be a student or mentor to get your own recordings"); } + const recs = await meetings.find(filters); + if (!recs) return res.status(400).json("User did not have any recordings available"); + return res.send(recs.reverse()); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); -// @route GET /meetings/usersRecordings -// @desc GET all recordings available for the student or mentor -// @access Public with jwt Authentication -router.get( - "/usersRecordings", - passport.authenticate("jwt"), - async (req, res) => { - // console.log(req); - try { - const { role, username, firstName, lastName } = req.user; - let filters = {}; - if (role === "student") { - filters.studentUsername = username; - } else if (role === "mentor") { - filters.mentorUsername = username; - } else { - return res - .status(404) - .json("Must be a student or mentor to get your own recordings"); - } - - const recordings = await meetings.find(filters); //Find all meetings with the listed filters above - // console.log('recordings = ',recordings); - //Error handling for query - if (!recordings) { - res.status(400).json("User did not have any recordings available"); - } else { - res.send(recordings.reverse()); - } - } catch (error) { - console.error(error.message); - res.status(500).json("Server error"); - } - }, -); -// @route GET /meetings/parents/recordings -// @desc GET all recordings available for the student from a parent account -// @access Public with jwt Authentication router.get( "/parents/recordings", [check("childUsername", "The child's username is required").not().isEmpty()], passport.authenticate("jwt"), async (req, res) => { const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } + if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const { role, username } = req.user; const { childUsername } = req.query; - try { - if (role === "parent") { - const child = await users.findOne({ - parentUsername: username, - username: childUsername, - }); - if (!child) { - return res - .status(400) - .json("You are not the parent to the requested child."); - } - const recordings = await meetings - .find({ - studentUsername: childUsername, - filesList: { $ne: null }, - }) - .select(["filesList", "meetingStartTime", "-_id"]); //Select only fileName and meetingStartTime fields for all found entries - - //Error checking for no recordings - if (recordings?.length === 0) { - res - .status(400) - .json("Could not find any recordings for the requested child."); - } else { - res.send(recordings); - } - } else { - res.status(404).json("You are not the parent of the requested child."); + if (role !== "parent") { + return res.status(404).json("You are not the parent of the requested child."); + } + const child = await users.findOne({ parentUsername: username, username: childUsername }); + if (!child) { + return res.status(400).json("You are not the parent to the requested child."); } + const recs = await meetings + .find({ studentUsername: childUsername, filesList: { $ne: null } }) + .select(["filesList", "meetingStartTime", "-_id"]); + if (!recs || recs.length === 0) { + return res.status(400).json("Could not find any recordings for the requested child."); + } + return res.send(recs); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }, ); -// @route GET /meetings/inMeeting -// @desc GET the meeting if the USER is in a meeting otherwise return message -// @access Public with jwt Authentication router.get("/inMeeting", passport.authenticate("jwt"), async (req, res) => { try { const { role, username } = req.user; - - let message = await inMeeting(role, username); - res.status(200).json(message); + const message = await meetingCtrl.inMeeting(role, username); + return res.status(200).json(message); } catch (error) { - console.error( - "something went wrong while executing /inMeeting api", - error.message, - ); - res.status(500).json("Server error"); + console.error(error.message); + return res.status(500).json("Server error"); } }); -// @route POST /meetings/queue -// @desc POST an entry to the waitingStudents or waitingMentors collection depending on role -// @access Public with jwt Authentication router.post("/queue", passport.authenticate("jwt"), async (req, res) => { try { - const { role, username, firstName, lastName } = req.user; //Data retrieved from jwt authentication - - //Check if the user is in a meeting - let message = await inMeeting(role, username); + const { role, username, firstName, lastName } = req.user; + const message = await meetingCtrl.inMeeting(role, username); if (message !== "There are no current meetings with this user.") { return res.status(400).json(message); } - if (role === "mentor") { await waitingMentors.create({ username, @@ -219,463 +155,194 @@ router.post("/queue", passport.authenticate("jwt"), async (req, res) => { requestedGameAt: new Date(), }); } else { - return res - .status(400) - .json("You must be a mentor or student to find a game"); + return res.status(400).json("You must be a mentor or student to find a game"); } - res.status(200).json("Person Added Successfully."); + return res.status(200).json("Person Added Successfully."); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); -// @route POST /meetings/pairUp -// @desc POST a meeting with a student and mentor -// @access Public with jwt Authentication router.post("/pairUp", passport.authenticate("jwt"), async (req, res) => { try { const { role, username, firstName, lastName } = req.user; - let studentInfo = {}; - let mentorInfo = {}; - - //Get a first person in queue from either waitingMentors or waitingStudents collection + let waitingQueue; if (role === "student") { - waitingQueue = await waitingMentors.findOne( - {}, - {}, - { sort: { created_at: 1 } }, - ); + waitingQueue = await waitingMentors.findOne({}, {}, { sort: { created_at: 1 } }); } else if (role === "mentor") { - waitingQueue = await waitingStudents.findOne( - {}, - {}, - { sort: { created_at: 1 } }, - ); + waitingQueue = await waitingStudents.findOne({}, {}, { sort: { created_at: 1 } }); } else { - return res - .status(404) - .json("Must be a student or mentor to pair up for a game."); + return res.status(404).json("Must be a student or mentor to pair up for a game."); } if (!waitingQueue) { return res .status(200) - .json( - "No one is available for matchmaking. Please wait for the next available person", - ); + .json("No one is available for matchmaking. Please wait for the next available person"); } - - const response = await inMeeting(role, username); //Check if user is in a meeting - - //Check if the user waiting for a game is in a meeting already - const secondResponse = await inMeeting( + const meInMeeting = await meetingCtrl.inMeeting(role, username); + const otherInMeeting = await meetingCtrl.inMeeting( role === "student" ? "mentor" : "student", waitingQueue.username, ); - if ( - response === "There are no current meetings with this user." && - secondResponse === "There are no current meetings with this user." && + meInMeeting === "There are no current meetings with this user." && + otherInMeeting === "There are no current meetings with this user." && !isBusy ) { - isBusy = true; //Change state to busy to complete query - - //Set information for ease of access and less redundant meeting creation code - if (role === "student") { - studentInfo.username = username; - studentInfo.firstName = firstName; - studentInfo.lastName = lastName; - mentorInfo.username = waitingQueue.username; - mentorInfo.firstName = waitingQueue.firstName; - mentorInfo.lastName = waitingQueue.lastName; - } else { - mentorInfo.username = username; - mentorInfo.firstName = firstName; - mentorInfo.lastName = lastName; - studentInfo.username = waitingQueue.username; - studentInfo.firstName = waitingQueue.firstName; - studentInfo.lastName = waitingQueue.lastName; - } - const meetingId = uuidv4(); //Generate a random meetingId - - const recordingInfo = await startRecording(meetingId); //Create and start the recording for the mentor and student - - //Error checking to see if a meeting was able to be created - if (recordingInfo === "Could not start recording. Server error.") { - return res.status(400).json(recordingInfo); - } - - const uniquePassword = uuidv4(); //Generated a random password for the meeting - - //Create the meeting with all the required fields - //Save the meeting - await meetings.create({ - meetingId: meetingId, - password: uniquePassword, - studentUsername: studentInfo.username, - studentFirstName: studentInfo.firstName, - studentLastName: studentInfo.lastName, - mentorUsername: mentorInfo.username, - mentorFirstName: mentorInfo.firstName, - mentorLastName: mentorInfo.lastName, - CurrentlyOngoing: true, - resourceId: recordingInfo.resourceId, - sid: recordingInfo.sid, - meetingStartTime: new Date(), - }); - - await deleteUser("student", studentInfo.username); //Remove user from the waitingStudents collection - await deleteUser("mentor", mentorInfo.username); //Remove user from the waitingMentors collection - isBusy = false; //Set state to not busy + isBusy = true; + const studentInfo = + role === "student" + ? { username, firstName, lastName } + : { + username: waitingQueue.username, + firstName: waitingQueue.firstName, + lastName: waitingQueue.lastName, + }; + const mentorInfo = + role === "mentor" + ? { username, firstName, lastName } + : { + username: waitingQueue.username, + firstName: waitingQueue.firstName, + lastName: waitingQueue.lastName, + }; + await meetingCtrl.createMeetingPair(studentInfo, mentorInfo); + await meetingCtrl.deleteUser("student", studentInfo.username); + await meetingCtrl.deleteUser("mentor", mentorInfo.username); + isBusy = false; } return res.status(200).json("Ok"); } catch (error) { + isBusy = false; console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); -// @route PUT /meetings/endMeeting -// @desc PUT a meeting to end and stop the agora recording -// @access Public with jwt Authentication router.put("/endMeeting", passport.authenticate("jwt"), async (req, res) => { try { - const { role, username, firstName, lastName } = req.user; //retrieve jwt info - let filters = { CurrentlyOngoing: true }; - if (role === "student") { - filters.studentUsername = username; - filters.studentFirstName = firstName; - filters.studentLastName = lastName; - } else if (role === "mentor") { - filters.mentorUsername = username; - filters.mentorFirstName = firstName; - filters.mentorLastName = lastName; - } else { - return res - .status(404) - .json("You must be a student or mentor to end the meeting!"); - } - const currMeeting = await meetings.findOne(filters); //Find the meeting the user is currently in - - //Error checking to ensure the user is actually in a meeting - if (!currMeeting) { - return res.status(400).json("You are currently not in a meeting!"); - } - - //Stop the agora recording - const stopResponse = await stopRecording( - currMeeting.meetingId, - currMeeting.resourceId, - currMeeting.sid, - ); - - //Error checking to ensure the recording was stopped - // if (stopResponse === "Could not stop recording. Server error.") { - // return res.status(400).json(stopResponse); - // } - - //Get all mp4 files from recording and push them onto an array - let filesList = []; - if ( - stopResponse && - stopResponse?.fileList && - stopResponse?.fileList?.length > 0 - ) { - await Promise.all( - stopResponse?.fileList?.map((file) => { - if (file?.fileName?.indexOf(".mp4") !== -1) { - filesList.push(file.fileName); - } - }), - ); - } - - //Update the fields to the meeting to change to inactive - currMeeting.CurrentlyOngoing = false; - currMeeting.meetingEndTime = new Date(); - currMeeting.filesList = filesList; - await currMeeting.save(); //Save the changes made - - //Calculate the number of minutes the recording went on for - const timePlayed = Math.floor( - (currMeeting.meetingEndTime.getTime() - - currMeeting.meetingStartTime.getTime()) / - 1000 / - 60, - ); - - //Update the student timePlayed field - let returnMessage = await updateTimePlayed( - currMeeting.studentUsername, - currMeeting.studentFirstName, - currMeeting.studentLastName, - timePlayed, - ); - - //Error checking to make sure it updated the field - if (returnMessage !== "Saved") { - return res.status(404).json(returnMessage); - } - + await meetingCtrl.endMeetingForUser(req.user); return res.sendStatus(200); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json(error.message || "Server error"); } }); -// @route DELETE meetings/dequeue -// @desc DELETE the user from the waitingStudents or waitingMentors collection depending on role -// @access Public with jwt Authentication router.delete("/dequeue", passport.authenticate("jwt"), async (req, res) => { try { const { role, username } = req.user; - let deleted = await deleteUser(role, username); - if (!deleted) { - return res.status(400).json("User was not queued for any meetings"); - } - res.status(200).json("Removed user"); + const deleted = await meetingCtrl.deleteUser(role, username); + if (!deleted) return res.status(400).json("User was not queued for any meetings"); + return res.status(200).json("Removed user"); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); -//Async function to check whether a username is in a current meeting -const inMeeting = async (role, username) => { - let filters = { CurrentlyOngoing: true }; - if (role === "student") { - filters.studentUsername = username; - } else if (role === "mentor") { - filters.mentorUsername = username; - } else { - return "Please be either a student or a mentor."; - } - const foundMeeting = await meetings.find(filters); - if (foundMeeting.length !== 0) { - await deleteUser(role, username); - return foundMeeting; - } - return "There are no current meetings with this user."; -}; - -//Async function to delete a user from the waitingStudents or waitingMentors collection -// changes by riken start -const deleteUser = async (role, username) => { - if (role === "student") { - const user = await waitingStudents.findOne({ username: username }); - if (user != null) { - user.delete(); - } - } else if (role === "mentor") { - const mentor = await waitingMentors.findOne({ username: username }); - if (mentor != null) { - mentor.delete(); - } - } - // await waitingStudents.findOneAndDelete( - // { - // username, - // }, - // function (error, doc) { - // if (error) return false; - // } - // ); - // } else if (role === "mentor") { - // await waitingMentors.findOneAndDelete( - // { - // username, - // }, - // function (error, doc) { - // if (error) return false; - // } - // ); - // } - // changes by riken end - - return true; -}; -//Async function to create and start an agora recording session - -//Async function to update the time played for a user -const updateTimePlayed = async (username, firstName, lastName, timePlayed) => { - //Find the user in question - const user = await users.findOne({ - username: username, - firstName: firstName, - lastName: lastName, - }); - if (!user) { - return "Could not find user"; - } - - user.timePlayed += timePlayed; //Update the time played - await user.save(); //Save the new time played - return "Saved"; -}; -//Async function to get moves from the database -const getMoves = async (meetingId) => { - const moves = await meetings.findOne({ - meetingId: meetingId, - CurrentlyOngoing: true, - }); - return moves; -}; - -//Async function to store moves in the database -const updateMoves = async (meetingId, oldMovesArr) => { - const newdata = await meetings.findOneAndUpdate( - { meetingId: meetingId }, - { moves: oldMovesArr }, - ); - return newdata; -}; - -//Async function to update undo permission in the database -const updateUndoPermission = async (meetingId, value) => { - if (value == "student") { - const newdata = await undoPermission.findOne({ meetingId: meetingId }); - if (!newdata) { - await undoPermission.create({ - meetingId: meetingId, - permission: true, - }); - } else { - await undoPermission.findOneAndUpdate( - { meetingId: meetingId }, - { permission: true }, - ); - } - } else { - await undoPermission.findOneAndUpdate( - { meetingId: meetingId }, - { permission: false }, - ); - } - - // return newdata; -}; - router.post("/boardState", passport.authenticate("jwt"), async (req, res) => { try { const { meetingId, fen, pos, image, role } = req.query; - if (pos == "") { - // do nothing + if (!pos) return res.status(200).send(); + const meeting = await meetingCtrl.getMoves(meetingId); + let moveArray = meeting?.moves || []; + let oldMovesArr = []; + let idx = moveArray.length; + if (idx > 0) { + oldMovesArr = moveArray[idx - 1]; + idx = idx - 1; + } + if (oldMovesArr.length === 0 || oldMovesArr[oldMovesArr.length - 1]?.fen !== fen) { + fen && oldMovesArr.push({ fen, pos, image }); + moveArray[idx] = oldMovesArr; + const updated = await meetingCtrl.updateMoves(meetingId, moveArray); + await meetingCtrl.updateUndoPermission(meetingId, role); + return res.status(200).send(updated); } else { - let meeting = await getMoves(meetingId); - let moveArray = meeting.moves; - let oldMovesArr = []; - let moveArrayLength = moveArray.length; - if (moveArray.length > 0) { - oldMovesArr = moveArray[moveArrayLength - 1]; - moveArrayLength = moveArray.length - 1; - } - if ( - oldMovesArr.length === 0 || - oldMovesArr[oldMovesArr.length - 1]?.fen !== fen - ) { - fen && oldMovesArr.push({ fen, pos, image }); - moveArray[moveArrayLength] = oldMovesArr; - let updatedMove = await updateMoves(meetingId, moveArray); - await updateUndoPermission(meetingId, role); - res.status(200).send(updatedMove); - } else { - res.status(202).send(oldMovesArr); - } + return res.status(202).send(oldMovesArr); } } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); router.get("/getBoardState", passport.authenticate("jwt"), async (req, res) => { try { const { meetingId } = req.query; - const getBoardStates = await getMoves(meetingId); - res.status(200).send(getBoardStates); + const doc = await meetingCtrl.getMoves(meetingId); + return res.status(200).send(doc); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); -router.post( - "/newBoardState", - passport.authenticate("jwt"), - async (req, res) => { - try { - const { meetingId } = req.query; - let meeting = await getMoves(meetingId); - let moveArray = meeting.moves; - let oldMovesArr = []; - let moveArrayLength = moveArray.length; - moveArray[moveArrayLength] = oldMovesArr; - let updatedMove = await updateMoves(meetingId, moveArray); - res.status(200).send(updatedMove); - } catch (error) { - console.error(error.message); - res.status(500).json("Server error"); - } - }, -); +router.post("/newBoardState", passport.authenticate("jwt"), async (req, res) => { + try { + const { meetingId } = req.query; + const meeting = await meetingCtrl.getMoves(meetingId); + const moveArray = meeting?.moves || []; + const idx = moveArray.length; + moveArray[idx] = []; + const updated = await meetingCtrl.updateMoves(meetingId, moveArray); + return res.status(200).send(updated); + } catch (error) { + console.error(error.message); + return res.status(500).json("Server error"); + } +}); router.post("/storeMoves", async (req, res) => { try { - const { gameId, fen, pos, image } = req.query; + const { gameId, fen, pos, image, userId } = req.query; if (gameId) { - const getbyId = await getMovesByGameId(gameId); - let moveArray = getbyId.moves; + const doc = await meetingCtrl.getMovesByGameId(gameId); + let moveArray = doc?.moves || []; let oldMovesArr = []; - let moveArrayLength = moveArray.length; - if (moveArray.length > 0) { - oldMovesArr = moveArray[moveArrayLength - 1]; - moveArrayLength = moveArray.length - 1; + let idx = moveArray.length; + if (idx > 0) { + oldMovesArr = moveArray[idx - 1]; + idx = idx - 1; } - if ( - oldMovesArr.length === 0 || - oldMovesArr[oldMovesArr.length - 1]?.fen !== fen - ) { + if (oldMovesArr.length === 0 || oldMovesArr[oldMovesArr.length - 1]?.fen !== fen) { fen && oldMovesArr.push({ fen, pos, image }); - moveArray[moveArrayLength] = oldMovesArr; - let updatedMove = await updateMoveByGameId(gameId, moveArray); - res.status(200).send(updatedMove); + moveArray[idx] = oldMovesArr; + const updated = await meetingCtrl.updateMoveByGameId(gameId, moveArray); + return res.status(200).send(updated); } else { - res.status(202).send(oldMovesArr); + return res.status(202).send(oldMovesArr); } } else { const newGameId = uuidv4(); const ipAddress = requestIp.getClientIp(req); - const { userId } = req?.query || null; - const moves = []; - // await movesList.find().populate("userId"); - let response = await movesList.create({ + const response = await movesList.create({ gameId: newGameId, - userId: userId, - moves: moves, - ipAddress: ipAddress, + userId: userId || null, + moves: [], + ipAddress, }); - res.status(200).send(response); + return res.status(200).send(response); } } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); router.post("/newGameStoreMoves", async (req, res) => { try { const { gameId } = req.query; - let meeting = await getMovesByGameId(gameId); - let moveArray = meeting.moves; - let oldMovesArr = []; - let moveArrayLength = moveArray.length; - moveArray[moveArrayLength] = oldMovesArr; - let updatedMove = await updateMoveByGameId(gameId, moveArray); - res.status(200).send(updatedMove); + const doc = await meetingCtrl.getMovesByGameId(gameId); + const moveArray = doc?.moves || []; + moveArray[moveArray.length] = []; + const updated = await meetingCtrl.updateMoveByGameId(gameId, moveArray); + return res.status(200).send(updated); } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); @@ -683,88 +350,59 @@ router.get("/getStoreMoves", async (req, res) => { try { const { gameId, meetingId } = req.query; if (meetingId) { - const getBoardStates = await getMoves(meetingId); - res.status(200).send(getBoardStates); + const doc = await meetingCtrl.getMoves(meetingId); + return res.status(200).send(doc); } else { - const getBoardStates = await getMovesByGameId(gameId); - res.status(200).send(getBoardStates); + const doc = await meetingCtrl.getMovesByGameId(gameId); + return res.status(200).send(doc); } } catch (error) { console.error(error.message); - res.status(500).json("Server error"); + return res.status(500).json("Server error"); } }); router.post("/checkUndoPermission", async (req, res) => { try { const { meetingId } = req.query; - const checkPermission = await undoPermission.findOne({ - meetingId: meetingId, + const checkPermission = await require("../models/undoPermission").findOne({ + meetingId, }); - res.status(200).send(checkPermission); + return res.status(200).send(checkPermission); } catch (error) { console.error(error.message); - res.status(500).json("server error"); + return res.status(500).json("server error"); } }); router.post("/undoMeetingMoves", async (req, res) => { try { const { meetingId } = req.query; - const getBoardState = await getMoves(meetingId); - const movesData = getBoardState.moves; - const newData = movesData[movesData.length - 1]; - const finalData = newData.splice(-2, 2); - const deletedData = await deleteMovesByMeetingId(meetingId, movesData); - res.status(200).send(deletedData); + const doc = await meetingCtrl.getMoves(meetingId); + const movesData = doc?.moves || []; + const last = movesData[movesData.length - 1] || []; + last.splice(-2, 2); + const updated = await meetingCtrl.deleteMovesByMeetingId(meetingId, movesData); + return res.status(200).send(updated); } catch (error) { console.error(error.message); - res.status(500).json("server error"); + return res.status(500).json("server error"); } }); -const deleteMovesByMeetingId = async (meetingId, deletedData) => { - const deletedMove = await meetings.findOneAndUpdate( - { meetingId: meetingId }, - { moves: deletedData }, - ); - return deletedMove; -}; - router.post("/undoMoves", async (req, res) => { try { const { gameId } = req.query; - const getBoardState = await getMovesByGameId(gameId); - const movesData = getBoardState.moves; - const newData = movesData[movesData.length - 1]; - const finalData = newData.splice(-2, 2); - const deletedData = await deleteMovesByGameId(gameId, movesData); - res.status(200).send(deletedData); + const doc = await meetingCtrl.getMovesByGameId(gameId); + const movesData = doc?.moves || []; + const last = movesData[movesData.length - 1] || []; + last.splice(-2, 2); + const updated = await meetingCtrl.deleteMovesByGameId(gameId, movesData); + return res.status(200).send(updated); } catch (error) { console.error(error.message); - res.status(500).json("server error"); + return res.status(500).json("server error"); } }); -const deleteMovesByGameId = async (gameId, deletedData) => { - const deletedMove = await movesList.findOneAndUpdate( - { gameId: gameId }, - { moves: deletedData }, - ); - return deletedMove; -}; -const getMovesByGameId = async (gameId) => { - const getMoves = await movesList.findOne({ - gameId: gameId, - }); - return getMoves; -}; -const updateMoveByGameId = async (gameId, oldMovesArr) => { - const getMoves = await movesList.findOneAndUpdate( - { gameId: gameId }, - { moves: oldMovesArr }, - ); - return getMoves; -}; - module.exports = router;