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