mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge remote-tracking branch 'prt/main' into PFEH25-merge-it2main
This commit is contained in:
commit
2c01badd76
87 changed files with 1959 additions and 3620 deletions
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||||
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal 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.
|
||||||
60
.github/workflows/tests.yml
vendored
60
.github/workflows/tests.yml
vendored
|
|
@ -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
3
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
||||||
}
|
|
||||||
1
LICENSE
1
LICENSE
|
|
@ -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
19
client/.eslintrc.cjs
Normal 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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -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']
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
/* eslint-disable no-undef */
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -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
394
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 & b \\\\ c & 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 & b \\\\ c & 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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
163
client/src/__tests__/components/LiveResults/LiveResults.test.tsx
Normal file
163
client/src/__tests__/components/LiveResults/LiveResults.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
253
client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
Normal file
253
client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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'éditeur</h2>
|
<h2 className="subtitle">Informations pratiques sur l'é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">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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'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;
|
||||||
|
|
@ -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%);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,6 +34,7 @@ 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) {
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
// JoinRoom.tsx
|
// JoinRoom.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
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 [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
@ -24,18 +25,21 @@ const SimpleLogin: React.FC = () => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async () => {
|
const register = async () => {
|
||||||
const result = await ApiService.login(email, password);
|
const result = await ApiService.register(email, password);
|
||||||
if (result !== true) {
|
|
||||||
|
if (typeof result === 'string') {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate("/teacher/login")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContainer
|
<LoginContainer
|
||||||
title=''
|
title='Créer un compte'
|
||||||
error={connectionError}>
|
error={connectionError}>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -43,7 +47,7 @@ const SimpleLogin: React.FC = () => {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Nom d'utilisateur"
|
placeholder="Adresse courriel"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
@ -51,38 +55,27 @@ const SimpleLogin: React.FC = () => {
|
||||||
<TextField
|
<TextField
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
|
||||||
value={password}
|
value={password}
|
||||||
|
type="password"
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Nom de la salle"
|
placeholder="Mot de passe"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
loading={isConnecting}
|
loading={isConnecting}
|
||||||
onClick={login}
|
onClick={register}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||||
disabled={!email || !password}
|
disabled={!email || !password}
|
||||||
>
|
>
|
||||||
Login
|
S'inscrire
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
||||||
<div className="login-links">
|
|
||||||
|
|
||||||
<Link to="/resetPassword">
|
|
||||||
Réinitialiser le mot de passe
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link to="/register">
|
|
||||||
Créer un compte
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SimpleLogin;
|
export default Register;
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
|
|
@ -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
1
server/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
auth_config.json
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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":{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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 = {
|
async hashPassword(password) {
|
||||||
name: userInfos.name ?? userInfos.email,
|
return await bcrypt.hash(password, 10)
|
||||||
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';
|
|
||||||
await this.folders.create(folderTitle, userId);
|
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
async editUser(userInfo) {
|
const userCollection = conn.collection('users');
|
||||||
await this.db.connect();
|
|
||||||
const conn = this.db.getConnection();
|
|
||||||
|
|
||||||
const userCollection = conn.collection("users");
|
const existingUser = await userCollection.findOne({ email: email });
|
||||||
|
|
||||||
const user = await userCollection.findOne({ _id: userInfo.id });
|
if (existingUser) {
|
||||||
|
throw new AppError(USER_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
const newUser = {
|
||||||
return false;
|
email: email,
|
||||||
|
password: await this.hashPassword(password),
|
||||||
|
created_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userCollection.insertOne(newUser);
|
||||||
|
// 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
548
server/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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};
|
|
||||||
Loading…
Reference in a new issue