diff --git a/client/src/constants.tsx b/client/src/constants.tsx
index dccc503..ad5b80b 100644
--- a/client/src/constants.tsx
+++ b/client/src/constants.tsx
@@ -1,6 +1,6 @@
// constants.tsx
const ENV_VARIABLES = {
- MODE: 'production',
+ MODE: process.env.MODE || "production",
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '',
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : ''
diff --git a/client/src/pages/AuthManager/authDrawer.css b/client/src/pages/AuthManager/authDrawer.css
index 1543fc2..b0d5263 100644
--- a/client/src/pages/AuthManager/authDrawer.css
+++ b/client/src/pages/AuthManager/authDrawer.css
@@ -3,11 +3,11 @@
flex-direction: column;
align-items: center;
padding: 20px;
- }
- h1 {
+}
+h1 {
margin-bottom: 20px;
- }
- .form-container{
+}
+.form-container {
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
@@ -15,34 +15,35 @@
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
- }
- form {
+}
+form {
display: flex;
flex-direction: column;
- }
- input {
+}
+input {
margin: 5px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
- }
- button {
+}
+button {
padding: 10px;
border: none;
border-radius: 4px;
background-color: #5271ff;
color: white;
cursor: pointer;
- }
- button:hover {
+}
+/* This hover was affecting the entire App */
+/* button:hover {
background-color: #5271ff;
- }
- .home-button-container{
+ } */
+.home-button-container {
background: none;
color: black;
- }
- .home-button-container:hover{
+}
+.home-button-container:hover {
background: none;
color: black;
text-decoration: underline;
- }
\ No newline at end of file
+}
diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx
index 6356d7a..ecc9a1c 100644
--- a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx
+++ b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx
@@ -25,6 +25,7 @@ const SimpleLogin: React.FC = () => {
}, []);
const login = async () => {
+ console.log(`SimpleLogin: login: email: ${email}, password: ${password}`);
const result = await ApiService.login(email, password);
if (result !== true) {
setConnectionError(result);
@@ -71,9 +72,10 @@ const SimpleLogin: React.FC = () => {
-
- Réinitialiser le mot de passe
-
+
+ {/*
*/}
+
Réinitialiser le mot de passe
+ {/* */}
Créer un compte
diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx
index a106876..5ff30b0 100644
--- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx
+++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx
@@ -39,17 +39,20 @@ const JoinRoom: React.FC = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
- socket.on('join-success', () => {
+ socket.on('join-success', (roomJoinedName) => {
setIsWaitingForTeacher(true);
setIsConnecting(false);
- console.log('Successfully joined the room.');
+ console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`);
});
socket.on('next-question', (question: QuestionType) => {
+ console.log('on(next-question): Received next-question:', question);
setQuizMode('teacher');
setIsWaitingForTeacher(false);
setQuestion(question);
});
socket.on('launch-student-mode', (questions: QuestionType[]) => {
+ console.log('on(launch-student-mode): Received launch-student-mode:', questions);
+
setQuizMode('student');
setIsWaitingForTeacher(false);
setQuestions(questions);
@@ -98,6 +101,8 @@ const JoinRoom: React.FC = () => {
}
if (username && roomName) {
+ console.log(`Tentative de rejoindre : ${roomName}, utilisateur : ${username}`);
+
webSocketService.joinRoom(roomName, username);
}
};
@@ -168,12 +173,12 @@ const JoinRoom: React.FC = () => {
error={connectionError}>
setRoomName(e.target.value)}
- placeholder="Numéro de la salle"
+ onChange={(e) => setRoomName(e.target.value.toUpperCase())}
+ placeholder="Nom de la salle"
sx={{ marginBottom: '1rem' }}
fullWidth={true}
onKeyDown={handleReturnKey}
diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx
index 3db6250..cdae619 100644
--- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx
+++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx
@@ -12,8 +12,13 @@ import ApiService from '../../../services/ApiService';
import './dashboard.css';
import ImportModal from 'src/components/ImportModal/ImportModal';
//import axios from 'axios';
-
+import { RoomType } from 'src/Types/RoomType';
+// import { useRooms } from '../ManageRoom/RoomContext';
import {
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
TextField,
IconButton,
InputAdornment,
@@ -23,6 +28,7 @@ import {
NativeSelect,
CardContent,
styled,
+ DialogContentText
} from '@mui/material';
import {
Search,
@@ -33,7 +39,7 @@ import {
FolderCopy,
ContentCopy,
Edit,
- Share,
+ Share
// DriveFileMove
} from '@mui/icons-material';
@@ -43,7 +49,7 @@ const CustomCard = styled(Card)({
position: 'relative',
margin: '40px 0 20px 0', // Add top margin to make space for the tab
borderRadius: '8px',
- paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
+ paddingTop: '20px' // Ensure content inside the card doesn't overlap with the tab
});
const Dashboard: React.FC = () => {
@@ -53,6 +59,13 @@ const Dashboard: React.FC = () => {
const [showImportModal, setShowImportModal] = useState(false);
const [folders, setFolders] = useState([]);
const [selectedFolderId, setSelectedFolderId] = useState(''); // Selected folder
+ const [rooms, setRooms] = useState([]);
+ const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
+ const [newRoomTitle, setNewRoomTitle] = useState('');
+ // const { selectedRoom, selectRoom, createRoom } = useRooms();
+ const [selectedRoom, selectRoom] = useState(); // menu
+ const [errorMessage, setErrorMessage] = useState('');
+ const [showErrorDialog, setShowErrorDialog] = useState(false);
// Filter quizzes based on search term
// const filteredQuizzes = quizzes.filter(quiz =>
@@ -65,7 +78,6 @@ const Dashboard: React.FC = () => {
);
}, [quizzes, searchTerm]);
-
// Group quizzes by folder
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
if (!acc[quiz.folderName]) {
@@ -77,20 +89,73 @@ const Dashboard: React.FC = () => {
useEffect(() => {
const fetchData = async () => {
- if (!ApiService.isLoggedIn()) {
- navigate("/login");
+ const isLoggedIn = await ApiService.isLoggedIn();
+ console.log(`Dashboard: isLoggedIn: ${isLoggedIn}`);
+ if (!isLoggedIn) {
+ navigate('/teacher/login');
return;
- }
- else {
- const userFolders = await ApiService.getUserFolders();
+ } else {
+ const userRooms = await ApiService.getUserRooms();
+ setRooms(userRooms as RoomType[]);
+ const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
}
-
};
fetchData();
}, []);
+
+ useEffect(() => {
+ if (rooms.length > 0 && !selectedRoom) {
+ selectRoom(rooms[rooms.length - 1]);
+ localStorage.setItem('selectedRoomId', rooms[rooms.length - 1]._id);
+ }
+ }, [rooms, selectedRoom]);
+
+ const handleSelectRoom = (event: React.ChangeEvent) => {
+ if (event.target.value === 'add-room') {
+ setOpenAddRoomDialog(true);
+ } else {
+ selectRoomByName(event.target.value);
+ }
+ };
+
+ // Créer une salle
+ const createRoom = async (title: string) => {
+ // Créer la salle et récupérer l'objet complet
+ const newRoom = await ApiService.createRoom(title);
+
+ // Mettre à jour la liste des salles
+ const updatedRooms = await ApiService.getUserRooms();
+ setRooms(updatedRooms as RoomType[]);
+
+ // Sélectionner la nouvelle salle avec son ID
+ selectRoomByName(newRoom); // Utiliser l'ID de l'objet retourné
+ };
+
+
+ // Sélectionner une salle
+ const selectRoomByName = (roomId: string) => {
+ const room = rooms.find(r => r._id === roomId);
+ selectRoom(room);
+ localStorage.setItem('selectedRoomId', roomId);
+ };
+
+ const handleCreateRoom = async () => {
+ if (newRoomTitle.trim()) {
+ try {
+ await createRoom(newRoomTitle);
+ const userRooms = await ApiService.getUserRooms();
+ setRooms(userRooms as RoomType[]);
+ setOpenAddRoomDialog(false);
+ setNewRoomTitle('');
+ } catch (error) {
+ setErrorMessage(error instanceof Error ? error.message : "Erreur inconnue");
+ setShowErrorDialog(true);
+ }
+ }
+ };
const handleSelectFolder = (event: React.ChangeEvent) => {
setSelectedFolderId(event.target.value);
@@ -98,7 +163,6 @@ const Dashboard: React.FC = () => {
useEffect(() => {
const fetchQuizzesForFolder = async () => {
-
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
//console.log("show all quizzes")
@@ -109,33 +173,29 @@ const Dashboard: React.FC = () => {
//console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
// add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title);
- quizzes = quizzes.concat(folderQuizzes as QuizType[])
+ quizzes = quizzes.concat(folderQuizzes as QuizType[]);
}
setQuizzes(quizzes as QuizType[]);
- }
- else {
- console.log("show some quizzes")
+ } else {
+ console.log('show some quizzes');
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
- console.log("folderQuizzes: ", folderQuizzes);
+ console.log('folderQuizzes: ', folderQuizzes);
// get the folder title from its id
- const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
+ const folderTitle =
+ folders.find((folder) => folder._id === selectedFolderId)?.title || '';
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
setQuizzes(folderQuizzes as QuizType[]);
}
-
-
};
fetchQuizzesForFolder();
}, [selectedFolderId]);
-
const handleSearch = (event: React.ChangeEvent) => {
setSearchTerm(event.target.value);
};
-
const handleRemoveQuiz = async (quiz: QuizType) => {
try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce quiz?');
@@ -149,30 +209,27 @@ const Dashboard: React.FC = () => {
}
};
-
const handleDuplicateQuiz = async (quiz: QuizType) => {
try {
await ApiService.duplicateQuiz(quiz._id);
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
- console.log("show all quizzes")
+ console.log('show all quizzes');
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
- console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
+ console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
}
setQuizzes(quizzes as QuizType[]);
- }
- else {
- console.log("show some quizzes")
+ } else {
+ console.log('show some quizzes');
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
setQuizzes(folderQuizzes as QuizType[]);
-
}
} catch (error) {
console.error('Error duplicating quiz:', error);
@@ -181,7 +238,6 @@ const Dashboard: React.FC = () => {
const handleOnImport = () => {
setShowImportModal(true);
-
};
const validateQuiz = (questions: string[]) => {
@@ -193,7 +249,6 @@ const Dashboard: React.FC = () => {
// Otherwise the quiz is invalid
for (let i = 0; i < questions.length; i++) {
try {
- // questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]);
Template(parsedItem[0]);
} catch (error) {
@@ -206,9 +261,8 @@ const Dashboard: React.FC = () => {
};
const downloadTxtFile = async (quiz: QuizType) => {
-
try {
- const selectedQuiz = await ApiService.getQuiz(quiz._id) as QuizType;
+ const selectedQuiz = (await ApiService.getQuiz(quiz._id)) as QuizType;
//quizzes.find((quiz) => quiz._id === quiz._id);
if (!selectedQuiz) {
@@ -216,7 +270,7 @@ const Dashboard: React.FC = () => {
}
//const { title, content } = selectedQuiz;
- let quizContent = "";
+ let quizContent = '';
const title = selectedQuiz.title;
console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => {
@@ -231,7 +285,9 @@ const Dashboard: React.FC = () => {
});
if (!validateQuiz(selectedQuiz.content)) {
- window.alert('Attention! Ce quiz contient des questions invalides selon le format GIFT.');
+ window.alert(
+ 'Attention! Ce quiz contient des questions invalides selon le format GIFT.'
+ );
}
const blob = new Blob([quizContent], { type: 'text/plain' });
const a = document.createElement('a');
@@ -239,8 +295,6 @@ const Dashboard: React.FC = () => {
a.download = `${filename}.gift`;
a.href = window.URL.createObjectURL(blob);
a.click();
-
-
} catch (error) {
console.error('Error exporting selected quiz:', error);
}
@@ -252,18 +306,16 @@ const Dashboard: React.FC = () => {
if (folderTitle) {
await ApiService.createFolder(folderTitle);
const userFolders = await ApiService.getUserFolders();
- setFolders(userFolders as FolderType[]);
+ setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolderId(newlyCreatedFolder._id);
-
}
} catch (error) {
console.error('Error creating folder:', error);
}
};
-
- const handleDeleteFolder = async () => {
+ const handleDeleteFolder = async () => {
try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
if (confirmed) {
@@ -273,18 +325,17 @@ const Dashboard: React.FC = () => {
}
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
- console.log("show all quizzes")
+ console.log('show all quizzes');
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
- console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
- quizzes = quizzes.concat(folderQuizzes as QuizType[])
+ console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
+ quizzes = quizzes.concat(folderQuizzes as QuizType[]);
}
setQuizzes(quizzes as QuizType[]);
setSelectedFolderId('');
-
} catch (error) {
console.error('Error deleting folder:', error);
}
@@ -294,12 +345,15 @@ const Dashboard: React.FC = () => {
try {
// folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: string GET THIS FROM CURRENT FOLDER
- const newTitle = prompt('Entrée le nouveau nom du fichier', folders.find((folder) => folder._id === selectedFolderId)?.title);
+ const newTitle = prompt(
+ 'Entrée le nouveau nom du fichier',
+ folders.find((folder) => folder._id === selectedFolderId)?.title
+ );
if (newTitle) {
const renamedFolderId = selectedFolderId;
const result = await ApiService.renameFolder(selectedFolderId, newTitle);
- if (result !== true ) {
+ if (result !== true) {
window.alert(`Une erreur est survenue: ${result}`);
return;
}
@@ -331,46 +385,94 @@ const Dashboard: React.FC = () => {
};
const handleCreateQuiz = () => {
- navigate("/teacher/editor-quiz/new");
- }
+ navigate('/teacher/editor-quiz/new');
+ };
const handleEditQuiz = (quiz: QuizType) => {
navigate(`/teacher/editor-quiz/${quiz._id}`);
- }
+ };
const handleLancerQuiz = (quiz: QuizType) => {
- navigate(`/teacher/manage-room/${quiz._id}`);
- }
+ if (selectedRoom) {
+ navigate(`/teacher/manage-room/${quiz._id}/${selectedRoom.title}`);
+ } else {
+ const randomSixDigit = Math.floor(100000 + Math.random() * 900000);
+ navigate(`/teacher/manage-room/${quiz._id}/${randomSixDigit}`);
+ }
+ };
const handleShareQuiz = async (quiz: QuizType) => {
try {
- const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, "");
+ const email = prompt(
+ `Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`,
+ ''
+ );
if (email) {
const result = await ApiService.ShareQuiz(quiz._id, email);
if (!result) {
- window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
+ window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`);
return;
}
- window.alert(`Quiz partagé avec succès!`)
+ window.alert(`Quiz partagé avec succès!`);
}
-
} catch (error) {
console.error('Erreur lors du partage du quiz:', error);
}
- }
-
-
-
+ };
return (
-
-
Tableau de bord
+
+
+
+
+
+
+ {selectedRoom && (
+
+
Salle sélectionnée: {selectedRoom.title}
+
+ )}
+
+
+
+
{
/>
-
-
+
+
{
{folders.map((folder: FolderType) => (
-
+
))}
-
+
-
+
+ {' '}
+ {' '}
+
+
+ >
+ {' '}
+ {' '}
+
+
+
+ >
+ {' '}
+ {' '}
+
+
+
+ >
+ {' '}
+ {' '}
+
+
-
-
+
-
-
- {Object.keys(quizzesByFolder).map(folderName => (
-
- {folderName}
+
+ {Object.keys(quizzesByFolder).map((folderName) => (
+
+ {folderName}
{quizzesByFolder[folderName].map((quiz: QuizType) => (
-
-
+
+
-
+
+
+
-
+
downloadTxtFile(quiz)}
- >
+ >
+ {' '}
+ {' '}
+
handleEditQuiz(quiz)}
- >
+ >
+ {' '}
+ {' '}
+
handleDuplicateQuiz(quiz)}
- >
+ >
+ {' '}
+ {' '}
+
@@ -507,14 +638,20 @@ const Dashboard: React.FC = () => {
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
- >
+ >
+ {' '}
+ {' '}
+
handleShareQuiz(quiz)}
- >
+ >
+ {' '}
+ {' '}
+
@@ -529,7 +666,6 @@ const Dashboard: React.FC = () => {
handleOnImport={handleOnImport}
selectedFolder={selectedFolderId}
/>
-
);
};
@@ -542,4 +678,3 @@ function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName:
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
});
}
-
diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
index e69dcea..3a67c27 100644
--- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
+++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx
@@ -1,71 +1,93 @@
-// ManageRoom.tsx
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client';
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 { QuestionService } from '../../../services/QuestionService';
-import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
+import webSocketService, {
+ AnswerReceptionFromBackendType
+} from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
-
import './manageRoom.css';
import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType';
-import { Button } from '@mui/material';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
-//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType';
+import { Button } from '@mui/material';
const ManageRoom: React.FC = () => {
const navigate = useNavigate();
- const [roomName, setRoomName] = useState
('');
const [socket, setSocket] = useState(null);
const [students, setStudents] = useState([]);
- const quizId = useParams<{ id: string }>();
+ const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>();
const [quizQuestions, setQuizQuestions] = useState();
const [quiz, setQuiz] = useState(null);
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState('');
const [currentQuestion, setCurrentQuestion] = useState(undefined);
const [quizStarted, setQuizStarted] = useState(false);
-
- useEffect(() => {
- if (quizId.id) {
- const fetchquiz = async () => {
+ const [formattedRoomName, setFormattedRoomName] = useState("");
- const quiz = await ApiService.getQuiz(quizId.id as string);
+ useEffect(() => {
+ const verifyLogin = async () => {
+ if (!ApiService.isLoggedIn()) {
+ navigate('/teacher/login');
+ return;
+ }
+ };
+
+ verifyLogin();
+ }, []);
+
+ useEffect(() => {
+ if (!roomName || !quizId) {
+ window.alert(
+ `Une erreur est survenue.\n La salle ou le quiz n'a pas été spécifié.\nVeuillez réessayer plus tard.`
+ );
+ console.error(`Room "${roomName}" or Quiz "${quizId}" not found.`);
+ navigate('/teacher/dashboard');
+ }
+ if (roomName && !socket) {
+ createWebSocketRoom();
+ }
+ return () => {
+ disconnectWebSocket();
+ };
+ }, [roomName, navigate]);
+
+ useEffect(() => {
+ if (quizId) {
+ const fetchQuiz = async () => {
+ const quiz = await ApiService.getQuiz(quizId);
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);
+ window.alert(
+ `Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
+ );
+ console.error('Quiz not found for id:', quizId);
navigate('/teacher/dashboard');
return;
}
setQuiz(quiz as QuizType);
-
- if (!socket) {
- console.log(`no socket in ManageRoom, creating one.`);
- createWebSocketRoom();
- }
-
- // return () => {
- // webSocketService.disconnect();
- // };
};
- fetchquiz();
-
+ fetchQuiz();
} else {
- 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);
+ window.alert(
+ `Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
+ );
+ console.error('Quiz not found for id:', quizId);
navigate('/teacher/dashboard');
return;
}
@@ -73,75 +95,69 @@ const ManageRoom: React.FC = () => {
const disconnectWebSocket = () => {
if (socket) {
- webSocketService.endQuiz(roomName);
+ webSocketService.endQuiz(formattedRoomName);
webSocketService.disconnect();
setSocket(null);
setQuizQuestions(undefined);
setCurrentQuestion(undefined);
setStudents(new Array());
- setRoomName('');
}
};
const createWebSocketRoom = () => {
- console.log('Creating WebSocket room...');
- setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
-
+ const roomNameUpper = roomName.toUpperCase();
+ setFormattedRoomName(roomNameUpper);
+ console.log(`Creating WebSocket room named ${roomNameUpper}`);
socket.on('connect', () => {
- webSocketService.createRoom();
+ webSocketService.createRoom(roomNameUpper);
});
+
socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
console.error('ManageRoom: WebSocket connection error:', error);
});
- socket.on('create-success', (roomName: string) => {
- setRoomName(roomName);
- });
- socket.on('create-failure', () => {
- console.log('Error creating room.');
+
+ socket.on('create-success', (createdRoomName: string) => {
+ console.log(`Room created: ${createdRoomName}`);
});
+
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}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
setStudents((prevStudents) => [...prevStudents, student]);
+ // only send nextQuestion if the quiz has started
+ if (!quizStarted) return;
+
if (quizMode === 'teacher') {
- webSocketService.nextQuestion(roomName, currentQuestion);
+ webSocketService.nextQuestion(formattedRoomName, currentQuestion);
} else if (quizMode === 'student') {
- webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
+ webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
}
});
socket.on('join-failure', (message) => {
setConnectingError(message);
setSocket(null);
});
+
socket.on('user-disconnected', (userId: string) => {
console.log(`Student left: id = ${userId}`);
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
});
+
setSocket(socket);
};
useEffect(() => {
- // This is here to make sure the correct value is sent when user join
- if (socket) {
- console.log(`Listening for user-joined in room ${roomName}`);
- socket.on('user-joined', (_student: StudentType) => {
- if (quizMode === 'teacher') {
- webSocketService.nextQuestion(roomName, currentQuestion);
- } else if (quizMode === 'student') {
- webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
- }
- });
- }
if (socket) {
- // 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 ${formattedRoomName}`);
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
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}`
+ );
if (!quizQuestions) {
console.log('Quiz questions not found (cannot update answers without them).');
return;
@@ -149,7 +165,6 @@ const ManageRoom: React.FC = () => {
// Update the students state using the functional form of setStudents
setStudents((prevStudents) => {
- // print the list of current student names
console.log('Current students:');
prevStudents.forEach((student) => {
console.log(student.name);
@@ -160,17 +175,31 @@ const ManageRoom: React.FC = () => {
console.log(`Comparing ${student.id} to ${idUser}`);
if (student.id === idUser) {
foundStudent = true;
- const existingAnswer = student.answers.find((ans) => ans.idQuestion === idQuestion);
+ const existingAnswer = student.answers.find(
+ (ans) => ans.idQuestion === idQuestion
+ );
let updatedAnswers: Answer[] = [];
if (existingAnswer) {
- // Update the existing answer
updatedAnswers = student.answers.map((ans) => {
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
- return (ans.idQuestion === idQuestion ? { ...ans, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) } : ans);
+ return ans.idQuestion === idQuestion
+ ? {
+ ...ans,
+ answer,
+ isCorrect: checkIfIsCorrect(
+ answer,
+ idQuestion,
+ quizQuestions!
+ )
+ }
+ : ans;
});
} else {
- // Add a new answer
- const newAnswer = { idQuestion, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) };
+ const newAnswer = {
+ idQuestion,
+ answer,
+ isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!)
+ };
updatedAnswers = [...student.answers, newAnswer];
}
return { ...student, answers: updatedAnswers };
@@ -185,73 +214,8 @@ const ManageRoom: React.FC = () => {
});
setSocket(socket);
}
-
}, [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 = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
@@ -260,7 +224,7 @@ const ManageRoom: React.FC = () => {
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
- webSocketService.nextQuestion(roomName, quizQuestions[nextQuestionIndex]);
+ webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]);
};
const previousQuestion = () => {
@@ -270,7 +234,7 @@ const ManageRoom: React.FC = () => {
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
- webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
+ webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]);
};
const initializeQuizQuestion = () => {
@@ -298,7 +262,7 @@ const ManageRoom: React.FC = () => {
}
setCurrentQuestion(quizQuestions[0]);
- webSocketService.nextQuestion(roomName, quizQuestions[0]);
+ webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]);
};
const launchStudentMode = () => {
@@ -310,13 +274,15 @@ const ManageRoom: React.FC = () => {
return;
}
setQuizQuestions(quizQuestions);
- webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
+ webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
};
const launchQuiz = () => {
- if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
+ if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly
- console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
+ console.log(
+ `Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
+ );
setQuizStarted(true);
return;
@@ -328,7 +294,6 @@ const ManageRoom: React.FC = () => {
case 'teacher':
setQuizStarted(true);
return launchTeacherMode();
-
}
};
@@ -337,7 +302,7 @@ const ManageRoom: React.FC = () => {
setCurrentQuestion(quizQuestions[questionIndex]);
if (quizMode === 'teacher') {
- webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]);
+ webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]);
}
}
};
@@ -347,7 +312,11 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard');
};
- function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number, questions: QuestionType[]): boolean {
+ function checkIfIsCorrect(
+ answer: string | number | boolean,
+ idQuestion: number,
+ questions: QuestionType[]
+ ): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
@@ -370,8 +339,7 @@ const ManageRoom: React.FC = () => {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
- answerNumber <= choice.numberHigh &&
- answerNumber >= choice.numberLow
+ answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
);
}
}
@@ -401,8 +369,7 @@ const ManageRoom: React.FC = () => {
return false;
}
-
- if (!roomName) {
+ if (!formattedRoomName) {
return (
{!connectingError ? (
@@ -425,47 +392,51 @@ const ManageRoom: React.FC = () => {
}
return (
-
-
-
+
+
Salle : {formattedRoomName}
+
+ message={`Êtes-vous sûr de vouloir quitter?`}
+ />
-
-
-
-
-
- {quizStarted && (
-
+
+ {(
+
{students.length}/60
)}
-
-
+
+
{/* the following breaks the css (if 'room' classes are nested) */}
-
-
+
{quizQuestions ? (
-
{quiz?.title}
{!isNaN(Number(currentQuestion?.question.id)) && (
-
- Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
+
+ Question {Number(currentQuestion?.question.id)}/
+ {quizQuestions?.length}
)}
{quizMode === 'teacher' && (
-
{/* {
nextQuestion={nextQuestion}
/> */}
-
)}
-
{currentQuestion && (
{
showSelectedQuestion={showSelectedQuestion}
students={students}
>
-
{quizMode === 'teacher' && (
-
+
-
-
-
)}
-
+
+ )}
-
) : (
-
-
)}
-
);
};
diff --git a/client/src/pages/Teacher/ManageRoom/RoomContext.tsx b/client/src/pages/Teacher/ManageRoom/RoomContext.tsx
new file mode 100644
index 0000000..f68548c
--- /dev/null
+++ b/client/src/pages/Teacher/ManageRoom/RoomContext.tsx
@@ -0,0 +1,59 @@
+import { useState, useEffect } from 'react';
+import ApiService from '../../../services/ApiService';
+import { RoomType } from 'src/Types/RoomType';
+import React from "react";
+import { RoomContext } from './useRooms';
+
+export const RoomProvider = ({ children }: { children: React.ReactNode }) => {
+ const [rooms, setRooms] = useState
([]);
+ const [selectedRoom, setSelectedRoom] = useState(null);
+
+ useEffect(() => {
+ const loadRooms = async () => {
+ const userRooms = await ApiService.getUserRooms();
+ const roomsList = userRooms as RoomType[];
+ setRooms(roomsList);
+
+ const savedRoomId = localStorage.getItem('selectedRoomId');
+ if (savedRoomId) {
+ const savedRoom = roomsList.find(r => r._id === savedRoomId);
+ if (savedRoom) {
+ setSelectedRoom(savedRoom);
+ return;
+ }
+ }
+
+ if (roomsList.length > 0) {
+ setSelectedRoom(roomsList[0]);
+ localStorage.setItem('selectedRoomId', roomsList[0]._id);
+ }
+ };
+
+ loadRooms();
+ }, []);
+
+ // Sélectionner une salle
+ const selectRoom = (roomId: string) => {
+ const room = rooms.find(r => r._id === roomId) || null;
+ setSelectedRoom(room);
+ localStorage.setItem('selectedRoomId', roomId);
+ };
+
+ // Créer une salle
+ const createRoom = async (title: string) => {
+ // Créer la salle et récupérer l'objet complet
+ const newRoom = await ApiService.createRoom(title);
+
+ // Mettre à jour la liste des salles
+ const updatedRooms = await ApiService.getUserRooms();
+ setRooms(updatedRooms as RoomType[]);
+
+ // Sélectionner la nouvelle salle avec son ID
+ selectRoom(newRoom); // Utiliser l'ID de l'objet retourné
+ };
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/src/pages/Teacher/ManageRoom/useRooms.ts b/client/src/pages/Teacher/ManageRoom/useRooms.ts
new file mode 100644
index 0000000..f0cacc8
--- /dev/null
+++ b/client/src/pages/Teacher/ManageRoom/useRooms.ts
@@ -0,0 +1,20 @@
+import { useContext } from 'react';
+import { RoomType } from 'src/Types/RoomType';
+import { createContext } from 'react';
+
+//import { RoomContext } from './RoomContext';
+
+type RoomContextType = {
+ rooms: RoomType[];
+ selectedRoom: RoomType | null;
+ selectRoom: (roomId: string) => void;
+ createRoom: (title: string) => Promise;
+ };
+
+export const RoomContext = createContext(undefined);
+
+export const useRooms = () => {
+ const context = useContext(RoomContext);
+ if (!context) throw new Error('useRooms must be used within a RoomProvider');
+ return context;
+};
diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx
index aae8823..b13a369 100644
--- a/client/src/services/ApiService.tsx
+++ b/client/src/services/ApiService.tsx
@@ -4,6 +4,7 @@ import { ENV_VARIABLES } from '../constants';
import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType';
+import { RoomType } from 'src/Types/RoomType';
type ApiResponse = boolean | string;
@@ -164,6 +165,7 @@ class ApiService {
* @returns A error string if unsuccessful,
*/
public async register(name: string, email: string, password: string, roles: string[]): Promise {
+ console.log(`ApiService.register: name: ${name}, email: ${email}, password: ${password}, roles: ${roles}`);
try {
if (!email || !password) {
@@ -178,7 +180,8 @@ class ApiService {
console.log(result);
if (result.status == 200) {
- window.location.href = result.request.responseURL;
+ //window.location.href = result.request.responseURL;
+ window.location.href = '/login';
}
else {
throw new Error(`La connexion a échoué. Status: ${result.status}`);
@@ -199,15 +202,12 @@ class ApiService {
}
}
- /**
- * @returns true if successful
- * @returns A error string if unsuccessful,
- */
- /**
+/**
* @returns true if successful
* @returns An error string if unsuccessful
*/
public async login(email: string, password: string): Promise {
+ console.log(`login: email: ${email}, password: ${password}`);
try {
if (!email || !password) {
throw new Error("L'email et le mot de passe sont requis.");
@@ -217,11 +217,16 @@ public async login(email: string, password: string): Promise {
const headers = this.constructRequestHeaders();
const body = { email, password };
+ console.log(`login: POST ${url} body: ${JSON.stringify(body)}`);
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
+ console.log(`login: result: ${result.status}, ${result.data}`);
// If login is successful, redirect the user
if (result.status === 200) {
- window.location.href = result.request.responseURL;
+ //window.location.href = result.request.responseURL;
+ this.saveToken(result.data.token);
+ this.saveUsername(result.data.username);
+ window.location.href = '/teacher/dashboard';
return true;
} else {
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
@@ -927,6 +932,195 @@ public async login(email: string, password: string): Promise {
}
}
+ //ROOM routes
+
+ public async getUserRooms(): Promise {
+ try {
+ const url: string = this.constructRequestUrl(`/room/getUserRooms`);
+ const headers = this.constructRequestHeaders();
+
+ const result: AxiosResponse = await axios.get(url, { headers: headers });
+
+ if (result.status !== 200) {
+ throw new Error(`L'obtention des salles utilisateur a échoué. Status: ${result.status}`);
+ }
+
+ return result.data.data.map((room: RoomType) => ({ _id: room._id, title: room.title }));
+
+ } catch (error) {
+ console.log("Error details: ", error);
+
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ const url = err.config?.url || 'URL inconnue';
+ return data?.error || `Erreur serveur inconnue lors de la requête (${url}).`;
+ }
+
+ return `Une erreur inattendue s'est produite.`
+ }
+ }
+
+ public async getRoomContent(roomId: string): Promise {
+ try {
+ const url = this.constructRequestUrl(`/room/${roomId}`);
+ const headers = this.constructRequestHeaders();
+
+ const response = await axios.get<{ data: RoomType }>(url, { headers });
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to get room: ${response.status}`);
+ }
+
+ return response.data.data;
+
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ const serverError = error.response?.data?.error;
+ throw new Error(serverError || 'Erreur serveur inconnue');
+ }
+ throw new Error('Erreur réseau');
+ }
+ }
+
+ public async getRoomTitleByUserId(userId: string): Promise {
+ try {
+ if (!userId) {
+ throw new Error(`L'ID utilisateur est requis.`);
+ }
+
+ const url: string = this.constructRequestUrl(`/room/getRoomTitleByUserId/${userId}`);
+ const headers = this.constructRequestHeaders();
+
+ const result: AxiosResponse = await axios.get(url, { headers });
+
+ if (result.status !== 200) {
+ throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`);
+ }
+
+ return result.data.titles;
+ } catch (error) {
+ console.log("Error details: ", error);
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ return data?.error || 'Erreur serveur inconnue lors de la requête.';
+ }
+ return `Une erreur inattendue s'est produite.`;
+ }
+ }
+ public async getRoomTitle(roomId: string): Promise {
+ try {
+ if (!roomId) {
+ throw new Error(`L'ID de la salle est requis.`);
+ }
+
+ const url: string = this.constructRequestUrl(`/room/getRoomTitle/${roomId}`);
+ const headers = this.constructRequestHeaders();
+
+ const result: AxiosResponse = await axios.get(url, { headers });
+
+ if (result.status !== 200) {
+ throw new Error(`L'obtention du titre de la salle a échoué. Status: ${result.status}`);
+ }
+
+ return result.data.title;
+ } catch (error) {
+ console.log("Error details: ", error);
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ return data?.error || 'Erreur serveur inconnue lors de la requête.';
+ }
+ return `Une erreur inattendue s'est produite.`;
+ }
+ }
+ public async createRoom(title: string): Promise {
+ try {
+ if (!title) {
+ throw new Error("Le titre de la salle est requis.");
+ }
+
+ const url: string = this.constructRequestUrl(`/room/create`);
+ const headers = this.constructRequestHeaders();
+ const body = { title };
+
+ const result = await axios.post<{ roomId: string }>(url, body, { headers });
+ return `Salle créée avec succès. ID de la salle: ${result.data.roomId}`;
+
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+
+ const serverMessage = (err.response?.data as { message?: string })?.message
+ || (err.response?.data as { error?: string })?.error
+ || err.message;
+
+ if (err.response?.status === 409) {
+ throw new Error(serverMessage);
+ }
+
+ throw new Error(serverMessage || "Erreur serveur inconnue");
+ }
+ throw error;
+ }
+ }
+
+ public async deleteRoom(roomId: string): Promise {
+ try {
+ if (!roomId) {
+ throw new Error(`L'ID de la salle est requis.`);
+ }
+
+ const url: string = this.constructRequestUrl(`/room/delete/${roomId}`);
+ const headers = this.constructRequestHeaders();
+
+ const result: AxiosResponse = await axios.delete(url, { headers });
+
+ if (result.status !== 200) {
+ throw new Error(`La suppression de la salle a échoué. Status: ${result.status}`);
+ }
+
+ return `Salle supprimée avec succès.`;
+ } catch (error) {
+ console.log("Error details: ", error);
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ return data?.error || 'Erreur serveur inconnue lors de la suppression de la salle.';
+ }
+ return `Une erreur inattendue s'est produite.`;
+ }
+ }
+
+ public async renameRoom(roomId: string, newTitle: string): Promise {
+ try {
+ if (!roomId || !newTitle) {
+ throw new Error(`L'ID de la salle et le nouveau titre sont requis.`);
+ }
+
+ const url: string = this.constructRequestUrl(`/room/rename`);
+ const headers = this.constructRequestHeaders();
+ const body = { roomId, newTitle };
+
+ const result: AxiosResponse = await axios.put(url, body, { headers });
+
+ if (result.status !== 200) {
+ throw new Error(`La mise à jour du titre de la salle a échoué. Status: ${result.status}`);
+ }
+
+ return `Titre de la salle mis à jour avec succès.`;
+ } catch (error) {
+ console.log("Error details: ", error);
+ if (axios.isAxiosError(error)) {
+ const err = error as AxiosError;
+ const data = err.response?.data as { error: string } | undefined;
+ return data?.error || 'Erreur serveur inconnue lors de la mise à jour du titre.';
+ }
+ return `Une erreur inattendue s'est produite.`;
+ }
+ }
+
// Images Route
/**
diff --git a/client/src/services/AuthService.tsx b/client/src/services/AuthService.tsx
index bca616d..050ac82 100644
--- a/client/src/services/AuthService.tsx
+++ b/client/src/services/AuthService.tsx
@@ -14,8 +14,13 @@ class AuthService {
async fetchAuthData(){
try {
+ // console.info(`MODE: ${ENV_VARIABLES.MODE}`);
+ // if (ENV_VARIABLES.MODE === 'development') {
+ // return { authActive: true };
+ // }
const response = await fetch(this.constructRequestUrl('/auth/getActiveAuth'));
const data = await response.json();
+ console.log('Data:', JSON.stringify(data));
return data.authActive;
} catch (error) {
console.error('Erreur lors de la récupération des données d\'auth:', error);
@@ -25,4 +30,4 @@ class AuthService {
}
const authService = new AuthService();
-export default authService;
\ No newline at end of file
+export default authService;
diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx
index 87cb188..3cacf36 100644
--- a/client/src/services/WebsocketService.tsx
+++ b/client/src/services/WebsocketService.tsx
@@ -1,4 +1,3 @@
-// WebSocketService.tsx
import { io, Socket } from 'socket.io-client';
// Must (manually) sync these types to server/socket/socket.js
@@ -46,19 +45,32 @@ class WebSocketService {
}
}
- createRoom() {
+ createRoom(roomName: string) {
if (this.socket) {
- this.socket.emit('create-room');
+ this.socket.emit('create-room', roomName);
}
}
+ // deleteRoom(roomName: string) {
+ // console.log('WebsocketService: deleteRoom', roomName);
+ // if (this.socket) {
+ // console.log('WebsocketService: emit: delete-room', roomName);
+ // this.socket.emit('delete-room', roomName);
+ // }
+ // }
+
nextQuestion(roomName: string, question: unknown) {
+ console.log('WebsocketService: nextQuestion', roomName, question);
+ if (!question) {
+ throw new Error('WebsocketService: nextQuestion: question is null');
+ }
if (this.socket) {
this.socket.emit('next-question', { roomName, question });
}
}
launchStudentModeQuiz(roomName: string, questions: unknown) {
+ console.log('WebsocketService: launchStudentModeQuiz', roomName, questions, this.socket);
if (this.socket) {
this.socket.emit('launch-student-mode', { roomName, questions });
}
@@ -76,21 +88,9 @@ class WebSocketService {
}
}
- submitAnswer(answerData: AnswerSubmissionToBackendType
- // roomName: string,
- // answer: string | number | boolean,
- // username: string,
- // idQuestion: string
- ) {
+ submitAnswer(answerData: AnswerSubmissionToBackendType) {
if (this.socket) {
- this.socket?.emit('submit-answer',
- // {
- // answer: answer,
- // roomName: roomName,
- // username: username,
- // idQuestion: idQuestion
- // }
- answerData
+ this.socket?.emit('submit-answer', answerData
);
}
}
diff --git a/server/__mocks__/AppError.js b/server/__mocks__/AppError.js
index 0073aa4..cb18755 100644
--- a/server/__mocks__/AppError.js
+++ b/server/__mocks__/AppError.js
@@ -1,8 +1,11 @@
class AppError extends Error {
constructor(message, statusCode) {
- super(message);
- this.statusCode = statusCode;
+ super(message);
+ this.statusCode = statusCode || 500;
+
+ Object.setPrototypeOf(this, new.target.prototype);
+
+ Error.captureStackTrace(this, this.constructor);
}
-}
-
+ }
module.exports = AppError;
diff --git a/server/__tests__/auth.test.js b/server/__tests__/auth.test.js
index ae5a192..2faa589 100644
--- a/server/__tests__/auth.test.js
+++ b/server/__tests__/auth.test.js
@@ -157,6 +157,7 @@ describe(
},
};
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
+ // TODO new AuthManager(...) essaie d'établir une connexion MongoDB et ça laisse un "open handle" dans Jest
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
authmanagerInstance.getUserModel();
expect(logSpy).toHaveBeenCalledTimes(0);
diff --git a/server/__tests__/rooms.test.js b/server/__tests__/rooms.test.js
new file mode 100644
index 0000000..ab623b7
--- /dev/null
+++ b/server/__tests__/rooms.test.js
@@ -0,0 +1,257 @@
+jest.mock("../middleware/AppError", () => {
+ const actualAppError = jest.requireActual("../middleware/AppError");
+
+ return jest.fn().mockImplementation((message, statusCode) => {
+ return new actualAppError(message, statusCode);
+ });
+});
+
+const Rooms = require("../models/room");
+const ObjectId = require("mongodb").ObjectId;
+describe("Rooms", () => {
+ let rooms;
+ let db;
+ let collection;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ collection = {
+ findOne: jest.fn(),
+ insertOne: jest.fn(),
+ find: jest.fn().mockReturnValue({ toArray: jest.fn() }),
+ deleteOne: jest.fn(),
+ deleteMany: jest.fn(),
+ updateOne: jest.fn(),
+ };
+
+ db = {
+ connect: jest.fn(),
+ getConnection: jest.fn().mockReturnThis(),
+ collection: jest.fn().mockReturnValue(collection),
+ };
+
+ rooms = new Rooms(db);
+ });
+
+ describe("create", () => {
+ it("should return insertedId on success", async () => {
+ collection.findOne.mockResolvedValue(null);
+ collection.insertOne.mockResolvedValue({ insertedId: "abc123" });
+
+ const result = await rooms.create("test", "userId");
+ expect(result).toBe("abc123");
+ });
+
+ it("should throw error when userId is missing", async () => {
+ await expect(rooms.create("test", undefined)).rejects.toThrowError(
+ new Error("Missing required parameter(s)", 400)
+ );
+ });
+
+ it("should throw conflict error when room exists", async () => {
+ collection.findOne.mockResolvedValue({
+ _id: "660c72b2f9b1d8b3a4c8e4d3b",
+ userId: "12345",
+ title: "existing room",
+ });
+
+ await expect(rooms.create("existing room", "12345")).rejects.toThrowError(
+ new Error("Room already exists", 409)
+ );
+ });
+ });
+ describe("getUserRooms", () => {
+ it("should return all rooms for a user", async () => {
+ const userId = "12345";
+ const userRooms = [
+ { title: "room 1", userId },
+ { title: "room 2", userId },
+ ];
+
+ collection.find().toArray.mockResolvedValue(userRooms);
+
+ const result = await rooms.getUserRooms(userId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.find).toHaveBeenCalledWith({ userId });
+ expect(result).toEqual(userRooms);
+ });
+ });
+
+ describe("getOwner", () => {
+ it("should return the owner of a room", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+ const userId = "12345";
+
+ collection.findOne.mockResolvedValue({ userId });
+
+ const result = await rooms.getOwner(roomId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.findOne).toHaveBeenCalledWith({
+ _id: new ObjectId(roomId),
+ });
+ expect(result).toBe(userId);
+ });
+ });
+
+ describe("delete", () => {
+ it("should delete a room and return true", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+
+ collection.deleteOne.mockResolvedValue({ deletedCount: 1 });
+
+ const result = await rooms.delete(roomId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.deleteOne).toHaveBeenCalledWith({
+ _id: new ObjectId(roomId),
+ });
+ expect(result).toBe(true);
+ });
+
+ it("should return false if the room does not exist", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+
+ collection.deleteOne.mockResolvedValue({ deletedCount: 0 });
+
+ const result = await rooms.delete(roomId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.deleteOne).toHaveBeenCalledWith({
+ _id: new ObjectId(roomId),
+ });
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("rename", () => {
+ it("should rename a room and return true", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+ const newTitle = "new room name";
+ const userId = "12345";
+
+ collection.updateOne.mockResolvedValue({ modifiedCount: 1 });
+
+ const result = await rooms.rename(roomId, userId, newTitle);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.updateOne).toHaveBeenCalledWith(
+ { _id: new ObjectId(roomId), userId: userId },
+ { $set: { title: newTitle } }
+ );
+ expect(result).toBe(true);
+ });
+
+ it("should return false if the room does not exist", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+ const newTitle = "new room name";
+ const userId = "12345";
+
+ collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
+
+ const result = await rooms.rename(roomId, userId, newTitle);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.updateOne).toHaveBeenCalledWith(
+ { _id: new ObjectId(roomId), userId: userId },
+ { $set: { title: newTitle } }
+ );
+ expect(result).toBe(false);
+ });
+
+ it("should throw an error if the new title is already in use", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+ const newTitle = "existing room";
+ const userId = "12345";
+
+ collection.findOne.mockResolvedValue({ title: newTitle });
+ collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
+
+ await expect(rooms.rename(roomId, userId, newTitle)).rejects.toThrow(
+ "Room with name 'existing room' already exists."
+ );
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.findOne).toHaveBeenCalledWith({
+ userId: userId,
+ title: newTitle,
+ });
+ });
+ });
+
+ describe("roomExists", () => {
+ it("should return true if room exists", async () => {
+ const title = "TEST ROOM";
+ const userId = '66fc70bea1b9e87655cf17c9';
+
+ collection.findOne.mockResolvedValue({ title, userId });
+
+ const result = await rooms.roomExists(title, userId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.findOne).toHaveBeenCalledWith({ title: title.toUpperCase(), userId });
+ expect(result).toBe(true);
+ });
+
+ it("should return false if room does not exist", async () => {
+ const title = "NONEXISTENT ROOM";
+ const userId = '66fc70bea1b9e87655cf17c9';
+
+ collection.findOne.mockResolvedValue(null);
+
+ const result = await rooms.roomExists(title, userId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith('rooms');
+ expect(collection.findOne).toHaveBeenCalledWith({ title: title.toUpperCase(), userId });
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("getRoomById", () => {
+ it("should return a room by ID", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+ const room = {
+ _id: new ObjectId(roomId),
+ title: "test room",
+ };
+
+ collection.findOne.mockResolvedValue(room);
+
+ const result = await rooms.getRoomById(roomId);
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.findOne).toHaveBeenCalledWith({
+ _id: new ObjectId(roomId),
+ });
+ expect(result).toEqual(room);
+ });
+
+ it("should throw an error if the room does not exist", async () => {
+ const roomId = "60c72b2f9b1d8b3a4c8e4d3b";
+
+ collection.findOne.mockResolvedValue(null);
+
+ await expect(rooms.getRoomById(roomId)).rejects.toThrowError(
+ new Error(`Room ${roomId} not found`, 404)
+ );
+
+ expect(db.connect).toHaveBeenCalled();
+ expect(db.collection).toHaveBeenCalledWith("rooms");
+ expect(collection.findOne).toHaveBeenCalledWith({
+ _id: new ObjectId(roomId),
+ });
+ });
+ });
+});
diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js
index 95c404f..739d79d 100644
--- a/server/__tests__/socket.test.js
+++ b/server/__tests__/socket.test.js
@@ -60,45 +60,42 @@ describe("websocket server", () => {
});
test("should create a room", (done) => {
- teacherSocket.emit("create-room", "room1");
teacherSocket.on("create-success", (roomName) => {
expect(roomName).toBe("ROOM1");
done();
});
+ teacherSocket.emit("create-room", "room1");
});
test("should not create a room if it already exists", (done) => {
- teacherSocket.emit("create-room", "room1");
teacherSocket.on("create-failure", () => {
done();
});
+ teacherSocket.emit("create-room", "room1");
});
test("should join a room", (done) => {
- studentSocket.emit("join-room", {
- enteredRoomName: "ROOM1",
- username: "student1",
- });
- studentSocket.on("join-success", () => {
+ studentSocket.on("join-success", (roomName) => {
+ expect(roomName).toBe("ROOM1");
done();
});
+ studentSocket.emit("join-room", {
+ enteredRoomName: "room1",
+ username: "student1",
+ });
});
test("should not join a room if it does not exist", (done) => {
+ studentSocket.on("join-failure", () => {
+ done();
+ });
studentSocket.emit("join-room", {
enteredRoomName: "ROOM2",
username: "student1",
});
- studentSocket.on("join-failure", () => {
- done();
- });
});
test("should launch student mode", (done) => {
- teacherSocket.emit("launch-student-mode", {
- roomName: "ROOM1",
- questions: [{ question: "question1" }, { question: "question2" }],
- });
studentSocket.on("launch-student-mode", (questions) => {
expect(questions).toEqual([
{ question: "question1" },
@@ -106,26 +103,24 @@ describe("websocket server", () => {
]);
done();
});
+ teacherSocket.emit("launch-student-mode", {
+ roomName: "ROOM1",
+ questions: [{ question: "question1" }, { question: "question2" }],
+ });
});
test("should send next question", (done) => {
- teacherSocket.emit("next-question", {
- roomName: "ROOM1",
- question: { question: "question2" },
- });
studentSocket.on("next-question", (question) => {
expect(question).toEqual({ question: "question2" });
done();
});
+ teacherSocket.emit("next-question", {
+ roomName: "ROOM1",
+ question: { question: "question2" },
+ });
});
test("should send answer", (done) => {
- studentSocket.emit("submit-answer", {
- roomName: "ROOM1",
- username: "student1",
- answer: "answer1",
- idQuestion: 1,
- });
teacherSocket.on("submit-answer-room", (answer) => {
expect(answer).toEqual({
idUser: studentSocket.id,
@@ -135,32 +130,38 @@ describe("websocket server", () => {
});
done();
});
+ studentSocket.emit("submit-answer", {
+ roomName: "ROOM1",
+ username: "student1",
+ answer: "answer1",
+ idQuestion: 1,
+ });
});
test("should not join a room if no room name is provided", (done) => {
+ studentSocket.on("join-failure", () => {
+ done();
+ });
studentSocket.emit("join-room", {
enteredRoomName: "",
username: "student1",
});
- studentSocket.on("join-failure", () => {
- done();
- });
});
test("should not join a room if the username is not provided", (done) => {
- studentSocket.emit("join-room", { enteredRoomName: "ROOM2", username: "" });
studentSocket.on("join-failure", () => {
done();
});
+ studentSocket.emit("join-room", { enteredRoomName: "ROOM2", username: "" });
});
test("should end quiz", (done) => {
- teacherSocket.emit("end-quiz", {
- roomName: "ROOM1",
- });
studentSocket.on("end-quiz", () => {
done();
});
+ teacherSocket.emit("end-quiz", {
+ roomName: "ROOM1",
+ });
});
test("should disconnect", (done) => {
diff --git a/server/app.js b/server/app.js
index 19ba3f9..32f64b9 100644
--- a/server/app.js
+++ b/server/app.js
@@ -12,6 +12,8 @@ const db = require('./config/db.js');
// instantiate the models
const quiz = require('./models/quiz.js');
const quizModel = new quiz(db);
+const room = require('./models/room.js');
+const roomModel = new room(db);
const folders = require('./models/folders.js');
const foldersModel = new folders(db, quizModel);
const users = require('./models/users.js');
@@ -22,6 +24,8 @@ const imageModel = new images(db);
// instantiate the controllers
const usersController = require('./controllers/users.js');
const usersControllerInstance = new usersController(userModel);
+const roomsController = require('./controllers/room.js');
+const roomsControllerInstance = new roomsController(roomModel);
const foldersController = require('./controllers/folders.js');
const foldersControllerInstance = new foldersController(foldersModel);
const quizController = require('./controllers/quiz.js');
@@ -31,12 +35,14 @@ const imagesControllerInstance = new imagesController(imageModel);
// export the controllers
module.exports.users = usersControllerInstance;
+module.exports.rooms = roomsControllerInstance;
module.exports.folders = foldersControllerInstance;
module.exports.quizzes = quizControllerInstance;
module.exports.images = imagesControllerInstance;
//import routers (instantiate controllers as side effect)
const userRouter = require('./routers/users.js');
+const roomRouter = require('./routers/room.js');
const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js')
@@ -89,6 +95,7 @@ app.use(bodyParser.json());
// Create routes
app.use('/api/user', userRouter);
+app.use('/api/room', roomRouter);
app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter);
diff --git a/server/auth/auth-manager.js b/server/auth/auth-manager.js
index bdcc4d7..0233672 100644
--- a/server/auth/auth-manager.js
+++ b/server/auth/auth-manager.js
@@ -16,6 +16,7 @@ class AuthManager{
this.addModules()
this.simpleregister = userModel;
this.registerAuths()
+ console.log(`AuthManager: constructor: this.configs: ${JSON.stringify(this.configs)}`);
}
getUserModel(){
@@ -54,17 +55,22 @@ class AuthManager{
// eslint-disable-next-line no-unused-vars
async login(userInfo,req,res,next){ //passport and simpleauth use next
- const tokenToSave = jwt.create(userInfo.email, userInfo._id,userInfo.roles);
+ const tokenToSave = jwt.create(userInfo.email, userInfo._id, userInfo.roles);
res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
}
// eslint-disable-next-line no-unused-vars
async loginSimple(email,pswd,req,res,next){ //passport and simpleauth use next
+ console.log(`auth-manager: loginSimple: email: ${email}, pswd: ${pswd}`);
const userInfo = await this.simpleregister.login(email, pswd);
- const tokenToSave = jwt.create(userInfo.email, userInfo._id,userInfo.roles);
- res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
- console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
+ console.log(`auth-manager: loginSimple: userInfo: ${JSON.stringify(userInfo)}`);
+ userInfo.roles = ['teacher']; // hard coded role
+ const tokenToSave = jwt.create(userInfo.email, userInfo._id, userInfo.roles);
+ console.log(`auth-manager: loginSimple: tokenToSave: ${tokenToSave}`);
+ //res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.email}`);
+ res.status(200).json({token: tokenToSave});
+ console.info(`L'utilisateur '${userInfo.email}' vient de se connecter`)
}
async register(userInfos){
diff --git a/server/auth/modules/simpleauth.js b/server/auth/modules/simpleauth.js
index e2c9300..8ed33bf 100644
--- a/server/auth/modules/simpleauth.js
+++ b/server/auth/modules/simpleauth.js
@@ -26,6 +26,7 @@ class SimpleAuth {
}
async register(self, req, res) {
+ console.log(`simpleauth.js.register: ${JSON.stringify(req.body)}`);
try {
let userInfos = {
name: req.body.name,
@@ -34,7 +35,11 @@ class SimpleAuth {
roles: req.body.roles
};
let user = await self.authmanager.register(userInfos)
- if (user) res.redirect("/login")
+ if (user) {
+ return res.status(200).json({
+ message: 'User created'
+ });
+ }
}
catch (error) {
return res.status(400).json({
@@ -44,6 +49,7 @@ class SimpleAuth {
}
async authenticate(self, req, res, next) {
+ console.log(`authenticate: ${JSON.stringify(req.body)}`);
try {
const { email, password } = req.body;
@@ -54,6 +60,7 @@ class SimpleAuth {
}
await self.authmanager.loginSimple(email, password, req, res, next);
+ // return res.status(200).json({ message: 'Logged in' });
} catch (error) {
const statusCode = error.statusCode || 500;
const message = error.message || "An internal server error occurred";
@@ -120,4 +127,4 @@ class SimpleAuth {
}
-module.exports = SimpleAuth;
\ No newline at end of file
+module.exports = SimpleAuth;
diff --git a/server/auth_config-development.json b/server/auth_config-development.json
new file mode 100644
index 0000000..6d53c7e
--- /dev/null
+++ b/server/auth_config-development.json
@@ -0,0 +1,9 @@
+{
+ "auth": {
+ "simpleauth": {
+ "enabled": true,
+ "name": "provider3",
+ "SESSION_SECRET": "your_session_secret"
+ }
+ }
+}
diff --git a/server/config/auth.js b/server/config/auth.js
index 72e89ed..b993def 100644
--- a/server/config/auth.js
+++ b/server/config/auth.js
@@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
-const pathAuthConfig = './auth_config.json';
+// set pathAuthConfig to './auth_config-development.json' if NODE_ENV is set to development
+const pathAuthConfig = process.env.NODE_ENV === 'development' ? './auth_config-development.json' : './auth_config.json';
const configPath = path.join(process.cwd(), pathAuthConfig);
@@ -12,10 +13,11 @@ class AuthConfig {
// Méthode pour lire le fichier de configuration JSON
loadConfig() {
try {
+ console.info(`Chargement du fichier de configuration: ${configPath}`);
const configData = fs.readFileSync(configPath, 'utf-8');
this.config = JSON.parse(configData);
} catch (error) {
- console.error("Erreur lors de la lecture du fichier de configuration. Ne pas se fier si vous n'avez pas mit de fichier de configuration.");
+ console.error("Erreur lors de la lecture du fichier de configuration. Ne pas se fier si vous n'avez pas mis de fichier de configuration.");
this.config = {};
throw error;
}
@@ -139,6 +141,8 @@ class AuthConfig {
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
getActiveAuth() {
+ console.log(`getActiveAuth: this.config: ${JSON.stringify(this.config)}`);
+ console.log(`getActiveAuth: this.config.auth: ${JSON.stringify(this.config.auth)}`);
if (this.config && this.config.auth) {
const passportConfig = {};
diff --git a/server/constants/errorCodes.js b/server/constants/errorCodes.js
index 36ae657..b7618b1 100644
--- a/server/constants/errorCodes.js
+++ b/server/constants/errorCodes.js
@@ -1,16 +1,16 @@
exports.UNAUTHORIZED_NO_TOKEN_GIVEN = {
message: 'Accès refusé. Aucun jeton fourni.',
code: 401
-}
+};
exports.UNAUTHORIZED_INVALID_TOKEN = {
message: 'Accès refusé. Jeton invalide.',
code: 401
-}
+};
exports.MISSING_REQUIRED_PARAMETER = {
message: 'Paramètre requis manquant.',
code: 400
-}
+};
exports.MISSING_OIDC_PARAMETER = (name) => {
return {
@@ -21,116 +21,135 @@ exports.MISSING_OIDC_PARAMETER = (name) => {
exports.USER_ALREADY_EXISTS = {
message: 'L\'utilisateur existe déjà.',
- code: 400
-}
+ code: 409
+};
exports.LOGIN_CREDENTIALS_ERROR = {
message: 'L\'email et le mot de passe ne correspondent pas.',
code: 401
-}
+};
exports.GENERATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
- code: 400
-}
+ code: 500
+};
exports.UPDATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du mot de passe.',
- code: 400
-}
+ code: 500
+};
exports.DELETE_USER_ERROR = {
message: 'Une erreur s\'est produite lors de suppression de l\'utilisateur.',
- code: 400
-}
+ code: 500
+};
exports.IMAGE_NOT_FOUND = {
message: 'Nous n\'avons pas trouvé l\'image.',
code: 404
-}
+};
exports.QUIZ_NOT_FOUND = {
message: 'Aucun quiz portant cet identifiant n\'a été trouvé.',
code: 404
-}
+};
exports.QUIZ_ALREADY_EXISTS = {
message: 'Le quiz existe déjà.',
- code: 400
-}
+ code: 409
+};
exports.UPDATE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jour du quiz.',
- code: 400
-}
+ code: 500
+};
exports.DELETE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la suppression du quiz.',
- code: 400
-}
+ code: 500
+};
exports.GETTING_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la récupération du quiz.',
- code: 400
-}
+ code: 500
+};
exports.MOVING_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors du déplacement du quiz.',
- code: 400
-}
+ code: 500
+};
exports.DUPLICATE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la duplication du quiz.',
- code: 400
-}
+ code: 500
+};
exports.COPY_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la copie du quiz.',
- code: 400
-}
+ code: 500
+};
exports.FOLDER_NOT_FOUND = {
message: 'Aucun dossier portant cet identifiant n\'a été trouvé.',
code: 404
-}
+};
exports.FOLDER_ALREADY_EXISTS = {
message: 'Le dossier existe déjà.',
code: 409
-}
+};
exports.UPDATE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jour du dossier.',
- code: 400
-}
+ code: 500
+};
exports.DELETE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la suppression du dossier.',
- code: 400
-}
+ code: 500
+};
exports.GETTING_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la récupération du dossier.',
- code: 400
-}
+ code: 500
+};
exports.MOVING_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors du déplacement du dossier.',
- code: 400
-}
+ code: 500
+};
exports.DUPLICATE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la duplication du dossier.',
- code: 400
-}
+ code: 500
+};
exports.COPY_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la copie du dossier.',
- code: 400
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ code: 500
+};
+exports.ROOM_NOT_FOUND = {
+ message: "Aucune salle trouvée avec cet identifiant.",
+ code: 404
+};
+exports.ROOM_ALREADY_EXISTS = {
+ message: 'Une salle avec ce nom existe déjà',
+ code: 409
+};
+exports.UPDATE_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors de la mise à jour de la salle.',
+ code: 500
+};
+exports.DELETE_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors de la suppression de la salle.',
+ code: 500
+};
+exports.GETTING_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors de la récupération de la salle.',
+ code: 500
+};
+exports.MOVING_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors du déplacement de la salle.',
+ code: 500
+};
+exports.DUPLICATE_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors de la duplication de la salle.',
+ code: 500
+};
+exports.COPY_ROOM_ERROR = {
+ message: 'Une erreur s\'est produite lors de la copie de la salle.',
+ code: 500
+};
exports.NOT_IMPLEMENTED = {
- message: 'Route not implemented yet!',
- code: 400
-}
+ message: "Route non encore implémentée. Fonctionnalité en cours de développement.",
+ code: 501
+};
+
+
// static ok(res, results) {200
diff --git a/server/controllers/room.js b/server/controllers/room.js
new file mode 100644
index 0000000..64643a4
--- /dev/null
+++ b/server/controllers/room.js
@@ -0,0 +1,219 @@
+const AppError = require("../middleware/AppError.js");
+const {
+ MISSING_REQUIRED_PARAMETER,
+ ROOM_NOT_FOUND,
+ ROOM_ALREADY_EXISTS,
+ GETTING_ROOM_ERROR,
+ DELETE_ROOM_ERROR,
+ UPDATE_ROOM_ERROR,
+} = require("../constants/errorCodes");
+
+class RoomsController {
+ constructor(roomsModel) {
+ this.rooms = roomsModel;
+ this.getRoomTitle = this.getRoomTitle.bind(this);
+ }
+
+ create = async (req, res, next) => {
+ try {
+ if (!req.user || !req.user.userId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const { title } = req.body;
+ if (!title) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const normalizedTitle = title.toUpperCase().trim();
+
+ const roomExists = await this.rooms.roomExists(normalizedTitle, req.user.userId);
+ if (roomExists) {
+ throw new AppError(ROOM_ALREADY_EXISTS);
+ }
+
+ const result = await this.rooms.create(normalizedTitle, req.user.userId);
+
+ return res.status(201).json({
+ message: "Room créée avec succès.",
+ roomId: result.insertedId,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+
+ getUserRooms = async (req, res, next) => {
+ try {
+ const rooms = await this.rooms.getUserRooms(req.user.userId);
+
+ if (!rooms) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ return res.status(200).json({
+ data: rooms,
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ getRoomContent = async (req, res, next) => {
+ try {
+ const { roomId } = req.params;
+
+ if (!roomId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+ const content = await this.rooms.getContent(roomId);
+
+ if (!content) {
+ throw new AppError(GETTING_ROOM_ERROR);
+ }
+
+ return res.status(200).json({
+ data: content,
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ delete = async (req, res, next) => {
+ try {
+ const { roomId } = req.params;
+
+ if (!roomId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const owner = await this.rooms.getOwner(roomId);
+
+ if (owner != req.user.userId) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ const result = await this.rooms.delete(roomId);
+
+ if (!result) {
+ throw new AppError(DELETE_ROOM_ERROR);
+ }
+
+ return res.status(200).json({
+ message: "Salle supprimé avec succès.",
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ rename = async (req, res, next) => {
+ try {
+ const { roomId, newTitle } = req.body;
+
+ if (!roomId || !newTitle) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const owner = await this.rooms.getOwner(roomId);
+
+ if (owner != req.user.userId) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ const exists = await this.rooms.roomExists(newTitle, req.user.userId);
+
+ if (exists) {
+ throw new AppError(ROOM_ALREADY_EXISTS);
+ }
+
+ const result = await this.rooms.rename(roomId, req.user.userId, newTitle);
+
+ if (!result) {
+ throw new AppError(UPDATE_ROOM_ERROR);
+ }
+
+ return res.status(200).json({
+ message: "Salle mis � jours avec succ�s.",
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ getRoomById = async (req, res, next) => {
+ try {
+ const { roomId } = req.params;
+
+ if (!roomId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ // Is this room mine
+ const owner = await this.rooms.getOwner(roomId);
+
+ if (owner != req.user.userId) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ const room = await this.rooms.getRoomById(roomId);
+
+ if (!room) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ return res.status(200).json({
+ data: room,
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+ getRoomTitle = async (req, res, next) => {
+ try {
+ const { roomId } = req.params;
+
+ if (!roomId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const room = await this.rooms.getRoomById(roomId);
+
+ if (room instanceof Error) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ return res.status(200).json({ title: room.title });
+ } catch (error) {
+ return next(error);
+ }
+ };
+
+ getRoomTitleByUserId = async (req, res, next) => {
+ try {
+ const { userId } = req.params;
+
+ if (!userId) {
+ throw new AppError(MISSING_REQUIRED_PARAMETER);
+ }
+
+ const rooms = await this.rooms.getUserRooms(userId);
+
+ if (!rooms || rooms.length === 0) {
+ throw new AppError(ROOM_NOT_FOUND);
+ }
+
+ const roomTitles = rooms.map((room) => room.title);
+
+ return res.status(200).json({
+ titles: roomTitles,
+ });
+ } catch (error) {
+ return next(error);
+ }
+ };
+}
+
+module.exports = RoomsController;
diff --git a/server/middleware/AppError.js b/server/middleware/AppError.js
index 58a4d83..b4ed258 100644
--- a/server/middleware/AppError.js
+++ b/server/middleware/AppError.js
@@ -1,9 +1,10 @@
class AppError extends Error {
constructor (errorCode) {
- super(errorCode.message)
- this.statusCode = errorCode.code;
- this.isOperational = true; // Optional: to distinguish operational errors from programming errors
+ super(errorCode.message);
+ this.statusCode = errorCode.code;
+ this.isOperational = true;
}
-}
-
-module.exports = AppError;
+ }
+
+ module.exports = AppError;
+
\ No newline at end of file
diff --git a/server/middleware/errorHandler.js b/server/middleware/errorHandler.js
index 73c3add..e595377 100644
--- a/server/middleware/errorHandler.js
+++ b/server/middleware/errorHandler.js
@@ -2,19 +2,20 @@ const AppError = require("./AppError");
const fs = require('fs');
const errorHandler = (error, req, res, _next) => {
+ res.setHeader('Cache-Control', 'no-store');
if (error instanceof AppError) {
- logError(error);
- return res.status(error.statusCode).json({
- error: error.message
- });
+ return res.status(error.statusCode).json({
+ message: error.message,
+ code: error.statusCode
+ });
}
logError(error.stack);
return res.status(505).send("Oups! We screwed up big time. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻");
-}
+ };
-const logError = (error) => {
+ const logError = (error) => {
const time = new Date();
var log_file = fs.createWriteStream(__dirname + '/../debug.log', {flags : 'a'});
log_file.write(time + '\n' + error + '\n\n');
diff --git a/server/models/room.js b/server/models/room.js
new file mode 100644
index 0000000..1234d07
--- /dev/null
+++ b/server/models/room.js
@@ -0,0 +1,173 @@
+const ObjectId = require("mongodb").ObjectId;
+
+class Rooms
+{
+ constructor(db)
+ {
+ this.db = db;
+ }
+
+ async create(title, userId) {
+ if (!title || !userId) {
+ throw new Error("Missing required parameter(s)");
+ }
+
+ const exists = await this.roomExists(title, userId);
+ if (exists) {
+ throw new Error("Room already exists");
+ }
+
+ await this.db.connect();
+ const conn = this.db.getConnection();
+ const roomsCollection = conn.collection("rooms");
+
+ const newRoom = {
+ userId: userId,
+ title: title,
+ created_at: new Date(),
+ };
+
+ const result = await roomsCollection.insertOne(newRoom);
+
+ return result.insertedId;
+ }
+
+ async getUserRooms(userId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const result = await roomsCollection.find({ userId: userId }).toArray();
+
+ return result;
+ }
+
+ async getOwner(roomId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const room = await roomsCollection.findOne({
+ _id: ObjectId.createFromHexString(roomId),
+ });
+
+ return room.userId;
+ }
+
+ async getContent(roomId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+ const roomsCollection = conn.collection("rooms");
+ if (!ObjectId.isValid(roomId))
+ {
+ return null; // Évite d'envoyer une requête invalide
+ }
+
+ const result = await roomsCollection.findOne({ _id: new ObjectId(roomId) });
+
+ return result;
+ }
+
+ async delete(roomId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const roomResult = await roomsCollection.deleteOne({
+ _id: ObjectId.createFromHexString(roomId),
+ });
+
+ if (roomResult.deletedCount != 1) return false;
+
+ return true;
+ }
+
+ async rename(roomId, userId, newTitle)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const existingRoom = await roomsCollection.findOne({
+ title: newTitle,
+ userId: userId,
+ });
+
+ if (existingRoom)
+ throw new Error(`Room with name '${newTitle}' already exists.`);
+
+ const result = await roomsCollection.updateOne(
+ { _id: ObjectId.createFromHexString(roomId), userId: userId },
+ { $set: { title: newTitle } }
+ );
+
+ if (result.modifiedCount != 1) return false;
+
+ return true;
+ }
+
+ async roomExists(title, userId)
+ {
+ try
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+ const existingRoom = await conn.collection("rooms").findOne({
+ title: title.toUpperCase(),
+ userId: userId,
+ });
+ return !!existingRoom;
+ } catch (error)
+ {
+ throw new Error(`Database error (${error})`);
+ }
+ }
+ async getRoomById(roomId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const room = await roomsCollection.findOne({
+ _id: ObjectId.createFromHexString(roomId),
+ });
+
+ if (!room) throw new Error(`Room ${roomId} not found`, 404);
+
+ return room;
+ }
+
+ async getRoomWithContent(roomId)
+ {
+ const room = await this.getRoomById(roomId);
+
+ const content = await this.getContent(roomId);
+
+ return {
+ ...room,
+ content: content,
+ };
+ }
+ async getRoomTitleByUserId(userId)
+ {
+ await this.db.connect();
+ const conn = this.db.getConnection();
+
+ const roomsCollection = conn.collection("rooms");
+
+ const rooms = await roomsCollection.find({ userId: userId }).toArray();
+
+ return rooms.map((room) => room.title);
+ }
+}
+
+module.exports = Rooms;
diff --git a/server/models/users.js b/server/models/users.js
index d048d9f..2be1aa3 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -54,6 +54,7 @@ class Users {
}
async login(email, password) {
+ console.log(`models/users: login: email: ${email}, password: ${password}`);
try {
await this.db.connect();
const conn = this.db.getConnection();
@@ -74,7 +75,7 @@ class Users {
error.statusCode = 401;
throw error;
}
-
+ console.log(`models/users: login: FOUND user: ${JSON.stringify(user)}`);
return user;
} catch (error) {
console.error(error);
diff --git a/server/routers/room.js b/server/routers/room.js
new file mode 100644
index 0000000..2c24453
--- /dev/null
+++ b/server/routers/room.js
@@ -0,0 +1,18 @@
+const express = require('express');
+const router = express.Router();
+const jwt = require('../middleware/jwtToken.js');
+const rooms = require('../app.js').rooms;
+const asyncHandler = require('./routerUtils.js');
+
+router.post("/create", jwt.authenticate, asyncHandler(rooms.create));
+router.post("/roomExists", jwt.authenticate, asyncHandler(rooms.roomExists));
+router.get("/getUserRooms", jwt.authenticate, asyncHandler(rooms.getUserRooms));
+router.get('/getRoomTitle/:roomId', jwt.authenticate, asyncHandler(rooms.getRoomTitle));
+router.get('/getRoomTitleByUserId/:userId', jwt.authenticate, asyncHandler(rooms.getRoomTitleByUserId));
+router.get("/getRoomContent/:roomId", jwt.authenticate, asyncHandler(rooms.getRoomContent));
+router.delete("/delete/:roomId", jwt.authenticate, asyncHandler(rooms.delete));
+router.put("/rename", jwt.authenticate, asyncHandler(rooms.rename));
+
+module.exports = router;
+
+module.exports.rooms = rooms;
\ No newline at end of file
diff --git a/server/socket/socket.js b/server/socket/socket.js
index fc21632..393135c 100644
--- a/server/socket/socket.js
+++ b/server/socket/socket.js
@@ -6,7 +6,7 @@ const setupWebsocket = (io) => {
io.on("connection", (socket) => {
if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
- console.log("Connection limit reached. Disconnecting client.");
+ console.log("socket.js: Connection limit reached. Disconnecting client.");
socket.emit(
"join-failure",
"Le nombre maximum de connexions a été atteint"
@@ -17,58 +17,67 @@ const setupWebsocket = (io) => {
totalConnections++;
console.log(
- "A user connected:",
+ "socket.js: A user connected:",
socket.id,
"| Total connections:",
totalConnections
);
socket.on("create-room", (sentRoomName) => {
+ console.log(`socket.js: Demande de création de salle avec le nom : ${sentRoomName}`);
+
if (sentRoomName) {
const roomName = sentRoomName.toUpperCase();
if (!io.sockets.adapter.rooms.get(roomName)) {
socket.join(roomName);
socket.emit("create-success", roomName);
+ console.log(`socket.js: Salle créée avec succès : ${roomName}`);
} else {
- socket.emit("create-failure");
- }
- } else {
- const roomName = generateRoomName();
- if (!io.sockets.adapter.rooms.get(roomName)) {
- socket.join(roomName);
- socket.emit("create-success", roomName);
- } else {
- socket.emit("create-failure");
+ socket.emit("create-failure", `La salle ${roomName} existe déjà.`);
+ console.log(`socket.js: Échec de création : ${roomName} existe déjà`);
}
}
+ reportSalles();
});
+
+ function reportSalles() {
+ console.log("socket.js: Salles existantes :", Array.from(io.sockets.adapter.rooms.keys()));
+ }
socket.on("join-room", ({ enteredRoomName, username }) => {
- if (io.sockets.adapter.rooms.has(enteredRoomName)) {
- const clientsInRoom =
- io.sockets.adapter.rooms.get(enteredRoomName).size;
+ const roomToCheck = enteredRoomName.toUpperCase();
+ console.log(
+ `socket.js: Requête de connexion : salle="${roomToCheck}", utilisateur="${username}"`
+ );
+ reportSalles();
+
+ if (io.sockets.adapter.rooms.has(roomToCheck)) {
+ console.log("socket.js: La salle existe");
+ const clientsInRoom = io.sockets.adapter.rooms.get(roomToCheck).size;
if (clientsInRoom <= MAX_USERS_PER_ROOM) {
+ console.log("socket.js: La salle n'est pas pleine avec ", clientsInRoom, " utilisateurs");
const newStudent = {
id: socket.id,
name: username,
answers: [],
};
- socket.join(enteredRoomName);
- socket
- .to(enteredRoomName)
- .emit("user-joined", newStudent);
- socket.emit("join-success");
+ socket.join(roomToCheck);
+ socket.to(roomToCheck).emit("user-joined", newStudent);
+ socket.emit("join-success", roomToCheck);
} else {
+ console.log("socket.js: La salle est pleine avec ", clientsInRoom, " utilisateurs");
socket.emit("join-failure", "La salle est remplie");
}
} else {
+ console.log("socket.js: La salle n'existe pas");
socket.emit("join-failure", "Le nom de la salle n'existe pas");
}
});
socket.on("next-question", ({ roomName, question }) => {
- // console.log("next-question", roomName, question);
+ console.log("socket.js: next-question", roomName, question);
+ console.log("socket.js: rediffusion de la question", question);
socket.to(roomName).emit("next-question", question);
});
@@ -77,22 +86,26 @@ const setupWebsocket = (io) => {
});
socket.on("end-quiz", ({ roomName }) => {
+ console.log("socket.js: end-quiz", roomName);
socket.to(roomName).emit("end-quiz");
+ io.sockets.adapter.rooms.delete(roomName);
+ reportSalles();
});
socket.on("message", (data) => {
- console.log("Received message from", socket.id, ":", data);
+ console.log("socket.js: Received message from", socket.id, ":", data);
});
socket.on("disconnect", () => {
totalConnections--;
console.log(
- "A user disconnected:",
+ "socket.js: A user disconnected:",
socket.id,
"| Total connections:",
totalConnections
);
-
+ reportSalles();
+
for (const [room] of io.sockets.adapter.rooms) {
if (room !== socket.id) {
io.to(room).emit("user-disconnected", socket.id);
@@ -109,17 +122,6 @@ const setupWebsocket = (io) => {
});
});
});
-
- 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 };