Compare commits

...

60 commits

Author SHA1 Message Date
NouhailaAater
9d24507f41
Merge pull request #259 from ets-cfuhrman-pfe/feature/add-room-collection
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 20s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 19s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / lint-and-tests (client) (push) Failing after 59s
Tests / lint-and-tests (server) (push) Failing after 50s
User Story 34 : Numéro de salle permanent par professeur
2025-03-06 15:04:28 -05:00
C. Fuhrman
55156de5f9 réintialiser n'est pas fonctionnel 2025-03-06 14:51:51 -05:00
C. Fuhrman
b6be822720 Créer compte fonctionne en dev (simpleauth) 2025-03-06 14:48:07 -05:00
C. Fuhrman
9f4414f68c Supporter simpleauth lorsque mode development 2025-03-06 12:03:18 -05:00
C. Fuhrman
5760469e60 documente un test qui laisse "open handles" dans jest (connexion BD) 2025-03-06 11:57:20 -05:00
C. Fuhrman
93c9f10197 hover gênant (affecte tous les boutons de l'App) 2025-03-06 09:48:06 -05:00
C. Fuhrman
65d2121f79 Erreur de merge 2025-03-06 09:47:27 -05:00
C. Fuhrman
9aab6fa610 simpleauth pour MODE=development 2025-03-06 00:37:22 -05:00
C. Fuhrman
3e1e3c7f0d Corrige problèmes identifiés par eslint 2025-03-06 00:30:27 -05:00
NouhailaAater
752e168a53 Remove unused import 2025-03-04 16:48:48 -05:00
NouhailaAater
4a971bba76 fix ESLint Jest plugin issue 2025-03-04 16:45:12 -05:00
NouhailaAater
8a740beab8 Correction tests 2025-03-04 16:43:11 -05:00
NouhailaAater
cd13c5f798 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into feature/add-room-collection 2025-03-04 16:17:17 -05:00
NouhailaAater
5ebe86a471 Merge branch 'main' into feature/add-room-collection 2025-03-02 03:18:41 -05:00
NouhailaAater
bd1ea4c2f6 Correction room controller 2025-03-02 02:56:49 -05:00
NouhailaAater
545d6551f6 Correction 2025-03-02 01:56:37 -05:00
NouhailaAater
9ece28a86b mise à jour des errorCodes 2025-03-02 01:21:32 -05:00
C. Fuhrman
bc01dd519f Utilise npm install plutôt que npm ci (workaround temporaire) 2025-02-28 11:13:18 -05:00
C. Fuhrman
31f94a93d9 sync package-lock.json (après maj de node à 20.18.3) 2025-02-28 10:55:46 -05:00
C. Fuhrman
4828c0b578 corriger package-lock.json 2025-02-28 10:43:59 -05:00
C. Fuhrman
833c953efb update vite (vulnérabilité) 2025-02-28 10:16:54 -05:00
C. Fuhrman
4b36b11957 npm-check -u (client) 2025-02-28 10:05:38 -05:00
NouhailaAater
e4739468ba fix tests 2025-02-28 03:16:54 -05:00
NouhailaAater
323efca180 fix tests 2025-02-28 03:13:28 -05:00
NouhailaAater
eb3e06f5d3 Rooms and selection update automatically without a refresh 2025-02-28 03:07:29 -05:00
NouhailaAater
f68806cfd1 Automatically select the newly created room 2025-02-28 02:48:57 -05:00
NouhailaAater
2fc922d01f Fix existing room error handling with AppError 2025-02-28 02:28:47 -05:00
C. Fuhrman
4cc6ee79e4 Permet d'ajouter une première salle
Nom de salle toujours en majuscules (bd)
2025-02-27 16:07:00 -05:00
C. Fuhrman
70d6d1bc56 Nom de la salle doiit être un majuscule
Supprimer des créations de socket/salle superflues
Suppression (nettoyage) des salles et socket plus robuste
diminue le bazaar de useEffect (!)
2025-02-27 15:49:09 -05:00
NouhailaAater
38e366a7de Remove automatic room creation 2025-02-27 13:34:56 -05:00
C. Fuhrman
0d56fa246d AppErreur lancée par les contrôleurs, Erreur lancée par les modèles. 2025-02-27 09:06:58 -05:00
NouhailaAater
3855eca6c6 Fix ManageRoom tests 2025-02-27 02:17:54 -05:00
NouhailaAater
fc69a37e53 Fix tests to correctly handle AppError mock and instance checks 2025-02-27 01:03:56 -05:00
NouhailaAater
d2bf18b88d import appError dans le model 2025-02-27 00:43:20 -05:00
NouhailaAater
068f97ac47 test a jour 2025-02-26 15:28:00 -05:00
C. Fuhrman
cf1a5ae4a0 ajouter nom de la salle à la navigation manage-room 2025-02-26 14:38:36 -05:00
NouhailaAater
2c3c6eed90 Merge branch 'feature/add-room-collection' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into feature/add-room-collection 2025-02-26 14:07:30 -05:00
NouhailaAater
a99664d8ff traitement des erreurs et afficher un dialogue erreur si room existe 2025-02-26 14:07:18 -05:00
C. Fuhrman
784ac277d0 corriger tests brisés
supprimer tests de contenu de salle (copier-coller des tests de dossier je crois)
2025-02-26 13:06:03 -05:00
C. Fuhrman
d584374347 add-room est dans le menu déroulant plutôt que dans un bouton
renommer des variables (Dashboard fait beaucoup, ça prend des noms précis)
actualiser la liste des salles après add (bug)
2025-02-26 09:48:40 -05:00
C. Fuhrman
0bf2bf7747 select first room by default on dashboard 2025-02-26 09:31:20 -05:00
NouhailaAater
5436fc3a1f Correction if aucune salle est selectionner 2025-02-25 16:12:57 -05:00
NouhailaAater
9486eacc53 Correction test ManageRoom 2025-02-24 14:32:08 -05:00
NouhailaAater
162117a58a Correction test server 2025-02-24 05:10:57 -05:00
NouhailaAater
b5547cb100 Ensure room title uniqueness by normalizing case sensitivity 2025-02-24 04:09:55 -05:00
NouhailaAater
bf2e6502f3 Generate a random room name on the client side and set the first created room name as the default 2025-02-24 03:50:15 -05:00
NouhailaAater
94c728fa09 Debug join quiz 2025-02-24 03:29:36 -05:00
NouhailaAater
39ce176ae7 Ajout de RoomContext et deplacement de choix/creation de liste room dans le dashboard 2025-02-23 22:40:46 -05:00
NouhailaAater
81c530eac6 correction rooms.test.js 2025-02-22 02:19:06 -05:00
NouhailaAater
9286fe6b9c Correction socket 2025-02-22 02:04:49 -05:00
NouhailaAater
c9b76df2cd correction user-joined 2025-02-22 01:23:42 -05:00
NouhailaAater
5c736f4ca0 branch 'main' into feature/add-room-collection 2025-02-22 00:24:04 -05:00
NouhailaAater
562fdfb791 ajout tests 2025-02-22 00:20:37 -05:00
NouhailaAater
5f87aa1b7a Correction ajout salle 2025-02-21 22:51:42 -05:00
NouhailaAater
fd9f04d116 Correction 2025-02-21 20:15:32 -05:00
NouhailaAater
29924a6786 Ajout boite dialog 2025-02-20 02:17:24 -05:00
NouhailaAater
b42cbb3647 ajout d'une nouvelle salle 2025-02-20 01:37:25 -05:00
NouhailaAater
c3e56502d8 Utilisation liste room 2025-02-20 00:37:01 -05:00
NouhailaAater
a9743ad5d4 Affichage de la liste des salles 2025-02-20 00:19:32 -05:00
NouhailaAater
3c7e4c68e7 ajout room collection 2025-02-19 18:56:37 -05:00
43 changed files with 3221 additions and 2254 deletions

View file

@ -35,6 +35,11 @@ jobs:
working-directory: ${{ matrix.directory }}
timeout-minutes: 5
run: |
echo "Installing dependencies..."
npm install
echo "Running ESLint..."
npx eslint .
echo "Running tests..."
echo "::group::Installing dependencies for ${{ matrix.directory }}"
npm ci
echo "::endgroup::"

2
.gitignore vendored
View file

@ -131,4 +131,4 @@ launch.json
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
db-backup/
db-backup/

View file

@ -2,7 +2,6 @@ import react from "eslint-plugin-react";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
import globals from "globals";
import pluginJs from "@eslint/js";
import jest from "eslint-plugin-jest";
import reactRefresh from "eslint-plugin-react-refresh";
import unusedImports from "eslint-plugin-unused-imports";

3085
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"dev": "cross-env MODE=development VITE_BACKEND_URL=http://localhost:4400 vite --host",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
@ -12,65 +12,65 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^6.4.1",
"@mui/icons-material": "^6.4.6",
"@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0",
"@mui/material": "^6.4.6",
"@types/uuid": "^9.0.7",
"axios": "^1.6.7",
"axios": "^1.8.1",
"dompurify": "^3.2.3",
"esbuild": "^0.23.1",
"esbuild": "^0.25.0",
"gift-pegjs": "^2.0.0-beta.1",
"jest-environment-jsdom": "^29.7.0",
"jwt-decode": "^4.0.0",
"katex": "^0.16.11",
"marked": "^14.1.2",
"nanoid": "^5.0.2",
"nanoid": "^5.1.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-modal": "^3.16.1",
"react-modal": "^3.16.3",
"react-router-dom": "^6.26.2",
"remark-math": "^6.0.0",
"socket.io-client": "^4.7.2",
"ts-node": "^10.9.1",
"uuid": "^9.0.1",
"vite-plugin-checker": "^0.8.0"
"vite-plugin-checker": "^0.9.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.23.3",
"@eslint/js": "^9.18.0",
"@eslint/js": "^9.21.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.13",
"@types/node": "^22.5.5",
"@types/node": "^22.13.5",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-latex": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.18.0",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.12",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.5",
"ts-jest": "^29.2.6",
"typescript": "^5.7.3",
"typescript-eslint": "^8.25.0",
"vite": "^6.2.0",
"vite-plugin-environment": "^1.1.3"
}
}

View file

@ -77,7 +77,7 @@ const App: React.FC = () => {
element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
/>
<Route
path="/teacher/manage-room/:id"
path="/teacher/manage-room/:quizId/:roomName"
element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
/>

View file

@ -0,0 +1,6 @@
export interface RoomType {
_id: string;
userId: string;
title: string;
created_at: string;
}

View file

@ -0,0 +1,17 @@
import { RoomType } from "../../Types/RoomType";
const room: RoomType = {
_id: '123',
userId: '456',
title: 'Test Room',
created_at: '2025-02-21T00:00:00Z'
};
describe('RoomType', () => {
test('creates a room with _id, userId, title, and created_at', () => {
expect(room._id).toBe('123');
expect(room.userId).toBe('456');
expect(room.title).toBe('Test Room');
expect(room.created_at).toBe('2025-02-21T00:00:00Z');
});
});

View file

@ -88,10 +88,10 @@ describe('LiveResultsTable', () => {
//50% because only one of the two questions have been answered (getALLByText, because there are a value 50% for the %reussite de la question
// and a second one for the student grade)
const gradeElements = screen.getAllByText('50 %');
expect(gradeElements.length).toBe(2);
expect(gradeElements).toHaveLength(2);
const gradeElements2 = screen.getAllByText('0 %');
expect(gradeElements2.length).toBe(2); });
expect(gradeElements2).toHaveLength(2); });
test('calculates and displays class average', () => {
render(
@ -107,4 +107,4 @@ describe('LiveResultsTable', () => {
//1 good answer out of 4 possible good answers (the second question has not been answered)
expect(screen.getByText('25 %')).toBeInTheDocument();
});
});
});

View file

@ -90,6 +90,6 @@ describe('LiveResultsTableBody', () => {
/>
);
expect(screen.getAllByText('******').length).toBe(2);
expect(screen.getAllByText('******')).toHaveLength(2);
});
});
});

View file

@ -10,14 +10,15 @@ describe('StudentWaitPage Component', () => {
{ id: '1', name: 'User1', answers: new Array<Answer>() },
{ id: '2', name: 'User2', answers: new Array<Answer>() },
{ id: '3', name: 'User3', answers: new Array<Answer>() },
];
];
const mockProps = {
const mockProps = {
students: mockUsers,
launchQuiz: jest.fn(),
roomName: 'Test Room',
setQuizMode: jest.fn(),
};
setIsRoomSelectionVisible: jest.fn()
};
test('renders StudentWaitPage with correct content', () => {
render(<StudentWaitPage {...mockProps} />);
@ -28,16 +29,15 @@ describe('StudentWaitPage Component', () => {
expect(launchButton).toBeInTheDocument();
mockUsers.forEach((user) => {
expect(screen.getByText(user.name)).toBeInTheDocument();
expect(screen.getByText(user.name)).toBeInTheDocument();
});
});
});
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
render(<StudentWaitPage {...mockProps} />);
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
})
});
});

View file

@ -8,6 +8,7 @@ import { QuizType } from 'src/Types/QuizType';
import webSocketService, { AnswerReceptionFromBackendType } from 'src/services/WebsocketService';
import ApiService from 'src/services/ApiService';
import { Socket } from 'socket.io-client';
import { RoomProvider } from 'src/pages/Teacher/ManageRoom/RoomContext';
jest.mock('src/services/WebsocketService');
jest.mock('src/services/ApiService');
@ -16,6 +17,7 @@ jest.mock('react-router-dom', () => ({
useNavigate: jest.fn(),
useParams: jest.fn(),
}));
jest.mock('src/pages/Teacher/ManageRoom/RoomContext');
const mockSocket = {
on: jest.fn(),
@ -33,7 +35,7 @@ const mockQuiz: QuizType = {
folderName: 'folder-name',
userId: 'user-id',
created_at: new Date(),
updated_at: new Date()
updated_at: new Date(),
};
const mockStudents: StudentType[] = [
@ -51,13 +53,18 @@ const mockAnswerData: AnswerReceptionFromBackendType = {
describe('ManageRoom', () => {
const navigate = jest.fn();
const useParamsMock = useParams as jest.Mock;
const mockSetSelectedRoom = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useNavigate as jest.Mock).mockReturnValue(navigate);
useParamsMock.mockReturnValue({ id: 'test-quiz-id' });
useParamsMock.mockReturnValue({ quizId: 'test-quiz-id', roomName: 'Test Room' });
(ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
(webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
(RoomProvider as jest.Mock).mockReturnValue({
selectedRoom: { id: '1', title: 'Test Room' },
setSelectedRoom: mockSetSelectedRoom,
});
});
test('prepares to launch quiz and fetches quiz data', async () => {
@ -68,33 +75,36 @@ describe('ManageRoom', () => {
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
createSuccessCallback('Test Room');
});
await waitFor(() => {
expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
await waitFor(() => {
expect(screen.getByText('Test Quiz')).toBeInTheDocument();
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
const roomHeader = document.querySelector('h1');
expect(roomHeader).toHaveTextContent('Salle : TEST ROOM');
expect(screen.getByText('0/60')).toBeInTheDocument();
expect(screen.getByText('Question 1/2')).toBeInTheDocument();
});
});
test('handles create-success event', async () => {
await act(async () => {
render(
@ -106,11 +116,11 @@ describe('ManageRoom', () => {
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
createSuccessCallback('Test Room');
});
await waitFor(() => {
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
expect(screen.getByText(/Salle\s*:\s*Test Room/i)).toBeInTheDocument();
});
});
@ -125,7 +135,7 @@ describe('ManageRoom', () => {
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
createSuccessCallback('Test Room');
});
await act(async () => {
@ -153,47 +163,6 @@ describe('ManageRoom', () => {
});
});
test('handles submit-answer-room event', async () => {
const consoleSpy = jest.spyOn(console, 'log');
await act(async () => {
render(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
await act(async () => {
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
userJoinedCallback(mockStudents[0]);
});
await act(async () => {
const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1];
submitAnswerCallback(mockAnswerData);
});
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith('Received answer from Student 1 for question 1: Answer1');
});
consoleSpy.mockRestore();
});
test('handles next question', async () => {
await act(async () => {
render(
@ -202,29 +171,30 @@ describe('ManageRoom', () => {
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
createSuccessCallback('Test Room');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
const nextQuestionButton = screen.getByText('Prochaine question');
fireEvent.click(screen.getByText('Lancer'));
fireEvent.click(screen.getByText('Rythme du professeur'));
fireEvent.click(screen.getAllByText('Lancer')[1]);
await waitFor(() => {
screen.debug();
});
const nextQuestionButton = await screen.findByRole('button', { name: /Prochaine question/i });
expect(nextQuestionButton).toBeInTheDocument();
fireEvent.click(nextQuestionButton);
await waitFor(() => {
expect(screen.getByText('Question 2/2')).toBeInTheDocument();
});
});
test('handles disconnect', async () => {
await act(async () => {
render(
@ -236,7 +206,7 @@ describe('ManageRoom', () => {
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
createSuccessCallback('Test Room');
});
const disconnectButton = screen.getByText('Quitter');
@ -250,4 +220,77 @@ describe('ManageRoom', () => {
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
});
});
});
test('handles submit-answer-room event', async () => {
const consoleSpy = jest.spyOn(console, 'log');
await act(async () => {
render(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
await act(async () => {
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
userJoinedCallback(mockStudents[0]);
});
await act(async () => {
const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1];
submitAnswerCallback(mockAnswerData);
});
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
'Received answer from Student 1 for question 1: Answer1'
);
});
consoleSpy.mockRestore();
});
test('vide la liste des étudiants après déconnexion', async () => {
await act(async () => {
render(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room');
});
await act(async () => {
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
userJoinedCallback(mockStudents[0]);
});
const disconnectButton = screen.getByText('Quitter');
fireEvent.click(disconnectButton);
const confirmButton = screen.getAllByText('Confirmer');
fireEvent.click(confirmButton[1]);
await waitFor(() => {
expect(screen.queryByText('Student 1')).not.toBeInTheDocument();
});
});
});

View file

@ -37,9 +37,10 @@ describe('WebSocketService', () => {
});
test('createRoom should emit create-room event', () => {
const roomName = 'Test Room';
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
WebsocketService.createRoom();
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
WebsocketService.createRoom(roomName);
expect(mockSocket.emit).toHaveBeenCalledWith('create-room', roomName);
});
test('nextQuestion should emit next-question event with correct parameters', () => {

View file

@ -16,8 +16,8 @@ function formatLatex(text: string): string {
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
katex.renderToString(inner, { displayMode: false })
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
console.log('Error rendering LaTeX (KaTeX):', error);
renderedText = text;
}

View file

@ -15,17 +15,21 @@ interface Props {
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const handleLaunchClick = () => {
setIsDialogOpen(true);
};
return (
<div className="wait">
<div className='button'>
<Button
variant="contained"
onClick={() => setIsDialogOpen(true)}
onClick={handleLaunchClick}
startIcon={<PlayArrow />}
fullWidth
sx={{ fontWeight: 600, fontSize: 20 }}
>
Lancer
Lancer
</Button>
</div>

View file

@ -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}`:''}` : ''

View file

@ -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;
}
}

View file

@ -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 = () => {
<div className="login-links">
<Link to="/resetPassword">
Réinitialiser le mot de passe
</Link>
{/* <Link to="/resetPassword"> */}
<del>Réinitialiser le mot de passe</del>
{/* </Link> */}
<Link to="/register">
Créer un compte

View file

@ -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}>
<TextField
type="number"
label="Numéro de la salle"
type="text"
label="Nom de la salle"
variant="outlined"
value={roomName}
onChange={(e) => 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}

View file

@ -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<boolean>(false);
const [folders, setFolders] = useState<FolderType[]>([]);
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
const [rooms, setRooms] = useState<RoomType[]>([]);
const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
const [newRoomTitle, setNewRoomTitle] = useState('');
// const { selectedRoom, selectRoom, createRoom } = useRooms();
const [selectedRoom, selectRoom] = useState<RoomType>(); // 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<HTMLSelectElement>) => {
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<HTMLSelectElement>) => {
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<HTMLInputElement>) => {
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 (
<div className="dashboard">
<div className="title">Tableau de bord</div>
<div className="roomSelection">
<label htmlFor="select-room">Sélectionner une salle: </label>
<select value={selectedRoom?._id || ''} onChange={(e) => handleSelectRoom(e)}>
<option value="" disabled>
-- Sélectionner une salle --
</option>
{rooms.map((room) => (
<option key={room._id} value={room._id}>
{room.title}
</option>
))}
<option value="add-room">Ajouter salle</option>
</select>
</div>
{selectedRoom && (
<div className="roomTitle">
<h2>Salle sélectionnée: {selectedRoom.title}</h2>
</div>
)}
<Dialog open={openAddRoomDialog} onClose={() => setOpenAddRoomDialog(false)}>
<DialogTitle>Créer une nouvelle salle</DialogTitle>
<DialogContent>
<TextField
value={newRoomTitle}
onChange={(e) => setNewRoomTitle(e.target.value.toUpperCase())}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddRoomDialog(false)}>Annuler</Button>
<Button onClick={handleCreateRoom}>Créer</Button>
</DialogActions>
</Dialog>
<Dialog open={showErrorDialog} onClose={() => setShowErrorDialog(false)}>
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
<DialogContentText>{errorMessage}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowErrorDialog(false)}>Fermer</Button>
</DialogActions>
</Dialog>
<div className="search-bar">
<TextField
onChange={handleSearch}
@ -389,8 +491,8 @@ const Dashboard: React.FC = () => {
/>
</div>
<div className='folder'>
<div className='select'>
<div className="folder">
<div className="select">
<NativeSelect
id="select-folder"
color="primary"
@ -400,48 +502,65 @@ const Dashboard: React.FC = () => {
<option value=""> Tous les dossiers... </option>
{folders.map((folder: FolderType) => (
<option value={folder._id} key={folder._id}> {folder.title} </option>
<option value={folder._id} key={folder._id}>
{' '}
{folder.title}{' '}
</option>
))}
</NativeSelect>
</div>
<div className='actions'>
<div className="actions">
<Tooltip title="Ajouter dossier" placement="top">
<IconButton
color="primary"
onClick={handleCreateFolder}
> <Add /> </IconButton>
<IconButton color="primary" onClick={handleCreateFolder}>
{' '}
<Add />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Renommer dossier" placement="top">
<div>
<IconButton
color="primary"
onClick={handleRenameFolder}
disabled={selectedFolderId == ''} // cannot action on all
> <Edit /> </IconButton>
>
{' '}
<Edit />{' '}
</IconButton>
</div>
</Tooltip>
<Tooltip title="Dupliquer dossier" placement="top">
<div>
<IconButton
color="primary"
onClick={handleDuplicateFolder}
disabled={selectedFolderId == ''} // cannot action on all
> <FolderCopy /> </IconButton>
>
{' '}
<FolderCopy />{' '}
</IconButton>
</div>
</Tooltip>
<Tooltip title="Supprimer dossier" placement="top">
<div>
<IconButton
aria-label="delete"
color="primary"
onClick={handleDeleteFolder}
disabled={selectedFolderId == ''} // cannot action on all
> <DeleteOutline /> </IconButton>
>
{' '}
<DeleteOutline />{' '}
</IconButton>
</div>
</Tooltip>
</div>
</div>
<div className='ajouter'>
<div className="ajouter">
<Button
variant="outlined"
color="primary"
@ -459,47 +578,59 @@ const Dashboard: React.FC = () => {
>
Import
</Button>
</div>
<div className='list'>
{Object.keys(quizzesByFolder).map(folderName => (
<CustomCard key={folderName} className='folder-card'>
<div className='folder-tab'>{folderName}</div>
<div className="list">
{Object.keys(quizzesByFolder).map((folderName) => (
<CustomCard key={folderName} className="folder-card">
<div className="folder-tab">{folderName}</div>
<CardContent>
{quizzesByFolder[folderName].map((quiz: QuizType) => (
<div className='quiz' key={quiz._id}>
<div className='title'>
<div className="quiz" key={quiz._id}>
<div className="title">
<Tooltip title="Lancer quiz" placement="top">
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
</Button>
<div>
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{`${quiz.title} (${quiz.content.length} question${
quiz.content.length > 1 ? 's' : ''
})`}
</Button>
</div>
</Tooltip>
</div>
<div className='actions'>
<div className="actions">
<Tooltip title="Télécharger quiz" placement="top">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
> <FileDownload /> </IconButton>
>
{' '}
<FileDownload />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
> <Edit /> </IconButton>
>
{' '}
<Edit />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
> <ContentCopy /> </IconButton>
>
{' '}
<ContentCopy />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Supprimer quiz" placement="top">
@ -507,14 +638,20 @@ const Dashboard: React.FC = () => {
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
>
{' '}
<DeleteOutline />{' '}
</IconButton>
</Tooltip>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
>
{' '}
<Share />{' '}
</IconButton>
</Tooltip>
</div>
</div>
@ -529,7 +666,6 @@ const Dashboard: React.FC = () => {
handleOnImport={handleOnImport}
selectedFolder={selectedFolderId}
/>
</div>
);
};
@ -542,4 +678,3 @@ function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName:
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
});
}

View file

@ -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<string>('');
const [socket, setSocket] = useState<Socket | null>(null);
const [students, setStudents] = useState<StudentType[]>([]);
const quizId = useParams<{ id: string }>();
const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null);
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(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<StudentType>());
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 (
<div className="center">
{!connectingError ? (
@ -425,47 +392,51 @@ const ManageRoom: React.FC = () => {
}
return (
<div className='room'>
<div className='roomHeader'>
<div className="room">
<h1>Salle : {formattedRoomName}</h1>
<div className="roomHeader">
<DisconnectButton
onReturn={handleReturn}
askConfirm
message={`Êtes-vous sûr de vouloir quitter?`} />
message={`Êtes-vous sûr de vouloir quitter?`}
/>
<div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
<div className='title'>Salle: {roomName}</div>
</div>
{quizStarted && (
<div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}>
<div
className="headerContent"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
}}
>
{(
<div
className="userCount subtitle smallText"
style={{ display: "flex", justifyContent: "flex-end" }}
>
<GroupIcon style={{ marginRight: '5px' }} />
{students.length}/60
</div>
)}
</div>
<div className='dumb'></div>
<div className="dumb"></div>
</div>
{/* the following breaks the css (if 'room' classes are nested) */}
<div className=''>
<div className="">
{quizQuestions ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div>
{!isNaN(Number(currentQuestion?.question.id)) && (
<strong className='number of questions'>
Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
<strong className="number of questions">
Question {Number(currentQuestion?.question.id)}/
{quizQuestions?.length}
</strong>
)}
{quizMode === 'teacher' && (
<div className="mb-1">
{/* <QuestionNavigation
currentQuestionId={Number(currentQuestion?.question.id)}
@ -474,12 +445,10 @@ const ManageRoom: React.FC = () => {
nextQuestion={nextQuestion}
/> */}
</div>
)}
<div className="mb-2 flex-column-wrapper">
<div className="preview-and-result-container">
{currentQuestion && (
<QuestionDisplay
showAnswer={false}
@ -494,42 +463,46 @@ const ManageRoom: React.FC = () => {
showSelectedQuestion={showSelectedQuestion}
students={students}
></LiveResultsComponent>
</div>
</div>
{quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div
className="questionNavigationButtons"
style={{ display: 'flex', justifyContent: 'center' }}
>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
<Button
onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
disabled={Number(currentQuestion?.question.id) <= 1}
>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button onClick={nextQuestion}
<Button
onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
disabled={
Number(currentQuestion?.question.id) >=
quizQuestions.length
}
>
Prochaine question
</Button>
</div>
</div>)}
</div>
)}
</div>
) : (
<StudentWaitPage
students={students}
launchQuiz={launchQuiz}
setQuizMode={setQuizMode}
/>
)}
</div>
</div>
);
};

View file

@ -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<RoomType[]>([]);
const [selectedRoom, setSelectedRoom] = useState<RoomType | null>(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 (
<RoomContext.Provider value={{ rooms, selectedRoom, selectRoom, createRoom }}>
{children}
</RoomContext.Provider>
);
};

View file

@ -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<void>;
};
export const RoomContext = createContext<RoomContextType | undefined>(undefined);
export const useRooms = () => {
const context = useContext(RoomContext);
if (!context) throw new Error('useRooms must be used within a RoomProvider');
return context;
};

View file

@ -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<any> {
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<any> {
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<any> {
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<any> {
}
}
//ROOM routes
public async getUserRooms(): Promise<RoomType[] | string> {
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<RoomType> {
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<string[] | string> {
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<string | string> {
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<string> {
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<string | string> {
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<string | string> {
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
/**

View file

@ -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;
export default authService;

View file

@ -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
);
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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),
});
});
});
});

View file

@ -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) => {

View file

@ -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);

View file

@ -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){

View file

@ -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;
module.exports = SimpleAuth;

View file

@ -0,0 +1,9 @@
{
"auth": {
"simpleauth": {
"enabled": true,
"name": "provider3",
"SESSION_SECRET": "your_session_secret"
}
}
}

View file

@ -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 = {};

View file

@ -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

219
server/controllers/room.js Normal file
View file

@ -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 <20> jours avec succ<63>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;

View file

@ -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;

View file

@ -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');

173
server/models/room.js Normal file
View file

@ -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;

View file

@ -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);

18
server/routers/room.js Normal file
View file

@ -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;

View file

@ -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 };