Merge pull request #144 from ets-cfuhrman-pfe/dev-it1-PFEA2024

Ajout itération 1 PFE A2024 sur dev-PFEA2024
This commit is contained in:
roesnerb 2024-10-07 10:49:52 -04:00 committed by GitHub
commit 207cd627f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1851 additions and 132 deletions

18
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug backend",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/server/app.js",
"cwd":"${workspaceFolder}/server/"
}
]
}

View file

@ -1,5 +1,4 @@
// App.tsx import { Routes, Route, Navigate } from 'react-router-dom';
import { Routes, Route } from 'react-router-dom';
// Page main // Page main
import Home from './pages/Home/Home'; import Home from './pages/Home/Home';
@ -16,28 +15,30 @@ import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
// Pages espace étudiant // Pages espace étudiant
import JoinRoom from './pages/Student/JoinRoom/JoinRoom'; import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
// Pages authentification selection
import AuthSelection from './pages/AuthSelection/AuthSelection';
// 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';
const handleLogout = () => { const handleLogout = () => {
ApiService.logout(); ApiService.logout();
} };
const isLoggedIn = () => { const isLoggedIn = () => {
return ApiService.isLoggedIn(); const test = ApiService.isLoggedIn();
} console.log("App.tsx: " + test);
return test;
};
function App() { function App() {
return ( return (
<div className="content"> <div className="content">
<Header isLoggedIn={isLoggedIn} handleLogout={handleLogout} />
<Header
isLoggedIn={isLoggedIn}
handleLogout={handleLogout}/>
<div className="app"> <div className="app">
<main> <main>
<Routes> <Routes>
@ -48,13 +49,34 @@ function App() {
<Route path="/teacher/login" element={<Login />} /> <Route path="/teacher/login" element={<Login />} />
<Route path="/teacher/register" element={<Register />} /> <Route path="/teacher/register" element={<Register />} />
<Route path="/teacher/resetPassword" element={<ResetPassword />} /> <Route path="/teacher/resetPassword" element={<ResetPassword />} />
<Route path="/teacher/dashboard" element={<Dashboard />} />
<Route path="/teacher/share/:id" element={<Share />} /> {/* Routes protégées : redirection si l'utilisateur n'est pas connecté */}
<Route path="/teacher/editor-quiz/:id" element={<QuizForm />} /> <Route
<Route path="/teacher/manage-room/:id" element={<ManageRoom />} /> path="/teacher/dashboard"
element={isLoggedIn() ? <Dashboard /> : <Navigate to="/auth-selection" />}
/>
<Route
path="/teacher/share/:id"
element={isLoggedIn() ? <Share /> : <Navigate to="/auth-selection" />}
/>
<Route
path="/teacher/editor-quiz/:id"
element={isLoggedIn() ? <QuizForm /> : <Navigate to="/auth-selection" />}
/>
<Route
path="/teacher/manage-room/:id"
element={isLoggedIn() ? <ManageRoom /> : <Navigate to="/auth-selection" />}
/>
{/* Pages espace étudiant */} {/* Pages espace étudiant */}
<Route path="/student/join-room" element={<JoinRoom />} /> <Route path="/student/join-room" element={isLoggedIn() ? <JoinRoom /> : <Navigate to="/auth-selection" />}
/>
{/* Pages authentification sélection */}
<Route path="/auth-selection" element={<AuthSelection />} />
{/* Pages authentification sélection */}
<Route path="/oauth/callback" element={<OAuthCallback />} />
</Routes> </Routes>
</main> </main>
</div> </div>

View file

@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import * as React from 'react'; import * as React from 'react';
import './header.css'; import './header.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
@ -32,6 +32,14 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
Logout Logout
</Button> </Button>
)} )}
{!isLoggedIn() && (
<div className="auth-selection-btn">
<Link to="/auth-selection">
<button className="auth-btn">Connexion</button>
</Link>
</div>
)}
</div> </div>
); );
}; };

View file

@ -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 <div>Loading...</div>;
};
export default OAuthCallback;

View file

@ -0,0 +1,83 @@
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

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

@ -0,0 +1,63 @@
.auth-selection-page {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
h1 {
margin-bottom: 20px;
}
.form-container,
.oauth-container,
.oidc-container {
border: 1px solid #ccc;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
form {
display: flex;
flex-direction: column;
}
input {
margin: 5px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px;
border: none;
border-radius: 4px;
background-color: #5271ff;
color: white;
cursor: pointer;
}
button:hover {
background-color: #5271ff;
}
.home-button-container{
background: none;
color: black;
}
.home-button-container:hover{
background: none;
color: black;
text-decoration: underline;
}
.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

@ -61,6 +61,25 @@
align-items: end; align-items: end;
} }
.auth-selection-btn {
position: absolute;
top: 20px;
right: 20px;
}
.auth-btn {
padding: 10px 20px;
background-color: #5271ff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.auth-btn:hover {
background-color: #5976fa;
}
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.btn-container { .btn-container {
flex-direction: column; flex-direction: column;

View file

@ -32,7 +32,7 @@ class ApiService {
} }
// Helpers // Helpers
private saveToken(token: string): void { public saveToken(token: string): void {
const now = new Date(); const now = new Date();
const object = { const object = {

View file

@ -3,14 +3,18 @@ version: '3'
services: services:
frontend: frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest build:
context: ./server
dockerfile: Dockerfile
container_name: frontend container_name: frontend
ports: ports:
- "5173:5173" - "5173:5173"
restart: always restart: always
backend: backend:
image: fuhrmanator/evaluetonsavoir-backend:latest build:
context: ./server
dockerfile: Dockerfile
container_name: backend container_name: backend
ports: ports:
- "3000:3000" - "3000:3000"
@ -22,7 +26,12 @@ services:
SENDER_EMAIL: infoevaluetonsavoir@gmail.com SENDER_EMAIL: infoevaluetonsavoir@gmail.com
EMAIL_PSW: 'vvml wmfr dkzb vjzb' EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
FRONTEND_URL: "http://localhost:5173" SESSION_Secret: 'lookMomImQuizzing'
SITE_URL: http://localhost
FRONTEND_PORT: 5173
USE_PORTS: false
volumes:
- ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json
depends_on: depends_on:
- mongo - mongo
restart: always restart: always

View file

@ -14,4 +14,8 @@ EMAIL_PSW='vvml wmfr dkzb vjzb'
JWT_SECRET=TOKEN! JWT_SECRET=TOKEN!
# Pour creer les liens images # Pour creer les liens images
FRONTEND_URL=http://localhost:5173 SESSION_Secret='session_secret'
SITE_URL=http://localhost
FRONTEND_PORT=5173
USE_PORTS=false

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
auth_config.json

View file

@ -0,0 +1,207 @@
const request = require("supertest");
const AuthConfig = require("../config/auth.js");
const AuthManager = require("../auth/auth-manager.js");
const mockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL: "https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
{
provider2: {
type: "oidc",
OIDC_CLIENT_ID: "your_oidc_client_id",
OIDC_CLIENT_SECRET: "your_oidc_client_secret",
OIDC_CONFIG_URL: "https://your-issuer.com",
OIDC_ADD_SCOPE: "groups",
OIDC_ROLE_TEACHER_VALUE: "teacher-claim-value",
OIDC_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
"simple-login": {
enabled: true,
name: "provider3",
SESSION_SECRET: "your_session_secret",
},
},
};
// Créez une instance de AuthConfig en utilisant la configuration mockée
describe(
"AuthConfig Class Tests",
() => {
let authConfigInstance;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
authConfigInstance.loadConfigTest(mockConfig); // On injecte la configuration mockée
});
it("devrait retourner la configuration PassportJS", () => {
const config = authConfigInstance.getPassportJSConfig();
expect(config).toHaveProperty("provider1");
expect(config).toHaveProperty("provider2");
});
it("devrait retourner la configuration Simple Login", () => {
const config = authConfigInstance.getSimpleLoginConfig();
expect(config).toHaveProperty("name", "provider3");
expect(config).toHaveProperty("SESSION_SECRET", "your_session_secret");
});
it("devrait retourner les providers OAuth", () => {
const oauthProviders = authConfigInstance.getOAuthProviders();
expect(Array.isArray(oauthProviders)).toBe(true);
expect(oauthProviders.length).toBe(1); // Il y a un seul provider OAuth
expect(oauthProviders[0]).toHaveProperty("provider1");
});
it("devrait valider la configuration des providers", () => {
expect(() => authConfigInstance.validateProvidersConfig()).not.toThrow();
});
it("devrait lever une erreur si une configuration manque", () => {
const invalidMockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_CLIENT_ID: "your_oauth_client_id", // Il manque des champs nécessaires
},
},
],
},
};
const instanceWithInvalidConfig = new AuthConfig();
instanceWithInvalidConfig.loadConfigTest(invalidMockConfig);
// Vérifiez que l'erreur est lancée avec les champs manquants corrects
expect(() => instanceWithInvalidConfig.validateProvidersConfig()).toThrow(
new Error(`Configuration invalide pour les providers suivants : [
{
"provider": "provider1",
"missingFields": [
"OAUTH_AUTHORIZATION_URL",
"OAUTH_TOKEN_URL",
"OAUTH_USERINFO_URL",
"OAUTH_CLIENT_SECRET",
"OAUTH_ROLE_TEACHER_VALUE",
"OAUTH_ROLE_STUDENT_VALUE"
]
}
]`)
);
});
},
describe("Auth Module Registration", () => {
let expressMock = jest.mock("express");
expressMock.use = () => {}
expressMock.get = () => {}
let authConfigInstance;
let logSpy;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
});
it("should load valid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModule = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
provider2: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(0);
logSpy.mockClear();
});
it("should not load invalid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const invalidModule = {
auth: {
ModuleX:{}
},
};
authConfigInstance.loadConfigTest(invalidModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(1);
logSpy.mockClear();
});
it("should not load invalid provider from passport", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModuleInvalidProvider = {
auth: {
passportjs: [
{
provider1: {
type: "x",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModuleInvalidProvider); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(2);
logSpy.mockClear();
});
})
);

View file

@ -12,9 +12,17 @@ const userRouter = require('./routers/users.js');
const folderRouter = require('./routers/folders.js'); const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js'); const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js') const imagesRouter = require('./routers/images.js')
const AuthManager = require('./auth/auth-manager.js')
const authRouter = require('./routers/auth.js')
// Setup environement // Setup environement
dotenv.config(); dotenv.config();
// Setup urls from configs
const use_ports = (process.env['USE_PORTS']).toLocaleLowerCase() == "true"
process.env['FRONTEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['FRONTEND_PORT']}`:"")
process.env['BACKEND_URL'] = process.env['SITE_URL'] + (use_ports ? `:${process.env['PORT']}`:"")
const db = require('./config/db.js'); const db = require('./config/db.js');
const errorHandler = require("./middleware/errorHandler.js"); const errorHandler = require("./middleware/errorHandler.js");
@ -48,7 +56,18 @@ app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter); app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter); app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter); app.use('/api/image', imagesRouter);
app.use('/api/auth', authRouter);
// Add Auths methods
const session = require('express-session');
app.use(session({
secret: process.env['SESSION_Secret'],
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production' }
}));
authManager = new AuthManager(app)
app.use(errorHandler) app.use(errorHandler)
// Start server // Start server

View file

@ -0,0 +1,57 @@
const fs = require('fs');
const AuthConfig = require('../config/auth.js');
class AuthManager{
constructor(expressapp,configs=null){
this.modules = []
this.app = expressapp
this.configs = configs ?? (new AuthConfig()).loadConfig()
this.addModules()
this.registerAuths()
}
async addModules(){
for(const module in this.configs.auth){
this.addModule(module)
}
}
async addModule(name){
const modulePath = `${process.cwd()}/auth/modules/${name}.js`
if(fs.existsSync(modulePath)){
const Module = require(modulePath);
this.modules.push(new Module(this,this.configs.auth[name]));
console.info(`Module d'authentification '${name}' ajouté`)
} else{
console.error(`Le module d'authentification ${name} n'as pas été chargé car il est introuvable`)
}
}
async registerAuths(){
for(const module of this.modules){
try{
module.registerAuth(this.app)
} catch(error){
console.error(`L'enregistrement du module ${module} a échoué.`)
}
}
}
async login(userInfos){
// TODO global user login method
console.log(userInfos)
}
async register(userInfos){
// TODO global user register method
console.log(userInfos)
}
async logout(){
// TODO global user logout method
}
}
module.exports = AuthManager;

View file

@ -0,0 +1,103 @@
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,
clientID: provider.OAUTH_CLIENT_ID,
clientSecret: provider.OAUTH_CLIENT_SECRET,
callbackURL: cb_url,
passReqToCallback: true
},
async function(req, accessToken, refreshToken, params, profile, done) {
try {
const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const userInfo = await userInfoResponse.json();
let received_user = {
auth_id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
roles: []
};
if(hasNestedValue(userInfo,provider.OAUTH_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if(hasNestedValue(userInfo,provider.OAUTH_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name._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 = {
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: params.expires_in
};
return done(null, user_account);
} catch (error) {
console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`);
return done(error);
}
}));
app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, {
scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent'
})(req, res, next);
});
app.get(`${endpoint}/${name}/callback`,
(req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
},
(req, res) => {
if (req.user) {
// 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é" });
}
}
);
}
}
module.exports = PassportOAuth;

View file

@ -0,0 +1,101 @@
var OpenIDConnectStrategy = require('passport-openidconnect')
var authUserAssoc = require('../../../models/authUserAssociation')
var users = require('../../../models/users')
var { hasNestedValue } = require('../../../utils')
var jwt = require('../../../middleware/jwtToken')
class PassportOpenIDConnect {
constructor(passportjs,auth_name){
this.passportjs = passportjs
this.auth_name = auth_name
}
async getConfigFromConfigURL(name,provider){
try{
const config = await fetch(provider.OIDC_CONFIG_URL)
return await config.json()
} catch (error) {
console.error(`Les informations de connexions de la connexion OIDC ${name} n'ont pu être chargées.`)
}
}
async register(app, passport,endpoint, name, provider) {
const config = await this.getConfigFromConfigURL(name,provider)
const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this
passport.use(name, new OpenIDConnectStrategy({
issuer: config.issuer,
authorizationURL: config.authorization_endpoint,
tokenURL: config.token_endpoint,
userInfoURL: config.userinfo_endpoint,
clientID: provider.OIDC_CLIENT_ID,
clientSecret: provider.OIDC_CLIENT_SECRET,
callbackURL: cb_url,
passReqToCallback: true,
scope: '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
async function(req, issuer, profile, times, tok, done) {
try {
const received_user = {
auth_id: profile.id,
email: profile.emails[0].value,
name: profile.name.givenName,
roles: []
};
if(hasNestedValue(profile,provider.OIDC_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if(hasNestedValue(profile,provider.OIDC_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name._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) {
}
}));
app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, {
scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent'
})(req, res, next);
});
app.get(`${endpoint}/${name}/callback`,
(req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
},
(req, res) => {
if (req.user) {
// 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é" });
}
}
);
}
}
module.exports = PassportOpenIDConnect;

View file

@ -0,0 +1,64 @@
const fs = require('fs');
var passport = require('passport')
var authprovider = require('../../models/authProvider')
class PassportJs{
constructor(authmanager,settings){
this.authmanager = authmanager
this.registeredProviders = {}
this.providers = settings
this.endpoint = "/api/auth"
}
async registerAuth(expressapp){
expressapp.use(passport.initialize());
expressapp.use(passport.session());
for(const p of this.providers){
for(const [name,provider] of Object.entries(p)){
const auth_id = `passportjs_${provider.type}_${name}`
if(!(provider.type in this.registeredProviders)){
this.registerProvider(provider.type,auth_id)
}
try{
this.registeredProviders[provider.type].register(expressapp,passport,this.endpoint,name,provider)
authprovider.create(auth_id)
} catch(error){
console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`)
}
}
}
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
}
async registerProvider(providerType,auth_id){
try{
const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js`
const Provider = require(providerPath);
this.registeredProviders[providerType]= new Provider(this,auth_id)
console.info(`Le type de connexion '${providerType}' a été ajouté dans passportjs.`)
} catch(error){
console.error(`Le type de connexion '${providerType}' n'as pas pu être chargé dans passportjs.`)
}
}
register(userinfos){
return this.authmanager.register(userinfos)
}
authenticate(userinfos){
return this.authmanager.login(userinfos)
}
}
module.exports = PassportJs;

View file

@ -0,0 +1,39 @@
{
"auth": {
"passportjs":
[
{
"gmatte": {
"type": "oauth",
"OAUTH_AUTHORIZATION_URL": "https://auth.gmatte.xyz/application/o/authorize/",
"OAUTH_TOKEN_URL": "https://auth.gmatte.xyz/application/o/token/",
"OAUTH_USERINFO_URL": "https://auth.gmatte.xyz/application/o/userinfo/",
"OAUTH_CLIENT_ID": "clientID",
"OAUTH_CLIENT_SECRET": "clientSecret",
"OAUTH_ADD_SCOPE": "groups",
"OAUTH_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof",
"OAUTH_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir"
}
},
{
"oidc_gmatte": {
"type": "oidc",
"OIDC_CONFIG_URL": "https://auth.gmatte.xyz/application/o/evaluetonsavoir/.well-known/openid-configuration",
"OIDC_CLIENT_ID": "clientID",
"OIDC_CLIENT_SECRET": "clientSecret",
"OIDC_ADD_SCOPE": "groups",
"OIDC_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof",
"OIDC_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir"
}
}
],
"simple-login": {
"enabled": true,
"name": "provider3",
"SESSION_SECRET": "your_session_secret"
}
"Module X":{
}
}
}

181
server/config/auth.js Normal file
View file

@ -0,0 +1,181 @@
const fs = require('fs');
const path = require('path');
const pathAuthConfig = './auth_config.json';
const configPath = path.join(process.cwd(), pathAuthConfig);
class AuthConfig {
config = null;
// Méthode pour lire le fichier de configuration JSON
loadConfig() {
try {
const configData = fs.readFileSync(configPath, 'utf-8');
this.config = JSON.parse(configData);
return this.config
} catch (error) {
console.error("Erreur lors de la lecture du fichier de configuration :", error);
return null;
}
}
// Méthode pour load le fichier de test
loadConfigTest(mockConfig) {
this.config = mockConfig;
}
// Méthode pour retourner la configuration des fournisseurs PassportJS
getPassportJSConfig() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const passportConfig = {};
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
passportConfig[providerName] = provider[providerName];
});
return passportConfig;
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner la configuration de Simple Login
getSimpleLoginConfig() {
if (this.config && this.config.auth && this.config.auth["simple-login"]) {
return this.config.auth["simple-login"];
} else {
return { error: "Aucune configuration Simple Login disponible." };
}
}
// Méthode pour retourner tous les providers de type OAuth
getOAuthProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oauthProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oauth';
});
if (oauthProviders.length > 0) {
return oauthProviders;
} else {
return { error: "Aucun fournisseur OAuth disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner tous les providers de type OIDC
getOIDCProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oidcProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oidc';
});
if (oidcProviders.length > 0) {
return oidcProviders;
} else {
return { error: "Aucun fournisseur OIDC disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour vérifier si tous les providers ont les variables nécessaires
validateProvidersConfig() {
const requiredOAuthFields = [
'OAUTH_AUTHORIZATION_URL', 'OAUTH_TOKEN_URL','OAUTH_USERINFO_URL', 'OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET', 'OAUTH_ROLE_TEACHER_VALUE', 'OAUTH_ROLE_STUDENT_VALUE'
];
const requiredOIDCFields = [
'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_CONFIG_URL', 'OIDC_ROLE_TEACHER_VALUE', 'OIDC_ROLE_STUDENT_VALUE','OIDC_ADD_SCOPE'
];
const missingFieldsReport = [];
if (this.config && this.config.auth && this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
let missingFields = [];
// Vérification des providers de type OAuth
if (providerConfig.type === 'oauth') {
missingFields = requiredOAuthFields.filter(field => !(field in providerConfig));
}
// Vérification des providers de type OIDC
else if (providerConfig.type === 'oidc') {
missingFields = requiredOIDCFields.filter(field => !(field in providerConfig));
}
// Si des champs manquent, on les ajoute au rapport
if (missingFields.length > 0) {
missingFieldsReport.push({
provider: providerName,
missingFields: missingFields
});
}
});
// Si des champs manquent, lever une exception
if (missingFieldsReport.length > 0) {
throw new Error(`Configuration invalide pour les providers suivants : ${JSON.stringify(missingFieldsReport, null, 2)}`);
} else {
console.log("Configuration auth_config.json: Tous les providers ont les variables nécessaires.")
return { success: "Tous les providers ont les variables nécessaires." };
}
} else {
throw new Error("Aucune configuration PassportJS disponible.");
}
}
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
getActiveAuth() {
if (this.config && this.config.auth) {
const passportConfig = {};
// Gestion des providers PassportJS
if (this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
passportConfig[providerName] = {};
if (providerConfig.type === 'oauth') {
passportConfig[providerName] = {
type: providerConfig.type
};
} else if (providerConfig.type === 'oidc') {
passportConfig[providerName] = {
type: providerConfig.type,
};
}
});
}
// Gestion du Simple Login
if (this.config.auth["simple-login"] && this.config.auth["simple-login"].enabled) {
passportConfig['simple-login'] = {
type: "simple-login",
name: this.config.auth["simple-login"].name
};
}
return passportConfig;
} else {
return { error: "Aucune configuration d'authentification disponible." };
}
}
}
module.exports = AuthConfig;

View file

@ -0,0 +1,25 @@
const AuthConfig = require('../config/auth.js');
class authController {
async getActive(req, res, next) {
try {
const authC = new AuthConfig();
authC.loadConfig();
const authActive = authC.getActiveAuth();
const response = {
authActive
};
return res.json(response);
}
catch (error) {
return next(error); // Gérer l'erreur
}
}
}
module.exports = new authController;

View file

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

View file

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

View file

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

View file

@ -1,14 +1,13 @@
//user //user
const db = require('../config/db.js'); const db = require("../config/db.js");
const bcrypt = require('bcrypt'); const bcrypt = require("bcrypt");
const AppError = require('../middleware/AppError.js'); const AppError = require("../middleware/AppError.js");
const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); const { USER_ALREADY_EXISTS } = require("../constants/errorCodes");
const Folders = require('../models/folders.js'); const Folders = require("../models/folders.js");
class Users { class Users {
async hashPassword(password) { async hashPassword(password) {
return await bcrypt.hash(password, 10) return await bcrypt.hash(password, 10);
} }
generatePassword() { generatePassword() {
@ -16,14 +15,14 @@ class Users {
} }
async verify(password, hash) { async verify(password, hash) {
return await bcrypt.compare(password, hash) return await bcrypt.compare(password, hash);
} }
async register(email, password) { async register(email, password) {
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: email });
@ -34,23 +33,39 @@ class Users {
const newUser = { const newUser = {
email: email, email: email,
password: await this.hashPassword(password), password: await this.hashPassword(password),
created_at: new Date() created_at: new Date(),
}; };
await userCollection.insertOne(newUser); let created_user = await userCollection.insertOne(newUser);
let user = await this.getById(created_user.insertedId)
const folderTitle = 'Dossier par Défaut'; const folderTitle = "Dossier par Défaut";
const userId = newUser._id.toString(); const userId = newUser._id.toString();
await Folders.create(folderTitle, userId); await Folders.create(folderTitle, userId);
// TODO: verif if inserted properly... // 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;
}
return user;
} }
async login(email, password) { async login(email, password) {
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 });
@ -74,25 +89,28 @@ class Users {
} }
async changePassword(email, newPassword) { async changePassword(email, newPassword) {
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 hashedPassword = await this.hashPassword(newPassword); const hashedPassword = await this.hashPassword(newPassword);
const result = await userCollection.updateOne({ email }, { $set: { password: hashedPassword } }); const result = await userCollection.updateOne(
{ email },
{ $set: { password: hashedPassword } }
);
if (result.modifiedCount != 1) return null; if (result.modifiedCount != 1) return null;
return newPassword return newPassword;
} }
async delete(email) { async delete(email) {
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 result = await userCollection.deleteOne({ email }); const result = await userCollection.deleteOne({ email });
@ -102,10 +120,10 @@ class Users {
} }
async getId(email) { async getId(email) {
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 });
@ -116,6 +134,47 @@ class Users {
return user._id; return user._id;
} }
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;
} }
module.exports = new Users; return user;
}
async editUser(userInfo) {
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: userInfo.id });
if (!user) {
return false;
}
const updatedFields = { ...userInfo };
delete updatedFields.id;
const result = await userCollection.updateOne(
{ _id: userInfo.id },
{ $set: updatedFields }
);
if (result.modifiedCount === 1) {
return true;
}
return false;
}
}
module.exports = new Users();

384
server/package-lock.json generated
View file

@ -7,16 +7,22 @@
"": { "": {
"name": "ets-pfe004-evaluetonsavoir-backend", "name": "ets-pfe004-evaluetonsavoir-backend",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
@ -1312,6 +1318,11 @@
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true "dev": true
}, },
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -1388,7 +1399,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@ -1460,6 +1470,14 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true "dev": true
}, },
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -1589,6 +1607,14 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@ -1647,7 +1673,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@ -1793,7 +1818,6 @@
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": { "dependencies": {
"ansi-styles": "^4.1.0", "ansi-styles": "^4.1.0",
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
@ -1809,7 +1833,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -1818,7 +1841,6 @@
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": { "dependencies": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
}, },
@ -1874,7 +1896,6 @@
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -1925,7 +1946,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
}, },
@ -1936,8 +1956,7 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"dev": true
}, },
"node_modules/color-support": { "node_modules/color-support": {
"version": "1.1.3", "version": "1.1.3",
@ -2105,7 +2124,6 @@
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@ -2535,6 +2553,29 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-session": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"dependencies": {
"cookie": "0.6.0",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -2560,7 +2601,6 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -2598,6 +2638,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -2643,6 +2691,20 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fs-minipass": { "node_modules/fs-minipass": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@ -2822,8 +2884,7 @@
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
"dev": true
}, },
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "3.0.0", "version": "3.0.0",
@ -3052,6 +3113,20 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -3094,7 +3169,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -3111,6 +3185,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -3119,8 +3204,7 @@
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"dev": true
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
"version": "3.2.2", "version": "3.2.2",
@ -3927,6 +4011,28 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true "dev": true
}, },
"node_modules/json-stable-stringify": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
"integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
"dependencies": {
"call-bind": "^1.0.5",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -3939,6 +4045,25 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.2", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@ -3984,6 +4109,14 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -4129,7 +4262,6 @@
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -4492,6 +4624,11 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"node_modules/oauth": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -4511,6 +4648,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -4522,6 +4667,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -4545,6 +4698,29 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -4607,6 +4783,115 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-openidconnect": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz",
"integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==",
"dependencies": {
"oauth": "0.10.x",
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/patch-package": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^9.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"rimraf": "^2.6.3",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.0.33",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/patch-package/node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4628,7 +4913,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -4644,6 +4928,11 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -4654,7 +4943,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -4783,6 +5071,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -5024,7 +5320,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
}, },
@ -5036,7 +5331,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -5475,6 +5769,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -5494,7 +5799,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -5571,6 +5875,22 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
},
"node_modules/undefsafe": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
@ -5582,6 +5902,14 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
}, },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -5688,7 +6016,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },
@ -5792,6 +6119,17 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}, },
"node_modules/yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View file

@ -7,7 +7,8 @@
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
"start": "node app.js", "start": "node app.js",
"dev": "nodemon app.js", "dev": "nodemon app.js",
"test": "jest" "test": "jest",
"postinstall": "patch-package"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -17,10 +18,15 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openidconnect": "^0.1.2",
"patch-package": "^8.0.0",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },

View file

@ -0,0 +1,12 @@
diff --git a/node_modules/passport-openidconnect/lib/profile.js b/node_modules/passport-openidconnect/lib/profile.js
index eeabf4e..8abe391 100644
--- a/node_modules/passport-openidconnect/lib/profile.js
+++ b/node_modules/passport-openidconnect/lib/profile.js
@@ -17,6 +17,7 @@ exports.parse = function(json) {
if (json.middle_name) { profile.name.middleName = json.middle_name; }
}
if (json.email) { profile.emails = [ { value: json.email } ]; }
+ if (json.groups) { profile.groups = [ { value: json.groups } ]; }
return profile;
};

9
server/routers/auth.js Normal file
View file

@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const jwt = require('../middleware/jwtToken.js');
const authController = require('../controllers/auth.js')
router.get("/getActiveAuth",authController.getActive);
module.exports = router;

35
server/utils.js Normal file
View file

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