From 6b4364c7c7482dca426be8ba0ac9ce7b0c8264f7 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Wed, 29 Jan 2025 21:45:41 -0500 Subject: [PATCH] PFEH2025 - merge entre main et dev-it2-PFEA2024 pour activation SSO --- .gitignore | 3 + client/package-lock.json | 19 +- client/package.json | 1 + client/src/App.tsx | 96 +++-- .../services/WebsocketService.test.tsx | 14 +- client/src/components/Header/Header.tsx | 14 +- client/src/constants.tsx | 6 +- client/src/pages/AuthManager/AuthDrawer.tsx | 61 +++ client/src/pages/AuthManager/authDrawer.css | 48 +++ .../AuthManager/callback/AuthCallback.tsx | 27 ++ .../providers/OAuth-Oidc/ButtonAuth.tsx | 27 ++ .../providers/SimpleLogin/Login.tsx} | 169 ++++---- .../providers/SimpleLogin/Register.tsx | 114 +++++ .../providers/SimpleLogin/ResetPassword.tsx | 68 +++ .../AuthManager/providers/css/buttonAuth.css | 23 + .../AuthManager/providers/css/simpleLogin.css | 17 + client/src/pages/Home/home.css | 19 + .../src/pages/Student/JoinRoom/JoinRoom.tsx | 8 +- .../src/pages/Teacher/Dashboard/Dashboard.tsx | 2 +- .../pages/Teacher/ManageRoom/ManageRoom.tsx | 2 +- client/src/pages/Teacher/Share/Share.tsx | 2 +- client/src/services/ApiService.tsx | 169 ++++++-- client/src/services/AuthService.tsx | 28 ++ docker-compose-auth.yaml | 96 +++++ docker-compose.yaml | 41 +- oauth-tester/config.json | 96 +++++ server/.env.example | 8 +- server/.gitignore | 1 + server/__tests__/auth.test.js | 244 +++++++++++ server/app.js | 25 +- server/auth/auth-manager.js | 66 +++ .../auth/modules/passport-providers/oauth.js | 101 +++++ .../auth/modules/passport-providers/oidc.js | 103 +++++ server/auth/modules/passportjs.js | 63 +++ server/auth/modules/simpleauth.js | 125 ++++++ server/auth_config.json.example | 26 ++ server/config/auth.js | 192 +++++++++ server/controllers/auth.js | 36 ++ server/middleware/jwtToken.js | 8 +- server/models/authProvider.js | 44 ++ server/models/authUserAssociation.js | 59 +++ server/models/userAuthAssociation.js | 13 + server/models/users.js | 274 +++++++----- server/package-lock.json | 397 +++++++++++++++++- server/package.json | 8 +- .../passport-openidconnect+0.1.2.patch | 12 + server/routers/auth.js | 10 + server/routers/users.js | 3 +- server/utils.js | 35 ++ 49 files changed, 2695 insertions(+), 328 deletions(-) create mode 100644 client/src/pages/AuthManager/AuthDrawer.tsx create mode 100644 client/src/pages/AuthManager/authDrawer.css create mode 100644 client/src/pages/AuthManager/callback/AuthCallback.tsx create mode 100644 client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx rename client/src/pages/{Teacher/Register/Register.tsx => AuthManager/providers/SimpleLogin/Login.tsx} (63%) create mode 100644 client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx create mode 100644 client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx create mode 100644 client/src/pages/AuthManager/providers/css/buttonAuth.css create mode 100644 client/src/pages/AuthManager/providers/css/simpleLogin.css create mode 100644 client/src/services/AuthService.tsx create mode 100644 docker-compose-auth.yaml create mode 100644 oauth-tester/config.json create mode 100644 server/.gitignore create mode 100644 server/__tests__/auth.test.js create mode 100644 server/auth/auth-manager.js create mode 100644 server/auth/modules/passport-providers/oauth.js create mode 100644 server/auth/modules/passport-providers/oidc.js create mode 100644 server/auth/modules/passportjs.js create mode 100644 server/auth/modules/simpleauth.js create mode 100644 server/auth_config.json.example create mode 100644 server/config/auth.js create mode 100644 server/controllers/auth.js create mode 100644 server/models/authProvider.js create mode 100644 server/models/authUserAssociation.js create mode 100644 server/models/userAuthAssociation.js create mode 100644 server/patches/passport-openidconnect+0.1.2.patch create mode 100644 server/routers/auth.js create mode 100644 server/utils.js diff --git a/.gitignore b/.gitignore index 6e8de7b..d4eb19a 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +.env +launch.json + # yarn v2 .yarn/cache .yarn/unplugged diff --git a/client/package-lock.json b/client/package-lock.json index 8105575..4ee571d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "esbuild": "^0.23.1", "gift-pegjs": "^2.0.0-beta.1", "jest-environment-jsdom": "^29.7.0", + "jwt-decode": "^4.0.0", "katex": "^0.16.11", "marked": "^14.1.2", "nanoid": "^5.0.2", @@ -9476,20 +9477,12 @@ "node": ">= 10.0.0" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "engines": { - "node": ">=4.0" + "node": ">=18" } }, "node_modules/katex": { diff --git a/client/package.json b/client/package.json index 690ed35..939257a 100644 --- a/client/package.json +++ b/client/package.json @@ -27,6 +27,7 @@ "esbuild": "^0.23.1", "gift-pegjs": "^2.0.0-beta.1", "jest-environment-jsdom": "^29.7.0", + "jwt-decode": "^4.0.0", "katex": "^0.16.11", "marked": "^14.1.2", "nanoid": "^5.0.2", diff --git a/client/src/App.tsx b/client/src/App.tsx index 8f8ecf8..9b16e2f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; -// App.tsx -import { Routes, Route } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; // Page main import Home from './pages/Home/Home'; @@ -8,37 +8,55 @@ import Home from './pages/Home/Home'; // Pages espace enseignant import Dashboard from './pages/Teacher/Dashboard/Dashboard'; import Share from './pages/Teacher/Share/Share'; -import Login from './pages/Teacher/Login/Login'; -import Register from './pages/Teacher/Register/Register'; -import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword'; +import Register from './pages/AuthManager/providers/SimpleLogin/Register'; +import ResetPassword from './pages/AuthManager/providers/SimpleLogin/ResetPassword'; import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom'; import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz'; // Pages espace étudiant import JoinRoom from './pages/Student/JoinRoom/JoinRoom'; +// Pages authentification selection +import AuthDrawer from './pages/AuthManager/AuthDrawer'; + // Header/Footer import import Header from './components/Header/Header'; import Footer from './components/Footer/Footer'; import ApiService from './services/ApiService'; +import OAuthCallback from './pages/AuthManager/callback/AuthCallback'; -const handleLogout = () => { - ApiService.logout(); -} +const App: React.FC = () => { + const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); + const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); + const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null); + const location = useLocation(); -const isLoggedIn = () => { - return ApiService.isLoggedIn(); -} + // Check login status every time the route changes + useEffect(() => { + 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 (
- -
- +
@@ -46,22 +64,46 @@ function App() { } /> {/* Pages espace enseignant */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> + : } + /> + : } + /> + : } + /> + : } + /> {/* Pages espace étudiant */} - } /> + : } + /> + + {/* Pages authentification */} + } /> + + {/* Pages enregistrement */} + } /> + + {/* Pages rest password */} + } /> + + {/* Pages authentification sélection */} + } />
-
+
); -} +}; export default App; diff --git a/client/src/__tests__/services/WebsocketService.test.tsx b/client/src/__tests__/services/WebsocketService.test.tsx index 5a98e3e..7c3b4e0 100644 --- a/client/src/__tests__/services/WebsocketService.test.tsx +++ b/client/src/__tests__/services/WebsocketService.test.tsx @@ -23,13 +23,13 @@ describe('WebSocketService', () => { }); test('connect should initialize socket connection', () => { - WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); expect(io).toHaveBeenCalled(); expect(WebsocketService['socket']).toBe(mockSocket); }); test('disconnect should terminate socket connection', () => { - mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); expect(WebsocketService['socket']).toBeTruthy(); WebsocketService.disconnect(); expect(mockSocket.disconnect).toHaveBeenCalled(); @@ -37,7 +37,7 @@ describe('WebSocketService', () => { }); test('createRoom should emit create-room event', () => { - WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.createRoom(); expect(mockSocket.emit).toHaveBeenCalledWith('create-room'); }); @@ -46,7 +46,7 @@ describe('WebSocketService', () => { const roomName = 'testRoom'; const question = { id: 1, text: 'Sample Question' }; - mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.nextQuestion(roomName, question); expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question }); }); @@ -55,7 +55,7 @@ describe('WebSocketService', () => { const roomName = 'testRoom'; const questions = [{ id: 1, text: 'Sample Question' }]; - mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.launchStudentModeQuiz(roomName, questions); expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', { roomName, @@ -66,7 +66,7 @@ describe('WebSocketService', () => { test('endQuiz should emit end-quiz event with correct parameters', () => { const roomName = 'testRoom'; - mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.endQuiz(roomName); expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName }); }); @@ -75,7 +75,7 @@ describe('WebSocketService', () => { const enteredRoomName = 'testRoom'; const username = 'testUser'; - mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); WebsocketService.joinRoom(enteredRoomName, username); expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username }); }); diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index a59f806..016d23e 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,10 +1,10 @@ -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import * as React from 'react'; import './header.css'; import { Button } from '@mui/material'; interface HeaderProps { - isLoggedIn: () => boolean; + isLoggedIn: boolean; handleLogout: () => void; } @@ -20,7 +20,7 @@ const Header: React.FC = ({ isLoggedIn, handleLogout }) => { onClick={() => navigate('/')} /> - {isLoggedIn() && ( + {isLoggedIn && ( )} + + {!isLoggedIn && ( +
+ + + +
+ )} ); }; diff --git a/client/src/constants.tsx b/client/src/constants.tsx index 1fc104b..dccc503 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -1,11 +1,11 @@ // constants.tsx const ENV_VARIABLES = { MODE: 'production', - VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "", - VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "", + VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", + BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', + FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; 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 }; diff --git a/client/src/pages/AuthManager/AuthDrawer.tsx b/client/src/pages/AuthManager/AuthDrawer.tsx new file mode 100644 index 0000000..093b7aa --- /dev/null +++ b/client/src/pages/AuthManager/AuthDrawer.tsx @@ -0,0 +1,61 @@ +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(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 ( +
+

Connexion

+ + {/* Formulaire de connexion Simple Login */} + {authData && authData['simpleauth'] && ( +
+ +
+ )} + + {/* Conteneur OAuth/OIDC */} + {authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && ( +
+ {Object.keys(authData).map((providerKey) => { + const providerType = authData[providerKey].type; + if (providerType === 'oidc' || providerType === 'oauth') { + return ( + + ); + } + return null; + })} +
+ )} + +
+ +
+
+ ); +}; + +export default AuthSelection; diff --git a/client/src/pages/AuthManager/authDrawer.css b/client/src/pages/AuthManager/authDrawer.css new file mode 100644 index 0000000..1543fc2 --- /dev/null +++ b/client/src/pages/AuthManager/authDrawer.css @@ -0,0 +1,48 @@ +.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; + } \ No newline at end of file diff --git a/client/src/pages/AuthManager/callback/AuthCallback.tsx b/client/src/pages/AuthManager/callback/AuthCallback.tsx new file mode 100644 index 0000000..6206294 --- /dev/null +++ b/client/src/pages/AuthManager/callback/AuthCallback.tsx @@ -0,0 +1,27 @@ +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
Loading...
; +}; + +export default OAuthCallback; diff --git a/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx b/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx new file mode 100644 index 0000000..c8f4efc --- /dev/null +++ b/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx @@ -0,0 +1,27 @@ +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 = ({ providerName, providerType }) => { + return ( + <> +
+

Se connecter avec {providerType.toUpperCase()}

+ +
+ + ); +}; + +export default ButtonAuth; \ No newline at end of file diff --git a/client/src/pages/Teacher/Register/Register.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx similarity index 63% rename from client/src/pages/Teacher/Register/Register.tsx rename to client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx index e09b316..6356d7a 100644 --- a/client/src/pages/Teacher/Register/Register.tsx +++ b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx @@ -1,81 +1,88 @@ - -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 'src/components/LoginContainer/LoginContainer' -import ApiService from '../../../services/ApiService'; - -const Register: React.FC = () => { - const navigate = useNavigate(); - - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - const [connectionError, setConnectionError] = useState(''); - const [isConnecting] = useState(false); - - useEffect(() => { - return () => { - - }; - }, []); - - const register = async () => { - const result = await ApiService.register(email, password); - - if (typeof result === 'string') { - setConnectionError(result); - return; - } - - navigate("/teacher/login") - }; - - - return ( - - - setEmail(e.target.value)} - placeholder="Adresse courriel" - sx={{ marginBottom: '1rem' }} - fullWidth - /> - - setPassword(e.target.value)} - placeholder="Mot de passe" - sx={{ marginBottom: '1rem' }} - fullWidth - /> - - - S'inscrire - - - - - ); -}; - -export default Register; +import { Link } from 'react-router-dom'; + +// JoinRoom.tsx +import React, { useEffect, useState } from 'react'; + +import '../css/simpleLogin.css'; +import { TextField } from '@mui/material'; +import LoadingButton from '@mui/lab/LoadingButton'; + +import LoginContainer from '../../../../components/LoginContainer/LoginContainer' +import ApiService from '../../../../services/ApiService'; + +const SimpleLogin: React.FC = () => { + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const [connectionError, setConnectionError] = useState(''); + const [isConnecting] = useState(false); + + useEffect(() => { + return () => { + + }; + }, []); + + const login = async () => { + const result = await ApiService.login(email, password); + if (result !== true) { + setConnectionError(result); + return; + } + }; + + + return ( + + + setEmail(e.target.value)} + placeholder="Nom d'utilisateur" + sx={{ marginBottom: '1rem' }} + fullWidth + /> + + setPassword(e.target.value)} + placeholder="Nom de la salle" + sx={{ marginBottom: '1rem' }} + fullWidth + /> + + + Login + + +
+ + + Réinitialiser le mot de passe + + + + Créer un compte + + +
+ +
+ ); +}; + +export default SimpleLogin; diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx new file mode 100644 index 0000000..d33527d --- /dev/null +++ b/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx @@ -0,0 +1,114 @@ +// 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(['student']); // Set 'student' as the default role + + const [connectionError, setConnectionError] = useState(''); + const [isConnecting] = useState(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 ( + + setName(e.target.value)} + placeholder="Votre nom" + sx={{ marginBottom: '1rem' }} + fullWidth + /> + + setEmail(e.target.value)} + placeholder="Adresse courriel" + sx={{ marginBottom: '1rem' }} + fullWidth + type="email" + error={!!connectionError && !isValidEmail(email)} + helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""} + /> + + setPassword(e.target.value)} + placeholder="Mot de passe" + sx={{ marginBottom: '1rem' }} + fullWidth + /> + + + Choisir votre rôle + handleRoleChange(e.target.value)} + > + } label="Étudiant" /> + } label="Professeur" /> + + + + + S'inscrire + + + ); +}; + +export default Register; diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx new file mode 100644 index 0000000..c33c9fa --- /dev/null +++ b/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx @@ -0,0 +1,68 @@ + +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(''); + const [isConnecting] = useState(false); + + useEffect(() => { + return () => { + + }; + }, []); + + const reset = async () => { + const result = await ApiService.resetPassword(email); + + if (!result) { + setConnectionError(result.toString()); + return; + } + + navigate("/login") + }; + + + return ( + + + setEmail(e.target.value)} + placeholder="Adresse courriel" + sx={{ marginBottom: '1rem' }} + fullWidth + /> + + + Réinitialiser le mot de passe + + + + ); +}; + +export default ResetPassword; diff --git a/client/src/pages/AuthManager/providers/css/buttonAuth.css b/client/src/pages/AuthManager/providers/css/buttonAuth.css new file mode 100644 index 0000000..98476ec --- /dev/null +++ b/client/src/pages/AuthManager/providers/css/buttonAuth.css @@ -0,0 +1,23 @@ +.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; +} \ No newline at end of file diff --git a/client/src/pages/AuthManager/providers/css/simpleLogin.css b/client/src/pages/AuthManager/providers/css/simpleLogin.css new file mode 100644 index 0000000..ddbebdb --- /dev/null +++ b/client/src/pages/AuthManager/providers/css/simpleLogin.css @@ -0,0 +1,17 @@ +.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; +} diff --git a/client/src/pages/Home/home.css b/client/src/pages/Home/home.css index 1fc8a8d..8a6a1a7 100644 --- a/client/src/pages/Home/home.css +++ b/client/src/pages/Home/home.css @@ -61,6 +61,25 @@ 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) { .btn-container { flex-direction: column; diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index f0ac8d7..65bc699 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -15,9 +15,11 @@ import LoadingButton from '@mui/lab/LoadingButton'; import LoginContainer from 'src/components/LoginContainer/LoginContainer' +import ApiService from '../../../services/ApiService' + const JoinRoom: React.FC = () => { const [roomName, setRoomName] = useState(''); - const [username, setUsername] = useState(''); + const [username, setUsername] = useState(ApiService.getUsername()); const [socket, setSocket] = useState(null); const [isWaitingForTeacher, setIsWaitingForTeacher] = useState(false); const [question, setQuestion] = useState(); @@ -34,8 +36,8 @@ const JoinRoom: React.FC = () => { }, []); const handleCreateSocket = () => { - console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`); - const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); + const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); socket.on('join-success', () => { setIsWaitingForTeacher(true); diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index f920d12..5e9ecba 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -78,7 +78,7 @@ const Dashboard: React.FC = () => { useEffect(() => { const fetchData = async () => { if (!ApiService.isLoggedIn()) { - navigate("/teacher/login"); + navigate("/login"); return; } else { diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index ae9bdfa..9d185dd 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -84,7 +84,7 @@ const ManageRoom: React.FC = () => { const createWebSocketRoom = () => { console.log('Creating WebSocket room...'); setConnectingError(''); - const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL); + const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); socket.on('connect', () => { webSocketService.createRoom(); diff --git a/client/src/pages/Teacher/Share/Share.tsx b/client/src/pages/Teacher/Share/Share.tsx index 31bb72c..0dc4fe7 100644 --- a/client/src/pages/Teacher/Share/Share.tsx +++ b/client/src/pages/Teacher/Share/Share.tsx @@ -33,7 +33,7 @@ const Share: React.FC = () => { if (!ApiService.isLoggedIn()) { window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); - navigate("/teacher/login"); + navigate("/login"); return; } diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index ef124b4..6d6e561 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -1,8 +1,9 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; +import { jwtDecode } from 'jwt-decode'; +import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; import { QuizType } from 'src/Types/QuizType'; -import { ENV_VARIABLES } from 'src/constants'; type ApiResponse = boolean | string; @@ -34,7 +35,7 @@ class ApiService { } // Helpers - private saveToken(token: string): void { + public saveToken(token: string): void { const now = new Date(); const object = { @@ -78,7 +79,71 @@ class ApiService { 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 { + 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 { + localStorage.removeItem("username"); return localStorage.removeItem("jwt"); } @@ -88,21 +153,25 @@ class ApiService { * @returns true if successful * @returns A error string if unsuccessful, */ - public async register(email: string, password: string): Promise { + public async register(name: string, email: string, password: string, roles: string[]): Promise { try { if (!email || !password) { throw new Error(`L'email et le mot de passe sont requis.`); } - const url: string = this.constructRequestUrl(`/user/register`); + const url: string = this.constructRequestUrl(`/auth/simple-auth/register`); const headers = this.constructRequestHeaders(); - const body = { email, password }; + const body = { name, email, password, roles }; const result: AxiosResponse = await axios.post(url, body, { headers: headers }); - if (result.status !== 200) { - throw new Error(`L'enregistrement a échoué. Status: ${result.status}`); + console.log(result); + if (result.status == 200) { + window.location.href = result.request.responseURL; + } + else { + throw new Error(`La connexion a échoué. Status: ${result.status}`); } return true; @@ -124,44 +193,52 @@ class ApiService { * @returns true if successful * @returns A error string if unsuccessful, */ - public async login(email: string, password: string): Promise { - try { - - if (!email || !password) { - throw new Error(`L'email et le mot de passe sont requis.`); - } - - const url: string = this.constructRequestUrl(`/user/login`); - 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.` + /** + * @returns true if successful + * @returns An error string if unsuccessful + */ +public async login(email: string, password: string): Promise { + 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`); + const headers = this.constructRequestHeaders(); + 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 + return "Erreur serveur inconnue lors de la requête."; + } + + // Handle other non-Axios errors + return "Une erreur inattendue s'est produite."; } +} + /** * @returns true if successful @@ -174,7 +251,7 @@ class ApiService { throw new Error(`L'email est requis.`); } - const url: string = this.constructRequestUrl(`/user/reset-password`); + const url: string = this.constructRequestUrl(`/auth/simple-auth/reset-password`); const headers = this.constructRequestHeaders(); const body = { email }; @@ -210,7 +287,7 @@ class ApiService { throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`); } - const url: string = this.constructRequestUrl(`/user/change-password`); + const url: string = this.constructRequestUrl(`/auth/simple-auth/change-password`); const headers = this.constructRequestHeaders(); const body = { email, oldPassword, newPassword }; @@ -891,4 +968,4 @@ class ApiService { } const apiService = new ApiService(); -export default apiService; +export default apiService; \ No newline at end of file diff --git a/client/src/services/AuthService.tsx b/client/src/services/AuthService.tsx new file mode 100644 index 0000000..bca616d --- /dev/null +++ b/client/src/services/AuthService.tsx @@ -0,0 +1,28 @@ +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; \ No newline at end of file diff --git a/docker-compose-auth.yaml b/docker-compose-auth.yaml new file mode 100644 index 0000000..749c6b4 --- /dev/null +++ b/docker-compose-auth.yaml @@ -0,0 +1,96 @@ +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 diff --git a/docker-compose.yaml b/docker-compose.yaml index 24bd3a6..0d8d61a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,19 +1,20 @@ +version: '3' + services: frontend: - image: fuhrmanator/evaluetonsavoir-frontend:latest + build: + context: ./client + dockerfile: Dockerfile 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: - "5173:5173" restart: always backend: - image: fuhrmanator/evaluetonsavoir-backend:latest + build: + context: ./server + dockerfile: Dockerfile container_name: backend ports: - "3000:3000" @@ -25,9 +26,16 @@ services: SENDER_EMAIL: infoevaluetonsavoir@gmail.com EMAIL_PSW: 'vvml wmfr dkzb vjzb' JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe - FRONTEND_URL: "http://localhost:5173" + 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 @@ -79,6 +87,23 @@ services: - WATCHTOWER_INCLUDE_RESTARTING=true 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: mongodb_data: external: false diff --git a/oauth-tester/config.json b/oauth-tester/config.json new file mode 100644 index 0000000..ef8f778 --- /dev/null +++ b/oauth-tester/config.json @@ -0,0 +1,96 @@ +{ + "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"] +} \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index 8608d36..3ab7212 100644 --- a/server/.env.example +++ b/server/.env.example @@ -14,4 +14,10 @@ EMAIL_PSW='vvml wmfr dkzb vjzb' JWT_SECRET=TOKEN! # Pour creer les liens images -FRONTEND_URL=http://localhost:5173 +SESSION_Secret='session_secret' + +SITE_URL=http://localhost +FRONTEND_PORT=5173 +USE_PORTS=false + +AUTHENTICATED_ROOMS=false diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..47c9c3b --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +auth_config.json \ No newline at end of file diff --git a/server/__tests__/auth.test.js b/server/__tests__/auth.test.js new file mode 100644 index 0000000..7a97c69 --- /dev/null +++ b/server/__tests__/auth.test.js @@ -0,0 +1,244 @@ +const request = require("supertest"); +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 logSpy; + + // 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); + 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(2); + 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); + } + ); + + } +) diff --git a/server/app.js b/server/app.js index 570ee8b..cba0f12 100644 --- a/server/app.js +++ b/server/app.js @@ -39,17 +39,25 @@ module.exports.images = imagesControllerInstance; const userRouter = require('./routers/users.js'); const folderRouter = require('./routers/folders.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 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"); // Start app const app = express(); const cors = require("cors"); const bodyParser = require('body-parser'); +let isDev = process.env.NODE_ENV === 'development'; const configureServer = (httpServer, isDev) => { console.log(`Configuring server with isDev: ${isDev}`); @@ -84,8 +92,19 @@ app.use('/api/user', userRouter); app.use('/api/folder', folderRouter); app.use('/api/quiz', quizRouter); app.use('/api/image', imagesRouter); +app.use('/api/auth', authRouter); -app.use(errorHandler); +// 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' } +})); + +authManager = new AuthManager(app,null,userModel) +app.use(errorHandler) // Start server async function start() { diff --git a/server/auth/auth-manager.js b/server/auth/auth-manager.js new file mode 100644 index 0000000..f3288f4 --- /dev/null +++ b/server/auth/auth-manager.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const AuthConfig = require('../config/auth.js'); +const jwt = require('../middleware/jwtToken.js'); +const emailer = require('../config/email.js'); +const model = require('../controllers/users.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é.`) + } + } + } + + async login(userInfo,req,res,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; \ No newline at end of file diff --git a/server/auth/modules/passport-providers/oauth.js b/server/auth/modules/passport-providers/oauth.js new file mode 100644 index 0000000..fe76922 --- /dev/null +++ b/server/auth/modules/passport-providers/oauth.js @@ -0,0 +1,101 @@ +var OAuth2Strategy = require('passport-oauth2') +var authUserAssoc = require('../../../models/authUserAssociation') +var users = require('../../../models/users') +var { hasNestedValue } = require('../../../utils') +var jwt = require('../../../middleware/jwtToken') + +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; diff --git a/server/auth/modules/passport-providers/oidc.js b/server/auth/modules/passport-providers/oidc.js new file mode 100644 index 0000000..77a557c --- /dev/null +++ b/server/auth/modules/passport-providers/oidc.js @@ -0,0 +1,103 @@ +var OpenIDConnectStrategy = require('passport-openidconnect') +var authUserAssoc = require('../../../models/authUserAssociation') +var users = require('../../../models/users') +var { hasNestedValue } = require('../../../utils') +var jwt = require('../../../middleware/jwtToken') + +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(`Les informations de connexions de la connexion OIDC ${name} n'ont pu être chargées.`) + } + } + + 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) { + } + })); + + 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; diff --git a/server/auth/modules/passportjs.js b/server/auth/modules/passportjs.js new file mode 100644 index 0000000..3d2d46c --- /dev/null +++ b/server/auth/modules/passportjs.js @@ -0,0 +1,63 @@ +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é.`) + } + } + } + + 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.`) + } + } + + + register(userInfos){ + return this.authmanager.register(userInfos) + } + + authenticate(userInfo,req,res,next){ + return this.authmanager.login(userInfo,req,res,next) + } + +} + +module.exports = PassportJs; \ No newline at end of file diff --git a/server/auth/modules/simpleauth.js b/server/auth/modules/simpleauth.js new file mode 100644 index 0000000..6836c4c --- /dev/null +++ b/server/auth/modules/simpleauth.js @@ -0,0 +1,125 @@ +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, next) => this.register(this, req, res)); + expressapp.post(`${this.endpoint}/login`, (req, res, next) => this.authenticate(this, req, res)); + expressapp.post(`${this.endpoint}/reset-password`, (req, res, next) => this.resetPassword(this, req, res)); + expressapp.post(`${this.endpoint}/change-password`, jwt.authenticate, (req, res, next) => this.changePassword(this, req, res)); + } catch (error) { + console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`) + } + } + + 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; \ No newline at end of file diff --git a/server/auth_config.json.example b/server/auth_config.json.example new file mode 100644 index 0000000..2a8fb11 --- /dev/null +++ b/server/auth_config.json.example @@ -0,0 +1,26 @@ +{ + "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":{ + + } + } +} \ No newline at end of file diff --git a/server/config/auth.js b/server/config/auth.js new file mode 100644 index 0000000..2b7c4df --- /dev/null +++ b/server/config/auth.js @@ -0,0 +1,192 @@ +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 = {} + } + 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; diff --git a/server/controllers/auth.js b/server/controllers/auth.js new file mode 100644 index 0000000..76769fb --- /dev/null +++ b/server/controllers/auth.js @@ -0,0 +1,36 @@ +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, next) { + const authC = new AuthConfig(); + const roomsRequireAuth = authC.getRoomsRequireAuth(); + + const response = { + roomsRequireAuth + } + + return res.json(response); + } + +} + +module.exports = new authController; \ No newline at end of file diff --git a/server/middleware/jwtToken.js b/server/middleware/jwtToken.js index 292e591..75ad458 100644 --- a/server/middleware/jwtToken.js +++ b/server/middleware/jwtToken.js @@ -7,8 +7,8 @@ dotenv.config(); class Token { - create(email, userId) { - return jwt.sign({ email, userId }, process.env.JWT_SECRET); + create(email, userId, roles) { + return jwt.sign({ email, userId, roles }, process.env.JWT_SECRET); } authenticate(req, res, next) { @@ -25,11 +25,11 @@ class Token { req.user = payload; }); - + } catch (error) { return next(error); } - + return next(); } } diff --git a/server/models/authProvider.js b/server/models/authProvider.js new file mode 100644 index 0000000..ab92da4 --- /dev/null +++ b/server/models/authProvider.js @@ -0,0 +1,44 @@ +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; \ No newline at end of file diff --git a/server/models/authUserAssociation.js b/server/models/authUserAssociation.js new file mode 100644 index 0000000..3c64644 --- /dev/null +++ b/server/models/authUserAssociation.js @@ -0,0 +1,59 @@ +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; \ No newline at end of file diff --git a/server/models/userAuthAssociation.js b/server/models/userAuthAssociation.js new file mode 100644 index 0000000..8e12717 --- /dev/null +++ b/server/models/userAuthAssociation.js @@ -0,0 +1,13 @@ +const db = require('../config/db.js') +const { ObjectId } = require('mongodb'); + + +class AuthUserAssoc { + constructor(authProviderId, authId, userId) { + this._id = new ObjectId(); + this.authProvider_id = authProviderId; + this.auth_id = authId; + this.user_id = userId; + } + } + \ No newline at end of file diff --git a/server/models/users.js b/server/models/users.js index 1a04d86..6b96bdb 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -1,125 +1,195 @@ -//user -const bcrypt = require('bcrypt'); -const AppError = require('../middleware/AppError.js'); -const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); +const bcrypt = require("bcrypt"); +const AppError = require("../middleware/AppError.js"); +const { USER_ALREADY_EXISTS } = require("../constants/errorCodes"); +const Folders = require("../models/folders.js"); class Users { - constructor(db, foldersModel) { - // console.log("Users constructor: db", db) - this.db = db; - this.folders = foldersModel; + + constructor(db, foldersModel) { + this.db = db; + this.folders = foldersModel; + } + + async hashPassword(password) { + return await bcrypt.hash(password, 10); + } + + generatePassword() { + return Math.random().toString(36).slice(-8); + } + + async verify(password, hash) { + return await bcrypt.compare(password, hash); + } + + async register(userInfos) { + await this.db.connect(); + const conn = this.db.getConnection(); + + const userCollection = conn.collection("users"); + + const existingUser = await userCollection.findOne({ email: userInfos.email }); + + if (existingUser) { + throw new AppError(USER_ALREADY_EXISTS); } + + let newUser = { + name: userInfos.name ?? userInfos.email, + email: userInfos.email, + password: await this.hashPassword(userInfos.password), + created_at: new Date(), + roles: userInfos.roles + }; + + let created_user = await userCollection.insertOne(newUser); + let user = await this.getById(created_user.insertedId) + + const folderTitle = "Dossier par Défaut"; - async hashPassword(password) { - return await bcrypt.hash(password, 10) + 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; } - generatePassword() { - return Math.random().toString(36).slice(-8); + return user; + } + + 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; } - async verify(password, hash) { - return await bcrypt.compare(password, hash) + return user._id; + } + + 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; } - async register(email, password) { - await this.db.connect() - const conn = this.db.getConnection(); - - const userCollection = conn.collection('users'); + return user; + } - const existingUser = await userCollection.findOne({ email: email }); + async editUser(userInfo) { + await this.db.connect(); + const conn = this.db.getConnection(); - if (existingUser) { - throw new AppError(USER_ALREADY_EXISTS); - } + const userCollection = conn.collection("users"); - const newUser = { - email: email, - password: await this.hashPassword(password), - created_at: new Date() - }; + const user = await userCollection.findOne({ _id: userInfo.id }); - 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; + if (!user) { + return false; } - async login(email, password) { - await this.db.connect() - const conn = this.db.getConnection(); + const updatedFields = { ...userInfo }; + delete updatedFields.id; - const userCollection = conn.collection('users'); + const result = await userCollection.updateOne( + { _id: userInfo.id }, + { $set: updatedFields } + ); - const user = await userCollection.findOne({ email: email }); - - 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; + if (result.modifiedCount === 1) { + return true; } + return false; + } } module.exports = Users; diff --git a/server/package-lock.json b/server/package-lock.json index 9b44c36..56ccead 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -7,16 +7,22 @@ "": { "name": "ets-pfe004-evaluetonsavoir-backend", "version": "1.0.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.4", "express": "^4.18.2", + "express-session": "^1.18.0", "jsonwebtoken": "^9.0.2", "mongodb": "^6.3.0", "multer": "^1.4.5-lts.1", "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-client": "^4.7.2" }, @@ -1618,6 +1624,11 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1734,7 +1745,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1806,6 +1816,14 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "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": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1935,6 +1953,14 @@ "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": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -1993,7 +2019,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2139,7 +2164,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2155,7 +2179,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2164,7 +2187,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2220,7 +2242,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -2271,7 +2292,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2282,8 +2302,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-support": { "version": "1.1.3", @@ -2449,9 +2468,14 @@ }, "node_modules/cross-env": { "version": "7.0.3", +<<<<<<< HEAD "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, +======= + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", +>>>>>>> dev-it2/dev-it2-PFEA2024 "dependencies": { "cross-spawn": "^7.0.1" }, @@ -3182,12 +3206,37 @@ "url": "https://opencollective.com/express" } }, +<<<<<<< HEAD "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" +======= + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "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-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" +>>>>>>> dev-it2/dev-it2-PFEA2024 }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -3234,7 +3283,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3272,6 +3320,7 @@ "node": ">=8" } }, +<<<<<<< HEAD "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -3293,6 +3342,16 @@ "dev": true, "license": "ISC" }, +======= + "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" + } + }, +>>>>>>> dev-it2/dev-it2-PFEA2024 "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3338,6 +3397,20 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3521,8 +3594,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/has-flag": { "version": "3.0.0", @@ -3788,6 +3860,20 @@ "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3830,7 +3916,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3847,6 +3932,17 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3855,8 +3951,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -4670,6 +4765,7 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, +<<<<<<< HEAD "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4683,6 +4779,29 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" +======= + "node_modules/json-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "dependencies": { + "call-bind": "^1.0.5", + "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/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" +>>>>>>> dev-it2/dev-it2-PFEA2024 }, "node_modules/json5": { "version": "2.2.3", @@ -4696,6 +4815,25 @@ "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": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -4741,6 +4879,7 @@ "safe-buffer": "^5.0.1" } }, +<<<<<<< HEAD "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4749,6 +4888,14 @@ "license": "MIT", "dependencies": { "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" +>>>>>>> dev-it2/dev-it2-PFEA2024 } }, "node_modules/kleur": { @@ -4917,7 +5064,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -5280,6 +5426,11 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5299,6 +5450,14 @@ "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": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5310,6 +5469,14 @@ "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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5333,6 +5500,7 @@ "url": "https://github.com/sponsors/sindresorhus" } }, +<<<<<<< HEAD "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5349,6 +5517,29 @@ }, "engines": { "node": ">= 0.8.0" +======= + "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/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" +>>>>>>> dev-it2/dev-it2-PFEA2024 } }, "node_modules/p-limit": { @@ -5426,6 +5617,115 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5447,7 +5747,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5464,6 +5763,11 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5474,7 +5778,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -5613,6 +5916,14 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5854,7 +6165,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5866,7 +6176,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6305,6 +6614,17 @@ "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6324,7 +6644,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -6414,6 +6733,22 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "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": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6425,6 +6760,14 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6541,7 +6884,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6655,6 +6997,17 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/server/package.json b/server/package.json index da602ee..4eb536d 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,8 @@ "build": "webpack --config webpack.config.js", "start": "node app.js", "dev": "cross-env NODE_ENV=development nodemon app.js", - "test": "jest --colors" + "test": "jest", + "postinstall": "patch-package" }, "keywords": [], "author": "", @@ -17,10 +18,15 @@ "cors": "^2.8.5", "dotenv": "^16.4.4", "express": "^4.18.2", + "express-session": "^1.18.0", "jsonwebtoken": "^9.0.2", "mongodb": "^6.3.0", "multer": "^1.4.5-lts.1", "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-client": "^4.7.2" }, diff --git a/server/patches/passport-openidconnect+0.1.2.patch b/server/patches/passport-openidconnect+0.1.2.patch new file mode 100644 index 0000000..e386741 --- /dev/null +++ b/server/patches/passport-openidconnect+0.1.2.patch @@ -0,0 +1,12 @@ +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; + }; diff --git a/server/routers/auth.js b/server/routers/auth.js new file mode 100644 index 0000000..7260669 --- /dev/null +++ b/server/routers/auth.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('../middleware/jwtToken.js'); + +const authController = require('../controllers/auth.js') + +router.get("/getActiveAuth",authController.getActive); +router.get("/getRoomsRequireAuth", authController.getRoomsRequireAuth); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/users.js b/server/routers/users.js index d1f81b7..f88436d 100644 --- a/server/routers/users.js +++ b/server/routers/users.js @@ -3,11 +3,12 @@ const router = express.Router(); const users = require('../app.js').users; const jwt = require('../middleware/jwtToken.js'); const asyncHandler = require('./routerUtils.js'); +const usersController = require('../controllers/users.js') router.post("/register", asyncHandler(users.register)); router.post("/login", asyncHandler(users.login)); router.post("/reset-password", asyncHandler(users.resetPassword)); router.post("/change-password", jwt.authenticate, asyncHandler(users.changePassword)); -router.post("/delete-user", jwt.authenticate, asyncHandler(users.delete)); +router.post("/delete-user", jwt.authenticate, usersController); module.exports = router; diff --git a/server/utils.js b/server/utils.js new file mode 100644 index 0000000..91f5972 --- /dev/null +++ b/server/utils.js @@ -0,0 +1,35 @@ +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}; \ No newline at end of file