Merge remote-tracking branch 'prt/main' into PFEH25-merge-it2main

This commit is contained in:
Eddi3_As 2025-02-21 13:27:22 -05:00
commit 2c01badd76
87 changed files with 1959 additions and 3620 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Créez un rapport pour nous aider à améliorer / Create a report to help us improve
title: "[BUG] "
labels: bug
assignees: ''
---
**Décrivez le bug / Describe the bug**
Une description claire et concise du bug. / A clear and concise description of what the bug is.
**Pour reproduire / To Reproduce**
Étapes pour reproduire le comportement : / Steps to reproduce the behavior:
1. Aller à '...' / Go to '...'
2. Cliquer sur '...' / Click on '...'
3. Faites défiler jusqu'à '...' / Scroll down to '...'
4. Voir l'erreur / See error
**Comportement attendu / Expected behavior**
Une description claire et concise de ce que vous attendiez. / A clear and concise description of what you expected to happen.
**Captures d'écran / Screenshots**
Si applicable, ajoutez des captures d'écran pour aider à expliquer votre problème. / If applicable, add screenshots to help explain your problem.
**Ordinateur (veuillez compléter les informations suivantes) / Desktop (please complete the following information):**
- Système d'exploitation : [par exemple, Windows, macOS, Linux] / OS: [e.g. Windows, macOS, Linux]
- Navigateur : [par exemple, Chrome, Firefox, Safari] / Browser [e.g. Chrome, Firefox, Safari]
- Version : [par exemple, 22] / Version [e.g. 22]
**Smartphone (veuillez compléter les informations suivantes) / Smartphone (please complete the following information):**
- Appareil : [par exemple, iPhone6, Samsung Galaxy S10] / Device: [e.g. iPhone6, Samsung Galaxy S10]
- Système d'exploitation : [par exemple, iOS 14.4, Android 11] / OS: [e.g. iOS 14.4, Android 11]
- Navigateur : [par exemple, navigateur par défaut, Safari] / Browser [e.g. stock browser, Safari]
- Version : [par exemple, 22] / Version [e.g. 22]
**Contexte supplémentaire / Additional context**
Ajoutez tout autre contexte concernant le problème ici. / Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggérez une idée pour ce projet / Suggest an idea for this project
title: "[FEATURE] "
labels: enhancement
assignees: ''
---
**Votre demande de fonctionnalité est-elle liée à un problème ? Veuillez décrire. / Is your feature request related to a problem? Please describe.**
Une description claire et concise du problème. Par exemple, je suis toujours frustré lorsque [...] / A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Décrivez la solution que vous souhaitez / Describe the solution you'd like**
Une description claire et concise de ce que vous voulez qu'il se passe. / A clear and concise description of what you want to happen.
**Décrivez les alternatives que vous avez envisagées / Describe alternatives you've considered**
Une description claire et concise de toute autre solution ou fonctionnalité que vous avez envisagée. / A clear and concise description of any alternative solutions or features you've considered.
**Contexte supplémentaire / Additional context**
Ajoutez tout autre contexte ou capture d'écran concernant la demande de fonctionnalité ici. / Add any other context or screenshots about the feature request here.

View file

@ -8,45 +8,29 @@ on:
branches: branches:
- main - main
env:
MONGO_URI: mongodb://localhost:27017
MONGO_DATABASE: evaluetonsavoir
jobs: jobs:
lint-and-tests: tests:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies, lint and Run Tests
run: |
echo "Installing dependencies..."
npm ci
echo "Running ESLint..."
npx eslint .
echo "Running tests..."
npm test
working-directory: ${{ matrix.directory }}
strategy: strategy:
matrix: matrix:
directory: [client, server] directory: [client, server]
fail-fast: false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: ${{ matrix.directory }}/package-lock.json
- name: Process ${{ matrix.directory }}
working-directory: ${{ matrix.directory }}
timeout-minutes: 5
run: |
echo "::group::Installing dependencies for ${{ matrix.directory }}"
npm ci
echo "::endgroup::"
echo "::group::Running ESLint"
npx eslint . || {
echo "ESLint failed with exit code $?"
exit 1
}
echo "::endgroup::"
echo "::group::Running Tests"
npm test
echo "::endgroup::"

3
.gitignore vendored
View file

@ -122,9 +122,6 @@ dist
# Stores VSCode versions used for testing VSCode extensions # Stores VSCode versions used for testing VSCode extensions
.vscode-test .vscode-test
.env
launch.json
# yarn v2 # yarn v2
.yarn/cache .yarn/cache
.yarn/unplugged .yarn/unplugged

View file

@ -1,33 +0,0 @@
{
"folders": [
{
"path": "."
},
{
"name": "server",
"path": "server"
},
{
"name": "client",
"path": "client"
}
],
"settings": {
"jest.disabledWorkspaceFolders": [
"EvalueTonSavoir"
]
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact"
],
// use the same eslint config as `npx eslint`
"eslint.experimental.useFlatConfig": true,
"eslint.nodePath": "./node_modules"
}

View file

@ -3,6 +3,7 @@ MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
Copyright (c) 2024 Gabriel Moisan-Matte, Mathieu Sévigny-Lavallée, Jerry Kwok Hiu Fung, Bruno Roesner, Florent Serres Copyright (c) 2024 Gabriel Moisan-Matte, Mathieu Sévigny-Lavallée, Jerry Kwok Hiu Fung, Bruno Roesner, Florent Serres
Copyright (c) 2025 Nouhaïla Aâter, Kendrick Chan Hing Wah, Philippe Côté, Edwin Stanley Lopez Andino, Ana Lucia Munteanu
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

19
client/.eslintrc.cjs Normal file
View file

@ -0,0 +1,19 @@
// eslint-disable-next-line no-undef
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View file

@ -1,4 +1,4 @@
/* eslint-disable no-undef */
module.exports = { module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'] presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
}; };

View file

@ -1,78 +1,29 @@
import react from "eslint-plugin-react";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
import globals from "globals"; import globals from "globals";
import pluginJs from "@eslint/js"; import pluginJs from "@eslint/js";
import jest from "eslint-plugin-jest"; import tseslint from "typescript-eslint";
import reactRefresh from "eslint-plugin-react-refresh"; import pluginReact from "eslint-plugin-react";
import unusedImports from "eslint-plugin-unused-imports";
import eslintComments from "eslint-plugin-eslint-comments";
/** @type {import('eslint').Linter.Config[]} */ /** @type {import('eslint').Linter.Config[]} */
export default [ export default [
{ {
ignores: ["node_modules", "dist/**/*"], files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: globals.browser,
}, },
{ rules: {
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"], "no-unused-vars": ["error", {
languageOptions: { "argsIgnorePattern": "^_",
parser: typescriptParser, "varsIgnorePattern": "^_",
parserOptions: { "caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
ecmaFeatures: { }],
jsx: true, },
}, settings: {
}, react: {
globals: { version: "detect", // Automatically detect the React version
...globals.serviceworker, },
...globals.browser, },
...globals.jest, },
...globals.node, pluginJs.configs.recommended,
process: "readonly", ...tseslint.configs.recommended,
}, pluginReact.configs.flat.recommended,
},
plugins: {
"@typescript-eslint": typescriptEslint,
react,
jest,
"react-refresh": reactRefresh,
"unused-imports": unusedImports,
"eslint-comments": eslintComments
},
rules: {
// Auto-fix unused variables
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
],
// Handle directive comments
"eslint-comments/no-unused-disable": "warn",
"eslint-comments/no-unused-enable": "warn",
// Jest configurations
"jest/valid-expect": ["error", { "alwaysAwait": true }],
"jest/prefer-to-have-length": "warn",
"jest/no-disabled-tests": "off",
"jest/no-focused-tests": "error",
"jest/no-identical-title": "error",
// React refresh
"react-refresh/only-export-components": ["warn", {
allowConstantExport: true
}],
},
settings: {
react: {
version: "detect",
},
},
}
]; ];

View file

@ -1,4 +1,4 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {

View file

@ -1,3 +1,3 @@
/* eslint-disable no-undef */
process.env.VITE_BACKEND_URL = 'http://localhost:4000/'; process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/'; process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';

394
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^6.1.0", "@mui/icons-material": "^6.4.1",
"@mui/lab": "^5.0.0-alpha.153", "@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0", "@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
@ -27,7 +27,6 @@
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"gift-pegjs": "^2.0.0-beta.1", "gift-pegjs": "^2.0.0-beta.1",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jwt-decode": "^4.0.0",
"katex": "^0.16.11", "katex": "^0.16.11",
"marked": "^14.1.2", "marked": "^14.1.2",
"nanoid": "^5.0.2", "nanoid": "^5.0.2",
@ -58,12 +57,9 @@
"@typescript-eslint/parser": "^8.5.0", "@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-react": "^7.37.3", "eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912", "eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.12", "eslint-plugin-react-refresh": "^0.4.12",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^15.14.0", "globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useEffect, useState } from 'react'; // App.tsx
import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
// Page main // Page main
import Home from './pages/Home/Home'; import Home from './pages/Home/Home';
@ -8,55 +8,37 @@ import Home from './pages/Home/Home';
// Pages espace enseignant // Pages espace enseignant
import Dashboard from './pages/Teacher/Dashboard/Dashboard'; import Dashboard from './pages/Teacher/Dashboard/Dashboard';
import Share from './pages/Teacher/Share/Share'; import Share from './pages/Teacher/Share/Share';
import Register from './pages/AuthManager/providers/SimpleLogin/Register'; import Login from './pages/Teacher/Login/Login';
import ResetPassword from './pages/AuthManager/providers/SimpleLogin/ResetPassword'; import Register from './pages/Teacher/Register/Register';
import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword';
import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom'; import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom';
import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz'; import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
// Pages espace étudiant // Pages espace étudiant
import JoinRoom from './pages/Student/JoinRoom/JoinRoom'; import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
// Pages authentification selection
import AuthDrawer from './pages/AuthManager/AuthDrawer';
// Header/Footer import // Header/Footer import
import Header from './components/Header/Header'; import Header from './components/Header/Header';
import Footer from './components/Footer/Footer'; import Footer from './components/Footer/Footer';
import ApiService from './services/ApiService'; import ApiService from './services/ApiService';
import OAuthCallback from './pages/AuthManager/callback/AuthCallback';
const App: React.FC = () => { const handleLogout = () => {
const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); ApiService.logout();
const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); }
const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null);
const location = useLocation();
// Check login status every time the route changes const isLoggedIn = () => {
useEffect(() => { return ApiService.isLoggedIn();
const checkLoginStatus = () => { }
setIsAuthenticated(ApiService.isLoggedIn());
setIsTeacherAuthenticated(ApiService.isLoggedInTeacher());
};
const fetchAuthenticatedRooms = async () => {
const data = await ApiService.getRoomsRequireAuth();
setRoomsRequireAuth(data);
};
checkLoginStatus();
fetchAuthenticatedRooms();
}, [location]);
const handleLogout = () => {
ApiService.logout();
setIsAuthenticated(false);
setIsTeacherAuthenticated(false);
};
function App() {
return ( return (
<div className="content"> <div className="content">
<Header isLoggedIn={isAuthenticated} handleLogout={handleLogout} />
<Header
isLoggedIn={isLoggedIn}
handleLogout={handleLogout}/>
<div className="app"> <div className="app">
<main> <main>
<Routes> <Routes>
@ -64,46 +46,22 @@ const App: React.FC = () => {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
{/* Pages espace enseignant */} {/* Pages espace enseignant */}
<Route <Route path="/teacher/login" element={<Login />} />
path="/teacher/dashboard" <Route path="/teacher/register" element={<Register />} />
element={isTeacherAuthenticated ? <Dashboard /> : <Navigate to="/login" />} <Route path="/teacher/resetPassword" element={<ResetPassword />} />
/> <Route path="/teacher/dashboard" element={<Dashboard />} />
<Route <Route path="/teacher/share/:id" element={<Share />} />
path="/teacher/share/:id" <Route path="/teacher/editor-quiz/:id" element={<QuizForm />} />
element={isTeacherAuthenticated ? <Share /> : <Navigate to="/login" />} <Route path="/teacher/manage-room/:id" element={<ManageRoom />} />
/>
<Route
path="/teacher/editor-quiz/:id"
element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
/>
<Route
path="/teacher/manage-room/:id"
element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
/>
{/* Pages espace étudiant */} {/* Pages espace étudiant */}
<Route <Route path="/student/join-room" element={<JoinRoom />} />
path="/student/join-room"
element={( !isRoomRequireAuthentication || isAuthenticated ) ? <JoinRoom /> : <Navigate to="/login" />}
/>
{/* Pages authentification */}
<Route path="/login" element={<AuthDrawer />} />
{/* Pages enregistrement */}
<Route path="/register" element={<Register />} />
{/* Pages rest password */}
<Route path="/resetPassword" element={<ResetPassword />} />
{/* Pages authentification sélection */}
<Route path="/auth/callback" element={<OAuthCallback />} />
</Routes> </Routes>
</main> </main>
</div> </div>
<Footer /> <Footer/>
</div> </div>
); );
}; }
export default App; export default App;

View file

@ -12,6 +12,6 @@ describe('StudentType', () => {
expect(user.name).toBe('Student'); expect(user.name).toBe('Student');
expect(user.id).toBe('123'); expect(user.id).toBe('123');
expect(user.answers).toHaveLength(0); expect(user.answers.length).toBe(0);
}); });
}); });

View file

@ -5,64 +5,47 @@ import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview
describe('GIFTTemplatePreview Component', () => { describe('GIFTTemplatePreview Component', () => {
test('renders error message when questions contain invalid syntax', () => { test('renders error message when questions contain invalid syntax', () => {
render(<GIFTTemplatePreview questions={[':: title']} />); render(<GIFTTemplatePreview questions={['Invalid GIFT syntax']} />);
const errorMessage = screen.getByText(/Title ::, Category, Description, or Question formatted stem but ":" found./i); const errorMessage = screen.findByText(/Erreur inconnue/i, {}, { timeout: 5000 });
expect(errorMessage).toBeInTheDocument(); expect(errorMessage).resolves.toBeInTheDocument();
}); });
test('renders preview when valid questions are provided', () => { test('renders preview when valid questions are provided', () => {
const questions = [ const questions = [
'Stem1 {=ans1 ~ans2 ~ans3}', 'Question 1 { A | B | C }',
'Question 2 { D | E | F }',
]; ];
render(<GIFTTemplatePreview questions={questions} />); render(<GIFTTemplatePreview questions={questions} />);
const previewContainer = screen.getByTestId('preview-container'); const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument(); expect(previewContainer).toBeInTheDocument();
// const question1 = screen.getByText('Stem1');
const mcQuestion1 = screen.getByText('Stem1');
const ans1 = screen.getByText('ans1');
const ans2 = screen.getByText('ans2');
const ans3 = screen.getByText('ans3');
expect(mcQuestion1).toBeInTheDocument();
expect(ans1).toBeInTheDocument();
expect(ans2).toBeInTheDocument();
expect(ans3).toBeInTheDocument();
// each answer should have a radio button before it
const radioButtons = screen.getAllByRole('radio');
expect(radioButtons).toHaveLength(3);
// ans1 should be the <label> for the first radio button
expect(radioButtons[0].nextElementSibling).toBe(ans1);
// ans2 should be the <label> for the second radio button
expect(radioButtons[1].nextElementSibling).toBe(ans2);
// ans3 should be the <label> for the third radio button
expect(radioButtons[2].nextElementSibling).toBe(ans3);
// after the <label> for correct answer (ans1) there should be an svg with aria-hidden="true"
expect(ans1.nextElementSibling).toHaveAttribute('aria-hidden', 'true');
// after the <label> for incorrect answer (ans2) there should be an svg with aria-hidden="true"
expect(ans2.nextElementSibling).toHaveAttribute('aria-hidden', 'true');
// after the <label> for incorrect answer (ans3) there should be an svg with aria-hidden="true"
expect(ans3.nextElementSibling).toHaveAttribute('aria-hidden', 'true');
}); });
test('hides correct/incorrect answers when hideAnswers prop is true', () => { test('hides answers when hideAnswers prop is true', () => {
const questions = [ const questions = [
'Stem1 {=ans1 ~ans2 ~ans3}', 'Question 1 { A | B | C }',
'Question 2 { D | E | F }',
]; ];
render(<GIFTTemplatePreview questions={questions} hideAnswers />); render(<GIFTTemplatePreview questions={questions} hideAnswers />);
const previewContainer = screen.getByTestId('preview-container'); const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument(); expect(previewContainer).toBeInTheDocument();
const ans1 = screen.queryByText('ans1');
const ans2 = screen.queryByText('ans2');
const ans3 = screen.queryByText('ans3');
const radioButtons = screen.getAllByRole('radio');
expect(radioButtons).toHaveLength(3);
expect(radioButtons[0].nextElementSibling).toBe(ans1);
expect(ans1?.nextElementSibling).toBeNull();
expect(radioButtons[1].nextElementSibling).toBe(ans2);
expect(ans2?.nextElementSibling).toBeNull();
expect(radioButtons[2].nextElementSibling).toBe(ans3);
expect(ans3?.nextElementSibling).toBeNull();
}); });
// it('renders images correctly', () => {
// const questions = [
// 'Question 1',
// '<img src="image1.jpg" alt="Image 1">',
// 'Question 2',
// '<img src="image2.jpg" alt="Image 2">',
// ];
// const { getByAltText } = render(<GIFTTemplatePreview questions={questions} />);
// const image1 = getByAltText('Image 1');
// const image2 = getByAltText('Image 2');
// expect(image1).toBeInTheDocument();
// expect(image2).toBeInTheDocument();
// });
// it('renders non-images correctly', () => {
// const questions = ['Question 1', 'Question 2'];
// const { queryByAltText } = render(<GIFTTemplatePreview questions={questions} />);
// const image1 = queryByAltText('Image 1');
// const image2 = queryByAltText('Image 2');
// expect(image1).toBeNull();
// expect(image2).toBeNull();
// });
}); });

View file

@ -0,0 +1,95 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import LiveResults from 'src/components/LiveResults/LiveResults';
import { QuestionType } from 'src/Types/QuestionType';
import { StudentType } from 'src/Types/StudentType';
import { BaseQuestion, parse } from 'gift-pegjs';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
::Sample Question 2:: Sample Question 2 {T}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
];
const mockShowSelectedQuestion = jest.fn();
describe('LiveResults', () => {
test('renders LiveResults component', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
expect(screen.getByText('Résultats du quiz')).toBeInTheDocument();
});
test('toggles show usernames switch', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
const switchElement = screen.getByLabelText('Afficher les noms');
expect(switchElement).toBeInTheDocument();
fireEvent.click(switchElement);
expect(switchElement).toBeChecked();
});
test('toggles show correct answers switch', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
const switchElement = screen.getByLabelText('Afficher les réponses');
expect(switchElement).toBeInTheDocument();
fireEvent.click(switchElement);
expect(switchElement).toBeChecked();
});
test('calls showSelectedQuestion when a table cell is clicked', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
const tableCell = screen.getByText('Q1');
fireEvent.click(tableCell);
expect(mockShowSelectedQuestion).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,110 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { StudentType } from 'src/Types/StudentType';
import LiveResultsTable from 'src/components/LiveResults/LiveResultsTable/LiveResultsTable';
import { QuestionType } from 'src/Types/QuestionType';
import { BaseQuestion, parse } from 'gift-pegjs';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
::Sample Question 2:: Sample Question 2 {T}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
];
const mockShowSelectedQuestion = jest.fn();
describe('LiveResultsTable', () => {
test('renders LiveResultsTable component', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={false}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
expect(screen.getByText('Student 1')).toBeInTheDocument();
expect(screen.getByText('Student 2')).toBeInTheDocument();
});
test('displays correct and incorrect answers', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
expect(screen.getByText('Answer 1')).toBeInTheDocument();
expect(screen.getByText('Answer 2')).toBeInTheDocument();
});
test('calls showSelectedQuestion when a table cell is clicked', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
const tableCell = screen.getByText('Q1');
fireEvent.click(tableCell);
expect(mockShowSelectedQuestion).toHaveBeenCalled();
});
test('calculates and displays student grades', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
//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);
const gradeElements2 = screen.getAllByText('0 %');
expect(gradeElements2.length).toBe(2); });
test('calculates and displays class average', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
//1 good answer out of 4 possible good answers (the second question has not been answered)
expect(screen.getByText('25 %')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,95 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { StudentType } from 'src/Types/StudentType';
import LiveResultsTableBody from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody';
import { QuestionType } from 'src/Types/QuestionType';
import { BaseQuestion, parse } from 'gift-pegjs';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
::Sample Question 2:: Sample Question 2 {T}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
];
const mockGetStudentGrade = jest.fn((student: StudentType) => {
const correctAnswers = student.answers.filter(answer => answer.isCorrect).length;
return (correctAnswers / mockQuestions.length) * 100;
});
describe('LiveResultsTableBody', () => {
test('renders LiveResultsTableBody component', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={false}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('Student 1')).toBeInTheDocument();
expect(screen.getByText('Student 2')).toBeInTheDocument();
});
test('displays correct and incorrect answers', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={true}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('Answer 1')).toBeInTheDocument();
expect(screen.getByText('Answer 2')).toBeInTheDocument();
});
test('displays icons for correct and incorrect answers when showCorrectAnswers is false', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={false}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByLabelText('correct')).toBeInTheDocument();
expect(screen.getByLabelText('incorrect')).toBeInTheDocument();
});
test('hides usernames when showUsernames is false', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={false}
showCorrectAnswers={true}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getAllByText('******').length).toBe(2);
});
});

View file

@ -0,0 +1,55 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { StudentType } from 'src/Types/StudentType';
import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter';
const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
];
const mockGetStudentGrade = jest.fn((student: StudentType) => {
const correctAnswers = student.answers.filter(answer => answer.isCorrect).length;
return (correctAnswers / 2) * 100; // Assuming there are 2 questions
});
describe('LiveResultsTableFooter', () => {
test('renders LiveResultsTableFooter component', () => {
render(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('% réussite')).toBeInTheDocument();
});
test('calculates and displays correct answers per question', () => {
render(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('50 %')).toBeInTheDocument();
expect(screen.getByText('0 %')).toBeInTheDocument();
});
test('calculates and displays class average', () => {
render(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('50 %')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,51 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import LiveResultsTableHeader from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader';
const mockShowSelectedQuestion = jest.fn();
describe('LiveResultsTableHeader', () => {
test('renders LiveResultsTableHeader component', () => {
render(
<LiveResultsTableHeader
maxQuestions={5}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
expect(screen.getByText("Nom d'utilisateur")).toBeInTheDocument();
for (let i = 1; i <= 5; i++) {
expect(screen.getByText(`Q${i}`)).toBeInTheDocument();
}
expect(screen.getByText('% réussite')).toBeInTheDocument();
});
test('calls showSelectedQuestion when a question header is clicked', () => {
render(
<LiveResultsTableHeader
maxQuestions={5}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
const questionHeader = screen.getByText('Q1');
fireEvent.click(questionHeader);
expect(mockShowSelectedQuestion).toHaveBeenCalledWith(0);
});
test('renders the correct number of question headers', () => {
render(
<LiveResultsTableHeader
maxQuestions={3}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
for (let i = 1; i <= 3; i++) {
expect(screen.getByText(`Q${i}`)).toBeInTheDocument();
}
});
});

View file

@ -54,10 +54,10 @@ describe('TextType', () => {
format: '' format: ''
}; };
// eslint-disable-next-line no-irregular-whitespace
// warning: there are zero-width spaces "" in the expected output -- you must enable seeing them with an extension such as Gremlins tracker in VSCode // warning: there are zero-width spaces "" in the expected output -- you must enable seeing them with an extension such as Gremlins tracker in VSCode
// eslint-disable-next-line no-irregular-whitespace
const expectedOutput = `Inline matrix: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mo fence="true">(</mo><mtable rowspacing="0.16em"><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>a</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>b</mi></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>c</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>d</mi></mstyle></mtd></mtr></mtable><mo fence="true">)</mo></mrow> \\begin{pmatrix} a &amp; b \\\\ c &amp; d \\end{pmatrix} </math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:2.4em;vertical-align:-0.95em;" class="strut"></span><span class="minner"><span style="top:0em;" class="mopen delimcenter"><span class="delimsizing size3">(</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span><span style="width:0.5em;" class="arraycolsep"></span><span style="width:0.5em;" class="arraycolsep"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">b</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span></span></span><span style="top:0em;" class="mclose delimcenter"><span class="delimsizing size3">)</span></span></span></span></span></span>`; const expectedOutput = `Inline matrix: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mo fence="true">(</mo><mtable rowspacing="0.16em"><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>a</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>b</mi></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>c</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>d</mi></mstyle></mtd></mtr></mtable><mo fence="true">)</mo></mrow> \\begin{pmatrix} a &amp; b \\\\ c &amp; d \\end{pmatrix} </math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:2.4em;vertical-align:-0.95em;" class="strut"></span><span class="minner"><span style="top:0em;" class="mopen delimcenter"><span class="delimsizing size3">(</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span><span style="width:0.5em;" class="arraycolsep"></span><span style="width:0.5em;" class="arraycolsep"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">b</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span></span></span><span style="top:0em;" class="mclose delimcenter"><span class="delimsizing size3">)</span></span></span></span></span></span>`;
expect(FormattedTextTemplate(input)).toContain(expectedOutput); expect(FormattedTextTemplate(input)).toContain(expectedOutput);
}); });

View file

@ -28,7 +28,7 @@ function convertStylesToObject(styles: string): React.CSSProperties {
styles.split(';').forEach((style) => { styles.split(';').forEach((style) => {
const [property, value] = style.split(':'); const [property, value] = style.split(':');
if (property && value) { if (property && value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(styleObject as any)[property.trim()] = value.trim(); (styleObject as any)[property.trim()] = value.trim();
} }
}); });

View file

@ -0,0 +1,163 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import LiveResults from 'src/components/LiveResults/LiveResults';
import { QuestionType } from 'src/Types/QuestionType';
import { StudentType } from 'src/Types/StudentType';
import { Socket } from 'socket.io-client';
import { BaseQuestion,parse } from 'gift-pegjs';
const mockSocket: Socket = {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
} as unknown as Socket;
const mockGiftQuestions = parse(
`::Sample Question 1:: Question stem
{
=Choice 1
~Choice 2
}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
const mockStudents: StudentType[] = [
{ id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] },
{ id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] },
];
describe('LiveResults', () => {
test('renders the component with questions and students', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
expect(screen.getByText(`Q${1}`)).toBeInTheDocument();
// Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch);
// Check if the component renders the students
mockStudents.forEach((student) => {
expect(screen.getByText(student.name)).toBeInTheDocument();
});
});
test('toggles the display of usernames', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch);
// Check if the usernames are shown again
mockStudents.forEach((student) => {
expect(screen.getByText(student.name)).toBeInTheDocument();
});
});
});
test('calculates and displays the correct student grades', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch);
// Check if the student grades are calculated and displayed correctly
mockStudents.forEach((student) => {
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument();
});
});
test('calculates and displays the class average', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch);
// Calculate the class average
const totalGrades = mockStudents.reduce((total, student) => {
return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100);
}, 0);
const classAverage = totalGrades / mockStudents.length;
// Check if the class average is displayed correctly
const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`);
const classAverageElement = classAverageElements.find((element) => {
return element.closest('td')?.classList.contains('MuiTableCell-footer');
});
expect(classAverageElement).toBeInTheDocument();
});
test('displays the correct answers per question', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Check if the correct answers per question are displayed correctly
mockQuestions.forEach((_, index) => {
const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length;
const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100;
const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`);
const correctAnswersElement = correctAnswersElements.find((element) => {
return element.closest('td')?.classList.contains('MuiTableCell-root');
});
expect(correctAnswersElement).toBeInTheDocument();
});
});

View file

@ -0,0 +1,253 @@
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter, useNavigate, useParams } from 'react-router-dom';
import ManageRoom from 'src/pages/Teacher/ManageRoom/ManageRoom';
import { StudentType } from 'src/Types/StudentType';
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';
jest.mock('src/services/WebsocketService');
jest.mock('src/services/ApiService');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
useParams: jest.fn(),
}));
const mockSocket = {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
} as unknown as Socket;
const mockQuiz: QuizType = {
_id: 'test-quiz-id',
title: 'Test Quiz',
content: ['::Q1:: Question 1 { =Answer1 ~Answer2 }', '::Q2:: Question 2 { =Answer1 ~Answer2 }'],
folderId: 'folder-id',
folderName: 'folder-name',
userId: 'user-id',
created_at: new Date(),
updated_at: new Date()
};
const mockStudents: StudentType[] = [
{ id: '1', name: 'Student 1', answers: [] },
{ id: '2', name: 'Student 2', answers: [] },
];
const mockAnswerData: AnswerReceptionFromBackendType = {
answer: 'Answer1',
idQuestion: 1,
idUser: '1',
username: 'Student 1',
};
describe('ManageRoom', () => {
const navigate = jest.fn();
const useParamsMock = useParams as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
(useNavigate as jest.Mock).mockReturnValue(navigate);
useParamsMock.mockReturnValue({ id: 'test-quiz-id' });
(ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
(webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
});
test('prepares to launch quiz and fetches quiz data', 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-name');
});
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();
expect(screen.getByText('0/60')).toBeInTheDocument();
expect(screen.getByText('Question 1/2')).toBeInTheDocument();
});
});
test('handles create-success event', 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-name');
});
await waitFor(() => {
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
});
});
test('handles user-joined event', 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-name');
});
await act(async () => {
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
userJoinedCallback(mockStudents[0]);
});
await waitFor(() => {
expect(screen.getByText('Student 1')).toBeInTheDocument();
});
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('1/60')).toBeInTheDocument();
});
});
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(
<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]);
const nextQuestionButton = screen.getByText('Prochaine question');
fireEvent.click(nextQuestionButton);
await waitFor(() => {
expect(screen.getByText('Question 2/2')).toBeInTheDocument();
});
});
test('handles disconnect', 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-name');
});
const disconnectButton = screen.getByText('Quitter');
fireEvent.click(disconnectButton);
const confirmButton = screen.getAllByText('Confirmer');
fireEvent.click(confirmButton[1]);
await waitFor(() => {
expect(webSocketService.disconnect).toHaveBeenCalled();
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
});
});
});

View file

@ -53,7 +53,7 @@ describe('TeacherModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument(); expect(screen.getByText('Votre réponse est:')).toBeInTheDocument();
}); });
test('handles disconnect button click', () => { test('handles disconnect button click', () => {

View file

@ -23,13 +23,13 @@ describe('WebSocketService', () => {
}); });
test('connect should initialize socket connection', () => { test('connect should initialize socket connection', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(io).toHaveBeenCalled(); expect(io).toHaveBeenCalled();
expect(WebsocketService['socket']).toBe(mockSocket); expect(WebsocketService['socket']).toBe(mockSocket);
}); });
test('disconnect should terminate socket connection', () => { test('disconnect should terminate socket connection', () => {
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(WebsocketService['socket']).toBeTruthy(); expect(WebsocketService['socket']).toBeTruthy();
WebsocketService.disconnect(); WebsocketService.disconnect();
expect(mockSocket.disconnect).toHaveBeenCalled(); expect(mockSocket.disconnect).toHaveBeenCalled();
@ -37,7 +37,7 @@ describe('WebSocketService', () => {
}); });
test('createRoom should emit create-room event', () => { test('createRoom should emit create-room event', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.createRoom(); WebsocketService.createRoom();
expect(mockSocket.emit).toHaveBeenCalledWith('create-room'); expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
}); });
@ -46,7 +46,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
const question = { id: 1, text: 'Sample Question' }; const question = { id: 1, text: 'Sample Question' };
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.nextQuestion(roomName, question); WebsocketService.nextQuestion(roomName, question);
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question }); expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
}); });
@ -55,7 +55,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
const questions = [{ id: 1, text: 'Sample Question' }]; const questions = [{ id: 1, text: 'Sample Question' }];
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.launchStudentModeQuiz(roomName, questions); WebsocketService.launchStudentModeQuiz(roomName, questions);
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', { expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
roomName, roomName,
@ -66,7 +66,7 @@ describe('WebSocketService', () => {
test('endQuiz should emit end-quiz event with correct parameters', () => { test('endQuiz should emit end-quiz event with correct parameters', () => {
const roomName = 'testRoom'; const roomName = 'testRoom';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.endQuiz(roomName); WebsocketService.endQuiz(roomName);
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName }); expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
}); });
@ -75,7 +75,7 @@ describe('WebSocketService', () => {
const enteredRoomName = 'testRoom'; const enteredRoomName = 'testRoom';
const username = 'testUser'; const username = 'testUser';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.joinRoom(enteredRoomName, username); WebsocketService.joinRoom(enteredRoomName, username);
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username }); expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
}); });

View file

@ -21,11 +21,11 @@ const GiftCheatSheet: React.FC = () => {
}; };
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}."; const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa"; const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse."; const QuestionChoixMulMany = "::Villes canadiennes:: \n Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte."; const QuestionCourte ="::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}"; const QuestionNum ="::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
return ( return (
<div className="gift-cheat-sheet"> <div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2> <h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
</div> </div>
<div className="question-type"> <div className="question-type">
<h4> 5. Question numérique </h4> <h4> 5. Questions numériques </h4>
<pre> <pre>
<code className="question-code-block selectable-text"> <code className="question-code-block selectable-text">
{ {

View file

@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import './styles.css'; import './styles.css';
import DOMPurify from 'dompurify'; import { FormattedTextTemplate } from './templates/TextTypeTemplate';
interface GIFTTemplatePreviewProps { interface GIFTTemplatePreviewProps {
questions: string[]; questions: string[];
@ -74,7 +74,8 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div> <div className="error">{error}</div>
) : isPreviewReady ? ( ) : isPreviewReady ? (
<div data-testid="preview-container"> <div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
</div> </div>
) : ( ) : (
<div className="loading">Chargement de la prévisualisation...</div> <div className="loading">Chargement de la prévisualisation...</div>

View file

@ -4,14 +4,24 @@ import katex from 'katex';
import { TextFormat } from 'gift-pegjs'; import { TextFormat } from 'gift-pegjs';
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc. import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
export function formatLatex(text: string): string { function formatLatex(text: string): string {
return text
let renderedText = '';
try {
renderedText = text
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true })) .replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false })) .replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false }))
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true })) .replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\\\((.*?)\\\)/g, (_, inner) => .replace(/\\\((.*?)\\\)/g, (_, inner) =>
katex.renderToString(inner, { displayMode: false }) katex.renderToString(inner, { displayMode: false })
); );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
renderedText = text;
}
return renderedText;
} }
/** /**

View file

@ -1,10 +1,10 @@
import { Link, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import * as React from 'react'; import * as React from 'react';
import './header.css'; import './header.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
interface HeaderProps { interface HeaderProps {
isLoggedIn: boolean; isLoggedIn: () => boolean;
handleLogout: () => void; handleLogout: () => void;
} }
@ -20,7 +20,7 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
onClick={() => navigate('/')} onClick={() => navigate('/')}
/> />
{isLoggedIn && ( {isLoggedIn() && (
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@ -32,14 +32,6 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
Logout Logout
</Button> </Button>
)} )}
{!isLoggedIn && (
<div className="auth-selection-btn">
<Link to="/login">
<button className="auth-btn">Connexion</button>
</Link>
</div>
)}
</div> </div>
); );
}; };

View file

@ -47,10 +47,10 @@ const LaunchQuizDialog: React.FC<Props> = ({ open, handleOnClose, launchQuiz, se
<DialogActions> <DialogActions>
<Button variant="outlined" onClick={handleOnClose}> <Button variant="outlined" onClick={handleOnClose}>
Annuler <div>Annuler</div>
</Button> </Button>
<Button variant="contained" onClick={launchQuiz}> <Button variant="contained" onClick={launchQuiz}>
Lancer <div>Lancer</div>
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -1,26 +1,16 @@
// LiveResults.tsx // LiveResults.tsx
import React, { useMemo, useState } from 'react'; import React, { useState } from 'react';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
import './liveResult.css'; import './liveResult.css';
import { import {
FormControlLabel, FormControlLabel,
FormGroup, FormGroup,
Paper,
Switch, Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow
} from '@mui/material'; } from '@mui/material';
import { StudentType } from '../../Types/StudentType'; import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate';
import LiveResultsTable from './LiveResultsTable/LiveResultsTable';
interface LiveResultsProps { interface LiveResultsProps {
socket: Socket | null; socket: Socket | null;
@ -30,241 +20,14 @@ interface LiveResultsProps {
students: StudentType[] students: StudentType[]
} }
// interface Answer {
// answer: string | number | boolean;
// isCorrect: boolean;
// idQuestion: number;
// }
// interface StudentResult {
// username: string;
// idUser: string;
// answers: Answer[];
// }
const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => { const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => {
const [showUsernames, setShowUsernames] = useState<boolean>(false); const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
// const [students, setStudents] = useState<StudentType[]>(initialStudents);
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
const maxQuestions = questions.length;
// useEffect(() => {
// // Initialize the map with the current students
// const newStudentResultsMap = new Map<string, StudentResult>();
// for (const student of students) {
// newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] });
// }
// setStudentResultsMap(newStudentResultsMap);
// }, [])
// update when students change
// useEffect(() => {
// // studentResultsMap is inconsistent with students -- need to update
// for (const student of students as StudentType[]) {
// }
// }, [students])
// useEffect(() => {
// if (socket) {
// const submitAnswerHandler = ({
// idUser,
// answer,
// idQuestion
// }: {
// idUser: string;
// username: string;
// answer: string | number | boolean;
// idQuestion: number;
// }) => {
// console.log(`Received answer from ${idUser} 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 === idUser) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), 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 ${idUser} 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 getStudentGrade = (student: StudentType): number => {
if (student.answers.length === 0) {
return 0;
}
const uniqueQuestions = new Set();
let correctAnswers = 0;
for (const answer of student.answers) {
const { idQuestion, isCorrect } = answer;
if (!uniqueQuestions.has(idQuestion)) {
uniqueQuestions.add(idQuestion);
if (isCorrect) {
correctAnswers++;
}
}
}
return (correctAnswers / questions.length) * 100;
};
const classAverage: number = useMemo(() => {
let classTotal = 0;
students.forEach((student) => {
classTotal += getStudentGrade(student);
});
return classTotal / students.length;
}, [students]);
const getCorrectAnswersPerQuestion = (index: number): number => {
return (
(students.filter((student) =>
student.answers.some(
(answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
)
).length / students.length) * 100
);
};
// (studentResults.filter((student) =>
// student.answers.some(
// (answer) =>
// parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
// )
// ).length /
// studentResults.length) *
// 100
// );
// };
// function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
// const questionInfo = questions.find((q) =>
// q.question.id ? q.question.id === idQuestion.toString() : false
// ) as QuestionType | undefined;
// const answerText = answer.toString();
// if (questionInfo) {
// const question = questionInfo.question as GIFTQuestion;
// if (question.type === 'TF') {
// return (
// (question.isTrue && answerText == 'true') ||
// (!question.isTrue && answerText == 'false')
// );
// } else if (question.type === 'MC') {
// return question.choices.some(
// (choice) => choice.isCorrect && choice.text.text === answerText
// );
// } else if (question.type === 'Numerical') {
// if (question.choices && !Array.isArray(question.choices)) {
// if (
// question.choices.type === 'high-low' &&
// question.choices.numberHigh &&
// question.choices.numberLow
// ) {
// const answerNumber = parseFloat(answerText);
// if (!isNaN(answerNumber)) {
// return (
// answerNumber <= question.choices.numberHigh &&
// answerNumber >= question.choices.numberLow
// );
// }
// }
// }
// if (question.choices && Array.isArray(question.choices)) {
// if (
// question.choices[0].text.type === 'range' &&
// question.choices[0].text.number &&
// question.choices[0].text.range
// ) {
// const answerNumber = parseFloat(answerText);
// const range = question.choices[0].text.range;
// const correctAnswer = question.choices[0].text.number;
// if (!isNaN(answerNumber)) {
// return (
// answerNumber <= correctAnswer + range &&
// answerNumber >= correctAnswer - range
// );
// }
// }
// if (
// question.choices[0].text.type === 'simple' &&
// question.choices[0].text.number
// ) {
// const answerNumber = parseFloat(answerText);
// if (!isNaN(answerNumber)) {
// return answerNumber === question.choices[0].text.number;
// }
// }
// }
// } else if (question.type === 'Short') {
// return question.choices.some(
// (choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
// );
// }
// }
// return false;
// }
return ( return (
<div> <div>
<div className="action-bar mb-1"> <div className="action-bar mb-1">
<div className="text-2xl text-bold">Résultats du quiz</div> <div className="text-2xl text-bold">Résultats du quiz</div>
@ -295,146 +58,14 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
</div> </div>
<div className="table-container"> <div className="table-container">
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{students.map((student) => (
<TableRow key={student.id}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.name : '******'}
</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => {
const answer = student.answers.find(
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
);
const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false;
return ( <LiveResultsTable
<TableCell students={students}
key={index} questions={questions}
sx={{ showCorrectAnswers={showCorrectAnswers}
textAlign: 'center', showSelectedQuestion={showSelectedQuestion}
borderStyle: 'solid', showUsernames={showUsernames}
borderWidth: 1, />
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div>{formatLatex(answerText)}</div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,75 @@
import React from 'react';
import { Paper, Table, TableContainer } from '@mui/material';
import { StudentType } from 'src/Types/StudentType';
import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsTableFooter from './TableComponents/LiveResultTableFooter';
import LiveResultsTableHeader from './TableComponents/LiveResultsTableHeader';
import LiveResultsTableBody from './TableComponents/LiveResultsTableBody';
interface LiveResultsTableProps {
students: StudentType[];
questions: QuestionType[];
showCorrectAnswers: boolean;
showSelectedQuestion: (index: number) => void;
showUsernames: boolean;
}
const LiveResultsTable: React.FC<LiveResultsTableProps> = ({
questions,
students,
showSelectedQuestion,
showUsernames,
showCorrectAnswers
}) => {
const maxQuestions = questions.length;
const getStudentGrade = (student: StudentType): number => {
if (student.answers.length === 0) {
return 0;
}
const uniqueQuestions = new Set();
let correctAnswers = 0;
for (const answer of student.answers) {
const { idQuestion, isCorrect } = answer;
if (!uniqueQuestions.has(idQuestion)) {
uniqueQuestions.add(idQuestion);
if (isCorrect) {
correctAnswers++;
}
}
}
return (correctAnswers / questions.length) * 100;
};
return (
<TableContainer component={Paper}>
<Table size="small">
<LiveResultsTableHeader
maxQuestions={maxQuestions}
showSelectedQuestion={showSelectedQuestion}
/>
<LiveResultsTableBody
maxQuestions={maxQuestions}
students={students}
showUsernames={showUsernames}
showCorrectAnswers={showCorrectAnswers}
getStudentGrade={getStudentGrade}
/>
<LiveResultsTableFooter
students={students}
maxQuestions={maxQuestions}
getStudentGrade={getStudentGrade}
/>
</Table>
</TableContainer>
);
};
export default LiveResultsTable;

View file

@ -0,0 +1,79 @@
import { TableCell, TableFooter, TableRow } from "@mui/material";
import React, { useMemo } from "react";
import { StudentType } from "src/Types/StudentType";
interface LiveResultsFooterProps {
students: StudentType[];
maxQuestions: number;
getStudentGrade: (student: StudentType) => number;
}
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
maxQuestions,
students,
getStudentGrade
}) => {
const getCorrectAnswersPerQuestion = (index: number): number => {
return (
(students.filter((student) =>
student.answers.some(
(answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
)
).length / students.length) * 100
);
};
const classAverage: number = useMemo(() => {
let classTotal = 0;
students.forEach((student) => {
classTotal += getStudentGrade(student);
});
return classTotal / students.length;
}, [students]);
return (
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
);
};
export default LiveResultsTableFooter;

View file

@ -0,0 +1,94 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TableBody, TableCell, TableRow } from "@mui/material";
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { FormattedTextTemplate } from '../../../GiftTemplate/templates/TextTypeTemplate';
import React from "react";
import { StudentType } from "src/Types/StudentType";
interface LiveResultsFooterProps {
maxQuestions: number;
students: StudentType[];
showUsernames: boolean;
showCorrectAnswers: boolean;
getStudentGrade: (student: StudentType) => number;
}
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
maxQuestions,
students,
showUsernames,
showCorrectAnswers,
getStudentGrade
}) => {
return (
<TableBody>
{students.map((student) => (
<TableRow key={student.id}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.name : '******'}
</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => {
const answer = student.answers.find(
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
);
const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false;
return (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: answerText }) }}></div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} aria-label="correct" />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} aria-label="incorrect"/>
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
);
};
export default LiveResultsTableFooter;

View file

@ -0,0 +1,50 @@
import { TableCell, TableHead, TableRow } from "@mui/material";
import React from "react";
interface LiveResultsFooterProps {
maxQuestions: number;
showSelectedQuestion: (index: number) => void;
}
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
maxQuestions,
showSelectedQuestion,
}) => {
return (
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
);
};
export default LiveResultsTableFooter;

View file

@ -27,7 +27,6 @@
} }
.question-wrapper .katex { .question-wrapper .katex {
display: block;
text-align: center; text-align: center;
} }
@ -120,9 +119,9 @@
} }
.feedback-container { .feedback-container {
margin-left: 1.1rem; display: inline-block !important; /* override the parent */
display: inline-flex !important; /* override the parent */
align-items: center; align-items: center;
margin-left: 1.1rem;
position: relative; position: relative;
padding: 0 0.5rem; padding: 0 0.5rem;
background-color: hsl(43, 100%, 94%); background-color: hsl(43, 100%, 94%);

View file

@ -12,7 +12,7 @@ import { Question } from 'gift-pegjs';
interface StudentModeQuizProps { interface StudentModeQuizProps {
questions: QuestionType[]; questions: QuestionType[];
submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }

View file

@ -9,7 +9,7 @@ import './studentWaitPage.css';
interface Props { interface Props {
students: StudentType[]; students: StudentType[];
launchQuiz: () => void; launchQuiz: () => void;
setQuizMode: (_mode: 'student' | 'teacher') => void; setQuizMode: (mode: 'student' | 'teacher') => void;
} }
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => { const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {

View file

@ -12,7 +12,7 @@ import { Question } from 'gift-pegjs';
interface TeacherModeQuizProps { interface TeacherModeQuizProps {
questionInfos: QuestionType; questionInfos: QuestionType;
submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }
@ -23,16 +23,33 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
}) => { }) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState(''); const [feedbackMessage, setFeedbackMessage] = useState<React.ReactNode>('');
const renderFeedbackMessage = (answer: string) => {
if(answer === 'true' || answer === 'false'){
return (<span>
<strong>Votre réponse est: </strong>{answer==="true" ? 'Vrai' : 'Faux'}
</span>)
}
else{
return (
<span>
<strong>Votre réponse est: </strong>{answer.toString()}
</span>
);}
};
useEffect(() => { useEffect(() => {
// Close the feedback dialog when the question changes
handleFeedbackDialogClose();
setIsAnswerSubmitted(false); setIsAnswerSubmitted(false);
}, [questionInfos]);
}, [questionInfos.question]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = Number(questionInfos.question.id) || -1; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`); setFeedbackMessage(renderFeedbackMessage(answer.toString()));
setIsFeedbackDialogOpen(true); setIsFeedbackDialogOpen(true);
}; };
@ -74,7 +91,17 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
> >
<DialogTitle>Rétroaction</DialogTitle> <DialogTitle>Rétroaction</DialogTitle>
<DialogContent> <DialogContent>
<div style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
{feedbackMessage} {feedbackMessage}
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px'}}
>Question : </div>
</div>
<QuestionComponent <QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question} question={questionInfos.question as Question}

View file

@ -1,11 +1,11 @@
// constants.tsx // constants.tsx
const ENV_VARIABLES = { const ENV_VARIABLES = {
MODE: 'production', MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", VITE_BACKEND_URL: import.meta.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 || '', VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : ''
}; };
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`); console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES }; export { ENV_VARIABLES };

View file

@ -1,61 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import './authDrawer.css';
import SimpleLogin from './providers/SimpleLogin/Login';
import authService from '../../services/AuthService';
import { ENV_VARIABLES } from '../../constants';
import ButtonAuth from './providers/OAuth-Oidc/ButtonAuth';
const AuthSelection: React.FC = () => {
const [authData, setAuthData] = useState<any>(null); // Stocke les données d'auth
const navigate = useNavigate();
ENV_VARIABLES.VITE_BACKEND_URL;
// Récupérer les données d'authentification depuis l'API
useEffect(() => {
const fetchData = async () => {
const data = await authService.fetchAuthData();
setAuthData(data);
};
fetchData();
}, []);
return (
<div className="auth-selection-page">
<h1>Connexion</h1>
{/* Formulaire de connexion Simple Login */}
{authData && authData['simpleauth'] && (
<div className="form-container">
<SimpleLogin />
</div>
)}
{/* Conteneur OAuth/OIDC */}
{authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && (
<div className="auth-button-container">
{Object.keys(authData).map((providerKey) => {
const providerType = authData[providerKey].type;
if (providerType === 'oidc' || providerType === 'oauth') {
return (
<ButtonAuth
key={providerKey}
providerName={providerKey}
providerType={providerType}
/>
);
}
return null;
})}
</div>
)}
<div>
<button className="home-button-container" onClick={() => navigate('/')}>Retour à l'accueil</button>
</div>
</div>
);
};
export default AuthSelection;

View file

@ -1,48 +0,0 @@
.auth-selection-page {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
h1 {
margin-bottom: 20px;
}
.form-container{
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
form {
display: flex;
flex-direction: column;
}
input {
margin: 5px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px;
border: none;
border-radius: 4px;
background-color: #5271ff;
color: white;
cursor: pointer;
}
button:hover {
background-color: #5271ff;
}
.home-button-container{
background: none;
color: black;
}
.home-button-container:hover{
background: none;
color: black;
text-decoration: underline;
}

View file

@ -1,27 +0,0 @@
import React from 'react';
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import apiService from '../../../services/ApiService';
const OAuthCallback: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const user = searchParams.get('user');
const username = searchParams.get('username');
if (user) {
apiService.saveToken(user);
apiService.saveUsername(username || "");
navigate('/');
} else {
navigate('/login');
}
}, []);
return <div>Loading...</div>;
};
export default OAuthCallback;

View file

@ -1,27 +0,0 @@
import React from 'react';
import { ENV_VARIABLES } from '../../../../constants';
import '../css/buttonAuth.css';
interface ButtonAuthContainerProps {
providerName: string;
providerType: 'oauth' | 'oidc';
}
const handleAuthLogin = (provider: string) => {
window.location.href = `${ENV_VARIABLES.BACKEND_URL}/api/auth/${provider}`;
};
const ButtonAuth: React.FC<ButtonAuthContainerProps> = ({ providerName, providerType }) => {
return (
<>
<div className={`${providerName}-${providerType}-container button-container`}>
<h2>Se connecter avec {providerType.toUpperCase()}</h2>
<button key={providerName} className={`provider-btn ${providerType}-btn`} onClick={() => handleAuthLogin(providerName)}>
Continuer avec {providerName}
</button>
</div>
</>
);
};
export default ButtonAuth;

View file

@ -1,114 +0,0 @@
// JoinRoom.tsx
import React, { useEffect, useState } from 'react';
import { TextField, FormLabel, RadioGroup, FormControlLabel, Radio, Box } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
import ApiService from '../../../../services/ApiService';
const Register: React.FC = () => {
const [name, setName] = useState(''); // State for name
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [roles, setRoles] = useState<string[]>(['student']); // Set 'student' as the default role
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting] = useState<boolean>(false);
useEffect(() => {
return () => { };
}, []);
const handleRoleChange = (role: string) => {
setRoles([role]); // Update the roles array to contain the selected role
};
const isValidEmail = (email: string) => {
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const register = async () => {
if (!isValidEmail(email)) {
setConnectionError("Veuillez entrer une adresse email valide.");
return;
}
const result = await ApiService.register(name, email, password, roles);
if (result !== true) {
setConnectionError(result);
return;
}
};
return (
<LoginContainer
title="Créer un compte"
error={connectionError}
>
<TextField
label="Nom"
variant="outlined"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Votre nom"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<TextField
label="Email"
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }}
fullWidth
type="email"
error={!!connectionError && !isValidEmail(email)}
helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""}
/>
<TextField
label="Mot de passe"
variant="outlined"
value={password}
type="password"
onChange={(e) => setPassword(e.target.value)}
placeholder="Mot de passe"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
<FormLabel component="legend" sx={{ marginRight: '1rem' }}>Choisir votre rôle</FormLabel>
<RadioGroup
row
aria-label="role"
name="role"
value={roles[0]}
onChange={(e) => handleRoleChange(e.target.value)}
>
<FormControlLabel value="student" control={<Radio />} label="Étudiant" />
<FormControlLabel value="teacher" control={<Radio />} label="Professeur" />
</RadioGroup>
</Box>
<LoadingButton
loading={isConnecting}
onClick={register}
variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!name || !email || !password}
>
S'inscrire
</LoadingButton>
</LoginContainer>
);
};
export default Register;

View file

@ -1,68 +0,0 @@
import { useNavigate } from 'react-router-dom';
// JoinRoom.tsx
import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
import ApiService from '../../../../services/ApiService';
const ResetPassword: React.FC = () => {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting] = useState<boolean>(false);
useEffect(() => {
return () => {
};
}, []);
const reset = async () => {
const result = await ApiService.resetPassword(email);
if (!result) {
setConnectionError(result.toString());
return;
}
navigate("/login")
};
return (
<LoginContainer
title='Récupération du compte'
error={connectionError}>
<TextField
label="Email"
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<LoadingButton
loading={isConnecting}
onClick={reset}
variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email}
>
Réinitialiser le mot de passe
</LoadingButton>
</LoginContainer>
);
};
export default ResetPassword;

View file

@ -1,23 +0,0 @@
.provider-btn {
background-color: #ffffff;
border: 1px solid #ccc;
color: black;
margin: 4px 0 4px 0;
}
.provider-btn:hover {
background-color: #dbdbdb;
border: 1px solid #ccc;
color: black;
margin: 4px 0 4px 0;
}
.button-container {
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}

View file

@ -1,17 +0,0 @@
.login-links {
padding-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.login-links a {
padding: 4px;
color: #333;
text-decoration: none;
}
.login-links a:hover {
text-decoration: underline;
}

View file

@ -61,25 +61,6 @@
align-items: end; align-items: end;
} }
.auth-selection-btn {
position: absolute;
top: 20px;
right: 20px;
}
.auth-btn {
padding: 10px 20px;
background-color: #5271ff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.auth-btn:hover {
background-color: #5976fa;
}
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.btn-container { .btn-container {
flex-direction: column; flex-direction: column;

View file

@ -15,11 +15,9 @@ import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from 'src/components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'
const JoinRoom: React.FC = () => { const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState(''); const [roomName, setRoomName] = useState('');
const [username, setUsername] = useState(ApiService.getUsername()); const [username, setUsername] = useState('');
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [isWaitingForTeacher, setIsWaitingForTeacher] = useState(false); const [isWaitingForTeacher, setIsWaitingForTeacher] = useState(false);
const [question, setQuestion] = useState<QuestionType>(); const [question, setQuestion] = useState<QuestionType>();
@ -36,8 +34,8 @@ const JoinRoom: React.FC = () => {
}, []); }, []);
const handleCreateSocket = () => { const handleCreateSocket = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('join-success', () => { socket.on('join-success', () => {
setIsWaitingForTeacher(true); setIsWaitingForTeacher(true);

View file

@ -78,7 +78,7 @@ const Dashboard: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
if (!ApiService.isLoggedIn()) { if (!ApiService.isLoggedIn()) {
navigate("/login"); navigate("/teacher/login");
return; return;
} }
else { else {
@ -196,8 +196,8 @@ const Dashboard: React.FC = () => {
// questions[i] = QuestionService.ignoreImgTags(questions[i]); // questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]); const parsedItem = parse(questions[i]);
Template(parsedItem[0]); Template(parsedItem[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
console.error('Error parsing question:', error);
return false; return false;
} }
} }

View file

@ -190,8 +190,9 @@ const QuizForm: React.FC = () => {
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = '';
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
} }
}; };

View file

@ -8,6 +8,7 @@ import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from 'src/constants'; import { ENV_VARIABLES } from 'src/constants';
@ -33,7 +34,8 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>(''); const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined); const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false);
useEffect(() => { useEffect(() => {
if (quizId.id) { if (quizId.id) {
const fetchquiz = async () => { const fetchquiz = async () => {
@ -84,7 +86,7 @@ const ManageRoom: React.FC = () => {
const createWebSocketRoom = () => { const createWebSocketRoom = () => {
console.log('Creating WebSocket room...'); console.log('Creating WebSocket room...');
setConnectingError(''); setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('connect', () => { socket.on('connect', () => {
webSocketService.createRoom(); webSocketService.createRoom();
@ -125,6 +127,7 @@ const ManageRoom: React.FC = () => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket) {
console.log(`Listening for user-joined in room ${roomName}`); console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => { socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
@ -144,7 +147,7 @@ const ManageRoom: React.FC = () => {
console.log('Quiz questions not found (cannot update answers without them).'); console.log('Quiz questions not found (cannot update answers without them).');
return; return;
} }
// Update the students state using the functional form of setStudents // Update the students state using the functional form of setStudents
setStudents((prevStudents) => { setStudents((prevStudents) => {
// print the list of current student names // print the list of current student names
@ -152,7 +155,7 @@ const ManageRoom: React.FC = () => {
prevStudents.forEach((student) => { prevStudents.forEach((student) => {
console.log(student.name); console.log(student.name);
}); });
let foundStudent = false; let foundStudent = false;
const updatedStudents = prevStudents.map((student) => { const updatedStudents = prevStudents.map((student) => {
console.log(`Comparing ${student.id} to ${idUser}`); console.log(`Comparing ${student.id} to ${idUser}`);
@ -172,7 +175,7 @@ const ManageRoom: React.FC = () => {
updatedAnswers = [...student.answers, newAnswer]; updatedAnswers = [...student.answers, newAnswer];
} }
return { ...student, answers: updatedAnswers }; return { ...student, answers: updatedAnswers };
} }
return student; return student;
}); });
if (!foundStudent) { if (!foundStudent) {
@ -315,13 +318,18 @@ const ManageRoom: React.FC = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly // 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: ${roomName}, quiz: ${quiz}`);
setQuizStarted(true);
return; return;
} }
switch (quizMode) { switch (quizMode) {
case 'student': case 'student':
setQuizStarted(true);
return launchStudentMode(); return launchStudentMode();
case 'teacher': case 'teacher':
setQuizStarted(true);
return launchTeacherMode(); return launchTeacherMode();
} }
}; };
@ -426,9 +434,19 @@ const ManageRoom: React.FC = () => {
askConfirm askConfirm
message={`Êtes-vous sûr de vouloir quitter?`} /> message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Salle: {roomName}</div>
<div className='userCount subtitle'>Utilisateurs: {students.length}/60</div>
<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' }}>
<GroupIcon style={{ marginRight: '5px' }} />
{students.length}/60
</div>
)}
</div> </div>
<div className='dumb'></div> <div className='dumb'></div>
@ -440,8 +458,12 @@ const ManageRoom: React.FC = () => {
{quizQuestions ? ( {quizQuestions ? (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div> <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>
)}
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
@ -478,23 +500,23 @@ const ManageRoom: React.FC = () => {
</div> </div>
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}> <div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton"> <div className="previousQuestionButton">
<Button onClick={previousQuestion} <Button onClick={previousQuestion}
variant="contained" variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}> disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente Question précédente
</Button> </Button>
</div> </div>
<div className="nextQuestionButton"> <div className="nextQuestionButton">
<Button onClick={nextQuestion} <Button onClick={nextQuestion}
variant="contained" variant="contained"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length} disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
> >
Prochaine question Prochaine question
</Button> </Button>
</div> </div>
</div> )} </div>)}
</div> </div>

View file

@ -18,8 +18,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: flex-end;
align-items: center; align-items: flex-end;
} }

View file

@ -1,88 +1,81 @@
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
// JoinRoom.tsx
import React, { useEffect, useState } from 'react'; // JoinRoom.tsx
import React, { useEffect, useState } from 'react';
import '../css/simpleLogin.css';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../../components/LoginContainer/LoginContainer' import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../../services/ApiService'; import ApiService from '../../../services/ApiService';
const SimpleLogin: React.FC = () => { const Register: React.FC = () => {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting] = useState<boolean>(false); const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting] = useState<boolean>(false);
useEffect(() => {
return () => { useEffect(() => {
return () => {
};
}, []); };
}, []);
const login = async () => {
const result = await ApiService.login(email, password); const register = async () => {
if (result !== true) { const result = await ApiService.register(email, password);
setConnectionError(result);
return; if (typeof result === 'string') {
} setConnectionError(result);
}; return;
}
return ( navigate("/teacher/login")
<LoginContainer };
title=''
error={connectionError}>
return (
<TextField <LoginContainer
label="Email" title='Créer un compte'
variant="outlined" error={connectionError}>
value={email}
onChange={(e) => setEmail(e.target.value)} <TextField
placeholder="Nom d'utilisateur" label="Email"
sx={{ marginBottom: '1rem' }} variant="outlined"
fullWidth value={email}
/> onChange={(e) => setEmail(e.target.value)}
placeholder="Adresse courriel"
<TextField sx={{ marginBottom: '1rem' }}
label="Mot de passe" fullWidth
variant="outlined" />
type="password"
value={password} <TextField
onChange={(e) => setPassword(e.target.value)} label="Mot de passe"
placeholder="Nom de la salle" variant="outlined"
sx={{ marginBottom: '1rem' }} value={password}
fullWidth type="password"
/> onChange={(e) => setPassword(e.target.value)}
placeholder="Mot de passe"
<LoadingButton sx={{ marginBottom: '1rem' }}
loading={isConnecting} fullWidth
onClick={login} />
variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }} <LoadingButton
disabled={!email || !password} loading={isConnecting}
> onClick={register}
Login variant="contained"
</LoadingButton> sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password}
<div className="login-links"> >
S&apos;inscrire
<Link to="/resetPassword"> </LoadingButton>
Réinitialiser le mot de passe
</Link> </LoginContainer>
<Link to="/register"> );
Créer un compte };
</Link>
export default Register;
</div>
</LoginContainer>
);
};
export default SimpleLogin;

View file

@ -33,7 +33,7 @@ const Share: React.FC = () => {
if (!ApiService.isLoggedIn()) { if (!ApiService.isLoggedIn()) {
window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`);
navigate("/login"); navigate("/teacher/login");
return; return;
} }

View file

@ -1,9 +1,8 @@
import axios, { AxiosError, AxiosResponse } from 'axios'; import axios, { AxiosError, AxiosResponse } from 'axios';
import { jwtDecode } from 'jwt-decode';
import { ENV_VARIABLES } from '../constants';
import { FolderType } from 'src/Types/FolderType'; import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType'; import { QuizType } from 'src/Types/QuizType';
import { ENV_VARIABLES } from 'src/constants';
type ApiResponse = boolean | string; type ApiResponse = boolean | string;
@ -35,7 +34,7 @@ class ApiService {
} }
// Helpers // Helpers
public saveToken(token: string): void { private saveToken(token: string): void {
const now = new Date(); const now = new Date();
const object = { const object = {
@ -79,71 +78,7 @@ class ApiService {
return true; return true;
} }
public isLoggedInTeacher(): boolean {
const token = this.getToken();
if (token == null) {
return false;
}
try {
const decodedToken = jwtDecode(token) as { roles: string[] };
const userRoles = decodedToken.roles;
const requiredRole = 'teacher';
if (!userRoles || !userRoles.includes(requiredRole)) {
return false;
}
// Update token expiry
this.saveToken(token);
return true;
} catch (error) {
console.error("Error decoding token:", error);
return false;
}
}
public saveUsername(username: string): void {
if (!username || username.length === 0) {
return;
}
const object = {
username: username
}
localStorage.setItem("username", JSON.stringify(object));
}
public getUsername(): string {
const objectStr = localStorage.getItem("username");
if (!objectStr) {
return "";
}
const object = JSON.parse(objectStr)
return object.username;
}
// Route to know if rooms need authentication to join
public async getRoomsRequireAuth(): Promise<any> {
const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`);
const result: AxiosResponse = await axios.get(url);
if (result.status == 200) {
return result.data.roomsRequireAuth;
}
return false;
}
public logout(): void { public logout(): void {
localStorage.removeItem("username");
return localStorage.removeItem("jwt"); return localStorage.removeItem("jwt");
} }
@ -153,25 +88,21 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async register(name: string, email: string, password: string, roles: string[]): Promise<any> { public async register(email: string, password: string): Promise<ApiResponse> {
try { try {
if (!email || !password) { if (!email || !password) {
throw new Error(`L'email et le mot de passe sont requis.`); throw new Error(`L'email et le mot de passe sont requis.`);
} }
const url: string = this.constructRequestUrl(`/auth/simple-auth/register`); const url: string = this.constructRequestUrl(`/user/register`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { name, email, password, roles }; const body = { email, password };
const result: AxiosResponse = await axios.post(url, body, { headers: headers }); const result: AxiosResponse = await axios.post(url, body, { headers: headers });
console.log(result); if (result.status !== 200) {
if (result.status == 200) { throw new Error(`L'enregistrement a échoué. Status: ${result.status}`);
window.location.href = result.request.responseURL;
}
else {
throw new Error(`La connexion a échoué. Status: ${result.status}`);
} }
return true; return true;
@ -193,52 +124,44 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
/** public async login(email: string, password: string): Promise<ApiResponse> {
* @returns true if successful try {
* @returns An error string if unsuccessful
*/
public async login(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
throw new Error("L'email et le mot de passe sont requis.");
}
const url: string = this.constructRequestUrl(`/auth/simple-auth/login`); if (!email || !password) {
const headers = this.constructRequestHeaders(); throw new Error(`L'email et le mot de passe sont requis.`);
const body = { email, password };
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
// If login is successful, redirect the user
if (result.status === 200) {
window.location.href = result.request.responseURL;
return true;
} else {
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
}
} catch (error) {
console.log("Error details:", error);
// Handle Axios-specific errors
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const responseData = err.response?.data as { message?: string } | undefined;
// If there is a message field in the response, print it
if (responseData?.message) {
console.log("Backend error message:", responseData.message);
return responseData.message;
} }
// If no message is found, return a fallback message const url: string = this.constructRequestUrl(`/user/login`);
return "Erreur serveur inconnue lors de la requête."; const headers = this.constructRequestHeaders();
const body = { email, password };
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
if (result.status !== 200) {
throw new Error(`La connexion a échoué. Status: ${result.status}`);
}
this.saveToken(result.data.token);
return true;
} catch (error) {
console.log("Error details: ", error);
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
if (err.status === 401) {
return 'Email ou mot de passe incorrect.';
}
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.`
} }
// Handle other non-Axios errors
return "Une erreur inattendue s'est produite.";
} }
}
/** /**
* @returns true if successful * @returns true if successful
@ -251,7 +174,7 @@ public async login(email: string, password: string): Promise<any> {
throw new Error(`L'email est requis.`); throw new Error(`L'email est requis.`);
} }
const url: string = this.constructRequestUrl(`/auth/simple-auth/reset-password`); const url: string = this.constructRequestUrl(`/user/reset-password`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { email }; const body = { email };
@ -287,7 +210,7 @@ public async login(email: string, password: string): Promise<any> {
throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`); throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`);
} }
const url: string = this.constructRequestUrl(`/auth/simple-auth/change-password`); const url: string = this.constructRequestUrl(`/user/change-password`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { email, oldPassword, newPassword }; const body = { email, oldPassword, newPassword };
@ -968,4 +891,4 @@ public async login(email: string, password: string): Promise<any> {
} }
const apiService = new ApiService(); const apiService = new ApiService();
export default apiService; export default apiService;

View file

@ -1,28 +0,0 @@
import { ENV_VARIABLES } from '../constants';
class AuthService {
private BASE_URL: string;
constructor() {
this.BASE_URL = ENV_VARIABLES.VITE_BACKEND_URL;
}
private constructRequestUrl(endpoint: string): string {
return `${this.BASE_URL}/api${endpoint}`;
}
async fetchAuthData(){
try {
const response = await fetch(this.constructRequestUrl('/auth/getActiveAuth'));
const data = await response.json();
return data.authActive;
} catch (error) {
console.error('Erreur lors de la récupération des données d\'auth:', error);
}
};
}
const authService = new AuthService();
export default authService;

View file

@ -1,96 +0,0 @@
version: '3'
services:
frontend:
build:
context: ./client
dockerfile: Dockerfile
container_name: frontend
ports:
- "5173:5173"
restart: always
backend:
build:
context: ./server
dockerfile: Dockerfile
container_name: backend
ports:
- "3000:3000"
environment:
PORT: 3000
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
MONGO_DATABASE: evaluetonsavoir
EMAIL_SERVICE: gmail
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
SESSION_Secret: 'lookMomImQuizzing'
SITE_URL: http://localhost
FRONTEND_PORT: 5173
USE_PORTS: false
AUTHENTICATED_ROOMS: false
volumes:
- ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json
depends_on:
- mongo
- keycloak
restart: always
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
nginx:
image: fuhrmanator/evaluetonsavoir-routeur:latest
container_name: nginx
ports:
- "80:80"
depends_on:
- backend
- frontend
restart: always
# Ce conteneur est la base de données principale pour l'application
mongo:
image: mongo
container_name: mongo
ports:
- "27017:27017"
tty: true
volumes:
- mongodb_data:/data/db
restart: always
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=America/Montreal
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
restart: always
keycloak:
container_name: keycloak
image: quay.io/keycloak/keycloak:latest
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin123
KC_HEALTH_ENABLED: 'true'
KC_FEATURES: preview
ports:
- "8080:8080"
volumes:
- ./oauth-tester/config.json:/opt/keycloak/data/import/realm-config.json
command:
- start-dev
- --import-realm
- --hostname-strict=false
volumes:
mongodb_data:
external: false

View file

@ -1,20 +1,19 @@
version: '3'
services: services:
frontend: frontend:
build: image: fuhrmanator/evaluetonsavoir-frontend:latest
context: ./client
dockerfile: Dockerfile
container_name: frontend container_name: frontend
environment:
# Define empty VITE_BACKEND_URL because it's production
- VITE_BACKEND_URL=
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
- VITE_BACKEND_SOCKET_URL=
ports: ports:
- "5173:5173" - "5173:5173"
restart: always restart: always
backend: backend:
build: image: fuhrmanator/evaluetonsavoir-backend:latest
context: ./server
dockerfile: Dockerfile
container_name: backend container_name: backend
ports: ports:
- "3000:3000" - "3000:3000"
@ -26,16 +25,9 @@ services:
SENDER_EMAIL: infoevaluetonsavoir@gmail.com SENDER_EMAIL: infoevaluetonsavoir@gmail.com
EMAIL_PSW: 'vvml wmfr dkzb vjzb' EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
SESSION_Secret: 'lookMomImQuizzing' FRONTEND_URL: "http://localhost:5173"
SITE_URL: http://localhost
FRONTEND_PORT: 5173
USE_PORTS: false
AUTHENTICATED_ROOMS: false
volumes:
- ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json
depends_on: depends_on:
- mongo - mongo
- keycloak
restart: always restart: always
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application # Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
@ -87,23 +79,6 @@ services:
- WATCHTOWER_INCLUDE_RESTARTING=true - WATCHTOWER_INCLUDE_RESTARTING=true
restart: "no" restart: "no"
keycloak:
container_name: keycloak
image: quay.io/keycloak/keycloak:latest
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin123
KC_HEALTH_ENABLED: 'true'
KC_FEATURES: preview
ports:
- "8080:8080"
volumes:
- ./oauth-tester/config.json:/opt/keycloak/data/import/realm-config.json
command:
- start-dev
- --import-realm
- --hostname-strict=false
volumes: volumes:
mongodb_data: mongodb_data:
external: false external: false

View file

@ -1,96 +0,0 @@
{
"id": "test-realm",
"realm": "EvalueTonSavoir",
"enabled": true,
"users": [
{
"username": "teacher",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "teacher123",
"temporary": false
}
],
"groups": ["teachers"]
},
{
"username": "student",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "student123",
"temporary": false
}
],
"groups": ["students"]
}
],
"groups": [
{
"name": "teachers",
"attributes": {
"role": ["teacher"]
}
},
{
"name": "students",
"attributes": {
"role": ["student"]
}
}
],
"roles": {
"realm": [
{
"name": "teacher",
"description": "Teacher role"
},
{
"name": "student",
"description": "Student role"
}
]
},
"clients": [
{
"clientId": "evaluetonsavoir-client",
"enabled": true,
"publicClient": false,
"clientAuthenticatorType": "client-secret",
"secret": "your-secret-key-123",
"redirectUris": ["http://localhost:5173/*","http://localhost/*"],
"webOrigins": ["http://localhost:5173","http://localhost/"]
}
],
"clientScopes": [
{
"name": "group",
"description": "Group scope for access control",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true"
},
"protocolMappers": [
{
"name": "group mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "group",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "group",
"jsonType.label": "String"
}
}
]
}
],
"defaultDefaultClientScopes": ["group"]
}

View file

@ -14,10 +14,4 @@ EMAIL_PSW='vvml wmfr dkzb vjzb'
JWT_SECRET=TOKEN! JWT_SECRET=TOKEN!
# Pour creer les liens images # Pour creer les liens images
SESSION_Secret='session_secret' FRONTEND_URL=http://localhost:5173
SITE_URL=http://localhost
FRONTEND_PORT=5173
USE_PORTS=false
AUTHENTICATED_ROOMS=false

1
server/.gitignore vendored
View file

@ -1 +0,0 @@
auth_config.json

View file

@ -1,245 +0,0 @@
const AuthConfig = require("../config/auth.js");
const AuthManager = require("../auth/auth-manager.js");
const mockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL: "https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
{
provider2: {
type: "oidc",
OIDC_CLIENT_ID: "your_oidc_client_id",
OIDC_CLIENT_SECRET: "your_oidc_client_secret",
OIDC_CONFIG_URL: "https://your-issuer.com",
OIDC_ADD_SCOPE: "groups",
OIDC_ROLE_TEACHER_VALUE: "teacher-claim-value",
OIDC_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
"simpleauth": {
enabled: true,
name: "provider3",
SESSION_SECRET: "your_session_secret",
},
},
};
// Créez une instance de AuthConfig en utilisant la configuration mockée
describe(
"AuthConfig Class Tests",
() => {
let authConfigInstance;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
authConfigInstance.loadConfigTest(mockConfig); // On injecte la configuration mockée
});
it("devrait retourner la configuration PassportJS", () => {
const config = authConfigInstance.getPassportJSConfig();
expect(config).toHaveProperty("provider1");
expect(config).toHaveProperty("provider2");
});
it("devrait retourner la configuration Simple Login", () => {
const config = authConfigInstance.getSimpleLoginConfig();
expect(config).toHaveProperty("name", "provider3");
expect(config).toHaveProperty("SESSION_SECRET", "your_session_secret");
});
it("devrait retourner les providers OAuth", () => {
const oauthProviders = authConfigInstance.getOAuthProviders();
expect(Array.isArray(oauthProviders)).toBe(true);
expect(oauthProviders.length).toBe(1); // Il y a un seul provider OAuth
expect(oauthProviders[0]).toHaveProperty("provider1");
});
it("devrait valider la configuration des providers", () => {
expect(() => authConfigInstance.validateProvidersConfig()).not.toThrow();
});
it("devrait lever une erreur si une configuration manque", () => {
const invalidMockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_CLIENT_ID: "your_oauth_client_id", // Il manque des champs nécessaires
},
},
],
},
};
const instanceWithInvalidConfig = new AuthConfig();
instanceWithInvalidConfig.loadConfigTest(invalidMockConfig);
// Vérifiez que l'erreur est lancée avec les champs manquants corrects
expect(() => instanceWithInvalidConfig.validateProvidersConfig()).toThrow(
new Error(`Configuration invalide pour les providers suivants : [
{
"provider": "provider1",
"missingFields": [
"OAUTH_AUTHORIZATION_URL",
"OAUTH_TOKEN_URL",
"OAUTH_USERINFO_URL",
"OAUTH_CLIENT_SECRET",
"OAUTH_ROLE_TEACHER_VALUE",
"OAUTH_ROLE_STUDENT_VALUE"
]
}
]`)
);
});
},
describe("Auth Module Registration", () => {
let expressMock = jest.mock("express");
expressMock.use = () => {}
expressMock.get = () => {}
let authConfigInstance;
let authmanagerInstance;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
});
it("should load valid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModule = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
provider2: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
authmanagerInstance.getUserModel();
expect(logSpy).toHaveBeenCalledTimes(0);
logSpy.mockClear();
});
it("should not load invalid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const invalidModule = {
auth: {
ModuleX:{}
},
};
authConfigInstance.loadConfigTest(invalidModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(1);
logSpy.mockClear();
});
it("should not load invalid provider from passport", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModuleInvalidProvider = {
auth: {
passportjs: [
{
provider1: {
type: "x",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModuleInvalidProvider); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(4);
logSpy.mockClear();
});
})
);
describe(
"Rooms requiring authentication", () => {
// Making a copy of env variables to restore them later
const OLD_ENV_VARIABLES = process.env;
let authConfigInstance;
beforeAll(() => {
authConfigInstance = new AuthConfig();
});
// Clearing cache just in case
beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV_VARIABLES };
});
// Resetting the old values
afterAll(() => {
process.env = OLD_ENV_VARIABLES;
});
// tests cases as [environment variable value, expected value]
const cases = [["true", true], ["false", false], ["", false], ["other_than_true_false", false]];
test.each(cases)(
"Given %p as AUTHENTICATED_ROOMS environment variable value, returns %p",
(envVarArg, expectedResult) => {
process.env.AUTHENTICATED_ROOMS = envVarArg;
const isAuthRequired = authConfigInstance.getRoomsRequireAuth();
expect(isAuthRequired).toEqual(expectedResult);
}
);
}
)

View file

@ -32,7 +32,7 @@ describe('Users', () => {
users = new Users(db, foldersModel); users = new Users(db, foldersModel);
}); });
it.skip('should register a new user', async () => { it('should register a new user', async () => {
db.collection().findOne.mockResolvedValue(null); // No user found db.collection().findOne.mockResolvedValue(null); // No user found
db.collection().insertOne.mockResolvedValue({ insertedId: new ObjectId() }); db.collection().insertOne.mockResolvedValue({ insertedId: new ObjectId() });
bcrypt.hash.mockResolvedValue('hashedPassword'); bcrypt.hash.mockResolvedValue('hashedPassword');

View file

@ -39,25 +39,17 @@ module.exports.images = imagesControllerInstance;
const userRouter = require('./routers/users.js'); const userRouter = require('./routers/users.js');
const folderRouter = require('./routers/folders.js'); const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js'); const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js') const imagesRouter = require('./routers/images.js');
const AuthManager = require('./auth/auth-manager.js')
const authRouter = require('./routers/auth.js')
// Setup environment // Setup environment
dotenv.config(); dotenv.config();
const isDev = process.env.NODE_ENV === 'development';
// Setup urls from configs
const use_ports = (process.env['USE_PORTS'] || 'false').toLowerCase() == "true"
process.env['FRONTEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['FRONTEND_PORT']}`:"")
process.env['BACKEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['PORT']}`:"")
const errorHandler = require("./middleware/errorHandler.js"); const errorHandler = require("./middleware/errorHandler.js");
// Start app // Start app
const app = express(); const app = express();
const cors = require("cors"); const cors = require("cors");
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
let isDev = process.env.NODE_ENV === 'development';
const configureServer = (httpServer, isDev) => { const configureServer = (httpServer, isDev) => {
console.log(`Configuring server with isDev: ${isDev}`); console.log(`Configuring server with isDev: ${isDev}`);
@ -92,19 +84,7 @@ app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter); app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter); app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter); app.use('/api/image', imagesRouter);
app.use('/api/auth', authRouter);
// Add Auths methods
const session = require('express-session');
app.use(session({
secret: process.env['SESSION_Secret'],
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production' }
}));
let authManager = new AuthManager(app,null,userModel);
authManager.getUserModel();
app.use(errorHandler); app.use(errorHandler);
// Start server // Start server

View file

@ -1,69 +0,0 @@
const fs = require('fs');
const AuthConfig = require('../config/auth.js');
const jwt = require('../middleware/jwtToken.js');
const emailer = require('../config/email.js');
const { MISSING_REQUIRED_PARAMETER } = require('../constants/errorCodes.js');
const AppError = require('../middleware/AppError.js');
class AuthManager{
constructor(expressapp,configs=null,userModel){
this.modules = []
this.app = expressapp
this.configs = configs ?? (new AuthConfig()).loadConfig()
this.addModules()
this.registerAuths()
this.simpleregister = userModel;
}
getUserModel(){
return this.simpleregister;
}
async addModules(){
for(const module in this.configs.auth){
this.addModule(module)
}
}
async addModule(name){
const modulePath = `${process.cwd()}/auth/modules/${name}.js`
if(fs.existsSync(modulePath)){
const Module = require(modulePath);
this.modules.push(new Module(this,this.configs.auth[name]));
console.info(`Module d'authentification '${name}' ajouté`)
} else{
console.error(`Le module d'authentification ${name} n'as pas été chargé car il est introuvable`);
}
}
async registerAuths(){
for(const module of this.modules){
try{
module.registerAuth(this.app)
} catch(error){
console.error(`L'enregistrement du module ${module} a échoué.`);
console.error(`Error: ${error} `);
}
}
}
// 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);
res.redirect(`/auth/callback?user=${tokenToSave}&username=${userInfo.name}`);
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
}
async register(userInfos){
if (!userInfos.email || !userInfos.password) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const user = await this.simpleregister.register(userInfos);
emailer.registerConfirmation(user.email)
return user
}
}
module.exports = AuthManager;

View file

@ -1,100 +0,0 @@
var OAuth2Strategy = require('passport-oauth2')
var authUserAssoc = require('../../../models/authUserAssociation')
var users = require('../../../models/users')
var { hasNestedValue } = require('../../../utils')
class PassportOAuth {
constructor(passportjs, auth_name) {
this.passportjs = passportjs
this.auth_name = auth_name
}
register(app, passport, endpoint, name, provider) {
const cb_url = `${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this
const scope = 'openid profile email offline_access' + ` ${provider.OAUTH_ADD_SCOPE}`;
passport.use(name, new OAuth2Strategy({
authorizationURL: provider.OAUTH_AUTHORIZATION_URL,
tokenURL: provider.OAUTH_TOKEN_URL,
clientID: provider.OAUTH_CLIENT_ID,
clientSecret: provider.OAUTH_CLIENT_SECRET,
callbackURL: cb_url,
passReqToCallback: true
},
async function (req, accessToken, refreshToken, params, profile, done) {
try {
const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const userInfo = await userInfoResponse.json();
let received_user = {
auth_id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
roles: []
};
if (hasNestedValue(userInfo, provider.OAUTH_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if (hasNestedValue(userInfo, provider.OAUTH_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name, received_user.auth_id)
let user_account
if (user_association) {
user_account = await users.getById(user_association.user_id)
}
else {
let user_id = await users.getId(received_user.email)
if (user_id) {
user_account = await users.getById(user_id);
} else {
received_user.password = users.generatePassword()
user_account = await self.passportjs.register(received_user)
}
await authUserAssoc.link(self.auth_name, received_user.auth_id, user_account._id)
}
user_account.name = received_user.name
user_account.roles = received_user.roles
await users.editUser(user_account)
// Store the tokens in the session
req.session.oauth2Tokens = {
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: params.expires_in
};
return done(null, user_account);
} catch (error) {
console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`);
return done(error);
}
}));
app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, {
scope: scope,
prompt: 'consent'
})(req, res, next);
});
app.get(`${endpoint}/${name}/callback`,
(req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
},
(req, res) => {
if (req.user) {
self.passportjs.authenticate(req.user, req, res)
} else {
res.status(401).json({ error: "L'authentification a échoué" });
}
}
);
console.info(`Ajout de la connexion : ${name}(OAuth)`)
}
}
module.exports = PassportOAuth;

View file

@ -1,106 +0,0 @@
var OpenIDConnectStrategy = require('passport-openidconnect');
var authUserAssoc = require('../../../models/authUserAssociation');
var users = require('../../../models/users');
var { hasNestedValue } = require('../../../utils');
const { MISSING_OIDC_PARAMETER } = require('../../../constants/errorCodes.js');
const AppError = require('../../../middleware/AppError.js');
class PassportOpenIDConnect {
constructor(passportjs, auth_name) {
this.passportjs = passportjs
this.auth_name = auth_name
}
async getConfigFromConfigURL(name, provider) {
try {
const config = await fetch(provider.OIDC_CONFIG_URL)
return await config.json()
} catch (error) {
console.error(`Error: ${error} `);
throw new AppError(MISSING_OIDC_PARAMETER(name));
}
}
async register(app, passport, endpoint, name, provider) {
const config = await this.getConfigFromConfigURL(name, provider)
const cb_url = `${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this
const scope = 'openid profile email ' + `${provider.OIDC_ADD_SCOPE}`
passport.use(name, new OpenIDConnectStrategy({
issuer: config.issuer,
authorizationURL: config.authorization_endpoint,
tokenURL: config.token_endpoint,
userInfoURL: config.userinfo_endpoint,
clientID: provider.OIDC_CLIENT_ID,
clientSecret: provider.OIDC_CLIENT_SECRET,
callbackURL: cb_url,
passReqToCallback: true,
scope: scope,
},
// patch pour la librairie permet d'obtenir les groupes, PR en cours mais "morte" : https://github.com/jaredhanson/passport-openidconnect/pull/101
async function (req, issuer, profile, times, tok, done) {
try {
const received_user = {
auth_id: profile.id,
email: profile.emails[0].value,
name: profile.name.givenName,
roles: []
};
if (hasNestedValue(profile, provider.OIDC_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if (hasNestedValue(profile, provider.OIDC_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name, received_user.auth_id)
let user_account
if (user_association) {
user_account = await users.getById(user_association.user_id)
}
else {
let user_id = await users.getId(received_user.email)
if (user_id) {
user_account = await users.getById(user_id);
} else {
received_user.password = users.generatePassword()
user_account = await self.passportjs.register(received_user)
}
await authUserAssoc.link(self.auth_name, received_user.auth_id, user_account._id)
}
user_account.name = received_user.name
user_account.roles = received_user.roles
await users.editUser(user_account);
return done(null, user_account);
} catch (error) {
console.error(`Error: ${error} `);
}
}));
app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, {
scope: scope,
prompt: 'consent'
})(req, res, next);
});
app.get(`${endpoint}/${name}/callback`,
(req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
},
(req, res) => {
if (req.user) {
self.passportjs.authenticate(req.user, req, res)
} else {
res.status(401).json({ error: "L'authentification a échoué" });
}
}
);
console.info(`Ajout de la connexion : ${name}(OIDC)`)
}
}
module.exports = PassportOpenIDConnect;

View file

@ -1,65 +0,0 @@
var passport = require('passport')
var authprovider = require('../../models/authProvider')
class PassportJs{
constructor(authmanager,settings){
this.authmanager = authmanager
this.registeredProviders = {}
this.providers = settings
this.endpoint = "/api/auth"
}
async registerAuth(expressapp){
expressapp.use(passport.initialize());
expressapp.use(passport.session());
for(const p of this.providers){
for(const [name,provider] of Object.entries(p)){
const auth_id = `passportjs_${provider.type}_${name}`
if(!(provider.type in this.registeredProviders)){
this.registerProvider(provider.type,auth_id)
}
try{
this.registeredProviders[provider.type].register(expressapp,passport,this.endpoint,name,provider)
authprovider.create(auth_id)
} catch(error){
console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`);
console.error(`Error: ${error} `);
}
}
}
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
}
async registerProvider(providerType,auth_id){
try{
const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js`
const Provider = require(providerPath);
this.registeredProviders[providerType]= new Provider(this,auth_id)
console.info(`Le type de connexion '${providerType}' a été ajouté dans passportjs.`)
} catch(error){
console.error(`Le type de connexion '${providerType}' n'as pas pu être chargé dans passportjs.`);
console.error(`Error: ${error} `);
}
}
register(userInfos){
return this.authmanager.register(userInfos)
}
authenticate(userInfo,req,res,next){
return this.authmanager.login(userInfo,req,res,next)
}
}
module.exports = PassportJs;

View file

@ -1,126 +0,0 @@
const jwt = require('../../middleware/jwtToken.js');
const emailer = require('../../config/email.js');
const model = require('../../models/users.js');
const AppError = require('../../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR } = require('../../constants/errorCodes');
const { name } = require('../../models/authProvider.js');
class SimpleAuth {
constructor(authmanager, settings) {
this.authmanager = authmanager
this.providers = settings
this.endpoint = "/api/auth/simple-auth"
}
async registerAuth(expressapp) {
try {
expressapp.post(`${this.endpoint}/register`, (req, res) => this.register(this, req, res));
expressapp.post(`${this.endpoint}/login`, (req, res, next) => this.authenticate(this, req, res, next));
expressapp.post(`${this.endpoint}/reset-password`, (req, res, next) => this.resetPassword(this, req, res, next));
expressapp.post(`${this.endpoint}/change-password`, jwt.authenticate, (req, res, next) => this.changePassword(this, req, res, next));
} catch (error) {
console.error(`La connexion ${name} de type ${this.providers.type} n'as pu être chargé.`);
console.error(`Error: ${error} `);
}
}
async register(self, req, res) {
try {
let userInfos = {
name: req.body.name,
email: req.body.email,
password: req.body.password,
roles: req.body.roles
}
let user = await self.authmanager.register(userInfos)
if (user) res.redirect("/login")
}
catch (error) {
return res.status(400).json({
message: error.message
});
}
}
async authenticate(self, req, res, next) {
try {
const { email, password } = req.body;
if (!email || !password) {
const error = new Error("Email or password is missing");
error.statusCode = 400;
throw error;
}
const userModel = self.authmanager.getUserModel();
const user = userModel.login(email, password);
await self.authmanager.login(user, req, res, next);
} catch (error) {
const statusCode = error.statusCode || 500;
const message = error.message || "An internal server error occurred";
console.error(error);
return res.status(statusCode).json({ message });
}
}
async resetPassword(self, req, res, next) {
try {
const { email } = req.body;
if (!email) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const newPassword = await model.resetPassword(email);
if (!newPassword) {
throw new AppError(GENERATE_PASSWORD_ERROR);
}
emailer.newPasswordConfirmation(email, newPassword);
return res.status(200).json({
message: 'Nouveau mot de passe envoyé par courriel.'
});
}
catch (error) {
return next(error);
}
}
async changePassword(self, req, res, next) {
try {
const { email, oldPassword, newPassword } = req.body;
if (!email || !oldPassword || !newPassword) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
// verify creds first
const user = await model.login(email, oldPassword);
if (!user) {
throw new AppError(LOGIN_CREDENTIALS_ERROR);
}
const password = await model.changePassword(email, newPassword)
if (!password) {
throw new AppError(UPDATE_PASSWORD_ERROR);
}
return res.status(200).json({
message: 'Mot de passe changé avec succès.'
});
}
catch (error) {
return next(error);
}
}
}
module.exports = SimpleAuth;

View file

@ -1,26 +0,0 @@
{
"auth": {
"passportjs":
[
{
"oidc_local": {
"type": "oidc",
"OIDC_CONFIG_URL": "http://localhost:8080/realms/EvalueTonSavoir/.well-known/openid-configuration",
"OIDC_CLIENT_ID": "evaluetonsavoir-client",
"OIDC_CLIENT_SECRET": "your-secret-key-123",
"OIDC_ADD_SCOPE": "group",
"OIDC_ROLE_TEACHER_VALUE": "teachers",
"OIDC_ROLE_STUDENT_VALUE": "students"
}
}
],
"simple-login": {
"enabled": true,
"name": "provider3",
"SESSION_SECRET": "your_session_secret"
},
"Module X":{
}
}
}

View file

@ -1,193 +0,0 @@
const fs = require('fs');
const path = require('path');
const pathAuthConfig = './auth_config.json';
const configPath = path.join(process.cwd(), pathAuthConfig);
class AuthConfig {
config = null;
// Méthode pour lire le fichier de configuration JSON
loadConfig() {
try {
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.");
this.config = {};
throw error;
}
return this.config
}
// Méthode pour load le fichier de test
loadConfigTest(mockConfig) {
this.config = mockConfig;
}
// Méthode pour retourner la configuration des fournisseurs PassportJS
getPassportJSConfig() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const passportConfig = {};
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
passportConfig[providerName] = provider[providerName];
});
return passportConfig;
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner la configuration de Simple Login
getSimpleLoginConfig() {
if (this.config && this.config.auth && this.config.auth["simpleauth"]) {
return this.config.auth["simpleauth"];
} else {
return { error: "Aucune configuration Simple Login disponible." };
}
}
// Méthode pour retourner tous les providers de type OAuth
getOAuthProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oauthProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oauth';
});
if (oauthProviders.length > 0) {
return oauthProviders;
} else {
return { error: "Aucun fournisseur OAuth disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner tous les providers de type OIDC
getOIDCProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oidcProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oidc';
});
if (oidcProviders.length > 0) {
return oidcProviders;
} else {
return { error: "Aucun fournisseur OIDC disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour vérifier si tous les providers ont les variables nécessaires
validateProvidersConfig() {
const requiredOAuthFields = [
'OAUTH_AUTHORIZATION_URL', 'OAUTH_TOKEN_URL','OAUTH_USERINFO_URL', 'OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET', 'OAUTH_ROLE_TEACHER_VALUE', 'OAUTH_ROLE_STUDENT_VALUE'
];
const requiredOIDCFields = [
'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_CONFIG_URL', 'OIDC_ROLE_TEACHER_VALUE', 'OIDC_ROLE_STUDENT_VALUE','OIDC_ADD_SCOPE'
];
const missingFieldsReport = [];
if (this.config && this.config.auth && this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
let missingFields = [];
// Vérification des providers de type OAuth
if (providerConfig.type === 'oauth') {
missingFields = requiredOAuthFields.filter(field => !(field in providerConfig));
}
// Vérification des providers de type OIDC
else if (providerConfig.type === 'oidc') {
missingFields = requiredOIDCFields.filter(field => !(field in providerConfig));
}
// Si des champs manquent, on les ajoute au rapport
if (missingFields.length > 0) {
missingFieldsReport.push({
provider: providerName,
missingFields: missingFields
});
}
});
// Si des champs manquent, lever une exception
if (missingFieldsReport.length > 0) {
throw new Error(`Configuration invalide pour les providers suivants : ${JSON.stringify(missingFieldsReport, null, 2)}`);
} else {
console.log("Configuration auth_config.json: Tous les providers ont les variables nécessaires.")
return { success: "Tous les providers ont les variables nécessaires." };
}
} else {
throw new Error("Aucune configuration PassportJS disponible.");
}
}
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
getActiveAuth() {
if (this.config && this.config.auth) {
const passportConfig = {};
// Gestion des providers PassportJS
if (this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
passportConfig[providerName] = {};
if (providerConfig.type === 'oauth') {
passportConfig[providerName] = {
type: providerConfig.type
};
} else if (providerConfig.type === 'oidc') {
passportConfig[providerName] = {
type: providerConfig.type,
};
}
});
}
// Gestion du Simple Login
if (this.config.auth["simpleauth"] && this.config.auth["simpleauth"].enabled) {
passportConfig['simpleauth'] = {
type: "simpleauth",
name: this.config.auth["simpleauth"].name
};
}
return passportConfig;
} else {
return { error: "Aucune configuration d'authentification disponible." };
}
}
// Check if students must be authenticated to join a room
getRoomsRequireAuth() {
const roomRequireAuth = process.env.AUTHENTICATED_ROOMS;
if (!roomRequireAuth || roomRequireAuth !== "true") {
return false;
}
return true;
}
}
module.exports = AuthConfig;

View file

@ -12,13 +12,6 @@ exports.MISSING_REQUIRED_PARAMETER = {
code: 400 code: 400
} }
exports.MISSING_OIDC_PARAMETER = (name) => {
return {
message: `Les informations de connexions de la connexion OIDC ${name} n'ont pu être chargées.`,
code: 400
}
}
exports.USER_ALREADY_EXISTS = { exports.USER_ALREADY_EXISTS = {
message: 'L\'utilisateur existe déjà.', message: 'L\'utilisateur existe déjà.',
code: 400 code: 400

View file

@ -1,36 +0,0 @@
const AuthConfig = require('../config/auth.js');
class authController {
async getActive(req, res, next) {
try {
const authC = new AuthConfig();
authC.loadConfig();
const authActive = authC.getActiveAuth();
const response = {
authActive
};
return res.json(response);
}
catch (error) {
return next(error); // Gérer l'erreur
}
}
async getRoomsRequireAuth(req, res) {
const authC = new AuthConfig();
const roomsRequireAuth = authC.getRoomsRequireAuth();
const response = {
roomsRequireAuth
}
return res.json(response);
}
}
module.exports = new authController;

View file

@ -7,8 +7,8 @@ dotenv.config();
class Token { class Token {
create(email, userId, roles) { create(email, userId) {
return jwt.sign({ email, userId, roles }, process.env.JWT_SECRET); return jwt.sign({ email, userId }, process.env.JWT_SECRET);
} }
authenticate(req, res, next) { authenticate(req, res, next) {
@ -25,11 +25,11 @@ class Token {
req.user = payload; req.user = payload;
}); });
} catch (error) { } catch (error) {
return next(error); return next(error);
} }
return next(); return next();
} }
} }

View file

@ -1,44 +0,0 @@
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class AuthProvider {
constructor(name) {
this._id = new ObjectId();
this.name = name;
}
async getId(name){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authprovider');
const existingauth = await collection.findOne({ name:name });
if(existingauth){
return existingauth._id
}
return null
}
async create(name) {
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authprovider');
const existingauth = await collection.findOne({ name:name });
if(existingauth){
return existingauth._id;
}
const newProvider = {
name:name
}
const result = await collection.insertOne(newProvider);
return result.insertedId;
}
}
module.exports = new AuthProvider;

View file

@ -1,59 +0,0 @@
const authProvider = require('./authProvider.js')
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class AuthUserAssociation {
constructor(authProviderId, authId, userId) {
this._id = new ObjectId();
this.authProvider_id = authProviderId;
this.auth_id = authId;
this.user_id = userId;
this.connected = false;
}
async find_user_association(provider_name,auth_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id, auth_id: auth_id });
return userAssociation
}
async link(provider_name,auth_id,user_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id, user_id: user_id });
if(!userAssociation){
return await collection.insertOne({
_id:ObjectId,
authProvider_id:provider_id,
auth_id:auth_id,
user_id:user_id,
})
}
}
async unlink(provider_name,user_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id, user_id: user_id });
if(userAssociation){
return await collection.deleteOne(userAssociation)
} else return null
}
}
module.exports = new AuthUserAssociation;

View file

@ -1,194 +1,125 @@
const bcrypt = require("bcrypt"); //user
const AppError = require("../middleware/AppError.js"); const bcrypt = require('bcrypt');
const { USER_ALREADY_EXISTS } = require("../constants/errorCodes"); const AppError = require('../middleware/AppError.js');
const { USER_ALREADY_EXISTS } = require('../constants/errorCodes');
class Users { class Users {
constructor(db, foldersModel) {
constructor(db, foldersModel) { // console.log("Users constructor: db", db)
this.db = db; this.db = db;
this.folders = foldersModel; this.folders = foldersModel;
}
async hashPassword(password) {
return await bcrypt.hash(password, 10);
}
generatePassword() {
return Math.random().toString(36).slice(-8);
}
async verify(password, hash) {
return await bcrypt.compare(password, hash);
}
async register(userInfos) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const existingUser = await userCollection.findOne({ email: userInfos.email });
if (existingUser) {
throw new AppError(USER_ALREADY_EXISTS);
} }
let newUser = {
name: userInfos.name ?? userInfos.email,
email: userInfos.email,
password: await this.hashPassword(userInfos.password),
created_at: new Date(),
roles: userInfos.roles
};
let created_user = await userCollection.insertOne(newUser);
let user = await this.getById(created_user.insertedId)
const folderTitle = "Dossier par Défaut";
const userId = newUser._id ? newUser._id.toString() : 'x'; async hashPassword(password) {
await this.folders.create(folderTitle, userId); return await bcrypt.hash(password, 10)
// TODO: verif if inserted properly...
return user;
}
async login(userid) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: userid });
if (!user) {
return false;
} }
return user; generatePassword() {
} return Math.random().toString(36).slice(-8);
/*
async login(email, password) {
try {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ email: email });
if (!user) {
const error = new Error("User not found");
error.statusCode = 404;
throw error;
}
const passwordMatch = await this.verify(password, user.password);
if (!passwordMatch) {
const error = new Error("Password does not match");
error.statusCode = 401;
throw error;
}
return user;
} catch (error) {
console.error(error);
throw error;
}
}
*/
async resetPassword(email) {
const newPassword = this.generatePassword();
return await this.changePassword(email, newPassword);
}
async changePassword(email, newPassword) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const hashedPassword = await this.hashPassword(newPassword);
const result = await userCollection.updateOne(
{ email },
{ $set: { password: hashedPassword } }
);
if (result.modifiedCount != 1) return null;
return newPassword;
}
async delete(email) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const result = await userCollection.deleteOne({ email });
if (result.deletedCount != 1) return false;
return true;
}
async getId(email) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ email: email });
if (!user) {
return false;
} }
return user._id; async verify(password, hash) {
} return await bcrypt.compare(password, hash)
async getById(id) {
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: id });
if (!user) {
return false;
} }
return user; async register(email, password) {
} await this.db.connect()
const conn = this.db.getConnection();
const userCollection = conn.collection('users');
async editUser(userInfo) { const existingUser = await userCollection.findOne({ email: email });
await this.db.connect();
const conn = this.db.getConnection();
const userCollection = conn.collection("users"); if (existingUser) {
throw new AppError(USER_ALREADY_EXISTS);
}
const user = await userCollection.findOne({ _id: userInfo.id }); const newUser = {
email: email,
password: await this.hashPassword(password),
created_at: new Date()
};
if (!user) { const result = await userCollection.insertOne(newUser);
return false; // console.log("userCollection.insertOne() result", result);
const userId = result.insertedId.toString();
const folderTitle = 'Dossier par Défaut';
await this.folders.create(folderTitle, userId);
return result;
} }
const updatedFields = { ...userInfo }; async login(email, password) {
delete updatedFields.id; await this.db.connect()
const conn = this.db.getConnection();
const result = await userCollection.updateOne( const userCollection = conn.collection('users');
{ _id: userInfo.id },
{ $set: updatedFields }
);
if (result.modifiedCount === 1) { const user = await userCollection.findOne({ email: email });
return true;
if (!user) {
return false;
}
const passwordMatch = await this.verify(password, user.password);
if (!passwordMatch) {
return false;
}
return user;
}
async resetPassword(email) {
const newPassword = this.generatePassword();
return await this.changePassword(email, newPassword);
}
async changePassword(email, newPassword) {
await this.db.connect()
const conn = this.db.getConnection();
const userCollection = conn.collection('users');
const hashedPassword = await this.hashPassword(newPassword);
const result = await userCollection.updateOne({ email }, { $set: { password: hashedPassword } });
if (result.modifiedCount != 1) return null;
return newPassword
}
async delete(email) {
await this.db.connect()
const conn = this.db.getConnection();
const userCollection = conn.collection('users');
const result = await userCollection.deleteOne({ email });
if (result.deletedCount != 1) return false;
return true;
}
async getId(email) {
await this.db.connect()
const conn = this.db.getConnection();
const userCollection = conn.collection('users');
const user = await userCollection.findOne({ email: email });
if (!user) {
return false;
}
return user._id;
} }
return false;
}
} }
module.exports = Users; module.exports = Users;

548
server/package-lock.json generated
View file

@ -7,22 +7,16 @@
"": { "": {
"name": "ets-pfe004-evaluetonsavoir-backend", "name": "ets-pfe004-evaluetonsavoir-backend",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
@ -1624,11 +1618,6 @@
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true "dev": true
}, },
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -1745,6 +1734,7 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@ -1816,14 +1806,6 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true "dev": true
}, },
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -1953,14 +1935,6 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@ -2019,6 +1993,7 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@ -2105,41 +2080,15 @@
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.8", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0", "es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"function-bind": "^1.1.2" "function-bind": "^1.1.2",
}, "get-intrinsic": "^1.2.4",
"engines": { "set-function-length": "^1.2.1"
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2190,6 +2139,7 @@
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-styles": "^4.1.0", "ansi-styles": "^4.1.0",
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
@ -2205,6 +2155,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -2213,6 +2164,7 @@
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": { "dependencies": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
}, },
@ -2268,6 +2220,7 @@
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -2318,6 +2271,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
}, },
@ -2328,7 +2282,8 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
}, },
"node_modules/color-support": { "node_modules/color-support": {
"version": "1.1.3", "version": "1.1.3",
@ -2514,6 +2469,7 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
@ -2656,19 +2612,6 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": { "node_modules/ecdsa-sig-formatter": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@ -2813,9 +2756,12 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -2828,17 +2774,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -3247,37 +3182,6 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-session": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -3330,6 +3234,7 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -3367,14 +3272,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/flat-cache": { "node_modules/flat-cache": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@ -3441,20 +3338,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fs-minipass": { "node_modules/fs-minipass": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@ -3482,6 +3365,20 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -3528,20 +3425,15 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.7", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"get-proto": "^1.0.0", "has-proto": "^1.0.1",
"gopd": "^1.2.0", "has-symbols": "^1.0.3",
"has-symbols": "^1.1.0", "hasown": "^2.0.0"
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3559,18 +3451,6 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -3628,11 +3508,11 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"engines": { "dependencies": {
"node": ">= 0.4" "get-intrinsic": "^1.1.3"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -3641,7 +3521,8 @@
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
}, },
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "3.0.0", "version": "3.0.0",
@ -3663,10 +3544,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -3680,9 +3572,9 @@
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@ -3896,20 +3788,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -3952,6 +3830,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -3968,17 +3847,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -3987,7 +3855,8 @@
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
"version": "3.2.2", "version": "3.2.2",
@ -4808,24 +4677,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz",
"integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": { "node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@ -4833,11 +4684,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -4850,25 +4696,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.2", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@ -4924,14 +4751,6 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -5059,14 +4878,6 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -5106,6 +4917,7 @@
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -5468,11 +5280,6 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"node_modules/oauth": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -5492,14 +5299,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -5511,14 +5310,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -5542,21 +5333,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -5575,14 +5351,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -5658,115 +5426,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-openidconnect": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz",
"integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==",
"dependencies": {
"oauth": "0.10.x",
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/patch-package": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^9.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.0.33",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/patch-package/node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -5788,6 +5447,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -5804,11 +5464,6 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -5819,6 +5474,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -5957,14 +5613,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -6206,6 +5854,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
}, },
@ -6217,6 +5866,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -6655,17 +6305,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -6685,6 +6324,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -6774,22 +6414,6 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
},
"node_modules/undefsafe": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@ -6801,14 +6425,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
}, },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -6925,6 +6541,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },
@ -7038,17 +6655,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}, },
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View file

@ -7,8 +7,7 @@
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
"start": "node app.js", "start": "node app.js",
"dev": "cross-env NODE_ENV=development nodemon app.js", "dev": "cross-env NODE_ENV=development nodemon app.js",
"test": "jest", "test": "jest --colors"
"postinstall": "patch-package"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -18,15 +17,10 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },

View file

@ -1,12 +0,0 @@
diff --git a/node_modules/passport-openidconnect/lib/profile.js b/node_modules/passport-openidconnect/lib/profile.js
index eeabf4e..8abe391 100644
--- a/node_modules/passport-openidconnect/lib/profile.js
+++ b/node_modules/passport-openidconnect/lib/profile.js
@@ -17,6 +17,7 @@ exports.parse = function(json) {
if (json.middle_name) { profile.name.middleName = json.middle_name; }
}
if (json.email) { profile.emails = [ { value: json.email } ]; }
+ if (json.groups) { profile.groups = [ { value: json.groups } ]; }
return profile;
};

View file

@ -1,9 +0,0 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/auth.js')
router.get("/getActiveAuth",authController.getActive);
router.get("/getRoomsRequireAuth", authController.getRoomsRequireAuth);
module.exports = router;

View file

@ -3,12 +3,11 @@ const router = express.Router();
const users = require('../app.js').users; const users = require('../app.js').users;
const jwt = require('../middleware/jwtToken.js'); const jwt = require('../middleware/jwtToken.js');
const asyncHandler = require('./routerUtils.js'); const asyncHandler = require('./routerUtils.js');
const usersController = require('../controllers/users.js')
router.post("/register", asyncHandler(users.register)); router.post("/register", asyncHandler(users.register));
router.post("/login", asyncHandler(users.login)); router.post("/login", asyncHandler(users.login));
router.post("/reset-password", asyncHandler(users.resetPassword)); router.post("/reset-password", asyncHandler(users.resetPassword));
router.post("/change-password", jwt.authenticate, asyncHandler(users.changePassword)); router.post("/change-password", jwt.authenticate, asyncHandler(users.changePassword));
router.post("/delete-user", jwt.authenticate, usersController); router.post("/delete-user", jwt.authenticate, asyncHandler(users.delete));
module.exports = router; module.exports = router;

View file

@ -1,35 +0,0 @@
function hasNestedValue(obj, path, delimiter = "_") {
const keys = path.split(delimiter);
let current = obj;
for (const key of keys) {
while(Array.isArray(current) && current.length == 1 && current[0]){
current = current[0]
}
while(current['value']){
current = current.value
}
if (current && typeof current === "object") {
if (Array.isArray(current)) {
const index = current.findIndex(x => x == key)
if (index != -1) {
current = current[index];
} else {
return false;
}
} else if (key in current) {
current = current[key];
} else {
return false;
}
} else {
return false;
}
}
return true;
}
module.exports = { hasNestedValue};