Ajout de roomID

This commit is contained in:
NouhailaAater 2025-02-09 16:10:25 -05:00
parent 0febb5a394
commit 369cc218f7
11 changed files with 509 additions and 547 deletions

59
.gitignore vendored
View file

@ -129,3 +129,62 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
db-backup/ db-backup/
# Node.js / React (évite de versionner les dépendances)
node_modules/
package-lock.json
yarn.lock
npm-debug.log
.DS_Store
# Dossier de sortie du build (React/Vite/Webpack)
dist/
build/
# Fichiers environnement (ne pas exposer tes variables sensibles)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Visual Studio & VS Code (évite les fichiers temporaires et caches)
.vscode/
.vs/
*.code-workspace
*.suo
*.user
*.cache
*.log
# Fichiers spécifiques de Visual Studio
FileContentIndex/
*.vsidx
.vsconfig
slnx.sqlite
VSWorkspaceState.json
ProjectSettings.json
DocumentLayout*.json
# Logs et fichiers temporaires
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Ignorer les fichiers de tests/jest
coverage/
*.test.js
*.test.ts
*.test.tsx
jest.config.js
# Ignorer les fichiers de dépendances frontend (si utilisés)
.bower_components/
jspm_packages/
# Cache TypeScript et fichiers de sortie (évite les fichiers générés)
*.tsbuildinfo
*.tscache
*.d.ts

300
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,11 +18,11 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^6.4.1", "@mui/icons-material": "^6.4.3",
"@mui/lab": "^5.0.0-alpha.153", "@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0", "@mui/material": "^6.4.3",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"axios": "^1.6.7", "axios": "^1.7.9",
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"gift-pegjs": "^2.0.0-beta.1", "gift-pegjs": "^2.0.0-beta.1",
@ -33,9 +33,9 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.29.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.8.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-plugin-checker": "^0.8.0" "vite-plugin-checker": "^0.8.0"

View file

@ -1,4 +1,3 @@
// QuizType.tsx
export interface QuizType { export interface QuizType {
_id: string; _id: string;
folderId: string; folderId: string;
@ -6,6 +5,7 @@ export interface QuizType {
userId: string; userId: string;
title: string; title: string;
content: string[]; content: string[];
roomId: string;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
} }

View file

@ -10,9 +10,10 @@ interface Props {
students: StudentType[]; students: StudentType[];
launchQuiz: () => void; launchQuiz: () => void;
setQuizMode: (mode: 'student' | 'teacher') => void; setQuizMode: (mode: 'student' | 'teacher') => void;
isSocketConnected?: boolean;
} }
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => { const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode, isSocketConnected = true }) => {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
return ( return (
@ -25,7 +26,7 @@ const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode })
fullWidth fullWidth
sx={{ fontWeight: 600, fontSize: 20 }} sx={{ fontWeight: 600, fontSize: 20 }}
> >
Lancer {isSocketConnected ? 'Lancer' : 'En attente de connexion...'}
</Button> </Button>
</div> </div>

View file

@ -1,11 +1,9 @@
// ManageRoom.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs'; import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards"; import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
import LiveResultsComponent from 'src/components/LiveResults/LiveResults'; import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group'; import GroupIcon from '@mui/icons-material/Group';
@ -18,14 +16,12 @@ import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material'; import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage'; import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay'; import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType'; import { QuestionType } from 'src/Types/QuestionType';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [roomName, setRoomName] = useState<string>('');
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [students, setStudents] = useState<StudentType[]>([]); const [students, setStudents] = useState<StudentType[]>([]);
const quizId = useParams<{ id: string }>(); const quizId = useParams<{ id: string }>();
@ -35,30 +31,39 @@ const ManageRoom: React.FC = () => {
const [connectingError, setConnectingError] = useState<string>(''); const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined); const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false); const [quizStarted, setQuizStarted] = useState(false);
const [isSocketConnected, setIsSocketConnected] = useState(false);
useEffect(() => { useEffect(() => {
if (quizId.id) { if (quizId.id) {
const fetchquiz = async () => { const fetchquiz = async () => {
try {
const quiz = await ApiService.getQuiz(quizId.id as string);
const quiz = await ApiService.getQuiz(quizId.id as string); if (!quiz) {
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
console.error('Quiz not found for id:', quizId.id);
navigate('/teacher/dashboard');
return;
}
if (!quiz) { if ((quiz as QuizType).roomId) {
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`) setQuiz(quiz as QuizType);
console.error('Quiz not found for id:', quizId.id);
if (!socket) {
console.log('Initializing WebSocket connection...');
initializeWebSocketConnection((quiz as QuizType).roomId);
}
} else {
console.error('Quiz data is not valid, roomId is missing.');
navigate('/teacher/dashboard');
}
} catch (error) {
console.error('Failed to load quiz:', error);
setConnectingError('Erreur de chargement du quiz');
navigate('/teacher/dashboard'); navigate('/teacher/dashboard');
return;
} }
setQuiz(quiz as QuizType);
if (!socket) {
console.log(`no socket in ManageRoom, creating one.`);
createWebSocketRoom();
}
// return () => {
// webSocketService.disconnect();
// };
}; };
fetchquiz(); fetchquiz();
@ -72,74 +77,83 @@ const ManageRoom: React.FC = () => {
}, [quizId]); }, [quizId]);
const disconnectWebSocket = () => { const disconnectWebSocket = () => {
if (socket) { if (socket && quiz) {
webSocketService.endQuiz(roomName); webSocketService.endQuiz(quiz.roomId);
webSocketService.disconnect(); webSocketService.disconnect();
setSocket(null); setSocket(null);
setQuizQuestions(undefined); setQuizQuestions(undefined);
setCurrentQuestion(undefined); setCurrentQuestion(undefined);
setStudents(new Array<StudentType>()); setStudents(new Array<StudentType>());
setRoomName('');
} }
}; };
const createWebSocketRoom = () => {
console.log('Creating WebSocket room...'); const initializeWebSocketConnection = (roomId: string) => {
setConnectingError(''); console.log('Initializing WebSocket for room:', roomId);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
console.log('Socket connection status:', webSocketService.isConnected());
socket.on('connect', () => { socket.on('connect', () => {
webSocketService.createRoom(); console.log('WebSocket connected, joining room:', roomId);
setIsSocketConnected(true);
webSocketService.joinRoom(roomId, 'Teacher');
}); });
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer'); setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
console.error('ManageRoom: WebSocket connection error:', error); console.error('Connection error:', error);
});
socket.on('create-success', (roomName: string) => {
setRoomName(roomName);
});
socket.on('create-failure', () => {
console.log('Error creating room.');
}); });
socket.on('user-joined', (student: StudentType) => { socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}`); console.log(`Student joined: name = ${student.name}, id = ${student.id}`);
setStudents((prevStudents) => [...prevStudents, student]); setStudents((prevStudents) => [...prevStudents, student]);
if (quizMode === 'teacher') { if (quiz && socket.connected) {
webSocketService.nextQuestion(roomName, currentQuestion); if (quizMode === 'teacher') {
} else if (quizMode === 'student') { webSocketService.nextQuestion(quiz.roomId, currentQuestion);
webSocketService.launchStudentModeQuiz(roomName, quizQuestions); } else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(quiz.roomId, quizQuestions);
}
} else {
console.error("Quiz data is not available or Socket is not connected.");
} }
}); });
socket.on('join-failure', (message) => { socket.on('join-failure', (message) => {
setConnectingError(message); setConnectingError(message);
setSocket(null); setSocket(null);
setIsSocketConnected(false);
}); });
socket.on('user-disconnected', (userId: string) => { socket.on('user-disconnected', (userId: string) => {
console.log(`Student left: id = ${userId}`); console.log(`Student left: id = ${userId}`);
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId)); setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
}); });
socket.on('disconnect', () => {
console.log('WebSocket disconnected');
setIsSocketConnected(false);
});
setSocket(socket); setSocket(socket);
}; };
useEffect(() => { useEffect(() => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket && quiz?.roomId) {
console.log(`Listening for user-joined in room ${roomName}`); console.log(`Listening for user-joined in room ${quiz.roomId}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => { socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(quiz.roomId, currentQuestion);
} else if (quizMode === 'student') { } else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(roomName, quizQuestions); webSocketService.launchStudentModeQuiz(quiz.roomId, quizQuestions);
} }
}); });
} }
if (socket) { if (socket) {
// handle the case where user submits an answer // handle the case where user submits an answer
console.log(`Listening for submit-answer-room in room ${roomName}`); console.log(`Listening for submit-answer-room in room ${quiz?.roomId}`);
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => { socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
const { answer, idQuestion, idUser, username } = answerData; const { answer, idQuestion, idUser, username } = answerData;
console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`); console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
@ -189,70 +203,6 @@ const ManageRoom: React.FC = () => {
}, [socket, currentQuestion, quizQuestions]); }, [socket, currentQuestion, quizQuestions]);
// useEffect(() => {
// if (socket) {
// const submitAnswerHandler = (answerData: answerSubmissionType) => {
// const { answer, idQuestion, username } = answerData;
// console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
// // print the list of current student names
// console.log('Current students:');
// students.forEach((student) => {
// console.log(student.name);
// });
// // Update the students state using the functional form of setStudents
// setStudents((prevStudents) => {
// let foundStudent = false;
// const updatedStudents = prevStudents.map((student) => {
// if (student.id === username) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!), idQuestion };
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
// }
// );
// return { ...student, answers: updatedAnswers };
// }
// return student;
// });
// if (!foundStudent) {
// console.log(`Student ${username} not found in the list of students in LiveResults`);
// }
// return updatedStudents;
// });
// // make a copy of the students array so we can update it
// // const updatedStudents = [...students];
// // const student = updatedStudents.find((student) => student.id === idUser);
// // if (!student) {
// // // this is a bad thing if an answer was submitted but the student isn't in the list
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// // return;
// // }
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
// // student.answers.push(newAnswer);
// // // print list of answers
// // console.log('Answers:');
// // student.answers.forEach((answer) => {
// // console.log(answer.answer);
// // });
// // setStudents(updatedStudents); // update the state
// };
// socket.on('submit-answer', submitAnswerHandler);
// return () => {
// socket.off('submit-answer');
// };
// }
// }, [socket]);
const nextQuestion = () => { const nextQuestion = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return; if (!quizQuestions || !currentQuestion || !quiz?.content) return;
@ -261,7 +211,7 @@ const ManageRoom: React.FC = () => {
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
setCurrentQuestion(quizQuestions[nextQuestionIndex]); setCurrentQuestion(quizQuestions[nextQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[nextQuestionIndex]); webSocketService.nextQuestion(quiz.roomId, quizQuestions[nextQuestionIndex]);
}; };
const previousQuestion = () => { const previousQuestion = () => {
@ -271,7 +221,7 @@ const ManageRoom: React.FC = () => {
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]); setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]); webSocketService.nextQuestion(quiz.roomId, quizQuestions[prevQuestionIndex]);
}; };
const initializeQuizQuestion = () => { const initializeQuizQuestion = () => {
@ -291,37 +241,37 @@ const ManageRoom: React.FC = () => {
const launchTeacherMode = () => { const launchTeacherMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchTeacherMode - quizQuestions:', quizQuestions); if (quizQuestions && quiz?.roomId) {
setCurrentQuestion(quizQuestions[0]);
if (!quizQuestions) { webSocketService.nextQuestion(quiz.roomId, quizQuestions[0]);
console.log('Error launching quiz (launchTeacherMode). No questions found.');
return;
} }
setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion(roomName, quizQuestions[0]);
}; };
const launchStudentMode = () => { const launchStudentMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchStudentMode - quizQuestions:', quizQuestions); if (quizQuestions && quiz?.roomId) {
setQuizQuestions(quizQuestions);
if (!quizQuestions) { webSocketService.launchStudentModeQuiz(quiz.roomId, quizQuestions);
console.log('Error launching quiz (launchStudentMode). No questions found.');
return;
} }
setQuizQuestions(quizQuestions);
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
}; };
const launchQuiz = () => { const launchQuiz = async () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { if (!isSocketConnected) {
// TODO: This error happens when token expires! Need to handle it properly console.log("Waiting for socket connection...");
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`); window.alert("En attente de la connexion au serveur... Veuillez réessayer dans quelques secondes.");
setQuizStarted(true);
return; return;
} }
if (!quiz?.roomId) {
console.error("Room ID is missing.");
return;
}
if (!quiz?.content || quiz.content.length === 0) {
console.error("Quiz content is missing or empty.");
return;
}
switch (quizMode) { switch (quizMode) {
case 'student': case 'student':
setQuizStarted(true); setQuizStarted(true);
@ -329,7 +279,9 @@ const ManageRoom: React.FC = () => {
case 'teacher': case 'teacher':
setQuizStarted(true); setQuizStarted(true);
return launchTeacherMode(); return launchTeacherMode();
default:
console.error("Invalid quiz mode.");
return;
} }
}; };
@ -338,7 +290,7 @@ const ManageRoom: React.FC = () => {
setCurrentQuestion(quizQuestions[questionIndex]); setCurrentQuestion(quizQuestions[questionIndex]);
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]); webSocketService.nextQuestion(quiz.roomId, quizQuestions[questionIndex]);
} }
} }
}; };
@ -403,11 +355,11 @@ const ManageRoom: React.FC = () => {
} }
if (!roomName) { if (!quiz || !quiz?.roomId) {
return ( return (
<div className="center"> <div className="center">
{!connectingError ? ( {!connectingError ? (
<LoadingCircle text="Veuillez attendre la connexion au serveur..." /> <LoadingCircle text="Chargement du quiz..." />
) : ( ) : (
<div className="center-v-align"> <div className="center-v-align">
<Error sx={{ padding: 0 }} /> <Error sx={{ padding: 0 }} />
@ -415,7 +367,13 @@ const ManageRoom: React.FC = () => {
<Button <Button
variant="contained" variant="contained"
startIcon={<Refresh />} startIcon={<Refresh />}
onClick={createWebSocketRoom} onClick={() => {
if (quiz?.roomId) {
initializeWebSocketConnection(quiz.roomId);
} else {
console.error("Room ID is not available");
}
}}
> >
Reconnecter Reconnecter
</Button> </Button>
@ -439,7 +397,7 @@ const ManageRoom: React.FC = () => {
<div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}> <div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}> <div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
<div className='title'>Salle: {roomName}</div> <div className='title'>Salle: {quiz?.roomId}</div>
</div> </div>
{quizStarted && ( {quizStarted && (
<div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}> <div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}>
@ -526,6 +484,7 @@ const ManageRoom: React.FC = () => {
students={students} students={students}
launchQuiz={launchQuiz} launchQuiz={launchQuiz}
setQuizMode={setQuizMode} setQuizMode={setQuizMode}
isSocketConnected={isSocketConnected}
/> />
)} )}
@ -535,4 +494,4 @@ const ManageRoom: React.FC = () => {
); );
}; };
export default ManageRoom; export default ManageRoom;

View file

@ -13,169 +13,185 @@ class QuizController {
create = async (req, res, next) => { create = async (req, res, next) => {
try { try {
const { title, content, folderId } = req.body; const { title, content, folderId } = req.body;
if (!title || !content || !folderId) { if (!title || !content || !folderId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
// Is this folder mine
const owner = await this.folders.getOwner(folderId); const owner = await this.folders.getOwner(folderId);
if (owner != req.user.userId) { if (owner != req.user.userId) {
throw new AppError(FOLDER_NOT_FOUND); throw new AppError(FOLDER_NOT_FOUND);
} }
const result = await this.quizzes.create(title, content, folderId, req.user.userId); const roomId = this.generateRoomId();
const result = await this.quizzes.create(title, content, folderId, req.user.userId, roomId);
if (!result) { if (!result) {
throw new AppError(QUIZ_ALREADY_EXISTS); throw new AppError(QUIZ_ALREADY_EXISTS);
} }
return res.status(200).json({ return res.status(200).json({
message: 'Quiz créé avec succès.' message: 'Quiz créé avec succès.',
roomId: roomId,
});
} catch (error) {
return next(error);
}
};
getRoomID = async (req, res, next) => {
try {
const { quizId } = req.params;
if (!quizId) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const roomId = await this.quizzes.getRoomID(quizId);
if (!roomId) {
throw new AppError(QUIZ_NOT_FOUND);
}
return res.status(200).json({
roomId: roomId
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
get = async (req, res, next) => { get = async (req, res, next) => {
try { try {
const { quizId } = req.params; const { quizId } = req.params;
if (!quizId) { if (!quizId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
const content = await this.quizzes.getContent(quizId); const content = await this.quizzes.getContent(quizId);
if (!content) { if (!content) {
throw new AppError(GETTING_QUIZ_ERROR); throw new AppError(GETTING_QUIZ_ERROR);
} }
// Is this quiz mine
if (content.userId != req.user.userId) { if (content.userId != req.user.userId) {
throw new AppError(QUIZ_NOT_FOUND); throw new AppError(QUIZ_NOT_FOUND);
} }
return res.status(200).json({ return res.status(200).json({
data: content data: content
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
delete = async (req, res, next) => { delete = async (req, res, next) => {
try { try {
const { quizId } = req.params; const { quizId } = req.params;
if (!quizId) { if (!quizId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
// Is this quiz mine
const owner = await this.quizzes.getOwner(quizId); const owner = await this.quizzes.getOwner(quizId);
if (owner != req.user.userId) { if (owner != req.user.userId) {
throw new AppError(QUIZ_NOT_FOUND); throw new AppError(QUIZ_NOT_FOUND);
} }
const result = await this.quizzes.delete(quizId); const result = await this.quizzes.delete(quizId);
if (!result) { if (!result) {
throw new AppError(DELETE_QUIZ_ERROR); throw new AppError(DELETE_QUIZ_ERROR);
} }
return res.status(200).json({ return res.status(200).json({
message: 'Quiz supprimé avec succès.' message: 'Quiz supprimé avec succès.'
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
update = async (req, res, next) => { update = async (req, res, next) => {
try { try {
const { quizId, newTitle, newContent } = req.body; const { quizId, newTitle, newContent } = req.body;
if (!newTitle || !newContent || !quizId) { if (!newTitle || !newContent || !quizId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
// Is this quiz mine
const owner = await this.quizzes.getOwner(quizId); const owner = await this.quizzes.getOwner(quizId);
if (owner != req.user.userId) { if (owner != req.user.userId) {
throw new AppError(QUIZ_NOT_FOUND); throw new AppError(QUIZ_NOT_FOUND);
} }
const result = await this.quizzes.update(quizId, newTitle, newContent); const result = await this.quizzes.update(quizId, newTitle, newContent);
if (!result) { if (!result) {
throw new AppError(UPDATE_QUIZ_ERROR); throw new AppError(UPDATE_QUIZ_ERROR);
} }
return res.status(200).json({ return res.status(200).json({
message: 'Quiz mis à jours avec succès.' message: 'Quiz mis à jours avec succès.'
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
move = async (req, res, next) => { move = async (req, res, next) => {
try { try {
const { quizId, newFolderId } = req.body; const { quizId, newFolderId } = req.body;
if (!quizId || !newFolderId) { if (!quizId || !newFolderId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
// Is this quiz mine
const quizOwner = await this.quizzes.getOwner(quizId); const quizOwner = await this.quizzes.getOwner(quizId);
if (quizOwner != req.user.userId) { if (quizOwner != req.user.userId) {
throw new AppError(QUIZ_NOT_FOUND); throw new AppError(QUIZ_NOT_FOUND);
} }
// Is this folder mine
const folderOwner = await this.folders.getOwner(newFolderId); const folderOwner = await this.folders.getOwner(newFolderId);
if (folderOwner != req.user.userId) { if (folderOwner != req.user.userId) {
throw new AppError(FOLDER_NOT_FOUND); throw new AppError(FOLDER_NOT_FOUND);
} }
const result = await this.quizzes.move(quizId, newFolderId); const result = await this.quizzes.move(quizId, newFolderId);
if (!result) { if (!result) {
throw new AppError(MOVING_QUIZ_ERROR); throw new AppError(MOVING_QUIZ_ERROR);
} }
return res.status(200).json({ return res.status(200).json({
message: 'Utilisateur déplacé avec succès.' message: 'Utilisateur déplacé avec succès.'
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
copy = async (req, _res, _next) => { copy = async (req, _res, _next) => {
const { quizId, newTitle, folderId } = req.body; const { quizId, newTitle, folderId } = req.body;
if (!quizId || !newTitle || !folderId) { if (!quizId || !newTitle || !folderId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
throw new AppError(NOT_IMPLEMENTED); throw new AppError(NOT_IMPLEMENTED);
// const { quizId } = req.params; // const { quizId } = req.params;
// const { newUserId } = req.body; // const { newUserId } = req.body;
// try { // try {
// //Trouver le quiz a dupliquer // //Trouver le quiz a dupliquer
// const conn = db.getConnection(); // const conn = db.getConnection();
@ -189,7 +205,7 @@ class QuizController {
// //Ajout du duplicata // //Ajout du duplicata
// await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: ObjectId.createFromHexString(newUserId) }); // await conn.collection('quiz').insertOne({ ...quiztoduplicate, userId: ObjectId.createFromHexString(newUserId) });
// res.json(Response.ok("Dossier dupliqué avec succès pour un autre utilisateur")); // res.json(Response.ok("Dossier dupliqué avec succès pour un autre utilisateur"));
// } catch (error) { // } catch (error) {
// if (error.message.startsWith("Quiz non trouvé")) { // if (error.message.startsWith("Quiz non trouvé")) {
// return res.status(404).json(Response.badRequest(error.message)); // return res.status(404).json(Response.badRequest(error.message));
@ -197,18 +213,18 @@ class QuizController {
// res.status(500).json(Response.serverError(error.message)); // res.status(500).json(Response.serverError(error.message));
// } // }
}; };
deleteQuizzesByFolderId = async (req, res, next) => { deleteQuizzesByFolderId = async (req, res, next) => {
try { try {
const { folderId } = req.body; const { folderId } = req.body;
if (!folderId) { if (!folderId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
// Call the method from the Quiz model to delete quizzes by folder ID // Call the method from the Quiz model to delete quizzes by folder ID
await this.quizzes.deleteQuizzesByFolderId(folderId); await this.quizzes.deleteQuizzesByFolderId(folderId);
return res.status(200).json({ return res.status(200).json({
message: 'Quizzes deleted successfully.' message: 'Quizzes deleted successfully.'
}); });
@ -216,10 +232,10 @@ class QuizController {
return next(error); return next(error);
} }
}; };
duplicate = async (req, res, next) => { duplicate = async (req, res, next) => {
const { quizId } = req.body; const { quizId } = req.body;
try { try {
const newQuizId = await this.quizzes.duplicate(quizId, req.user.userId); const newQuizId = await this.quizzes.duplicate(quizId, req.user.userId);
res.status(200).json({ success: true, newQuizId }); res.status(200).json({ success: true, newQuizId });
@ -227,7 +243,7 @@ class QuizController {
return next(error); return next(error);
} }
}; };
quizExists = async (title, userId) => { quizExists = async (title, userId) => {
try { try {
const existingFile = await this.quizzes.quizExists(title, userId); const existingFile = await this.quizzes.quizExists(title, userId);
@ -236,82 +252,90 @@ class QuizController {
throw new AppError(GETTING_QUIZ_ERROR); throw new AppError(GETTING_QUIZ_ERROR);
} }
}; };
share = async (req, res, next) => { share = async (req, res, next) => {
try { try {
const { quizId, email } = req.body; const { quizId, email } = req.body;
if (!quizId || !email) { if (!quizId || !email) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
const link = `${process.env.FRONTEND_URL}/teacher/Share/${quizId}`; const link = `${process.env.FRONTEND_URL}/teacher/Share/${quizId}`;
emailer.quizShare(email, link); emailer.quizShare(email, link);
return res.status(200).json({ return res.status(200).json({
message: 'Quiz partagé avec succès.' message: 'Quiz partagé avec succès.'
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
getShare = async (req, res, next) => { getShare = async (req, res, next) => {
try { try {
const { quizId } = req.params; const { quizId } = req.params;
if (!quizId) { if (!quizId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
const content = await this.quizzes.getContent(quizId); const content = await this.quizzes.getContent(quizId);
if (!content) { if (!content) {
throw new AppError(GETTING_QUIZ_ERROR); throw new AppError(GETTING_QUIZ_ERROR);
} }
return res.status(200).json({ return res.status(200).json({
data: content.title data: content.title
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
receiveShare = async (req, res, next) => { receiveShare = async (req, res, next) => {
try { try {
const { quizId, folderId } = req.body; const { quizId, folderId } = req.body;
if (!quizId || !folderId) { if (!quizId || !folderId) {
throw new AppError(MISSING_REQUIRED_PARAMETER); throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
const folderOwner = await this.folders.getOwner(folderId); const folderOwner = await this.folders.getOwner(folderId);
if (folderOwner != req.user.userId) { if (folderOwner != req.user.userId) {
throw new AppError(FOLDER_NOT_FOUND); throw new AppError(FOLDER_NOT_FOUND);
} }
const content = await this.quizzes.getContent(quizId); const content = await this.quizzes.getContent(quizId);
if (!content) { if (!content) {
throw new AppError(GETTING_QUIZ_ERROR); throw new AppError(GETTING_QUIZ_ERROR);
} }
const result = await this.quizzes.create(content.title, content.content, folderId, req.user.userId); const result = await this.quizzes.create(content.title, content.content, folderId, req.user.userId);
if (!result) { if (!result) {
throw new AppError(QUIZ_ALREADY_EXISTS); throw new AppError(QUIZ_ALREADY_EXISTS);
} }
return res.status(200).json({ return res.status(200).json({
message: 'Quiz partagé reçu.' message: 'Quiz partagé reçu.'
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
}; };
generateRoomId(length = 6) {
const characters = "0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
} }
module.exports = QuizController; module.exports = QuizController;

View file

@ -1,9 +1,10 @@
class AppError extends Error { class AppError extends Error {
constructor (errorCode) { constructor(errorCode = { message: "Something went wrong", code: 500 }) {
super(errorCode.message) super(errorCode.message);
this.statusCode = errorCode.code; this.statusCode = errorCode.code || 500;
this.isOperational = true; // Optional: to distinguish operational errors from programming errors this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
} }
} }
module.exports = AppError; module.exports = AppError;

View file

@ -2,20 +2,21 @@ const { ObjectId } = require('mongodb');
const { generateUniqueTitle } = require('./utils'); const { generateUniqueTitle } = require('./utils');
class Quiz { class Quiz {
constructor(db) { constructor(db) {
// console.log("Quiz constructor: db", db)
this.db = db; this.db = db;
} }
async create(title, content, folderId, userId) {
// console.log(`quizzes: create title: ${title}, folderId: ${folderId}, userId: ${userId}`); async create(title, content, folderId, userId, roomId) {
await this.db.connect() await this.db.connect()
const conn = this.db.getConnection(); const conn = this.db.getConnection();
const quizCollection = conn.collection('files'); const quizCollection = conn.collection('files');
const existingQuiz = await quizCollection.findOne({ title: title, folderId: folderId, userId: userId }) const existingQuiz = await quizCollection.findOne({
title: title,
folderId: folderId,
userId: userId
});
if (existingQuiz) { if (existingQuiz) {
throw new Error(`Quiz already exists with title: ${title}, folderId: ${folderId}, userId: ${userId}`); throw new Error(`Quiz already exists with title: ${title}, folderId: ${folderId}, userId: ${userId}`);
@ -26,13 +27,12 @@ class Quiz {
userId: userId, userId: userId,
title: title, title: title,
content: content, content: content,
roomId: roomId,
created_at: new Date(), created_at: new Date(),
updated_at: new Date() updated_at: new Date()
} }
const result = await quizCollection.insertOne(newQuiz); const result = await quizCollection.insertOne(newQuiz);
// console.log("quizzes: create insertOne result", result);
return result.insertedId; return result.insertedId;
} }
@ -58,6 +58,14 @@ class Quiz {
return quiz; return quiz;
} }
async getRoomID(quizId) {
await this.db.connect()
const conn = this.db.getConnection();
const quizCollection = conn.collection('files');
const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) });
return quiz.roomId;
}
async delete(quizId) { async delete(quizId) {
await this.db.connect() await this.db.connect()
const conn = this.db.getConnection(); const conn = this.db.getConnection();
@ -89,12 +97,12 @@ class Quiz {
const result = await quizCollection.updateOne( const result = await quizCollection.updateOne(
{ _id: ObjectId.createFromHexString(quizId) }, { _id: ObjectId.createFromHexString(quizId) },
{ {
$set: { $set: {
title: newTitle, title: newTitle,
content: newContent, content: newContent,
updated_at: new Date() updated_at: new Date()
} }
} }
); );
@ -108,7 +116,7 @@ class Quiz {
const quizCollection = conn.collection('files'); const quizCollection = conn.collection('files');
const result = await quizCollection.updateOne( const result = await quizCollection.updateOne(
{ _id: ObjectId.createFromHexString(quizId) }, { _id: ObjectId.createFromHexString(quizId) },
{ $set: { folderId: newFolderId } } { $set: { folderId: newFolderId } }
); );
@ -143,13 +151,12 @@ class Quiz {
async quizExists(title, userId) { async quizExists(title, userId) {
await this.db.connect(); await this.db.connect();
const conn = this.db.getConnection(); const conn = this.db.getConnection();
const filesCollection = conn.collection('files'); const filesCollection = conn.collection('files');
const existingFolder = await filesCollection.findOne({ title: title, userId: userId }); const existingFolder = await filesCollection.findOne({ title: title, userId: userId });
return existingFolder !== null; return existingFolder !== null;
} }
} }
module.exports = Quiz; module.exports = Quiz;

View file

@ -10,6 +10,7 @@ if (!quizzes) {
router.post("/create", jwt.authenticate, asyncHandler(quizzes.create)); router.post("/create", jwt.authenticate, asyncHandler(quizzes.create));
router.get("/get/:quizId", jwt.authenticate, asyncHandler(asyncHandler(quizzes.get))); router.get("/get/:quizId", jwt.authenticate, asyncHandler(asyncHandler(quizzes.get)));
router.get('/getRoomID/:quizId', jwt.authenticate, asyncHandler(quizzes.getRoomID));
router.delete("/delete/:quizId", jwt.authenticate, asyncHandler(quizzes.delete)); router.delete("/delete/:quizId", jwt.authenticate, asyncHandler(quizzes.delete));
router.put("/update", jwt.authenticate, asyncHandler(quizzes.update)); router.put("/update", jwt.authenticate, asyncHandler(quizzes.update));
router.put("/move", jwt.authenticate, asyncHandler(quizzes.move)); router.put("/move", jwt.authenticate, asyncHandler(quizzes.move));
@ -20,4 +21,4 @@ router.put("/Share", jwt.authenticate, asyncHandler(quizzes.share));
router.get("/getShare/:quizId", jwt.authenticate, asyncHandler(quizzes.getShare)); router.get("/getShare/:quizId", jwt.authenticate, asyncHandler(quizzes.getShare));
router.post("/receiveShare", jwt.authenticate, asyncHandler(quizzes.receiveShare)); router.post("/receiveShare", jwt.authenticate, asyncHandler(quizzes.receiveShare));
module.exports = router; module.exports = router;

View file

@ -2,124 +2,92 @@ const MAX_USERS_PER_ROOM = 60;
const MAX_TOTAL_CONNECTIONS = 2000; const MAX_TOTAL_CONNECTIONS = 2000;
const setupWebsocket = (io) => { const setupWebsocket = (io) => {
let totalConnections = 0; let totalConnections = 0;
io.on("connection", (socket) => { io.on("connection", (socket) => {
if (totalConnections >= MAX_TOTAL_CONNECTIONS) { if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
console.log("Connection limit reached. Disconnecting client."); console.log("Connection limit reached. Disconnecting client.");
socket.emit( socket.emit(
"join-failure", "join-failure",
"Le nombre maximum de connexions a été atteint" "Le nombre maximum de connexions a été atteint"
); );
socket.disconnect(true); socket.disconnect(true);
return; return;
}
totalConnections++;
console.log(
"A user connected:",
socket.id,
"| Total connections:",
totalConnections
);
socket.on("create-room", (sentRoomName) => {
if (sentRoomName) {
const roomName = sentRoomName.toUpperCase();
if (!io.sockets.adapter.rooms.get(roomName)) {
socket.join(roomName);
socket.emit("create-success", roomName);
} else {
socket.emit("create-failure");
} }
} else {
const roomName = generateRoomName(); totalConnections++;
if (!io.sockets.adapter.rooms.get(roomName)) { console.log(
socket.join(roomName); "A user connected:",
socket.emit("create-success", roomName); socket.id,
} else { "| Total connections:",
socket.emit("create-failure"); totalConnections
} );
}
socket.on("join-room", ({ enteredRoomName, username }) => {
if (io.sockets.adapter.rooms.has(enteredRoomName)) {
const clientsInRoom =
io.sockets.adapter.rooms.get(enteredRoomName).size;
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
const newStudent = {
id: socket.id,
name: username,
answers: [],
};
socket.join(enteredRoomName);
socket
.to(enteredRoomName)
.emit("user-joined", newStudent);
socket.emit("join-success");
} else {
socket.emit("join-failure", "La salle est remplie");
}
} else {
socket.emit("join-failure", "Le nom de la salle n'existe pas");
}
});
socket.on("next-question", ({ roomName, question }) => {
socket.to(roomName).emit("next-question", question);
});
socket.on("launch-student-mode", ({ roomName, questions }) => {
socket.to(roomName).emit("launch-student-mode", questions);
});
socket.on("end-quiz", ({ roomName }) => {
socket.to(roomName).emit("end-quiz");
});
socket.on("message", (data) => {
console.log("Received message from", socket.id, ":", data);
});
socket.on("disconnect", () => {
totalConnections--;
console.log(
"A user disconnected:",
socket.id,
"| Total connections:",
totalConnections
);
for (const [room] of io.sockets.adapter.rooms) {
if (room !== socket.id) {
io.to(room).emit("user-disconnected", socket.id);
}
}
});
socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => {
socket.to(roomName).emit("submit-answer-room", {
idUser: socket.id,
username,
answer,
idQuestion,
});
});
}); });
socket.on("join-room", ({ enteredRoomName, username }) => {
if (io.sockets.adapter.rooms.has(enteredRoomName)) {
const clientsInRoom =
io.sockets.adapter.rooms.get(enteredRoomName).size;
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
const newStudent = {
id: socket.id,
name: username,
answers: [],
};
socket.join(enteredRoomName);
socket
.to(enteredRoomName)
.emit("user-joined", newStudent);
socket.emit("join-success");
} else {
socket.emit("join-failure", "La salle est remplie");
}
} else {
socket.emit("join-failure", "Le nom de la salle n'existe pas");
}
});
socket.on("next-question", ({ roomName, question }) => {
// console.log("next-question", roomName, question);
socket.to(roomName).emit("next-question", question);
});
socket.on("launch-student-mode", ({ roomName, questions }) => {
socket.to(roomName).emit("launch-student-mode", questions);
});
socket.on("end-quiz", ({ roomName }) => {
socket.to(roomName).emit("end-quiz");
});
socket.on("message", (data) => {
console.log("Received message from", socket.id, ":", data);
});
socket.on("disconnect", () => {
totalConnections--;
console.log(
"A user disconnected:",
socket.id,
"| Total connections:",
totalConnections
);
for (const [room] of io.sockets.adapter.rooms) {
if (room !== socket.id) {
io.to(room).emit("user-disconnected", socket.id);
}
}
});
socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => {
socket.to(roomName).emit("submit-answer-room", {
idUser: socket.id,
username,
answer,
idQuestion,
});
});
});
const generateRoomName = (length = 6) => {
const characters = "0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * characters.length)
);
}
return result;
};
}; };
module.exports = { setupWebsocket }; module.exports = { setupWebsocket };