diff --git a/client/src/App.tsx b/client/src/App.tsx index e242cfa..923288d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -23,13 +23,16 @@ import Header from './components/Header/Header'; import Footer from './components/Footer/Footer'; import ApiService from './services/ApiService'; +import OAuthCallback from './pages/AuthSelection/AuthCallback'; const handleLogout = () => { ApiService.logout(); }; const isLoggedIn = () => { - return ApiService.isLoggedIn(); + const test = ApiService.isLoggedIn(); + console.log("App.tsx: " + test); + return test; }; function App() { @@ -71,6 +74,9 @@ function App() { {/* Pages authentification sélection */} } /> + + {/* Pages authentification sélection */} + } /> diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index a59f806..d0bc98e 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,4 +1,4 @@ -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'; @@ -32,6 +32,14 @@ const Header: React.FC = ({ isLoggedIn, handleLogout }) => { Logout )} + + {!isLoggedIn() && ( +
+ + + +
+ )} ); }; diff --git a/client/src/pages/AuthSelection/AuthCallback.tsx b/client/src/pages/AuthSelection/AuthCallback.tsx new file mode 100644 index 0000000..1ae90c4 --- /dev/null +++ b/client/src/pages/AuthSelection/AuthCallback.tsx @@ -0,0 +1,27 @@ +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'); + + if (user) { + // Save user data to localStorage or sessionStorage + apiService.saveToken(user); + + // Navigate to the dashboard or another page + navigate('/'); + } else { + navigate('/auth-selection'); + } + }, [location, navigate]); + + return
Loading...
; +}; + +export default OAuthCallback; diff --git a/client/src/pages/AuthSelection/AuthSelection.tsx b/client/src/pages/AuthSelection/AuthSelection.tsx index 5f25bdb..a29e118 100644 --- a/client/src/pages/AuthSelection/AuthSelection.tsx +++ b/client/src/pages/AuthSelection/AuthSelection.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import './authselection.css'; +import SimpleLogin from './SimpleLogin'; const AuthSelection: React.FC = () => { - const [simpleLoginData, setSimpleLoginData] = useState({ username: '', password: '' }); const [authData, setAuthData] = useState(null); // Stocke les données d'auth const navigate = useNavigate(); @@ -22,17 +22,6 @@ const AuthSelection: React.FC = () => { fetchAuthData(); // Appel de la fonction pour récupérer les données }, []); - const handleSimpleLoginChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setSimpleLoginData((prev) => ({ ...prev, [name]: value })); - }; - - const handleSimpleLoginSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Logique d'authentification pour Simple Login - console.log('Simple Login Data:', simpleLoginData); - }; - const handleAuthLogin = (provider: string) => { window.location.href = 'http://localhost:3000/api/auth/' + provider; }; @@ -44,25 +33,7 @@ const AuthSelection: React.FC = () => { {/* Formulaire de connexion Simple Login */} {authData && authData['simple-login'] && (
-
- - - -
+
)} diff --git a/client/src/pages/AuthSelection/SimpleLogin.tsx b/client/src/pages/AuthSelection/SimpleLogin.tsx new file mode 100644 index 0000000..ce5f475 --- /dev/null +++ b/client/src/pages/AuthSelection/SimpleLogin.tsx @@ -0,0 +1,94 @@ +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(''); + const [isConnecting] = useState(false); + + useEffect(() => { + return () => { + + }; + }, []); + + const login = async () => { + const result = await ApiService.login(email, password); + + if (result != true) { + setConnectionError(result); + return; + } + else { + navigate("/") + } + + }; + + + 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/AuthSelection/simpleLogin.css b/client/src/pages/AuthSelection/simpleLogin.css new file mode 100644 index 0000000..e69de29 diff --git a/client/src/pages/Home/Home.tsx b/client/src/pages/Home/Home.tsx index bc0cfc9..b2abf1f 100644 --- a/client/src/pages/Home/Home.tsx +++ b/client/src/pages/Home/Home.tsx @@ -6,13 +6,6 @@ import { Link } from 'react-router-dom'; const Home: React.FC = () => { return (
- -
- - - -
-
diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 55dccb7..13c9c34 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -32,7 +32,7 @@ class ApiService { } // Helpers - private saveToken(token: string): void { + public saveToken(token: string): void { const now = new Date(); const object = { diff --git a/docker-compose.yaml b/docker-compose.yaml index c5d30c1..b116e82 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,14 +3,18 @@ version: '3' services: frontend: - image: fuhrmanator/evaluetonsavoir-frontend:latest + build: + context: ./server + dockerfile: Dockerfile container_name: frontend ports: - "5173:5173" restart: always backend: - image: fuhrmanator/evaluetonsavoir-backend:latest + build: + context: ./server + dockerfile: Dockerfile container_name: backend ports: - "3000:3000" diff --git a/server/app.js b/server/app.js index ecf1319..c5d3e68 100644 --- a/server/app.js +++ b/server/app.js @@ -68,7 +68,6 @@ app.use(session({ })); authManager = new AuthManager(app) - app.use(errorHandler) // Start server diff --git a/server/auth/modules/passport-providers/oauth.js b/server/auth/modules/passport-providers/oauth.js index a38dc44..5490eb9 100644 --- a/server/auth/modules/passport-providers/oauth.js +++ b/server/auth/modules/passport-providers/oauth.js @@ -1,8 +1,19 @@ 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 + passport.use(name, new OAuth2Strategy({ authorizationURL: provider.OAUTH_AUTHORIZATION_URL, tokenURL: provider.OAUTH_TOKEN_URL, @@ -18,15 +29,32 @@ class PassportOAuth { }); const userInfo = await userInfoResponse.json(); - const user = { - id: userInfo.sub, + let received_user = { + auth_id: userInfo.sub, email: userInfo.email, name: userInfo.name, - groups: userInfo.groups ?? [], - accessToken: accessToken, - refreshToken: refreshToken, - expiresIn: params.expires_in + 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._id,received_user.auth_id) + + let user_account = null + if(user_association){ + user_account = await users.getById(user_association.user_id) + } + else { + let user_id = await users.getId(received_user.email) + user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"") + 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) + self.passportjs.authenticate(user_account) // Store the tokens in the session req.session.oauth2Tokens = { @@ -35,7 +63,7 @@ class PassportOAuth { expiresIn: params.expires_in }; - return done(null, user); + return done(null, user_account); } catch (error) { console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`); return done(error); @@ -55,7 +83,14 @@ class PassportOAuth { }, (req, res) => { if (req.user) { - res.json(req.user) + // res.json(req.user) + + //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 { res.status(401).json({ error: "L'authentification a échoué" }); diff --git a/server/auth/modules/passport-providers/oidc.js b/server/auth/modules/passport-providers/oidc.js index 44cadb7..6a10d9d 100644 --- a/server/auth/modules/passport-providers/oidc.js +++ b/server/auth/modules/passport-providers/oidc.js @@ -1,6 +1,14 @@ 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{ @@ -15,6 +23,7 @@ class PassportOpenIDConnect { const config = await this.getConfigFromConfigURL(name,provider) const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback` + const self = this passport.use(name, new OpenIDConnectStrategy({ issuer: config.issuer, @@ -30,15 +39,35 @@ class PassportOpenIDConnect { // 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 user = { - id: profile.id, + const received_user = { + auth_id: profile.id, email: profile.emails[0].value, name: profile.name.givenName, - groups: profile.groups[0].value ?? [] + roles: [] }; - return done(null, user); + + 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) + + let user_account = null + if(user_association){ + user_account = await users.getById(user_association.user_id) + } + else { + let user_id = await users.getId(received_user.email) + user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"") + 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) + self.passportjs.authenticate(user_account) + + return done(null, user_account); } catch (error) { - } })); @@ -55,7 +84,11 @@ class PassportOpenIDConnect { }, (req, res) => { if (req.user) { - res.json(req.user) + // res.json(req.user) + + 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 { res.status(401).json({ error: "L'authentification a échoué" }); diff --git a/server/auth/modules/passportjs.js b/server/auth/modules/passportjs.js index e65b53c..865f66b 100644 --- a/server/auth/modules/passportjs.js +++ b/server/auth/modules/passportjs.js @@ -1,5 +1,6 @@ const fs = require('fs'); var passport = require('passport') +var authprovider = require('../../models/authProvider') class PassportJs{ constructor(authmanager,settings){ @@ -9,17 +10,20 @@ class PassportJs{ this.endpoint = "/api/auth" } - registerAuth(expressapp){ + 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) + 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é.`) } @@ -35,16 +39,25 @@ class PassportJs{ }); } - registerProvider(providerType){ + 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.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(userinfos){ + return this.authmanager.login(userinfos) + } } 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 3790fce..58a1563 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -1,121 +1,180 @@ //user -const db = require('../config/db.js'); -const bcrypt = require('bcrypt'); -const AppError = require('../middleware/AppError.js'); -const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); -const Folders = require('../models/folders.js'); +const db = require("../config/db.js"); +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 { - - async hashPassword(password) { - return await bcrypt.hash(password, 10) + 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(email, password) { + await db.connect(); + const conn = db.getConnection(); + + const userCollection = conn.collection("users"); + + const existingUser = await userCollection.findOne({ email: email }); + + if (existingUser) { + throw new AppError(USER_ALREADY_EXISTS); } - generatePassword() { - return Math.random().toString(36).slice(-8); + const newUser = { + email: email, + password: await this.hashPassword(password), + created_at: new Date(), + }; + + let created_user = await userCollection.insertOne(newUser); + let user = await this.getById(created_user.insertedId) + + const folderTitle = "Dossier par Défaut"; + const userId = newUser._id.toString(); + await Folders.create(folderTitle, userId); + + // TODO: verif if inserted properly... + return user; + } + + async login(userid) { + await db.connect(); + const conn = db.getConnection(); + + const userCollection = conn.collection("users"); + const user = await userCollection.findOne({ _id: userid }); + + if (!user) { + return false; } - async verify(password, hash) { - return await bcrypt.compare(password, hash) + return user; + } + + async login(email, password) { + await db.connect(); + const conn = db.getConnection(); + + const userCollection = conn.collection("users"); + + const user = await userCollection.findOne({ email: email }); + + if (!user) { + return false; } - async register(email, password) { - await db.connect() - const conn = db.getConnection(); - - const userCollection = conn.collection('users'); + const passwordMatch = await this.verify(password, user.password); - const existingUser = await userCollection.findOne({ email: email }); - - if (existingUser) { - throw new AppError(USER_ALREADY_EXISTS); - } - - const newUser = { - email: email, - password: await this.hashPassword(password), - created_at: new Date() - }; - - await userCollection.insertOne(newUser); - - const folderTitle = 'Dossier par Défaut'; - const userId = newUser._id.toString(); - await Folders.create(folderTitle, userId); - - // TODO: verif if inserted properly... + if (!passwordMatch) { + return false; } - async login(email, password) { - await db.connect() - const conn = db.getConnection(); + return user; + } - const userCollection = conn.collection('users'); + async resetPassword(email) { + const newPassword = this.generatePassword(); - const user = await userCollection.findOne({ email: email }); + return await this.changePassword(email, newPassword); + } - if (!user) { - return false; - } + async changePassword(email, newPassword) { + await db.connect(); + const conn = db.getConnection(); - const passwordMatch = await this.verify(password, user.password); + const userCollection = conn.collection("users"); - if (!passwordMatch) { - return false; - } + const hashedPassword = await this.hashPassword(newPassword); - return user; + const result = await userCollection.updateOne( + { email }, + { $set: { password: hashedPassword } } + ); + + if (result.modifiedCount != 1) return null; + + return newPassword; + } + + async delete(email) { + await db.connect(); + const conn = 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 db.connect(); + const conn = db.getConnection(); + + const userCollection = conn.collection("users"); + + const user = await userCollection.findOne({ email: email }); + + if (!user) { + return false; } - async resetPassword(email) { - const newPassword = this.generatePassword(); + return user._id; + } - return await this.changePassword(email, newPassword); + async getById(id) { + await db.connect(); + const conn = db.getConnection(); + + const userCollection = conn.collection("users"); + + const user = await userCollection.findOne({ _id: id }); + + if (!user) { + return false; } - async changePassword(email, newPassword) { - await db.connect() - const conn = db.getConnection(); + return user; + } - const userCollection = conn.collection('users'); + async editUser(userInfo) { + await db.connect(); + const conn = db.getConnection(); - const hashedPassword = await this.hashPassword(newPassword); + const userCollection = conn.collection("users"); - const result = await userCollection.updateOne({ email }, { $set: { password: hashedPassword } }); + const user = await userCollection.findOne({ _id: userInfo.id }); - if (result.modifiedCount != 1) return null; - - return newPassword + if (!user) { + return false; } - async delete(email) { - await db.connect() - const conn = db.getConnection(); + const updatedFields = { ...userInfo }; + delete updatedFields.id; - const userCollection = conn.collection('users'); + const result = await userCollection.updateOne( + { _id: userInfo.id }, + { $set: updatedFields } + ); - const result = await userCollection.deleteOne({ email }); - - if (result.deletedCount != 1) return false; - - return true; - } - - async getId(email) { - await db.connect() - const conn = 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 = new Users; +module.exports = new Users(); 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