Merge pull request #151 from ets-cfuhrman-pfe/frontend-refactor-auth

Frontend refactor auth
This commit is contained in:
Gabriel Moisan Matte 2024-10-19 13:58:47 -04:00 committed by GitHub
commit ccce303693
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 840 additions and 774 deletions

View file

@ -22,6 +22,7 @@
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2", "gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jwt-decode": "^4.0.0",
"katex": "^0.16.11", "katex": "^0.16.11",
"marked": "^14.1.2", "marked": "^14.1.2",
"nanoid": "^5.0.2", "nanoid": "^5.0.2",
@ -9417,6 +9418,14 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"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": ">=18"
}
},
"node_modules/katex": { "node_modules/katex": {
"version": "0.16.11", "version": "0.16.11",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",

View file

@ -26,6 +26,7 @@
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2", "gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jwt-decode": "^4.0.0",
"katex": "^0.16.11", "katex": "^0.16.11",
"marked": "^14.1.2", "marked": "^14.1.2",
"nanoid": "^5.0.2", "nanoid": "^5.0.2",

View file

@ -1,4 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { useEffect, useState } from 'react';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
// Page main // Page main
import Home from './pages/Home/Home'; import Home from './pages/Home/Home';
@ -6,9 +7,8 @@ import Home from './pages/Home/Home';
// Pages espace enseignant // Pages espace enseignant
import Dashboard from './pages/Teacher/Dashboard/Dashboard'; import Dashboard from './pages/Teacher/Dashboard/Dashboard';
import Share from './pages/Teacher/Share/Share'; import Share from './pages/Teacher/Share/Share';
import Login from './pages/Teacher/Login/Login'; import Register from './pages/AuthManager/providers/SimpleLogin/Register';
import Register from './pages/Teacher/Register/Register'; import ResetPassword from './pages/AuthManager/providers/SimpleLogin/ResetPassword';
import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword';
import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom'; import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom';
import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz'; import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
@ -16,29 +16,39 @@ import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
import JoinRoom from './pages/Student/JoinRoom/JoinRoom'; import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
// Pages authentification selection // Pages authentification selection
import AuthSelection from './pages/AuthSelection/AuthSelection'; import AuthDrawer from './pages/AuthManager/AuthDrawer';
// Header/Footer import // Header/Footer import
import Header from './components/Header/Header'; import Header from './components/Header/Header';
import Footer from './components/Footer/Footer'; import Footer from './components/Footer/Footer';
import ApiService from './services/ApiService'; import ApiService from './services/ApiService';
import OAuthCallback from './pages/AuthSelection/AuthCallback'; import OAuthCallback from './pages/AuthManager/callback/AuthCallback';
const handleLogout = () => { const App: React.FC = () => {
const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn());
const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher());
const location = useLocation();
// Check login status every time the route changes
useEffect(() => {
const checkLoginStatus = () => {
setIsAuthenticated(ApiService.isLoggedIn());
setIsTeacherAuthenticated(ApiService.isLoggedInTeacher());
};
checkLoginStatus();
}, [location]);
const handleLogout = () => {
ApiService.logout(); ApiService.logout();
}; setIsAuthenticated(false);
setIsTeacherAuthenticated(false);
};
const isLoggedIn = () => {
const test = ApiService.isLoggedIn();
console.log("App.tsx: " + test);
return test;
};
function App() {
return ( return (
<div className="content"> <div className="content">
<Header isLoggedIn={isLoggedIn} handleLogout={handleLogout} /> <Header isLoggedIn={isAuthenticated} handleLogout={handleLogout} />
<div className="app"> <div className="app">
<main> <main>
<Routes> <Routes>
@ -46,43 +56,46 @@ function App() {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
{/* Pages espace enseignant */} {/* Pages espace enseignant */}
<Route path="/teacher/login" element={<Login />} />
<Route path="/teacher/register" element={<Register />} />
<Route path="/teacher/resetPassword" element={<ResetPassword />} />
{/* Routes protégées : redirection si l'utilisateur n'est pas connecté */}
<Route <Route
path="/teacher/dashboard" path="/teacher/dashboard"
element={isLoggedIn() ? <Dashboard /> : <Navigate to="/auth-selection" />} element={isTeacherAuthenticated ? <Dashboard /> : <Navigate to="/login" />}
/> />
<Route <Route
path="/teacher/share/:id" path="/teacher/share/:id"
element={isLoggedIn() ? <Share /> : <Navigate to="/auth-selection" />} element={isTeacherAuthenticated ? <Share /> : <Navigate to="/login" />}
/> />
<Route <Route
path="/teacher/editor-quiz/:id" path="/teacher/editor-quiz/:id"
element={isLoggedIn() ? <QuizForm /> : <Navigate to="/auth-selection" />} element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
/> />
<Route <Route
path="/teacher/manage-room/:id" path="/teacher/manage-room/:id"
element={isLoggedIn() ? <ManageRoom /> : <Navigate to="/auth-selection" />} element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
/> />
{/* Pages espace étudiant */} {/* Pages espace étudiant */}
<Route path="/student/join-room" element={isLoggedIn() ? <JoinRoom /> : <Navigate to="/auth-selection" />} <Route
path="/student/join-room"
element={isAuthenticated ? <JoinRoom /> : <Navigate to="/login" />}
/> />
{/* Pages authentification sélection */} {/* Pages authentification */}
<Route path="/auth-selection" element={<AuthSelection />} /> <Route path="/login" element={<AuthDrawer />} />
{/* Pages enregistrement */}
<Route path="/register" element={<Register />} />
{/* Pages rest password */}
<Route path="/resetPassword" element={<ResetPassword />} />
{/* Pages authentification sélection */} {/* Pages authentification sélection */}
<Route path="/oauth/callback" element={<OAuthCallback />} /> <Route path="/auth/callback" element={<OAuthCallback />} />
</Routes> </Routes>
</main> </main>
</div> </div>
<Footer /> <Footer />
</div> </div>
); );
} };
export default App; export default App;

View file

@ -4,7 +4,7 @@ import './header.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
interface HeaderProps { interface HeaderProps {
isLoggedIn: () => boolean; isLoggedIn: boolean;
handleLogout: () => void; handleLogout: () => void;
} }
@ -20,7 +20,7 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
onClick={() => navigate('/')} onClick={() => navigate('/')}
/> />
{isLoggedIn() && ( {isLoggedIn && (
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@ -33,9 +33,9 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
</Button> </Button>
)} )}
{!isLoggedIn() && ( {!isLoggedIn && (
<div className="auth-selection-btn"> <div className="auth-selection-btn">
<Link to="/auth-selection"> <Link to="/login">
<button className="auth-btn">Connexion</button> <button className="auth-btn">Connexion</button>
</Link> </Link>
</div> </div>

View file

@ -1,3 +1,5 @@
process.env['base']
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App.tsx'; import App from './App.tsx';

View file

@ -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<any>(null); // Stocke les données d'auth
const navigate = useNavigate();
ENV_VARIABLES.VITE_BACKEND_URL;
// Récupérer les données d'authentification depuis l'API
useEffect(() => {
const fetchData = async () => {
const data = await authService.fetchAuthData();
setAuthData(data);
};
fetchData();
}, []);
return (
<div className="auth-selection-page">
<h1>Connexion</h1>
{/* Formulaire de connexion Simple Login */}
{authData && authData['simpleauth'] && (
<div className="form-container">
<SimpleLogin />
</div>
)}
{/* Conteneur OAuth/OIDC */}
{authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && (
<div className="auth-button-container">
{Object.keys(authData).map((providerKey) => {
const providerType = authData[providerKey].type;
if (providerType === 'oidc' || providerType === 'oauth') {
return (
<ButtonAuth
key={providerKey}
providerName={providerKey}
providerType={providerType}
/>
);
}
return null;
})}
</div>
)}
<div>
<button className="home-button-container" onClick={() => navigate('/')}>Retour à l'accueil</button>
</div>
</div>
);
};
export default AuthSelection;

View file

@ -7,9 +7,7 @@
h1 { h1 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.form-container, .form-container{
.oauth-container,
.oidc-container {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
@ -48,16 +46,3 @@
color: black; color: black;
text-decoration: underline; text-decoration: underline;
} }
.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;
}

View file

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import apiService from '../../services/ApiService'; import apiService from '../../../services/ApiService';
const OAuthCallback: React.FC = () => { const OAuthCallback: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -11,15 +11,12 @@ const OAuthCallback: React.FC = () => {
const user = searchParams.get('user'); const user = searchParams.get('user');
if (user) { if (user) {
// Save user data to localStorage or sessionStorage
apiService.saveToken(user); apiService.saveToken(user);
// Navigate to the dashboard or another page
navigate('/'); navigate('/');
} else { } else {
navigate('/auth-selection'); navigate('/login');
} }
}, [location, navigate]); }, []);
return <div>Loading...</div>; return <div>Loading...</div>;
}; };

View file

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

View file

@ -1,17 +1,16 @@
import { useNavigate, Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// JoinRoom.tsx // JoinRoom.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import './Login.css'; import '../css/simpleLogin.css';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'; import ApiService from '../../../../services/ApiService';
const Login: React.FC = () => { const SimpleLogin: React.FC = () => {
const navigate = useNavigate();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -27,21 +26,16 @@ const Login: React.FC = () => {
const login = async () => { const login = async () => {
const result = await ApiService.login(email, password); const result = await ApiService.login(email, password);
if (result !== true) {
if (result != true) {
setConnectionError(result); setConnectionError(result);
return; return;
} }
else {
navigate("/teacher/Dashboard")
}
}; };
return ( return (
<LoginContainer <LoginContainer
title='Login' title=''
error={connectionError}> error={connectionError}>
<TextField <TextField
@ -77,11 +71,11 @@ const Login: React.FC = () => {
<div className="login-links"> <div className="login-links">
<Link to="/teacher/resetPassword"> <Link to="/resetPassword">
Réinitialiser le mot de passe Réinitialiser le mot de passe
</Link> </Link>
<Link to="/teacher/register"> <Link to="/register">
Créer un compte Créer un compte
</Link> </Link>
@ -91,4 +85,4 @@ const Login: React.FC = () => {
); );
}; };
export default Login; export default SimpleLogin;

View file

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

View file

@ -7,8 +7,8 @@ import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer' import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'; import ApiService from '../../../../services/ApiService';
const ResetPassword: React.FC = () => { const ResetPassword: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -32,7 +32,7 @@ const ResetPassword: React.FC = () => {
return; return;
} }
navigate("/teacher/login") navigate("/login")
}; };

View file

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

View file

@ -1,83 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import './authselection.css';
import SimpleLogin from './SimpleLogin';
const AuthSelection: React.FC = () => {
const [authData, setAuthData] = useState<any>(null); // Stocke les données d'auth
const navigate = useNavigate();
// Récupérer les données d'authentification depuis l'API
useEffect(() => {
const fetchAuthData = async () => {
try {
const response = await fetch('http://localhost:3000/api/auth/getActiveAuth');
const data = await response.json();
console.log('Auth Data:', data); // Affichage dans la console
setAuthData(data.authActive); // Stocke les données dans l'état
} catch (error) {
console.error('Erreur lors de la récupération des données d\'auth:', error);
}
};
fetchAuthData(); // Appel de la fonction pour récupérer les données
}, []);
const handleAuthLogin = (provider: string) => {
window.location.href = 'http://localhost:3000/api/auth/' + provider;
};
return (
<div className="auth-selection-page">
<h1>Connexion</h1>
{/* Formulaire de connexion Simple Login */}
{authData && authData['simple-login'] && (
<div className="form-container">
<SimpleLogin/>
</div>
)}
{/* Conteneur OAuth */}
{authData && Object.keys(authData).some(key => authData[key].type === 'oauth') && (
<div className="oauth-container">
<h2>Se connecter avec OAuth</h2>
{Object.keys(authData).map((providerKey) => {
const provider = authData[providerKey];
if (provider.type === 'oauth') {
return (
<button key={providerKey} className="provider-btn oauth-btn" onClick={() => handleAuthLogin(providerKey)}>
Continuer avec {providerKey}
</button>
);
}
return null;
})}
</div>
)}
{/* Conteneur OIDC */}
{authData && Object.keys(authData).some(key => authData[key].type === 'oidc') && (
<div className="oidc-container">
<h2>Se connecter avec OIDC</h2>
{Object.keys(authData).map((providerKey) => {
const provider = authData[providerKey];
if (provider.type === 'oidc') {
return (
<button key={providerKey} className="provider-btn oidc-btn" onClick={() => handleAuthLogin(providerKey)}>
Continuer avec {providerKey}
</button>
);
}
return null;
})}
</div>
)}
<div>
<button className="home-button-container" onClick={() => navigate('/')}>Retour à l'accueil</button>
</div>
</div>
);
};
export default AuthSelection;

View file

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

View file

@ -44,7 +44,7 @@ const Dashboard: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
if (!ApiService.isLoggedIn()) { if (!ApiService.isLoggedIn()) {
navigate("/teacher/login"); navigate("/login");
return; return;
} }
else { else {

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import axios, { AxiosError, AxiosResponse } from 'axios'; import axios, { AxiosError, AxiosResponse } from 'axios';
import { jwtDecode } from 'jwt-decode';
import { ENV_VARIABLES } from '../constants'; import { ENV_VARIABLES } from '../constants';
import { QuizType } from '../Types/QuizType'; import { QuizType } from '../Types/QuizType';
@ -76,6 +77,34 @@ class ApiService {
return true; return true;
} }
public isLoggedInTeacher(): boolean {
const token = this.getToken();
if (token == null) {
return false;
}
try {
const decodedToken = jwtDecode(token) as { roles: string[] };
const userRoles = decodedToken.roles;
const requiredRole = 'teacher';
if (!userRoles || !userRoles.includes(requiredRole)) {
return false;
}
// Update token expiry
this.saveToken(token);
return true;
} catch (error) {
console.error("Error decoding token:", error);
return false;
}
}
public logout(): void { public logout(): void {
return localStorage.removeItem("jwt"); return localStorage.removeItem("jwt");
} }
@ -86,21 +115,25 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async register(email: string, password: string): Promise<any> { public async register(name: string, email: string, password: string, roles: string[]): Promise<any> {
try { try {
if (!email || !password) { if (!email || !password) {
throw new Error(`L'email et le mot de passe sont requis.`); throw new Error(`L'email et le mot de passe sont requis.`);
} }
const url: string = this.constructRequestUrl(`/user/register`); const url: string = this.constructRequestUrl(`/auth/simple-auth/register`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { email, password }; const body = { name, email, password, roles };
const result: AxiosResponse = await axios.post(url, body, { headers: headers }); const result: AxiosResponse = await axios.post(url, body, { headers: headers });
if (result.status !== 200) { console.log(result);
throw new Error(`L'enregistrement a échoué. Status: ${result.status}`); if (result.status == 200) {
window.location.href = result.request.responseURL;
}
else {
throw new Error(`La connexion a échoué. Status: ${result.status}`);
} }
return true; return true;
@ -122,39 +155,52 @@ class ApiService {
* @returns true if successful * @returns true if successful
* @returns A error string if unsuccessful, * @returns A error string if unsuccessful,
*/ */
public async login(email: string, password: string): Promise<any> { /**
* @returns true if successful
* @returns An error string if unsuccessful
*/
public async login(email: string, password: string): Promise<any> {
try { try {
if (!email || !password) { if (!email || !password) {
throw new Error(`L'email et le mot de passe sont requis.`); throw new Error("L'email et le mot de passe sont requis.");
} }
const url: string = this.constructRequestUrl(`/user/login`); const url: string = this.constructRequestUrl(`/auth/simple-auth/login`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { email, password }; const body = { email, password };
const result: AxiosResponse = await axios.post(url, body, { headers: headers }); const result: AxiosResponse = await axios.post(url, body, { headers: headers });
if (result.status !== 200) { // If login is successful, redirect the user
throw new Error(`La connexion a échoué. Status: ${result.status}`); if (result.status === 200) {
} window.location.href = result.request.responseURL;
this.saveToken(result.data.token);
return true; return true;
} else {
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
}
} catch (error) { } catch (error) {
console.log("Error details: ", error); console.log("Error details:", error);
// Handle Axios-specific errors
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const err = error as AxiosError; const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined; const responseData = err.response?.data as { message?: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.';
// If there is a message field in the response, print it
if (responseData?.message) {
console.log("Backend error message:", responseData.message);
return responseData.message;
} }
return `Une erreur inattendue s'est produite.` // 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 * @returns true if successful
@ -167,7 +213,7 @@ class ApiService {
throw new Error(`L'email est requis.`); 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 headers = this.constructRequestHeaders();
const body = { email }; const body = { email };
@ -203,7 +249,7 @@ class ApiService {
throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`); throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`);
} }
const url: string = this.constructRequestUrl(`/user/change-password`); const url: string = this.constructRequestUrl(`/auth/simple-auth/change-password`);
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { email, oldPassword, newPassword }; const body = { email, oldPassword, newPassword };

View file

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

View file

@ -4,7 +4,7 @@ services:
frontend: frontend:
build: build:
context: ./server context: ./client
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: frontend container_name: frontend
ports: ports:

View file

@ -19,7 +19,7 @@ const authRouter = require('./routers/auth.js')
dotenv.config(); dotenv.config();
// Setup urls from configs // Setup urls from configs
const use_ports = (process.env['USE_PORTS']).toLocaleLowerCase() == "true" const use_ports = (process.env['USE_PORTS']).toLowerCase() == "true"
process.env['FRONTEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['FRONTEND_PORT']}`:"") 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']}`:"") process.env['BACKEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['PORT']}`:"")

View file

@ -1,5 +1,8 @@
const fs = require('fs'); const fs = require('fs');
const AuthConfig = require('../config/auth.js'); const AuthConfig = require('../config/auth.js');
const jwt = require('../middleware/jwtToken.js');
const emailer = require('../config/email.js');
const model = require('../models/users.js');
class AuthManager{ class AuthManager{
constructor(expressapp,configs=null){ constructor(expressapp,configs=null){
@ -39,18 +42,19 @@ class AuthManager{
} }
} }
async login(userInfos){ async login(userInfo,req,res,next){
// TODO global user login method const tokenToSave = jwt.create(userInfo.email, userInfo._id,userInfo.roles);
console.log(userInfos) res.redirect(`/auth/callback?user=${tokenToSave}`);
console.info(`L'utilisateur '${userInfo.name}' vient de se connecter`)
} }
async register(userInfos){ async register(userInfos){
// TODO global user register method if (!userInfos.email || !userInfos.password) {
console.log(userInfos) throw new AppError(MISSING_REQUIRED_PARAMETER);
} }
const user = await model.register(userInfos);
async logout(){ emailer.registerConfirmation(user.email)
// TODO global user logout method return user
} }
} }

View file

@ -5,13 +5,13 @@ var { hasNestedValue } = require('../../../utils')
var jwt = require('../../../middleware/jwtToken') var jwt = require('../../../middleware/jwtToken')
class PassportOAuth { class PassportOAuth {
constructor(passportjs,auth_name){ constructor(passportjs, auth_name) {
this.passportjs = passportjs this.passportjs = passportjs
this.auth_name = auth_name this.auth_name = auth_name
} }
register(app, passport,endpoint, name, provider) { register(app, passport, endpoint, name, provider) {
const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback` const cb_url = `${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this const self = this
passport.use(name, new OAuth2Strategy({ passport.use(name, new OAuth2Strategy({
@ -22,7 +22,7 @@ class PassportOAuth {
callbackURL: cb_url, callbackURL: cb_url,
passReqToCallback: true passReqToCallback: true
}, },
async function(req, accessToken, refreshToken, params, profile, done) { async function (req, accessToken, refreshToken, params, profile, done) {
try { try {
const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, { const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, {
headers: { 'Authorization': `Bearer ${accessToken}` } headers: { 'Authorization': `Bearer ${accessToken}` }
@ -36,25 +36,29 @@ class PassportOAuth {
roles: [] roles: []
}; };
if(hasNestedValue(userInfo,provider.OAUTH_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher') 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') if (hasNestedValue(userInfo, provider.OAUTH_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name._id,received_user.auth_id) const user_association = await authUserAssoc.find_user_association(self.auth_name, received_user.auth_id)
let user_account = null let user_account
if(user_association){ if (user_association) {
user_account = await users.getById(user_association.user_id) user_account = await users.getById(user_association.user_id)
} }
else { else {
let user_id = await users.getId(received_user.email) let user_id = await users.getId(received_user.email)
user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"") if (user_id) {
await authUserAssoc.link(self.auth_name,received_user.auth_id,user_account._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.name = received_user.name
user_account.roles = received_user.roles user_account.roles = received_user.roles
await users.editUser(user_account) await users.editUser(user_account)
self.passportjs.authenticate(user_account)
// Store the tokens in the session // Store the tokens in the session
req.session.oauth2Tokens = { req.session.oauth2Tokens = {
@ -72,7 +76,7 @@ class PassportOAuth {
app.get(`${endpoint}/${name}`, (req, res, next) => { app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, { passport.authenticate(name, {
scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`, scope: 'openid profile email offline_access' + ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent' prompt: 'consent'
})(req, res, next); })(req, res, next);
}); });
@ -83,15 +87,7 @@ class PassportOAuth {
}, },
(req, res) => { (req, res) => {
if (req.user) { if (req.user) {
// res.json(req.user) self.passportjs.authenticate(req.user, req, res)
//const redirectUrl = `http://your-frontend-url.com/oauth/callback?user=${encodeURIComponent(req.user)}`;
//res.redirect(redirectUrl);
const tokenToSave = jwt.create(req.user.email, req.user._id);
res.redirect('/oauth/callback?user=' + tokenToSave);
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else { } else {
res.status(401).json({ error: "L'authentification a échoué" }); res.status(401).json({ error: "L'authentification a échoué" });
} }

View file

@ -5,13 +5,13 @@ var { hasNestedValue } = require('../../../utils')
var jwt = require('../../../middleware/jwtToken') var jwt = require('../../../middleware/jwtToken')
class PassportOpenIDConnect { class PassportOpenIDConnect {
constructor(passportjs,auth_name){ constructor(passportjs, auth_name) {
this.passportjs = passportjs this.passportjs = passportjs
this.auth_name = auth_name this.auth_name = auth_name
} }
async getConfigFromConfigURL(name,provider){ async getConfigFromConfigURL(name, provider) {
try{ try {
const config = await fetch(provider.OIDC_CONFIG_URL) const config = await fetch(provider.OIDC_CONFIG_URL)
return await config.json() return await config.json()
} catch (error) { } catch (error) {
@ -19,10 +19,10 @@ class PassportOpenIDConnect {
} }
} }
async register(app, passport,endpoint, name, provider) { async register(app, passport, endpoint, name, provider) {
const config = await this.getConfigFromConfigURL(name,provider) const config = await this.getConfigFromConfigURL(name, provider)
const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback` const cb_url = `${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this const self = this
passport.use(name, new OpenIDConnectStrategy({ passport.use(name, new OpenIDConnectStrategy({
@ -37,7 +37,7 @@ class PassportOpenIDConnect {
scope: 'openid profile email ' + `${provider.OIDC_ADD_SCOPE}`, scope: 'openid profile email ' + `${provider.OIDC_ADD_SCOPE}`,
}, },
// patch pour la librairie permet d'obtenir les groupes, PR en cours mais "morte" : https://github.com/jaredhanson/passport-openidconnect/pull/101 // 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) { async function (req, issuer, profile, times, tok, done) {
try { try {
const received_user = { const received_user = {
auth_id: profile.id, auth_id: profile.id,
@ -46,25 +46,30 @@ class PassportOpenIDConnect {
roles: [] 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._id,received_user.auth_id) 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')
let user_account = null const user_association = await authUserAssoc.find_user_association(self.auth_name, received_user.auth_id)
if(user_association){
let user_account
if (user_association) {
user_account = await users.getById(user_association.user_id) user_account = await users.getById(user_association.user_id)
} }
else { else {
let user_id = await users.getId(received_user.email) let user_id = await users.getId(received_user.email)
user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"") if (user_id) {
await authUserAssoc.link(self.auth_name,received_user.auth_id,user_account._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.name = received_user.name
user_account.roles = received_user.roles user_account.roles = received_user.roles
await users.editUser(user_account) await users.editUser(user_account)
self.passportjs.authenticate(user_account)
return done(null, user_account); return done(null, user_account);
} catch (error) { } catch (error) {
@ -73,7 +78,7 @@ class PassportOpenIDConnect {
app.get(`${endpoint}/${name}`, (req, res, next) => { app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, { passport.authenticate(name, {
scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`, scope: 'openid profile email offline_access' + ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent' prompt: 'consent'
})(req, res, next); })(req, res, next);
}); });
@ -84,12 +89,7 @@ class PassportOpenIDConnect {
}, },
(req, res) => { (req, res) => {
if (req.user) { if (req.user) {
// res.json(req.user) self.passportjs.authenticate(req.user, req, res)
const tokenToSave = jwt.create(req.user.email, req.user._id);
res.redirect('/oauth/callback?user=' + tokenToSave);
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else { } else {
res.status(401).json({ error: "L'authentification a échoué" }); res.status(401).json({ error: "L'authentification a échoué" });
} }

View file

@ -1,4 +1,3 @@
const fs = require('fs');
var passport = require('passport') var passport = require('passport')
var authprovider = require('../../models/authProvider') var authprovider = require('../../models/authProvider')
@ -51,12 +50,12 @@ class PassportJs{
} }
register(userinfos){ register(userInfos){
return this.authmanager.register(userinfos) return this.authmanager.register(userInfos)
} }
authenticate(userinfos){ authenticate(userInfo,req,res,next){
return this.authmanager.login(userinfos) return this.authmanager.login(userInfo,req,res,next)
} }
} }

View file

@ -0,0 +1,124 @@
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 user = await model.login(email, password);
await self.authmanager.login(user, req, res, next);
} catch (error) {
const statusCode = error.statusCode || 500;
const message = error.message || "An internal server error occurred";
console.error(error);
return res.status(statusCode).json({ message });
}
}
async resetPassword(self, req, res, next) {
try {
const { email } = req.body;
if (!email) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const newPassword = await model.resetPassword(email);
if (!newPassword) {
throw new AppError(GENERATE_PASSWORD_ERROR);
}
emailer.newPasswordConfirmation(email, newPassword);
return res.status(200).json({
message: 'Nouveau mot de passe envoyé par courriel.'
});
}
catch (error) {
return next(error);
}
}
async changePassword(self, req, res, next) {
try {
const { email, oldPassword, newPassword } = req.body;
if (!email || !oldPassword || !newPassword) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
// verify creds first
const user = await model.login(email, oldPassword);
if (!user) {
throw new AppError(LOGIN_CREDENTIALS_ERROR);
}
const password = await model.changePassword(email, newPassword)
if (!password) {
throw new AppError(UPDATE_PASSWORD_ERROR);
}
return res.status(200).json({
message: 'Mot de passe changé avec succès.'
});
}
catch (error) {
return next(error);
}
}
}
module.exports = SimpleAuth;

View file

@ -31,7 +31,7 @@
"enabled": true, "enabled": true,
"name": "provider3", "name": "provider3",
"SESSION_SECRET": "your_session_secret" "SESSION_SECRET": "your_session_secret"
} },
"Module X":{ "Module X":{
} }

View file

@ -44,8 +44,8 @@ class AuthConfig {
// Méthode pour retourner la configuration de Simple Login // Méthode pour retourner la configuration de Simple Login
getSimpleLoginConfig() { getSimpleLoginConfig() {
if (this.config && this.config.auth && this.config.auth["simple-login"]) { if (this.config && this.config.auth && this.config.auth["simpleauth"]) {
return this.config.auth["simple-login"]; return this.config.auth["simpleauth"];
} else { } else {
return { error: "Aucune configuration Simple Login disponible." }; return { error: "Aucune configuration Simple Login disponible." };
} }
@ -162,10 +162,10 @@ class AuthConfig {
} }
// Gestion du Simple Login // Gestion du Simple Login
if (this.config.auth["simple-login"] && this.config.auth["simple-login"].enabled) { if (this.config.auth["simpleauth"] && this.config.auth["simpleauth"].enabled) {
passportConfig['simple-login'] = { passportConfig['simpleauth'] = {
type: "simple-login", type: "simpleauth",
name: this.config.auth["simple-login"].name name: this.config.auth["simpleauth"].name
}; };
} }

View file

@ -7,110 +7,6 @@ const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_E
class UsersController { class UsersController {
async register(req, res, next) {
try {
const { email, password } = req.body;
if (!email || !password) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
await model.register(email, password);
emailer.registerConfirmation(email)
return res.status(200).json({
message: 'Utilisateur créé avec succès.'
});
}
catch (error) {
return next(error);
}
}
async login(req, res, next) {
try {
const { email, password } = req.body;
if (!email || !password) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const user = await model.login(email, password);
if (!user) {
throw new AppError(LOGIN_CREDENTIALS_ERROR);
}
const token = jwt.create(user.email, user._id);
return res.status(200).json({
token: token,
id: user.email
});
}
catch (error) {
return next(error);
}
}
async resetPassword(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(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);
}
}
async delete(req, res, next) { async delete(req, res, next) {
try { try {
const { email, password } = req.body; const { email, password } = req.body;

View file

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

View file

@ -18,22 +18,24 @@ class Users {
return await bcrypt.compare(password, hash); return await bcrypt.compare(password, hash);
} }
async register(email, password) { async register(userInfos) {
await db.connect(); await db.connect();
const conn = db.getConnection(); const conn = db.getConnection();
const userCollection = conn.collection("users"); const userCollection = conn.collection("users");
const existingUser = await userCollection.findOne({ email: email }); const existingUser = await userCollection.findOne({ email: userInfos.email });
if (existingUser) { if (existingUser) {
throw new AppError(USER_ALREADY_EXISTS); throw new AppError(USER_ALREADY_EXISTS);
} }
const newUser = { const newUser = {
email: email, name: userInfos.name ?? userInfos.email,
password: await this.hashPassword(password), email: userInfos.email,
password: await this.hashPassword(userInfos.password),
created_at: new Date(), created_at: new Date(),
roles: userInfos.roles
}; };
let created_user = await userCollection.insertOne(newUser); let created_user = await userCollection.insertOne(newUser);
@ -62,24 +64,32 @@ class Users {
} }
async login(email, password) { async login(email, password) {
try {
await db.connect(); await db.connect();
const conn = db.getConnection(); const conn = db.getConnection();
const userCollection = conn.collection("users"); const userCollection = conn.collection("users");
const user = await userCollection.findOne({ email: email }); const user = await userCollection.findOne({ email: email });
if (!user) { if (!user) {
return false; const error = new Error("User not found");
error.statusCode = 404;
throw error;
} }
const passwordMatch = await this.verify(password, user.password); const passwordMatch = await this.verify(password, user.password);
if (!passwordMatch) { if (!passwordMatch) {
return false; const error = new Error("Password does not match");
error.statusCode = 401;
throw error;
} }
return user; return user;
} catch (error) {
console.error(error);
throw error;
}
} }
async resetPassword(email) { async resetPassword(email) {

View file

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