From 6362e848d75fdbcca791ed2b02164bc62480224e Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Thu, 4 Apr 2024 17:18:21 +0200 Subject: [PATCH 001/130] Start cookie session with frontend --- backend/web-bff/App/app.js | 56 ++++++------ backend/web-bff/App/bin/www.js | 12 +-- backend/web-bff/App/package.json | 5 ++ frontend/package-lock.json | 142 +++++++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/setupProxy.tsx | 5 ++ 6 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 frontend/src/setupProxy.tsx diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index d4031e83..f6aa8491 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -1,38 +1,46 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - require('dotenv').config(); -var path = require('path'); -var express = require('express'); -var session = require('express-session'); -var createError = require('http-errors'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); +const path = require('path'); +const express = require('express'); +const session = require('cookie-session'); +const createError = require('http-errors'); +const cookieParser = require('cookie-parser'); +const logger = require('morgan'); +const helmet = require('helmet'); +const hpp = require('hpp'); +const csurf = require('csurf'); +const rateLimit = require('express-rate-limit') + +const indexRouter = require('./routes/index'); +const usersRouter = require('./routes/users'); +const authRouter = require('./routes/auth'); + +/* initialize express */ +const app = express(); -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); -var authRouter = require('./routes/auth'); +/* Set security configs */ +app.use(helmet()); +app.use(hpp()); -// initialize express -var app = express(); /** - * Using express-session middleware for persistent user session. Be sure to - * familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session + * Using cookie-session middleware for persistent user session. */ app.use(session({ + name: 'session', secret: process.env.EXPRESS_SESSION_SECRET, - resave: false, - saveUninitialized: false, - cookie: { - httpOnly: true, - secure: false, // set this to true on production - } + expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days })); +app.use(csurf(undefined)); + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, +}); + +app.use(limiter); + // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); diff --git a/backend/web-bff/App/bin/www.js b/backend/web-bff/App/bin/www.js index 092eb15d..0c794afe 100644 --- a/backend/web-bff/App/bin/www.js +++ b/backend/web-bff/App/bin/www.js @@ -4,22 +4,22 @@ * Module dependencies. */ -var app = require('../app'); -var debug = require('debug')('msal:server'); -var http = require('http'); +const app = require('../app'); +const debug = require('debug')('msal:server'); +const http = require('http'); /** * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || '3000'); +const port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ -var server = http.createServer(app); +const server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -34,7 +34,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - var port = parseInt(val, 10); + const port = parseInt(val, 10); if (isNaN(port)) { // named pipe diff --git a/backend/web-bff/App/package.json b/backend/web-bff/App/package.json index 1deeab35..f1b2a7d5 100644 --- a/backend/web-bff/App/package.json +++ b/backend/web-bff/App/package.json @@ -8,11 +8,16 @@ "@azure/msal-node": "^2.6.4", "axios": "^1.6.8", "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "csurf": "^1.11.0", "debug": "^4.3.4", "dotenv": "^16.4.1", "express": "^4.19.1", + "express-rate-limit": "^7.2.0", "express-session": "^1.18.0", "hbs": "^4.2.0", + "helmet": "^7.1.0", + "hpp": "^0.2.3", "http-errors": "^2.0.0", "morgan": "^1.10.0" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e85a99dd..f4e5bbf4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "antd": "^5.14.2", "axios": "^1.6.7", "highlight.js": "^11.9.0", + "http-proxy-middleware": "^3.0.0", "i18next-localstorage-cache": "^1.1.1", "lowlight": "^3.1.0", "react": "^18.2.0", @@ -1671,6 +1672,14 @@ "@types/unist": "*" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/jest": { "version": "27.5.2", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", @@ -1957,6 +1966,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", @@ -2398,6 +2418,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2415,6 +2440,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -2734,6 +2770,46 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "dependencies": { + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/i18next": { "version": "23.10.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.0.tgz", @@ -2900,6 +2976,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -2917,6 +3012,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -3849,6 +3952,18 @@ } ] }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3980,6 +4095,17 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -4949,6 +5075,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -5160,6 +5291,17 @@ "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index e05fee19..08ad1a99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "antd": "^5.14.2", "axios": "^1.6.7", "highlight.js": "^11.9.0", + "http-proxy-middleware": "^3.0.0", "i18next-localstorage-cache": "^1.1.1", "lowlight": "^3.1.0", "react": "^18.2.0", diff --git a/frontend/src/setupProxy.tsx b/frontend/src/setupProxy.tsx new file mode 100644 index 00000000..192825f8 --- /dev/null +++ b/frontend/src/setupProxy.tsx @@ -0,0 +1,5 @@ +const proxy = require('http-proxy-middleware').createProxyMiddleware; + +module.exports = function (app) { + app.use(proxy(`/auth/**`, {target: 'http://localhost:3000' })); +} \ No newline at end of file From 15bb9a2249a00adbd59e71646700a8a1979e6287 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 7 Apr 2024 18:40:04 +0200 Subject: [PATCH 002/130] use client side session cookies --- backend/web-bff/App/app.js | 4 ++-- backend/web-bff/App/auth/AuthProvider.js | 9 ++++----- backend/web-bff/App/fetch.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index f6aa8491..7babb3ac 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -32,7 +32,7 @@ app.use(session({ expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days })); -app.use(csurf(undefined)); + const limiter = rateLimit({ windowMs: 15 * 60 * 1000, @@ -71,4 +71,4 @@ app.use(function (err, req, res, next) { res.render('error'); }); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index f2fb8b8f..5e055205 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -125,7 +125,6 @@ class AuthProvider { if (!req.body || !req.body.state) { return next(new Error('Error: response not found')); } - const authCodeRequest = { ...req.session.authCodeRequest, code: req.body.code, @@ -141,11 +140,11 @@ class AuthProvider { const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body); - req.session.tokenCache = msalInstance.getTokenCache().serialize(); + // req.session.tokenCache = msalInstance.getTokenCache().serialize(); req.session.idToken = tokenResponse.idToken; - req.session.account = tokenResponse.account; + // req.session.account = tokenResponse.account; req.session.isAuthenticated = true; - + const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state)); res.redirect(state.successRedirect); } catch (error) { @@ -270,4 +269,4 @@ class AuthProvider { const authProvider = new AuthProvider(msalConfig); -module.exports = authProvider; \ No newline at end of file +module.exports = authProvider; diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index b3a2f6f8..bea3d975 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -38,4 +38,4 @@ async function fetch(endpoint, accessToken) { } } -module.exports = fetch; \ No newline at end of file +module.exports = fetch; From 328b25a7429959ec7b867f128cc4ec6f744d7636 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 8 Apr 2024 18:04:14 +0200 Subject: [PATCH 003/130] small changes --- backend/web-bff/App/auth/AuthProvider.js | 5 ++--- backend/web-bff/App/authConfig.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index 5e055205..ef5cd1c4 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -167,9 +167,8 @@ class AuthProvider { logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`; } - req.session.destroy(() => { - res.redirect(logoutUri); - }); + req.session = null; + res.redirect(logoutUri) } } diff --git a/backend/web-bff/App/authConfig.js b/backend/web-bff/App/authConfig.js index 26481118..9fe08c49 100644 --- a/backend/web-bff/App/authConfig.js +++ b/backend/web-bff/App/authConfig.js @@ -36,4 +36,4 @@ module.exports = { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, BACKEND_API_ENDPOINT -}; \ No newline at end of file +}; From 64eb4f0ae1ec207b1d8ee83bc1b7be5ee88050bc Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 8 Apr 2024 18:05:49 +0200 Subject: [PATCH 004/130] modified gitignore to ignore env --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a44d69e0..dbf66ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +backend/web-bff/App/.env.dev + HELP.md .gradle build/ From 546985f31ea0683db562c0aff73e6f202f98c5e1 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Thu, 25 Apr 2024 14:38:10 +0200 Subject: [PATCH 005/130] mongodb and express in docker-compose --- backend/web-bff/App/app.js | 31 +- backend/web-bff/App/auth/AuthProvider.js | 9 +- backend/web-bff/App/bin/www.js | 8 +- backend/web-bff/App/fetch.js | 12 +- backend/web-bff/App/package-lock.json | 1934 ++++++++++++++++++++++ backend/web-bff/App/package.json | 7 +- backend/web-bff/App/routes/api.js | 26 + backend/web-bff/App/routes/auth.js | 2 +- backend/web-bff/App/routes/index.js | 4 +- backend/web-bff/App/routes/users.js | 8 +- backend/web-bff/Dockerfile | 11 + docker-compose.yaml | 27 + 12 files changed, 2047 insertions(+), 32 deletions(-) create mode 100644 backend/web-bff/App/package-lock.json create mode 100644 backend/web-bff/App/routes/api.js create mode 100644 backend/web-bff/Dockerfile diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index 7babb3ac..91fe0c86 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -2,13 +2,11 @@ require('dotenv').config(); const path = require('path'); const express = require('express'); -const session = require('cookie-session'); +const session = require('express-session'); +const MongoStore = require('connect-mongo'); const createError = require('http-errors'); -const cookieParser = require('cookie-parser'); const logger = require('morgan'); -const helmet = require('helmet'); -const hpp = require('hpp'); -const csurf = require('csurf'); + const rateLimit = require('express-rate-limit') const indexRouter = require('./routes/index'); @@ -18,18 +16,28 @@ const authRouter = require('./routes/auth'); /* initialize express */ const app = express(); -/* Set security configs */ -app.use(helmet()); -app.use(hpp()); - /** * Using cookie-session middleware for persistent user session. */ + +connection_string = `mongodb://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}?authSource=admin` + +console.log(connection_string) + app.use(session({ - name: 'session', + name: 'pigeon session', secret: process.env.EXPRESS_SESSION_SECRET, - expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + resave: false, + saveUninitialized: false, + // expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + cookie: { + httpOnly: true, + secure: false, // make sure this is true in production + maxAge: 7*24*60*60*1000, + }, + store: MongoStore.create( + {mongoUrl: connection_string}) })); @@ -47,7 +55,6 @@ app.set('view engine', 'hbs'); app.use(logger('dev')); app.use(express.json()); -app.use(cookieParser()); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index ef5cd1c4..c27c9069 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -140,9 +140,9 @@ class AuthProvider { const tokenResponse = await msalInstance.acquireTokenByCode(authCodeRequest, req.body); - // req.session.tokenCache = msalInstance.getTokenCache().serialize(); + req.session.tokenCache = msalInstance.getTokenCache().serialize(); req.session.idToken = tokenResponse.idToken; - // req.session.account = tokenResponse.account; + req.session.account = tokenResponse.account; req.session.isAuthenticated = true; const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state)); @@ -167,8 +167,9 @@ class AuthProvider { logoutUri += `logout?post_logout_redirect_uri=${options.postLogoutRedirectUri}`; } - req.session = null; - res.redirect(logoutUri) + req.session.destroy(() => { + res.redirect(logoutUri); + }); } } diff --git a/backend/web-bff/App/bin/www.js b/backend/web-bff/App/bin/www.js index 0c794afe..c1b544be 100644 --- a/backend/web-bff/App/bin/www.js +++ b/backend/web-bff/App/bin/www.js @@ -34,7 +34,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - const port = parseInt(val, 10); + let port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -58,7 +58,7 @@ function onError(error) { throw error; } - var bind = typeof port === 'string' + let bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -82,8 +82,8 @@ function onError(error) { */ function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' + let addr = server.address(); + let bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index bea3d975..2b1de573 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -var axios = require('axios'); +const axios = require('axios'); const https = require('https'); const {BACKEND_API_ENDPOINT} = require("./authConfig"); @@ -12,17 +12,21 @@ const {BACKEND_API_ENDPOINT} = require("./authConfig"); * Attaches a given access token to a Backend API Call * @param endpoint REST API endpoint to call * @param accessToken raw access token string + * @param method The http method for the call. Choice out of 'GET' */ -async function fetch(endpoint, accessToken) { +async function fetch(endpoint, accessToken, method) { + let methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] + if (!(method in methods)) { + throw new Error('Not a valid HTTP method'); + } const url = new URL(endpoint, BACKEND_API_ENDPOINT) - console.log(accessToken) const headers = { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", } const config= { - method: "GET", + method: method, url: url.toString(), headers: headers, } diff --git a/backend/web-bff/App/package-lock.json b/backend/web-bff/App/package-lock.json new file mode 100644 index 00000000..054f7a67 --- /dev/null +++ b/backend/web-bff/App/package-lock.json @@ -0,0 +1,1934 @@ +{ + "name": "web-bff", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-bff", + "version": "1.0.0", + "dependencies": { + "@azure/msal-node": "^2.6.4", + "axios": "^1.6.8", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "csurf": "^1.11.0", + "debug": "^4.3.4", + "dotenv": "^16.4.1", + "express": "^4.19.1", + "express-rate-limit": "^7.2.0", + "express-session": "^1.18.0", + "hbs": "^4.2.0", + "helmet": "^7.1.0", + "hpp": "^0.2.3", + "http-errors": "^2.0.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", + "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.7.0.tgz", + "integrity": "sha512-wXD8LkUvHICeSWZydqg6o8Yvv+grlBEcmLGu+QEI4FcwFendbTEZrlSygnAXXSOCVaGAirWLchca35qrgpO6Jw==", + "dependencies": { + "@azure/msal-common": "14.9.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", + "integrity": "sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==", + "peer": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "peer": true + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "peer": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.6.0.tgz", + "integrity": "sha512-BVINv2SgcMjL4oYbBuCQTpE3/VKOSxrOA8Cj/wQP7izSzlBGVomdm+TcUd0Pzy0ytLSSDweCKQ6X3f5veM5LQA==", + "peer": true, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-session": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.0.tgz", + "integrity": "sha512-u73BDmR8QLGcs+Lprs0cfbcAPKl2HnPcjpwRXT41sEV4DRJ2+W0vJEEZkG31ofkx+HZflA70siRIjiTdIodmOQ==", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-session/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "Please use another csrf package", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-rate-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", + "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, + "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": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "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/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==" + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/kruptein": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.6.tgz", + "integrity": "sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "peer": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mongodb": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", + "integrity": "sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==", + "peer": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.4.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "peer": true, + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "peer": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/backend/web-bff/App/package.json b/backend/web-bff/App/package.json index f1b2a7d5..37a778d3 100644 --- a/backend/web-bff/App/package.json +++ b/backend/web-bff/App/package.json @@ -2,11 +2,13 @@ "name": "web-bff", "version": "1.0.0", "scripts": { - "start": "node ./bin/www" + "start": "node ./bin/www", + "dev": "nodemon ./bin/www" }, "dependencies": { "@azure/msal-node": "^2.6.4", "axios": "^1.6.8", + "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", "csurf": "^1.11.0", @@ -20,5 +22,8 @@ "hpp": "^0.2.3", "http-errors": "^2.0.0", "morgan": "^1.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" } } diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js new file mode 100644 index 00000000..7f8de46f --- /dev/null +++ b/backend/web-bff/App/routes/api.js @@ -0,0 +1,26 @@ +const express = require('express'); +const router = express.Router(); + +const fetch = require('../fetch'); + +const { BACKEND_API_ENDPOINT } = require('../authConfig'); + +// custom middleware to check auth state +function isAuthenticated(req, res, next) { + if (!req.session.isAuthenticated) { + return res.redirect('/auth/signin'); // redirect to sign-in route + } + + next(); +} + +router.all('/*', + isAuthenticated, + async function(req, res, next) { + try { + const response = await fetch(req.url , req.session.accessToken, req.method) + res.send(response) + } catch(error) { + next(error); + } + }) \ No newline at end of file diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index de43525e..47843e37 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -var express = require('express'); +const express = require('express'); const authProvider = require('../auth/AuthProvider'); const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, msalConfig} = require('../authConfig'); diff --git a/backend/web-bff/App/routes/index.js b/backend/web-bff/App/routes/index.js index e6bd1ddb..a7ed3411 100644 --- a/backend/web-bff/App/routes/index.js +++ b/backend/web-bff/App/routes/index.js @@ -3,9 +3,9 @@ * Licensed under the MIT License. */ -var express = require('express'); +const express = require('express'); const authProvider = require("../auth/AuthProvider"); -var router = express.Router(); +const router = express.Router(); router.get('/', function (req, res, next) { res.render('index', { diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index ce01f651..834d59cd 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -3,12 +3,12 @@ * Licensed under the MIT License. */ -var express = require('express'); -var router = express.Router(); +const express = require('express'); +const router = express.Router(); -var fetch = require('../fetch'); +const fetch = require('../fetch'); -var { BACKEND_API_ENDPOINT } = require('../authConfig'); +const { BACKEND_API_ENDPOINT } = require('../authConfig'); // custom middleware to check auth state function isAuthenticated(req, res, next) { diff --git a/backend/web-bff/Dockerfile b/backend/web-bff/Dockerfile new file mode 100644 index 00000000..b8607e0b --- /dev/null +++ b/backend/web-bff/Dockerfile @@ -0,0 +1,11 @@ +FROM node:21-bookworm + +WORKDIR /express-web-bff + +COPY App/package*.json ./ + +RUN npm install + +COPY App/ . + +CMD npm start diff --git a/docker-compose.yaml b/docker-compose.yaml index 5515884d..26055560 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -51,8 +51,35 @@ services: volumes: - ./certbot/www/:/var/www/certbot/:rw - ./certbot/conf/:/etc/letsencrypt/:rw + mongodb: + image: mongo:latest + container_name: mongodb + env_file: backend/web-bff/App/.env.dev + environment: + - MONGO_INTIDB_ROOT_USERNAME=$MONGODB_USER + - MONGO_INTIDB_ROOT_PASSWORD=$MONGODB_PASSWORD + ports: + - '27017:27017' + volumes: + - mongo-session-data:/data/mongodb + express: + build: backend/web-bff/ + container_name: express-container + env_file: backend/web-bff/App/.env.dev + ports: + - '3000:3000' + depends_on: + - mongodb + environment: + - NODE_PORT=3000 + - DB_HOST=mongodb + - DB_USER=$MONGODB_USER + - DB_PASSWORD=$MONGODB_PASSWORD + - DB_NAME=$MONGODB_DATABASE + - DB_PORT=$MONGODB_DOCKER_PORT volumes: postgres-data: + mongo-session-data: secrets: db-password: file: backend/db/password.txt From ec3c10d181314fa6d144399c624073f2f237f0dc Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Wed, 1 May 2024 00:37:33 +0200 Subject: [PATCH 006/130] skelet for submitting files --- frontend/src/pages/submit/Submit.tsx | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 5d4399da..396df722 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" import { useNavigate } from "react-router-dom" +import React, { useState, useRef} from 'react'; + const Submit = () => { const { t } = useTranslation() @@ -10,7 +12,34 @@ const Submit = () => { const navigate = useNavigate() + // file upload system + const [selectedFile, setSelectedFile] = useState(undefined); + const fileInputRef = useRef(null); + + const handleFileChange = (event: React.ChangeEvent) => { + setSelectedFile(event.target.files?.[0]); + } + + const handleFileUpload = async () => { + if (!selectedFile){ + return alert("Please select a file to upload"); + } + const formData = new FormData(); + formData.append("file", selectedFile as Blob); // Blob atm ma mss is er iets da het moet zijn voor de backend + + const response = await fetch('https://selab2-6.ugent.be/api/submissions/submit', { // juiste url nog toevoegen en body info enzo + method: 'POST', + body: formData, + }); + + if (response.ok) { + alert("File uploaded successfully"); + } else { + alert("Failed to upload file"); + } + + } return ( <>
From 439e61c358728ab301bcd4417a450308753c838f Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Fri, 3 May 2024 01:27:50 +0200 Subject: [PATCH 007/130] api routes en zoeken toegevoegd --- frontend/src/@types/requests.d.ts | 28 +-- frontend/src/i18n/en/translation.json | 10 +- frontend/src/i18n/nl/translation.json | 10 +- frontend/src/pages/editRole/EditRole.tsx | 173 ++++++++++++------ .../pages/editRole/components/UserList.tsx | 34 +++- 5 files changed, 174 insertions(+), 81 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 40104f52..ad0f0e12 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -9,7 +9,6 @@ export enum ApiRoutes { COURSE = "api/courses/:courseId", COURSE_MEMBERS = "api/courses/:courseId/members", - COURSE_MEMBER = "api/courses/:courseId/members/:userId", COURSE_PROJECTS = "api/courses/:id/projects", COURSE_CLUSTERS = "api/courses/:id/clusters", COURSE_GRADES = '/api/courses/:id/grades', @@ -39,7 +38,7 @@ export enum ApiRoutes { TEST = "api/test", USER = "api/users/:id", - USERS = "api/users", + USERS = "api/users?:params", USER_AUTH = "api/user", } @@ -87,30 +86,32 @@ export type DELETE_Requests = { [ApiRoutes.PROJECT]: undefined [ApiRoutes.GROUP_MEMBER]: undefined [ApiRoutes.COURSE_LEAVE]: undefined - [ApiRoutes.COURSE_MEMBER]: undefined } /** - * the body of the PUT & PATCH requests + * the body of the PUT requests */ export type PUT_Requests = { [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] [ApiRoutes.PROJECT]: ProjectFormData - [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } - [ApiRoutes.PROJECT_SCORE]: { score: number | null , feedback: string} + [ApiRoutes.USER]: { + name: string + surname: string + email: string + role: UserRole + } } - - +/** + * The response you get from the PUT request + */ export type PUT_Responses = { [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] - [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] - [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] + [ApiRoutes.USER]: GET_Responses[ApiRoutes.USER] } - type CourseTeacher = { name: string surname: string @@ -203,11 +204,12 @@ export type GET_Responses = { } [ApiRoutes.USERS]: { name: string - userId: number + surname: string + id: number url: string email: string role: UserRole - } + }[] [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 80d7b1ab..8fcb7b58 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -62,6 +62,15 @@ "editRole": "Edit a user's role" }, "editRole": { + "email": "Email", + "name": "Name", + "surname": "Surname", + "search": "Search", + "emailError": "Please enter a valid email", + "nameError": "Name must be at least 3 characters long", + "surnameError": "Surname must be at least 3 characters long", + "searchTutorial": "Enter a name, surname, or email and press \"Search\" to find users.", + "noUsersFound": "No users found", "student": "Student", "teacher": "Teacher", "admin": "Admin", @@ -98,7 +107,6 @@ "options": "Update", "groupMembers": "Group members", "newProject": "New project", - "scoreTooHigh": "Score is higher than maximum score", "change": { "title": "Create project", "updateTitle": "Update {{name}}", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 5f927d7f..ec43fe2d 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -65,6 +65,15 @@ }, "editRole": { + "email": "Email", + "name": "Naam", + "surname": "Achternaam", + "search": "Zoeken", + "emailError": "Vul een geldig email adres in", + "nameError": "Naam moet minstens 3 karakters lang zijn", + "surnameError": "Achternaam moet minstens 3 karakters lang zijn", + "searchTutorial": "Vul een email adres, naam of achternaam in en druk dan op \"Zoeken\" om gebruikers op te zoeken.", + "noUsersFound": "Geen gebruikers gevonden", "student": "Student", "teacher": "Professor", "admin": "Admin", @@ -101,7 +110,6 @@ "options": "Aanpassen", "groupMembers": "Groepsleden", "newProject": "Nieuw project", - "scoreTooHigh": "Score is hoger dan maximum score", "change": { "title": "Maak project aan", "name": "Naam", diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 0dc4713c..9f2f9339 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -1,75 +1,132 @@ import { useEffect, useState } from "react"; -import { Spin } from "antd"; +import { Row, Col, Form, Input, Button, Spin } from "antd"; import UserList from "./components/UserList" import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d"; import apiCall from "../../util/apiFetch"; +import { useTranslation } from "react-i18next"; +import { UsersListItem } from "./components/UserList"; export type UsersType = GET_Responses[ApiRoutes.USERS] const ProfileContent = () => { - const [users, setUsers] = useState(null); + const [users, setUsers] = useState(null); + const [searched, setSearched] = useState(false); + const [form] = Form.useForm(); + const { t } = useTranslation(); - function updateRole(user: UsersType, role: UserRole) { - //TODO: PUT of PATCH call - console.log("User: ", user); - console.log("Role: ", role); - if(!users) return; - const updatedUsers = users.map((u) => { - if (u.userId === user.userId) { - return { ...u, role: role }; - } - return u; - }); - setUsers(updatedUsers); + function updateRole(user: UsersListItem, role: UserRole) { + //here user is of type User (not UsersListItem), but it seems to work because the needed properties are named the same + console.log(user) + apiCall.patch(ApiRoutes.USER, {role: role}, {id: user.id}).then((res) => { + console.log(res.data); + //replace this user in the userlist with the updated one from res.data + const updatedUsers = users?.map((u) => { + if (u.id === user.id) { + return { ...u, role: res.data.role }; + } + return u; + }); + setUsers(updatedUsers?updatedUsers:null); + }) } - useEffect(() => { - //TODO: moet met GET call - /*apiCall.get(ApiRoutes.USERS).then((res) => { - console.log(res.data) - setUsers(res.data) - })*/ - setUsers([ - { - userId: 1, - name: "Alice Kornelis", - role: "student", - email: "test@test.test", - url: "test" - }, - { - userId: 2, - name: "Bob Kornelis", - role: "teacher", - email: "test@test.test", - url: "test" - }, - { - userId: 3, - name: "Charlie Kornelis", - role: "admin", - email: "test@test.test", - url: "test" - } - ]); - }, []); + const onSearch = (values: any) => { + setSearched(true); + setUsers(null); + //search operation here + const params = Object.entries(values) + .filter(([key, value]) => value !== undefined) + .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); + const queryString = Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + console.log(queryString); + apiCall.get(ApiRoutes.USERS,{params:queryString}).then((res) => { + console.log(res.data) + setUsers(res.data); + }) + + }; - if (users === null) { - return ( -
- -
- ) - } +return ( +
+
+ + + + + + - return ( -
- + + + + + + + + + + + + + + + {() => ( + + )} + + + + + {searched ? ( + users === null ? ( +
+ +
+ ) : ( + + ) + ) : ( +
+

{t("editRole.searchTutorial")}

- ); + )} +
+ ); }; export function EditRole() { diff --git a/frontend/src/pages/editRole/components/UserList.tsx b/frontend/src/pages/editRole/components/UserList.tsx index eac1c14d..ed4b77bf 100644 --- a/frontend/src/pages/editRole/components/UserList.tsx +++ b/frontend/src/pages/editRole/components/UserList.tsx @@ -4,14 +4,20 @@ import { useTranslation } from "react-i18next" import { UserRole } from "../../../@types/requests" import { useState } from "react" import { UsersType } from "../EditRole" +import { GET_Responses, ApiRoutes } from "../../../@types/requests.d" +import { User } from "../../../providers/UserProvider" -const UserList: React.FC<{ users: UsersType[]; updateRole: (user: UsersType, role: UserRole) => void }> = ({ users, updateRole }) => { +//this is ugly, but if I put this in GET_responses, it will be confused with the User type (and there's no GET request with this as a response). +//this is also the only place this is used, so I think it's fine. +export type UsersListItem = { name: string, surname: string, id: number, url: string, email: string, role: UserRole } + +const UserList: React.FC<{ users: UsersType; updateRole: (user: UsersListItem, role: UserRole) => void }> = ({ users, updateRole }) => { const { t } = useTranslation() const [visible, setVisible] = useState(false) - const [selectedUser, setSelectedUser] = useState(null) + const [selectedUser, setSelectedUser] = useState(null) const [selectedRole, setSelectedRole] = useState(null) - const handleMenuClick = (user: UsersType, role: UserRole) => { + const handleMenuClick = (user: UsersListItem, role: UserRole) => { setSelectedUser(user) setSelectedRole(role) setVisible(true) @@ -27,9 +33,20 @@ const UserList: React.FC<{ users: UsersType[]; updateRole: (user: UsersType, rol setVisible(false) } - const renderUserItem = (user: UsersType) => ( + //sort based on name, then surname, then email + const sortedUsers = [...users].sort((a, b) => { + const nameComparison = a.name.localeCompare(b.name); + if (nameComparison !== 0) return nameComparison; + + const surnameComparison = a.surname.localeCompare(b.surname); + if (surnameComparison !== 0) return surnameComparison; + + return a.email.localeCompare(b.email); + }); + + const renderUserItem = (user: UsersListItem) => ( - + handleMenuClick(user, e.key as UserRole), + onClick: (e) => handleMenuClick(user, e.key as UserRole), }} > e.preventDefault()}> @@ -66,8 +83,9 @@ const UserList: React.FC<{ users: UsersType[]; updateRole: (user: UsersType, rol
- {t("editRole.confirmationText",{role: selectedRole, name: selectedUser?.name })} + {t("editRole.confirmationText",{role: selectedRole, name: selectedUser?.name + " " + selectedUser?.surname})}
From ef647bb5c8ccfff00aedae63bb5275745d621600 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:29 +0200 Subject: [PATCH 008/130] support multipart file --- frontend/src/util/apiFetch.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index 66e6162c..d3544be7 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -21,7 +21,7 @@ type ApiCallPathValues = {[param: string]: string | number} * const newCourse = await apiFetch("POST", ApiRoutes.COURSES, { name: "New Course" }); * */ -async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues): Promise> { +async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}): Promise> { const account = msalInstance.getActiveAccount() if (!account) { @@ -47,30 +47,31 @@ async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", rou tokenExpiry = response.expiresOn // convert expiry time to JavaScript Date } - const headers = { + const defaultHeaders = { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + "Content-Type": body instanceof FormData ? undefined : "application/json", } + const finalHeaders = headers ? {...defaultHeaders, ...headers} : defaultHeaders; + const url = new URL(route, serverHost) - const config: AxiosRequestConfig = { - method: method, - url: url.toString(), - headers: headers, - data: body, - } - +const config: AxiosRequestConfig = { + method: method, + url: url.toString(), + headers: finalHeaders, + data: body instanceof FormData ? body : JSON.stringify(body), +} return axios(config) } const apiCall = { - get: async (route: T, pathValues?:ApiCallPathValues) => apiFetch("GET", route,undefined,pathValues) as Promise>, - post: async (route: T, body: POST_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("POST", route, body,pathValues) as Promise>, - put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("PUT", route, body,pathValues) as Promise>, - delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues) => apiFetch("DELETE", route, body,pathValues), - patch: async (route: T, body: Partial, pathValues?:ApiCallPathValues) => apiFetch("PATCH", route, body,pathValues) as Promise>, + get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("GET", route, undefined, pathValues, headers) as Promise>, + post: async (route: T, body: POST_Requests[T] | FormData, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("POST", route, body, pathValues, headers) as Promise>, + put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PUT", route, body, pathValues, headers) as Promise>, + delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("DELETE", route, body, pathValues, headers), + patch: async (route: T, body: Partial, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PATCH", route, body, pathValues, headers) as Promise>, } const apiCallInit = async () => { From 25cff9251798922cf432de6a0d56066778887687 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:41 +0200 Subject: [PATCH 009/130] submission support route --- frontend/src/@types/requests.d.ts | 415 +++++++++++++++--------------- 1 file changed, 209 insertions(+), 206 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 40104f52..6c0c3c67 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -4,43 +4,44 @@ import type {ProjectFormData} from "../pages/projectCreate/components/ProjectCre * Routes used to make API calls */ export enum ApiRoutes { - USER_COURSES = "api/courses", - COURSES = "api/courses", - - COURSE = "api/courses/:courseId", - COURSE_MEMBERS = "api/courses/:courseId/members", - COURSE_MEMBER = "api/courses/:courseId/members/:userId", - COURSE_PROJECTS = "api/courses/:id/projects", - COURSE_CLUSTERS = "api/courses/:id/clusters", - COURSE_GRADES = '/api/courses/:id/grades', - COURSE_LEAVE = "api/courses/:courseId/leave", + USER_COURSES = "api/courses", + COURSES = "api/courses", - PROJECTS = "api/projects", - PROJECT = "api/projects/:id", - PROJECT_CREATE = "api/courses/:courseId/projects", - PROJECT_TESTS = "api/projects/:id/tests", - PROJECT_SUBMISSIONS = "api/projects/:id/submissions", - PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", - PROJECT_GROUP = "api/projects/:id/groups/:groupId", - PROJECT_GROUPS = "api/projects/:id/groups", - PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", + COURSE = "api/courses/:courseId", + COURSE_MEMBERS = "api/courses/:courseId/members", + COURSE_MEMBER = "api/courses/:courseId/members/:userId", + COURSE_PROJECTS = "api/courses/:id/projects", + COURSE_CLUSTERS = "api/courses/:id/clusters", + COURSE_GRADES = '/api/courses/:id/grades', + COURSE_LEAVE = "api/courses/:courseId/leave", - SUBMISSION = "api/submissions/:id", - SUBMISSION_FILE = "api/submissions/:id/file", - SUBMISSION_STRUCTURE_FEEDBACK= "/api/submissions/:id/structurefeedback", - SUBMISSION_DOCKER_FEEDBACK= "/api/submissions/:id/dockerfeedback", + PROJECTS = "api/projects", + PROJECT = "api/projects/:id", + PROJECT_CREATE = "api/courses/:courseId/projects", + PROJECT_TESTS = "api/projects/:id/tests", + PROJECT_SUBMISSIONS = "api/projects/:id/submissions", + PROJECT_SUBMIT = "api/projects/:id/submit", + PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", + PROJECT_GROUP = "api/projects/:id/groups/:groupId", + PROJECT_GROUPS = "api/projects/:id/groups", + PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", - CLUSTER = "api/clusters/:id", + SUBMISSION = "api/submissions/:id", + SUBMISSION_FILE = "api/submissions/:id/file", + SUBMISSION_STRUCTURE_FEEDBACK = "/api/submissions/:id/structurefeedback", + SUBMISSION_DOCKER_FEEDBACK = "/api/submissions/:id/dockerfeedback", - GROUP = "api/groups/:id", - GROUP_MEMBERS = "api/groups/:id/members", - GROUP_MEMBER = "api/groups/:id/members/:userId", - GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", + CLUSTER = "api/clusters/:id", - TEST = "api/test", - USER = "api/users/:id", - USERS = "api/users", - USER_AUTH = "api/user", + GROUP = "api/groups/:id", + GROUP_MEMBERS = "api/groups/:id/members", + GROUP_MEMBER = "api/groups/:id/members/:userId", + GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", + + TEST = "api/test", + USER = "api/users/:id", + USERS = "api/users", + USER_AUTH = "api/user", } export type Timestamp = string @@ -49,22 +50,25 @@ export type Timestamp = string * the body of the POST requests */ export type POST_Requests = { - [ApiRoutes.COURSES]: { - name: string - description:string - } - [ApiRoutes.PROJECT_CREATE]: - ProjectFormData + [ApiRoutes.COURSES]: { + name: string + description: string + } + [ApiRoutes.PROJECT_CREATE]: + ProjectFormData [ApiRoutes.GROUP_MEMBERS]: { - id: number + id: number + } + [ApiRoutes.PROJECT_SUBMIT]: { + file: FormData } - [ApiRoutes.COURSE_CLUSTERS]: { - name: string - capacity: number - groupCount: number - } + [ApiRoutes.COURSE_CLUSTERS]: { + name: string + capacity: number + groupCount: number + } } /** @@ -72,22 +76,22 @@ export type POST_Requests = { */ export type POST_Responses = { - [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], - [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] - [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] - [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER] - + [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], + [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] + [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] + [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER] + [ApiRoutes.PROJECT_SUBMIT]: GET_Responses[ApiRoutes.SUBMISSION] } /** * the body of the DELETE requests */ export type DELETE_Requests = { - [ApiRoutes.COURSE]: undefined - [ApiRoutes.PROJECT]: undefined - [ApiRoutes.GROUP_MEMBER]: undefined - [ApiRoutes.COURSE_LEAVE]: undefined - [ApiRoutes.COURSE_MEMBER]: undefined + [ApiRoutes.COURSE]: undefined + [ApiRoutes.PROJECT]: undefined + [ApiRoutes.GROUP_MEMBER]: undefined + [ApiRoutes.COURSE_LEAVE]: undefined + [ApiRoutes.COURSE_MEMBER]: undefined } @@ -95,31 +99,30 @@ export type DELETE_Requests = { * the body of the PUT & PATCH requests */ export type PUT_Requests = { - [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] - [ApiRoutes.PROJECT]: ProjectFormData - [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } - [ApiRoutes.PROJECT_SCORE]: { score: number | null , feedback: string} + [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: ProjectFormData + [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } + [ApiRoutes.PROJECT_SCORE]: { score: number | null, feedback: string } } - export type PUT_Responses = { - [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] - [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] - [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] - [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] + [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] + [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] + [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] } type CourseTeacher = { - name: string - surname: string - url: string, + name: string + surname: string + url: string, } type Course = { - courseUrl: string - name: string + courseUrl: string + name: string } export type ProjectStatus = "correct" | "incorrect" | "not started" @@ -131,148 +134,148 @@ export type UserRole = "student" | "teacher" | "admin" */ export type GET_Responses = { - [ApiRoutes.TEST]: { - name: string - firstName: string - lastName: string - email: string - oid: string - } - [ApiRoutes.PROJECT_SUBMISSIONS]: { - feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, - group: GET_Responses[ApiRoutes.GROUP], - submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet - }[], - [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] - [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] - [ApiRoutes.SUBMISSION]: { - submissionId: number - projectId: number - groupId: number - structureAccepted: boolean - dockerAccepted: boolean - submissionTime: Timestamp - projectUrl: ApiRoutes.PROJECT - groupUrl: ApiRoutes.GROUP - fileUrl: ApiRoutes.SUBMISSION_FILE - structureFeedbackUrl: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK - dockerFeedbackUrl: ApiRoutes.SUBMISSION_DOCKER_FEEDBACK - } - [ApiRoutes.SUBMISSION_FILE]: BlobPart - [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] - [ApiRoutes.PROJECT]: { - course: { - name: string - url: string - courseId: number + [ApiRoutes.TEST]: { + name: string + firstName: string + lastName: string + email: string + oid: string } - deadline: Timestamp - description: string - clusterId: number | null; - projectId: number - name: string - submissionUrl: ApiRoutes.PROJECT_GROUP_SUBMISSIONS - testsUrl: string - maxScore:number - visible: boolean - status?: ProjectStatus - progress: { - completed: number - total: number + [ApiRoutes.PROJECT_SUBMISSIONS]: { + feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, + group: GET_Responses[ApiRoutes.GROUP], + submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet + }[], + [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] + [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] + [ApiRoutes.SUBMISSION]: { + submissionId: number + projectId: number + groupId: number + structureAccepted: boolean + dockerAccepted: boolean + submissionTime: Timestamp + projectUrl: ApiRoutes.PROJECT + groupUrl: ApiRoutes.GROUP + fileUrl: ApiRoutes.SUBMISSION_FILE + structureFeedbackUrl: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK + dockerFeedbackUrl: ApiRoutes.SUBMISSION_DOCKER_FEEDBACK + } + [ApiRoutes.SUBMISSION_FILE]: BlobPart + [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] + [ApiRoutes.PROJECT]: { + course: { + name: string + url: string + courseId: number + } + deadline: Timestamp + description: string + clusterId: number | null; + projectId: number + name: string + submissionUrl: ApiRoutes.PROJECT_GROUP_SUBMISSIONS + testsUrl: string + maxScore: number + visible: boolean + status?: ProjectStatus + progress: { + completed: number + total: number + }, + groupId: number | null // null if not in a group + } + [ApiRoutes.PROJECT_TESTS]: {} // ?? + [ApiRoutes.GROUP]: { + groupId: number, + capacity: number, + name: string + groupClusterUrl: ApiRoutes.CLUSTER + members: GET_Responses[ApiRoutes.GROUP_MEMBER][] + } + [ApiRoutes.PROJECT_SCORE]: { + score: number | null, + feedback: string | null, + projectId: number, + groupId: number }, - groupId: number | null // null if not in a group - } - [ApiRoutes.PROJECT_TESTS]: {} // ?? - [ApiRoutes.GROUP]: { - groupId: number, - capacity: number, - name: string - groupClusterUrl: ApiRoutes.CLUSTER - members: GET_Responses[ApiRoutes.GROUP_MEMBER][] - } - [ApiRoutes.PROJECT_SCORE]: { - score: number | null, - feedback:string | null, - projectId: number, - groupId: number - }, - [ApiRoutes.GROUP_MEMBER]: { - email: string - name: string - userId: number - } - [ApiRoutes.USERS]: { - name: string - userId: number - url: string - email: string - role: UserRole - } - [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] + [ApiRoutes.GROUP_MEMBER]: { + email: string + name: string + userId: number + } + [ApiRoutes.USERS]: { + name: string + userId: number + url: string + email: string + role: UserRole + } + [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] - [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] - - [ApiRoutes.CLUSTER]: { - clusterId: number; - name: string; - capacity: number; - groupCount: number; - createdAt: Timestamp; - groups: GET_Responses[ApiRoutes.GROUP][] - courseUrl: ApiRoutes.COURSE - } - [ApiRoutes.COURSE]: { - description: string - courseId: number - memberUrl: ApiRoutes.COURSE_MEMBERS - name: string - teacher: CourseTeacher - assistents: CourseTeacher[] - joinUrl: string - archivedAt: Timestamp | null // null if not archived - year: number - createdAt: Timestamp - } - [ApiRoutes.COURSE_MEMBERS]: { - relation: CourseRelation, - user: GET_Responses[ApiRoutes.GROUP_MEMBER] - }[], - [ApiRoutes.USER]: { - courseUrl: string - projects_url: string - url: string - role: UserRole - email: string - id: number - name: string - surname: string - }, - [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], - [ApiRoutes.USER_COURSES]: { - courseId:number, - name:string, - relation: CourseRelation, - memberCount: number, - archivedAt: Timestamp | null, // null if not archived - year: number // Year of the course - url:string - }[], - //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] - [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] + [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] - [ApiRoutes.PROJECTS]: { - enrolledProjects: {project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus}[], - adminProjects: Omit[] - }, + [ApiRoutes.CLUSTER]: { + clusterId: number; + name: string; + capacity: number; + groupCount: number; + createdAt: Timestamp; + groups: GET_Responses[ApiRoutes.GROUP][] + courseUrl: ApiRoutes.COURSE + } + [ApiRoutes.COURSE]: { + description: string + courseId: number + memberUrl: ApiRoutes.COURSE_MEMBERS + name: string + teacher: CourseTeacher + assistents: CourseTeacher[] + joinUrl: string + archivedAt: Timestamp | null // null if not archived + year: number + createdAt: Timestamp + } + [ApiRoutes.COURSE_MEMBERS]: { + relation: CourseRelation, + user: GET_Responses[ApiRoutes.GROUP_MEMBER] + }[], + [ApiRoutes.USER]: { + courseUrl: string + projects_url: string + url: string + role: UserRole + email: string + id: number + name: string + surname: string + }, + [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], + [ApiRoutes.USER_COURSES]: { + courseId: number, + name: string, + relation: CourseRelation, + memberCount: number, + archivedAt: Timestamp | null, // null if not archived + year: number // Year of the course + url: string + }[], + //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] + [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] + + [ApiRoutes.PROJECTS]: { + enrolledProjects: { project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus }[], + adminProjects: Omit[] + }, - [ApiRoutes.COURSE_GRADES]: { - projectName: string, - projectUrl: string, - projectId: number, - maxScore: number, - groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null - }[] + [ApiRoutes.COURSE_GRADES]: { + projectName: string, + projectUrl: string, + projectId: number, + maxScore: number, + groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null + }[] - [ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK]: string | null // Null if no feedback is given - [ApiRoutes.SUBMISSION_DOCKER_FEEDBACK]: string | null // Null if no feedback is given + [ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK]: string | null // Null if no feedback is given + [ApiRoutes.SUBMISSION_DOCKER_FEEDBACK]: string | null // Null if no feedback is given } From c47bb93f349bc4e50b97c139799bfd6cb2ed8265 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:39:49 +0200 Subject: [PATCH 010/130] support file submitting --- frontend/src/pages/submit/Submit.tsx | 169 ++++++++---------- .../pages/submit/components/SubmitForm.tsx | 92 +++++----- 2 files changed, 124 insertions(+), 137 deletions(-) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 396df722..dd4f2f79 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -1,103 +1,88 @@ -import { Affix, Button, Card, Col, Form, Row, Typography } from "antd" -import { useTranslation } from "react-i18next" +import {Affix, Button, Card, Col, Form, Row, Typography} from "antd" +import {useTranslation} from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" -import { useNavigate } from "react-router-dom" -import React, { useState, useRef} from 'react'; - +import {useNavigate, useParams} from "react-router-dom" +import React, {useState, useRef} from 'react'; +import apiCall from "../../util/apiFetch"; +import {ApiRoutes} from "../../@types/requests.d"; const Submit = () => { - const { t } = useTranslation() - const [form] = Form.useForm() - - const navigate = useNavigate() + const {t} = useTranslation() + const [form] = Form.useForm() + const {projectId} = useParams<{ projectId: string }>() + const [fileAdded, setFileAdded] = useState(false); + const navigate = useNavigate() - // file upload system - const [selectedFile, setSelectedFile] = useState(undefined); - const fileInputRef = useRef(null); - - const handleFileChange = (event: React.ChangeEvent) => { - setSelectedFile(event.target.files?.[0]); +const onSubmit = async (values: any) => { + console.log("Received values of form: ", values) + const file = values[t("project.addFiles")][0].originFileObj + if (!file) { + console.error("No file selected") + return } + const formData = new FormData() + formData.append("file", file) + if (!projectId) return; + const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) +} + return ( + <> +
+ + + + + - const handleFileUpload = async () => { - if (!selectedFile){ - return alert("Please select a file to upload"); - } - - const formData = new FormData(); - formData.append("file", selectedFile as Blob); // Blob atm ma mss is er iets da het moet zijn voor de backend - - const response = await fetch('https://selab2-6.ugent.be/api/submissions/submit', { // juiste url nog toevoegen en body info enzo - method: 'POST', - body: formData, - }); - - if (response.ok) { - alert("File uploaded successfully"); - } else { - alert("Failed to upload file"); - } - - } - return ( - <> -
- - - - - - + + + + + + + + + + + + +
- - - - - -
- - - - - - -
- - - ) + + ) } export default Submit diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 32c6f9ad..87022133 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -1,51 +1,53 @@ -import { InboxOutlined } from "@ant-design/icons" -import { Form, FormInstance, Upload } from "antd" -import { FC } from "react" -import { useTranslation } from "react-i18next" - - - -const SubmitForm:FC<{form:FormInstance}> = ({form}) => { - - const {t} = useTranslation() - const normFile = (e: any) => { - console.log("Upload event:", e) - if (Array.isArray(e)) { - return e +import {InboxOutlined} from "@ant-design/icons" +import {Form, FormInstance, Upload} from "antd" +import {FC} from "react" +import {useTranslation} from "react-i18next" + + +const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => void, onSubmit: (values: any) => void }> = ({form, setFileAdded, onSubmit}) => { + + const {t} = useTranslation() + const normFile = (e: any) => { + console.log("Upload event:", e) + console.log("ye") + if (Array.isArray(e)) { + return e + } + return e?.fileList } - return e?.fileList - } const onFinish = (values: any) => { - console.log("Received values of form: ", values) - - // TODO: make api call - } - - return ( -
- - - -

- -

-

{t("project.uploadAreaTitle")}

-

{t("project.uploadAreaSubtitle")}

-
-
-
- ) + onSubmit(values); + }; + + return ( +
+ + + { + if (file.status !== 'uploading') { + setFileAdded(true); + } + }} + > +

+ +

+

{t("project.uploadAreaTitle")}

+

{t("project.uploadAreaSubtitle")}

+
+
+
+ ) } export default SubmitForm From fe57b89dcd87b9ae6e1f9ae1f0548c51ece0251d Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 17:41:44 +0200 Subject: [PATCH 011/130] remove debug log --- frontend/src/pages/submit/components/SubmitForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 87022133..9d258b5d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -9,7 +9,6 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi const {t} = useTranslation() const normFile = (e: any) => { console.log("Upload event:", e) - console.log("ye") if (Array.isArray(e)) { return e } From c7b6b1b5eda9f46654696539b592fce6f7daf3cb Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sun, 5 May 2024 18:35:13 +0200 Subject: [PATCH 012/130] added zip support --- frontend/package-lock.json | 85 +++++++++++++++++++++++++++- frontend/package.json | 1 + frontend/src/pages/submit/Submit.tsx | 35 ++++++++---- 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4a781014..71f887d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -3545,6 +3546,11 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -4852,6 +4858,11 @@ "node": ">=4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -4901,8 +4912,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inline-style-parser": { "version": "0.2.3", @@ -7464,6 +7474,17 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7482,6 +7503,14 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8435,6 +8464,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parse-entities": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", @@ -8627,6 +8661,11 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9430,6 +9469,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -9699,6 +9757,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9772,6 +9835,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9895,6 +9963,14 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -10425,6 +10501,11 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0d0e5eec..165c39b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index dd4f2f79..cfd023ae 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -6,6 +6,7 @@ import {useNavigate, useParams} from "react-router-dom" import React, {useState, useRef} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; +import JSZip from 'jszip'; const Submit = () => { const {t} = useTranslation() @@ -14,18 +15,28 @@ const Submit = () => { const [fileAdded, setFileAdded] = useState(false); const navigate = useNavigate() -const onSubmit = async (values: any) => { - console.log("Received values of form: ", values) - const file = values[t("project.addFiles")][0].originFileObj - if (!file) { - console.error("No file selected") - return - } - const formData = new FormData() - formData.append("file", file) - if (!projectId) return; - const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) -} + const onSubmit = async (values: any) => { + console.log("Received values of form: ", values) + const file = values[t("project.addFiles")][0].originFileObj + if (!file) { + console.error("No file selected") + return + } + const formData = new FormData() + + if (file.type === 'application/zip') { + formData.append("file", file); + } else { + const zip = new JSZip(); + zip.file(file.name, file); + const content = await zip.generateAsync({type: "blob"}); + formData.append("file", content, "files.zip"); + } + + if (!projectId) return; + const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) + console.log(response) + } return ( <>
From 60986e029f552b2653eba77e5067db3065c8888e Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 6 May 2024 22:35:06 +0200 Subject: [PATCH 013/130] Small bug fixes --- .gitignore | 1 + frontend/src/i18n/en/translation.json | 2 +- frontend/src/i18n/nl/translation.json | 2 +- frontend/src/pages/submit/Submit.tsx | 19 +++++++++---------- .../pages/submit/components/SubmitForm.tsx | 5 ++++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 991023f6..0a6bb2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ out/ .vscode/ backend/app/data/* backend/data/* +data/* ### Secrets ### backend/app/src/main/resources/application-secrets.properties diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index fb0ba65a..228adcfd 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -84,7 +84,7 @@ "submit": "Submit", "back": "Cancel", "uploadAreaTitle": "Click or drag file to this area to upload", - "uploadAreaSubtitle": "Maximum file size is 10MB", + "uploadAreaSubtitle": "Maximum file size is 100MB", "deadlinePassed": "Deadline passed", "downloadSubmissions": "Download all submissions", "group": "Group", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 95cf948e..475c43ad 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -88,7 +88,7 @@ "addFiles": "Bestanden toevoegen", "submit": "Indienen", "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", - "uploadAreaSubtitle": "Maximum bestandsgrootte is 10MB", + "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", "downloadSubmissions": "Download alle indieningen", "group": "Groep", diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index cfd023ae..2b887e74 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -17,21 +17,20 @@ const Submit = () => { const onSubmit = async (values: any) => { console.log("Received values of form: ", values) - const file = values[t("project.addFiles")][0].originFileObj - if (!file) { - console.error("No file selected") + const files = values.files.map((file: any) => file.originFileObj); + if (files.length === 0) { + console.error("No files selected") return } + console.log(files); const formData = new FormData() - if (file.type === 'application/zip') { - formData.append("file", file); - } else { - const zip = new JSZip(); + const zip = new JSZip(); + files.forEach((file: any) => { zip.file(file.name, file); - const content = await zip.generateAsync({type: "blob"}); - formData.append("file", content, "files.zip"); - } + }); + const content = await zip.generateAsync({type: "blob"}); + formData.append("file", content, "files.zip"); if (!projectId) return; const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 9d258b5d..3cbd1b9d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -23,14 +23,17 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi
false} multiple={false} + directory={true} style={{height: "100%"}} onChange={({file}) => { if (file.status !== 'uploading') { From ef0dabfd6d0ed13c9aa8b74de83776ac5c3adb2f Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Thu, 9 May 2024 18:42:36 +0200 Subject: [PATCH 014/130] allow folders to be uploaded --- frontend/src/pages/submit/components/SubmitForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 3cbd1b9d..0cb1ec7c 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -33,7 +33,7 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi name="file" beforeUpload={() => false} multiple={false} - directory={true} + directory={false} style={{height: "100%"}} onChange={({file}) => { if (file.status !== 'uploading') { From 089a961085b40e9c3983b49f239610e0f4da05f8 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Thu, 9 May 2024 22:13:56 +0200 Subject: [PATCH 015/130] better upload & download --- .../com/ugent/pidgeon/config/WebConfig.java | 2 + .../controllers/SubmissionController.java | 2 + frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/nl/translation.json | 1 + .../submission/components/SubmissionCard.tsx | 245 ++++++++++-------- frontend/src/pages/submit/Submit.tsx | 13 +- .../pages/submit/components/SubmitForm.tsx | 207 ++++++++++++++- frontend/src/util/apiFetch.ts | 19 +- 8 files changed, 359 insertions(+), 131 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java index 0009ac5d..ea2dc330 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java @@ -23,7 +23,9 @@ public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*") .allowedOrigins("*") + .exposedHeaders("Content-Disposition") .allowedHeaders("*"); + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index a943ce41..4f9d6d15 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -159,6 +160,7 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti */ @PostMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submit") //Route to submit a file, it accepts a multiform with the file and submissionTime + @Transactional @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @PathVariable("projectid") long projectid, Auth auth) { long userId = auth.getUserEntity().getId(); diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 228adcfd..6349a566 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -93,6 +93,7 @@ "groupEmpty": "No members in this group", "testFailed": "Tests failed", "structureFailed": "Structure failed", + "uploadDirectory": "Upload Directory", "submission": "Submission", "passed": "Passed", "notSubmitted": "Not submitted", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 475c43ad..64f5d73c 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -90,6 +90,7 @@ "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", + "uploadDirectory": "folder uploaden", "downloadSubmissions": "Download alle indieningen", "group": "Groep", "status": "Status", diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index 5e4022d1..f218535a 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -1,130 +1,155 @@ -import { Card, Spin, theme, Input, Button, Typography } from "antd" -import { useTranslation } from "react-i18next" -import { GET_Responses } from "../../../@types/requests" -import { ApiRoutes } from "../../../@types/requests" -import { ArrowLeftOutlined } from "@ant-design/icons" -import { useNavigate } from "react-router-dom" +import {Card, Spin, theme, Input, Button, Typography} from "antd" +import {useTranslation} from "react-i18next" +import {GET_Responses} from "../../../@types/requests" +import {ApiRoutes} from "../../../@types/requests" +import {ArrowLeftOutlined} from "@ant-design/icons" +import {useNavigate} from "react-router-dom" import "@fontsource/jetbrains-mono" -import { useEffect, useState } from "react" +import {useEffect, useState} from "react" import apiCall from "../../../util/apiFetch" export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] -const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission }) => { - const { token } = theme.useToken() - const { t } = useTranslation() - const [structureFeedback, setStructureFeedback] = useState(null) - const [dockerFeedback, setDockerFeedback] = useState(null) - const navigate = useNavigate() - useEffect(() => { - if (!submission.dockerAccepted) apiCall.get(submission.dockerFeedbackUrl).then((res) => setDockerFeedback(res.data ? res.data : "")) - if (!submission.structureAccepted) apiCall.get(submission.structureFeedbackUrl).then((res) => setStructureFeedback(res.data ? res.data : "")) - }, [submission.dockerFeedbackUrl, submission.structureFeedbackUrl]) +const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) => { + const {token} = theme.useToken() + const {t} = useTranslation() + const [structureFeedback, setStructureFeedback] = useState(null) + const [dockerFeedback, setDockerFeedback] = useState(null) + const navigate = useNavigate() + useEffect(() => { + if (!submission.dockerAccepted) apiCall.get(submission.dockerFeedbackUrl).then((res) => setDockerFeedback(res.data ? res.data : "")) + if (!submission.structureAccepted) apiCall.get(submission.structureFeedbackUrl).then((res) => setStructureFeedback(res.data ? res.data : "")) + }, [submission.dockerFeedbackUrl, submission.structureFeedbackUrl]) - const downloadSubmission = async () => { - //TODO: testen of dit wel echt werkt - try { - const fileContent = await apiCall.get(submission.fileUrl) - console.log(fileContent) - const blob = new Blob([fileContent.data], { type: "text/plain" }) - const url = URL.createObjectURL(blob) - const link = document.createElement("a") - link.href = url - link.download = "indiening.zip" - document.body.appendChild(link) - link.click() - URL.revokeObjectURL(url) - document.body.removeChild(link) - } catch (err) { - // TODO: handle error + const downloadSubmission = async () => { + try { + const response = await apiCall.get(submission.fileUrl, undefined, undefined, { + responseType: 'blob', + transformResponse: [(data) => data], + }); + console.log(response); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + const contentDisposition = response.headers['content-disposition']; + console.log(contentDisposition); + let fileName = 'file.zip'; // default filename + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename=([^;]+)/); + console.log(fileNameMatch); + if (fileNameMatch && fileNameMatch[1]) { + fileName = fileNameMatch[1]; // use the filename from the headers + } + } + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + } catch (err) { + console.error(err); + } } - } - return ( - + return ( + - {t("submission.submission")} + {t("submission.submission")} - } - > - {t("submission.submittedFiles")} + } + > + {t("submission.submittedFiles")} -
    -
  • - -
  • -
+
    +
  • + +
  • +
- {t("submission.structuretest")} + {t("submission.structuretest")} -
    -
  • - {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.structureAccepted ? null : ( -
    - {structureFeedback === null ? ( - - ) : ( - - )} -
    - )} -
  • -
+
    +
  • + {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.structureAccepted ? null : ( +
    + {structureFeedback === null ? ( + + ) : ( + + )} +
    + )} +
  • +
- {t("submission.dockertest")} + {t("submission.dockertest")} -
    -
  • - {submission.dockerAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.dockerAccepted ? null : ( -
    - {dockerFeedback === null ? ( - - ) : ( - - )} -
    - )} -
  • -
-
- ) +
    +
  • + {submission.dockerAccepted ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.dockerAccepted ? null : ( +
    + {dockerFeedback === null ? ( + + ) : ( + + )} +
    + )} +
  • +
+
+ ) } export default SubmissionCard diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 2b887e74..950fa72e 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -27,7 +27,7 @@ const Submit = () => { const zip = new JSZip(); files.forEach((file: any) => { - zip.file(file.name, file); + zip.file(file.webkitRelativePath || file.name, file); }); const content = await zip.generateAsync({type: "blob"}); formData.append("file", content, "files.zip"); @@ -35,7 +35,16 @@ const Submit = () => { if (!projectId) return; const response = await apiCall.post(ApiRoutes.PROJECT_SUBMIT, formData, {id: projectId}) console.log(response) + + const projectUrl = new URL(response.data.projectUrl, 'http://localhost:3001'); + + const courseUrl = new URL(projectUrl.origin + projectUrl.pathname.split('/').slice(0, 3).join('/'), 'http://localhost:3001'); + const courseId = courseUrl.pathname.split('/')[2]; + + const submissionId = response.data.submissionId; + navigate(`/courses/${courseId}/projects/${projectId}/submissions/${submissionId}`); } + return ( <>
@@ -95,4 +104,4 @@ const Submit = () => { ) } -export default Submit +export default Submit \ No newline at end of file diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 0cb1ec7c..5216d8e9 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -1,12 +1,27 @@ import {InboxOutlined} from "@ant-design/icons" import {Form, FormInstance, Upload} from "antd" -import {FC} from "react" +import {FC, useRef, useState} from "react" import {useTranslation} from "react-i18next" +import {Button} from "antd"; +import {Tree} from 'antd'; +import {CloseOutlined} from '@ant-design/icons'; - -const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => void, onSubmit: (values: any) => void }> = ({form, setFileAdded, onSubmit}) => { +const SubmitForm: FC<{ + form: FormInstance, + setFileAdded: (added: boolean) => void, + onSubmit: (values: any) => void +}> = ({form, setFileAdded, onSubmit}) => { const {t} = useTranslation() + const directoryInputRef = useRef(null); + const [directoryTree, setDirectoryTree] = useState([]); + type TreeNode = { + type: string; + title: string; + key: string; + children: TreeNode[]; + }; + const normFile = (e: any) => { console.log("Upload event:", e) if (Array.isArray(e)) { @@ -15,12 +30,141 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi return e?.fileList } - const onFinish = (values: any) => { - onSubmit(values); - }; + const onFinish = (values: any) => { + onSubmit(values); + }; + + + const removeEmptyParentNodes = (nodes: TreeNode[]) => { + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i].type === 'folder') { + if (!nodes[i].children || nodes[i].children.length === 0) { + nodes.splice(i, 1); + } else { + removeEmptyParentNodes(nodes[i].children); + } + } + } + }; + + const removeNode = (key: string) => { + const removeNodeRecursive = (nodes: TreeNode[]): boolean => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].key === key) { + nodes.splice(i, 1); + return true; + } else if (nodes[i].children) { + const childRemoved = removeNodeRecursive(nodes[i].children); + if (childRemoved) { + removeEmptyParentNodes(nodes); + return true; + } + } + } + return false; + }; + + const newDirectoryTree = [...directoryTree]; + removeNodeRecursive(newDirectoryTree); + setDirectoryTree(newDirectoryTree); + + + const newFileList = form.getFieldValue('files').filter((file: any) => !file.uid.startsWith(key)); + form.setFieldsValue({ + files: newFileList + }); + + if (newDirectoryTree.length === 0) { + setFileAdded(false); + } + }; + const markFolders = (nodes: TreeNode[]) => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].children && nodes[i].children.length > 0) { + nodes[i].type = 'folder'; + markFolders(nodes[i].children); + } + } + }; + const onDirectoryUpload = (event: React.ChangeEvent) => { + const files = event.target.files; + if (files) { + const currentFileList = form.getFieldValue('files') || []; + const newDirectoryTree: TreeNode[] = [...directoryTree]; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + currentFileList.push({ + uid: file.webkitRelativePath, + name: file.name, + status: 'done', + originFileObj: file + }); + + + const pathParts = file.webkitRelativePath.split('/'); + let currentNode = newDirectoryTree; + for (let j = 0; j < pathParts.length; j++) { + let foundNode = currentNode.find(node => node.title === pathParts[j]); + if (!foundNode) { + foundNode = { + title: pathParts[j], + key: pathParts.slice(0, j + 1).join('/'), + children: [], + type: 'file' + }; + currentNode.push(foundNode); + } + currentNode = foundNode.children; + } + } + markFolders(newDirectoryTree); + + form.setFieldsValue({ + files: currentFileList + }); + setDirectoryTree(newDirectoryTree); + setFileAdded(true); + } + } + const renderTreeNodes = (data: TreeNode[]) => + data.map((item) => { + if (item.children) { + return { + title: ( +
+ {item.title} +
+ ), + key: item.key, + children: renderTreeNodes(item.children), + }; + } + + return { + title: ( +
+ {item.title} +
+ ), + key: item.key, + }; + }); return ( - + voi false} - multiple={false} - directory={false} + multiple={true} style={{height: "100%"}} + showUploadList={false} onChange={({file}) => { if (file.status !== 'uploading') { + const currentFileList = form.getFieldValue('files') || []; + currentFileList.push({ + uid: file.uid, + name: file.name, + status: 'done', + originFileObj: file + }); + form.setFieldsValue({ + files: currentFileList + }); + + + const newDirectoryTree: TreeNode[] = [...directoryTree]; + newDirectoryTree.push({ + title: file.name, + key: file.uid, + children: [], + type: "" + }); + setDirectoryTree(newDirectoryTree); + setFileAdded(true); } }} @@ -47,9 +212,31 @@ const SubmitForm: FC<{ form: FormInstance, setFileAdded: (added: boolean) => voi

{t("project.uploadAreaTitle")}

{t("project.uploadAreaSubtitle")}

+ + +
+ +
+
) } -export default SubmitForm +export default SubmitForm \ No newline at end of file diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index d3544be7..8985fb15 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -21,7 +21,7 @@ type ApiCallPathValues = {[param: string]: string | number} * const newCourse = await apiFetch("POST", ApiRoutes.COURSES, { name: "New Course" }); * */ -async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}): Promise> { +async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", route: string, body?: any, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}, config?: AxiosRequestConfig): Promise> { const account = msalInstance.getActiveAccount() if (!account) { @@ -56,18 +56,19 @@ async function apiFetch(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", rou const url = new URL(route, serverHost) -const config: AxiosRequestConfig = { - method: method, - url: url.toString(), - headers: finalHeaders, - data: body instanceof FormData ? body : JSON.stringify(body), -} - return axios(config) + const finalConfig: AxiosRequestConfig = { + method: method, + url: url.toString(), + headers: finalHeaders, + data: body instanceof FormData ? body : JSON.stringify(body), + ...config, // spread the config object to merge it with the existing configuration + } + return axios(finalConfig) } const apiCall = { - get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("GET", route, undefined, pathValues, headers) as Promise>, + get: async (route: T, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}, config?: AxiosRequestConfig) => apiFetch("GET", route, undefined, pathValues, headers, config) as Promise>, post: async (route: T, body: POST_Requests[T] | FormData, pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("POST", route, body, pathValues, headers) as Promise>, put: async (route: T, body: PUT_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("PUT", route, body, pathValues, headers) as Promise>, delete: async (route: T, body: DELETE_Requests[T], pathValues?:ApiCallPathValues, headers?: {[header: string]: string}) => apiFetch("DELETE", route, body, pathValues, headers), From f75bebab8414c5913532a27eb6c8323464e15e43 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 10 May 2024 01:25:22 +0200 Subject: [PATCH 016/130] Fixed ts errors + set tree to DirectoryTree --- frontend/src/@types/types.d.ts | 7 ++++ frontend/src/i18n/en/translation.json | 2 +- frontend/src/i18n/nl/translation.json | 2 +- .../pages/submit/components/SubmitForm.tsx | 37 ++++++++++++------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/frontend/src/@types/types.d.ts b/frontend/src/@types/types.d.ts index daec4a4b..719ad8de 100644 --- a/frontend/src/@types/types.d.ts +++ b/frontend/src/@types/types.d.ts @@ -1,4 +1,11 @@ +declare module "react" { + interface InputHTMLAttributes extends HTMLAttributes { + webkitdirectory?: string; + directory?:string + mozdirectory?: string + } +} diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 6349a566..598ba661 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -93,7 +93,7 @@ "groupEmpty": "No members in this group", "testFailed": "Tests failed", "structureFailed": "Structure failed", - "uploadDirectory": "Upload Directory", + "uploadDirectory": "Upload directory", "submission": "Submission", "passed": "Passed", "notSubmitted": "Not submitted", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 64f5d73c..84ea7743 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -90,7 +90,7 @@ "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", "deadlinePassed": "Deadline is verstreken", - "uploadDirectory": "folder uploaden", + "uploadDirectory": "Folder uploaden", "downloadSubmissions": "Download alle indieningen", "group": "Groep", "status": "Status", diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 5216d8e9..0cdd2993 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -5,6 +5,15 @@ import {useTranslation} from "react-i18next" import {Button} from "antd"; import {Tree} from 'antd'; import {CloseOutlined} from '@ant-design/icons'; +import { DataNode } from "antd/es/tree"; + +type TreeNode = { + type: string; + title: string; + key: string; + children: TreeNode[]; +}; + const SubmitForm: FC<{ form: FormInstance, @@ -14,13 +23,7 @@ const SubmitForm: FC<{ const {t} = useTranslation() const directoryInputRef = useRef(null); - const [directoryTree, setDirectoryTree] = useState([]); - type TreeNode = { - type: string; - title: string; - key: string; - children: TreeNode[]; - }; + const [directoryTree, setDirectoryTree] = useState([]); const normFile = (e: any) => { console.log("Upload event:", e) @@ -127,37 +130,41 @@ const SubmitForm: FC<{ setFileAdded(true); } } - const renderTreeNodes = (data: TreeNode[]) => + const renderTreeNodes = (data: TreeNode[]): DataNode[] => data.map((item) => { - if (item.children) { + if (item.children?.length) { return { title: ( -
+ {item.title}
+ ), key: item.key, children: renderTreeNodes(item.children), + isLeaf:false }; } return { + isLeaf:true, title: ( -
+ <> {item.title}
+ ), key: item.key, }; @@ -215,6 +222,7 @@ const SubmitForm: FC<{
-
From 1fb44a838d922fd35e2e136509f339d01f5b6427 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 13:17:09 +0200 Subject: [PATCH 017/130] Updated apiFetch to useApi hook --- frontend/.gitignore | 1 + .../forms/projectFormTabs/GroupsFormTab.tsx | 10 ++-- .../components/other/GroupMembersTransfer.tsx | 11 ++-- .../components/gradesTab/GradesCard.tsx | 31 ++++++----- .../course/components/groupTab/GroupList.tsx | 32 ++++++----- .../course/components/groupTab/GroupsCard.tsx | 8 +-- .../components/membersTab/MemberCard.tsx | 12 +++-- .../components/membersTab/MembersList.tsx | 18 +++---- .../components/settingsTab/SettingsCard.tsx | 30 +++++------ frontend/src/pages/index/Home.tsx | 15 ++++-- .../index/components/CreateCourseModal.tsx | 27 ++++------ .../pages/index/components/ProjectCard.tsx | 15 +++--- frontend/src/pages/project/Project.tsx | 7 +-- .../src/pages/project/components/GroupTab.tsx | 10 ++-- .../src/pages/project/components/ScoreTab.tsx | 48 +++++++++-------- .../project/components/SubmissionTab.tsx | 19 ++++--- .../project/components/SubmissionsTab.tsx | 20 ++++--- .../project/components/SubmissionsTable.tsx | 9 ++-- .../components/GroupClusterDropdown.tsx | 8 +-- .../components/GroupClusterModalContent.tsx | 14 ++--- frontend/src/pages/submission/Submission.tsx | 18 ++++--- .../submission/components/SubmissionCard.tsx | 53 +++++++++---------- frontend/src/providers/UserProvider.tsx | 20 ++++--- frontend/src/router/CourseRoutes.tsx | 1 - frontend/src/router/ProjectRoutes.tsx | 1 - 25 files changed, 236 insertions(+), 202 deletions(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index 98497d56..a851d9f2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,6 +5,7 @@ /.pnp .pnp.js package-lock.json +/package-lock.json # testing /coverage diff --git a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx index f138df66..2303f54e 100644 --- a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx @@ -4,18 +4,17 @@ import { useParams } from "react-router-dom" import { useTranslation } from "react-i18next" import { FC, useEffect, useState } from "react" import { FormInstance } from "antd/lib" -import apiCall from "../../../util/apiFetch" import { ApiRoutes } from "../../../@types/requests.d" import { ClusterType } from "../../../pages/course/components/groupTab/GroupsCard" import { Spin } from "antd" -import GroupList from "../../../pages/course/components/groupTab/GroupList" import GroupMembersTransfer from "../../other/GroupMembersTransfer" +import useApi from "../../../hooks/useApi" const GroupsFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { courseId } = useParams<{ courseId: string }>() const { t } = useTranslation() const [selectedCluster, setSelectedCluster] = useState(null) - + const API = useApi() const selectedClusterId = Form.useWatch("groupClusterId", form) useEffect(() => { @@ -26,8 +25,9 @@ const GroupsFormTab: FC<{ form: FormInstance }> = ({ form }) => { }, [selectedClusterId]) const fetchCluster = async () => { - const response = await apiCall.get(ApiRoutes.CLUSTER, { id: selectedClusterId }) - setSelectedCluster(response.data) + const response = await API.GET(ApiRoutes.CLUSTER, { pathValues: {id: selectedClusterId} }) + if (!response.success) return + setSelectedCluster(response.response.data) } return ( diff --git a/frontend/src/components/other/GroupMembersTransfer.tsx b/frontend/src/components/other/GroupMembersTransfer.tsx index b6fac421..1c4b334d 100644 --- a/frontend/src/components/other/GroupMembersTransfer.tsx +++ b/frontend/src/components/other/GroupMembersTransfer.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useMemo, useState } from "react" import { GroupType } from "../../pages/project/components/GroupTab" -import { Alert, Button, Select, Space, Switch, Table, Transfer } from "antd" +import { Alert, Button, Select, Table, Transfer } from "antd" import type { GetProp, SelectProps, TableColumnsType, TableProps, TransferProps } from "antd" -import apiCall from "../../util/apiFetch" import { ApiRoutes } from "../../@types/requests.d" import { CourseMemberType } from "../../pages/course/components/membersTab/MemberCard" import { useTranslation } from "react-i18next" +import useApi from "../../hooks/useApi" type TransferItem = GetProp[number] type TableRowSelection = TableProps["rowSelection"] @@ -62,6 +62,7 @@ const GroupMembersTransfer: FC<{ groups: GroupType[]; onChanged: () => void; cou const [courseMembers, setCourseMembers] = useState(null) const [selectedGroup, setSelectedGroup] = useState(null) const { t } = useTranslation() + const API = useApi() useEffect(()=> { @@ -75,8 +76,10 @@ const GroupMembersTransfer: FC<{ groups: GroupType[]; onChanged: () => void; cou }, [courseId]) const fetchCourseMembers = async () => { - const response = await apiCall.get(ApiRoutes.COURSE_MEMBERS, { courseId }) - setCourseMembers(response.data.filter(m => m.relation === "enrolled")) + const response = await API.GET(ApiRoutes.COURSE_MEMBERS, { pathValues: { courseId } },"message") + if(!response.success) return + + setCourseMembers(response.response.data.filter(m => m.relation === "enrolled")) } const onChange: TableTransferProps["onChange"] = (nextTargetKeys) => { diff --git a/frontend/src/pages/course/components/gradesTab/GradesCard.tsx b/frontend/src/pages/course/components/gradesTab/GradesCard.tsx index 373bba26..e311f29c 100644 --- a/frontend/src/pages/course/components/gradesTab/GradesCard.tsx +++ b/frontend/src/pages/course/components/gradesTab/GradesCard.tsx @@ -3,28 +3,35 @@ import { useEffect, useState } from "react" import { ApiRoutes, GET_Responses } from "../../../../@types/requests.d" import GradesList from "./GradesList" import useCourse from "../../../../hooks/useCourse" -import apiCall from "../../../../util/apiFetch" +import useApi from "../../../../hooks/useApi" export type CourseGradesType = GET_Responses[ApiRoutes.COURSE_GRADES][number] const GradesCard = () => { const [feedback, setFeedback] = useState(null) const course = useCourse() - + const API = useApi() useEffect(() => { - // TODO: do this fetch, (atm there's no way to get all the grades in a single request, maybe add new api route that gives all the grades of a course) - - apiCall.get(ApiRoutes.COURSE_GRADES, { id: course.courseId }).then((res) => { - console.log(res.data); - setFeedback(res.data) + let ignore = false + + API.GET(ApiRoutes.COURSE_GRADES, { pathValues: { id: course.courseId } }, "message").then((res) => { + if (!ignore && res.success) setFeedback(res.response.data) }) - - }, []) + + return () => { + ignore = true + } + }, [API]) if (feedback === null) return - return - - + return ( + + + + ) } export default GradesCard diff --git a/frontend/src/pages/course/components/groupTab/GroupList.tsx b/frontend/src/pages/course/components/groupTab/GroupList.tsx index 1fe2d15f..ca97981b 100644 --- a/frontend/src/pages/course/components/groupTab/GroupList.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupList.tsx @@ -5,9 +5,9 @@ import useUser from "../../../../hooks/useUser" import { useTranslation } from "react-i18next" import GroupInfoModal from "./GroupInfoModal" import useAppApi from "../../../../hooks/useAppApi" -import apiCall from "../../../../util/apiFetch" import { ProjectType } from "../../../project/Project" import { useParams } from "react-router-dom" +import useApi from "../../../../hooks/useApi" export type GroupType = GET_Responses[ApiRoutes.GROUP] @@ -69,6 +69,7 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType const { message } = useAppApi() const { user } = useUser() const { courseId } = useParams<{ courseId: string }>() + const API = useApi() useEffect(() => { if (typeof project === "number") return setGroupId(project) @@ -77,17 +78,19 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType let ignore = false - const fetchOwnGroup = async () => { - if (!user) return - try { - const res = await apiCall.get(ApiRoutes.PROJECT, { id: courseId }) - if (!ignore) setGroupId(res.data.groupId ?? null) + // const fetchOwnGroup = async () => { + // if (!user) return + // try { + // const response = await API.GET(ApiRoutes.PROJECT, { pathValues: { id: typeof project === "number"? project.toString() : project } }, "message") + // if(!response.success) return - } catch (err) { - console.error(err) - } - } - fetchOwnGroup() + // if (!ignore) setGroupId(response.response.data.groupId ?? null) + + // } catch (err) { + // console.error(err) + // } + // } + // fetchOwnGroup() return () => { ignore = true } @@ -101,7 +104,9 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType const removeUserFromGroup = async (userId: number, groupId: number) => { try { setLoading(true) - await apiCall.delete(ApiRoutes.GROUP_MEMBER, undefined, { id: groupId, userId: userId }) + const response = await API.DELETE(ApiRoutes.GROUP_MEMBER, { pathValues: { id: groupId, userId: userId } }, "message") + if (!response.success) return + if(onChanged) await onChanged() setGroupId(null) @@ -124,7 +129,8 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType if (!user) return try { setLoading(true) - await apiCall.post(ApiRoutes.GROUP_MEMBERS, { id: user.id }, { id: group.groupId }) + const response = await API.POST(ApiRoutes.GROUP_MEMBERS, { body:{id: user.id},pathValues: { id: group.groupId } }, "message") + if(!response.success) return if(onChanged) await onChanged() message.success(t("course.joinedGroup")) diff --git a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx index 06ba189b..e7de7e25 100644 --- a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx @@ -3,14 +3,15 @@ import { FC, useEffect, useState } from "react" import { ApiRoutes, GET_Responses } from "../../../../@types/requests.d" import GroupList from "./GroupList" import { CardProps } from "antd/lib" -import apiCall from "../../../../util/apiFetch" import { useTranslation } from "react-i18next" +import useApi from "../../../../hooks/useApi" export type ClusterType = GET_Responses[ApiRoutes.COURSE_CLUSTERS][number] const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ courseId, cardProps }) => { const [groups, setGroups] = useState(null) const { t } = useTranslation() + const API = useApi() useEffect(() => { // TODO: do the fetch (get all clusters from the course ) fetchGroups().catch(console.error) @@ -19,8 +20,9 @@ const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ co const fetchGroups = async () => { if (!courseId) return // if course is null that means it hasn't been fetched yet by the parent component - const res = await apiCall.get(ApiRoutes.COURSE_CLUSTERS, { id: courseId }) - setGroups(res.data) + const res = await API.GET(ApiRoutes.COURSE_CLUSTERS, { pathValues: { id: courseId } }) + if(!res.success) return + setGroups(res.response.data) } // if(!groups) return
diff --git a/frontend/src/pages/course/components/membersTab/MemberCard.tsx b/frontend/src/pages/course/components/membersTab/MemberCard.tsx index 895eb791..d3467dba 100644 --- a/frontend/src/pages/course/components/membersTab/MemberCard.tsx +++ b/frontend/src/pages/course/components/membersTab/MemberCard.tsx @@ -2,9 +2,9 @@ import { Card, Input } from "antd" import { useTranslation } from "react-i18next" import MembersList from "./MembersList" import { useEffect, useMemo, useState } from "react" -import apiCall from "../../../../util/apiFetch" import { ApiRoutes, GET_Responses } from "../../../../@types/requests.d" import useCourse from "../../../../hooks/useCourse" +import useApi from "../../../../hooks/useApi" export type CourseMemberType = GET_Responses[ApiRoutes.COURSE_MEMBERS][number] @@ -13,15 +13,17 @@ const MembersCard = () => { const course = useCourse() const [members, setMembers] = useState(null) const [search, setSearch] = useState("") - + const API = useApi() + useEffect(() => { if (!course) return console.error("No courseId found") let ignore = false - apiCall.get(course.memberUrl).then((res) => { - console.log(res.data) - setMembers(res.data) + + API.GET(ApiRoutes.COURSE_MEMBERS, { pathValues: { id: course.courseId } }, "message").then((res) => { + if (!ignore && res.success) setMembers(res.response.data) }) + return () => { ignore = true } diff --git a/frontend/src/pages/course/components/membersTab/MembersList.tsx b/frontend/src/pages/course/components/membersTab/MembersList.tsx index cb6ef456..ab70c80e 100644 --- a/frontend/src/pages/course/components/membersTab/MembersList.tsx +++ b/frontend/src/pages/course/components/membersTab/MembersList.tsx @@ -8,15 +8,15 @@ import { MenuProps } from "antd/lib" import { ApiRoutes, CourseRelation } from "../../../../@types/requests.d" import useUser from "../../../../hooks/useUser" import { CourseContext } from "../../../../router/CourseRoutes" -import apiCall from "../../../../util/apiFetch" import { useParams } from "react-router-dom" +import useApi from "../../../../hooks/useApi" const MembersList: FC<{ members: CourseMemberType[] | null; onChange: (members: CourseMemberType[]) => void }> = ({ members, onChange }) => { const { t } = useTranslation() const isCourseAdmin = useIsCourseAdmin() const relation = useContext(CourseContext).member.relation const { courseId } = useParams() - + const API = useApi() const { user } = useUser() const items: MenuProps["items"] = [ @@ -44,20 +44,18 @@ const MembersList: FC<{ members: CourseMemberType[] | null; onChange: (members: const removeUserFromCourse = async (userId: number) => { if (!courseId) return - //TODO: test this - const req = await apiCall.delete(ApiRoutes.COURSE_MEMBER, undefined, { userId, courseId }) - console.log(req.data) + const req = await API.DELETE(ApiRoutes.COURSE_MEMBER, { pathValues: { userId, courseId } }, "message") + if(!req.success) return + const newMembers = members?.filter((m) => m.user.userId !== userId) onChange(newMembers ?? []) } const onRoleChange = async (userId: number, role: CourseRelation) => { - // TODO: test this if(!courseId) return - const response = await apiCall.patch(ApiRoutes.COURSE_MEMBER, {relation: role}, { userId, courseId }) - console.log(response.data); - onChange(response.data) - + const response = await API.PATCH(ApiRoutes.COURSE_MEMBER, { body: { relation: role },pathValues: { userId, courseId } }, "message") + if(!response.success) return + onChange(response.response.data) } return ( diff --git a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx index 7eaeb72b..8337cfb0 100644 --- a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx +++ b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx @@ -5,12 +5,12 @@ import { useTranslation } from "react-i18next" import useCourse from "../../../../hooks/useCourse" import useAppApi from "../../../../hooks/useAppApi" import { DeleteOutlined, SaveOutlined } from "@ant-design/icons" -import apiCall from "../../../../util/apiFetch" import { ApiRoutes } from "../../../../@types/requests.d" import useUser from "../../../../hooks/useUser" import { useNavigate } from "react-router-dom" import { AppRoutes } from "../../../../@types/routes" import { CourseContext } from "../../../../router/CourseRoutes" +import useApi from "../../../../hooks/useApi" const SettingsCard: FC = () => { const course = useCourse() @@ -21,6 +21,7 @@ const SettingsCard: FC = () => { const { updateCourses } = useUser() const navigate = useNavigate() const {setCourse} = useContext(CourseContext) + const API = useApi() useEffect(() => { form.setFieldsValue(course) @@ -35,31 +36,24 @@ const SettingsCard: FC = () => { console.log(values); values.description ??= "" setLoading(true) - try { - const res = await apiCall.patch(ApiRoutes.COURSE, values, { courseId: course.courseId }) - message.success(t("course.changesSaved")) - setCourse(res.data) - await updateCourses() - } catch(err){ - console.error(err) - } finally { - setLoading(false) - } + const res = await API.PATCH(ApiRoutes.COURSE, { body: values, pathValues: { courseId: course.courseId } }, "message") + if(!res.success) return setLoading(false) + message.success(t("course.changesSaved")) + setCourse(res.response.data) + await updateCourses() + } const deleteCourse = async () => { setLoading(true) - try { - await apiCall.delete(ApiRoutes.COURSE, undefined, { courseId: course.courseId }) + const res = await API.DELETE(ApiRoutes.COURSE, { pathValues: { courseId: course.courseId } }, "message") + if(!res.success) return setLoading(false) + message.success(t("course.courseDeleted")) await updateCourses() setLoading(false) navigate(AppRoutes.HOME) - } catch (err) { - console.log(err) - //TODO: handle error - setLoading(false) - } + } return ( diff --git a/frontend/src/pages/index/Home.tsx b/frontend/src/pages/index/Home.tsx index 551821d7..d487e007 100644 --- a/frontend/src/pages/index/Home.tsx +++ b/frontend/src/pages/index/Home.tsx @@ -2,7 +2,6 @@ import { Card, Segmented, Typography } from "antd" import { useTranslation } from "react-i18next" import CreateCourseModal from "./components/CreateCourseModal" import { useEffect, useState } from "react" -import apiCall from "../../util/apiFetch" import { ApiRoutes, GET_Responses } from "../../@types/requests.d" import ProjectTable from "./components/ProjectTable" import ProjectTimeline from "../../components/other/ProjectTimeline" @@ -10,6 +9,7 @@ import { useLocalStorage } from "usehooks-ts" import { CalendarOutlined, NodeIndexOutlined, OrderedListOutlined, UnorderedListOutlined } from "@ant-design/icons" import ProjectCalander from "../../components/other/ProjectCalander" import CourseSection from "./components/CourseSection" +import useApi from "../../hooks/useApi" export type ProjectsType = GET_Responses[ApiRoutes.COURSE_PROJECTS] @@ -20,13 +20,20 @@ const Home = () => { const [projects, setProjects] = useLocalStorage("__projects_cache",null) const [open, setOpen] = useState(false) const [projectsViewMode, setProjectsViewMode] = useLocalStorage("projects_view", "table") + const API = useApi() useEffect(() => { - apiCall.get(ApiRoutes.PROJECTS).then((res) => { - const projects: ProjectsType = [...res.data.adminProjects, ...res.data.enrolledProjects.map((p) => ({ ...p.project, status: p.status }))] - console.log("=>", projects) + let ignore= false + + API.GET(ApiRoutes.PROJECTS, {}).then((res) => { + if(!res.success || ignore) return + const projects: ProjectsType = [...res.response.data.adminProjects, ...res.response.data.enrolledProjects.map((p) => ({ ...p.project, status: p.status }))] setProjects(projects) }) + + return () => { + ignore = true + } }, []) return ( diff --git a/frontend/src/pages/index/components/CreateCourseModal.tsx b/frontend/src/pages/index/components/CreateCourseModal.tsx index 9e4ae652..278edbc4 100644 --- a/frontend/src/pages/index/components/CreateCourseModal.tsx +++ b/frontend/src/pages/index/components/CreateCourseModal.tsx @@ -2,13 +2,12 @@ import { Alert, Form, Modal } from "antd" import { FC, useEffect, useState } from "react" import { useTranslation } from "react-i18next" import CourseForm from "../../../components/forms/CourseForm" -import apiCall from "../../../util/apiFetch" import { ApiRoutes } from "../../../@types/requests.d" import useAppApi from "../../../hooks/useAppApi" -import axios, { AxiosError } from "axios" import { useNavigate } from "react-router-dom" import { AppRoutes } from "../../../@types/routes" import useUser from "../../../hooks/useUser" +import useApi from "../../../hooks/useApi" const CreateCourseModal: FC<{ open: boolean,setOpen:(b:boolean)=>void }> = ({ open,setOpen }) => { const { t } = useTranslation() @@ -18,7 +17,7 @@ const CreateCourseModal: FC<{ open: boolean,setOpen:(b:boolean)=>void }> = ({ op const {message} = useAppApi() const navigate = useNavigate() const {updateCourses} = useUser() - + const API = useApi() useEffect(()=> { form.setFieldValue("year", new Date().getFullYear()-1) @@ -32,21 +31,13 @@ const CreateCourseModal: FC<{ open: boolean,setOpen:(b:boolean)=>void }> = ({ op console.log(values); values.description ??= "" setLoading(true) - try { - const course = await apiCall.post(ApiRoutes.COURSES, values) - message.success(t("home.courseCreated")) - await updateCourses() - navigate(AppRoutes.COURSE.replace(":courseId", course.data.courseId.toString())) - } catch(err){ - console.error(err); - if(axios.isAxiosError(err)){ - setError(err.response?.data.message || t("woops")) - } else { - message.error(t("woops")) - } - } finally { - setLoading(false) - } + const res = await API.POST(ApiRoutes.COURSES, { body:values}, "message") + if(!res.success) return setLoading(false) + const course= res.response + message.success(t("home.courseCreated")) + await updateCourses() + navigate(AppRoutes.COURSE.replace(":courseId", course.data.courseId.toString())) + } return ( diff --git a/frontend/src/pages/index/components/ProjectCard.tsx b/frontend/src/pages/index/components/ProjectCard.tsx index 980b0e91..70752eec 100644 --- a/frontend/src/pages/index/components/ProjectCard.tsx +++ b/frontend/src/pages/index/components/ProjectCard.tsx @@ -1,32 +1,33 @@ import { FC, useEffect, useState } from "react" import ProjectTable, { ProjectType } from "./ProjectTable" import { Button, Card } from "antd" -import apiCall from "../../../util/apiFetch" import { ApiRoutes } from "../../../@types/requests.d" -import useIsTeacher from "../../../hooks/useIsTeacher" import { useTranslation } from "react-i18next" import { AppRoutes } from "../../../@types/routes" -import { Link, useNavigate } from "react-router-dom" +import { useNavigate } from "react-router-dom" import CourseAdminView from "../../../hooks/CourseAdminView" import { PlusOutlined } from "@ant-design/icons" +import useApi from "../../../hooks/useApi" const ProjectCard: FC<{ courseId?: number }> = ({ courseId }) => { const [projects, setProjects] = useState(null) const { t } = useTranslation() const navigate = useNavigate() + const API = useApi() useEffect(() => { if (courseId) { - apiCall.get(ApiRoutes.COURSE_PROJECTS, { id: courseId }).then((res) => { - setProjects(res.data) + API.GET(ApiRoutes.COURSE_PROJECTS, { pathValues: { id: courseId } }).then((res) => { + if (!res.success) return + setProjects(res.response.data) }) } - }, [courseId]) + }, [courseId, API]) return ( <> -
+
: ( <> {menu} diff --git a/frontend/src/pages/projectCreate/components/GroupClusterModalContent.tsx b/frontend/src/pages/projectCreate/components/GroupClusterModalContent.tsx index 3fc8eb9b..77672f3f 100644 --- a/frontend/src/pages/projectCreate/components/GroupClusterModalContent.tsx +++ b/frontend/src/pages/projectCreate/components/GroupClusterModalContent.tsx @@ -2,17 +2,27 @@ import { Button, Form, Space } from "antd" import ClusterForm from "../../../components/forms/ClusterForm" import { useTranslation } from "react-i18next" import { ApiRoutes, POST_Requests } from "../../../@types/requests.d" -import { FC } from "react" +import { FC, useState } from "react" import { ClusterType } from "../../course/components/groupTab/GroupsCard" import useApi from "../../../hooks/useApi" -const GroupClusterModalContent: FC<{ onClose: () => void; onClusterCreated: (cluster: ClusterType) => void,courseId:number|string }> = ({courseId,onClose,onClusterCreated}) => { +const GroupClusterModalContent: FC<{ onClose: () => void; onClusterCreated: (cluster: ClusterType) => void; courseId: number | string }> = ({ courseId, onClose, onClusterCreated }) => { const { t } = useTranslation() + const [loading, setLoading] = useState(false) const API = useApi() - + const createCluster = async (values: POST_Requests[ApiRoutes.COURSE_CLUSTERS]) => { if (!courseId) return console.error("courseId is undefined") - const response = await API.POST(ApiRoutes.COURSE_CLUSTERS, {body:values, pathValues:{courseId:courseId.toString()}}) + setLoading(true) + const response = await API.POST( + ApiRoutes.COURSE_CLUSTERS, + { body: values, pathValues: { id: courseId.toString() } }, + { + mode: "message", + successMessage: t("project.change.groupClusterCreated"), + } + ) + setLoading(false) if (!response.success) return console.log(response.response.data) @@ -31,6 +41,7 @@ const GroupClusterModalContent: FC<{ onClose: () => void; onClusterCreated: (clu - )} - - {searched ? ( From 9e1b0af76fe4a59fd63488560c85d72cd8c6ce84 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 13 May 2024 18:03:51 +0200 Subject: [PATCH 025/130] Should fetch the backend on /api urls --- backend/web-bff/App/app.js | 18 +- backend/web-bff/App/auth/AuthProvider.js | 1 + backend/web-bff/App/fetch.js | 6 +- backend/web-bff/App/routes/api.js | 7 +- backend/web-bff/App/routes/users.js | 11 - backend/web-bff/temp-frontend/.eslintrc.cjs | 18 + backend/web-bff/temp-frontend/.gitignore | 24 + backend/web-bff/temp-frontend/README.md | 30 + backend/web-bff/temp-frontend/index.html | 13 + .../web-bff/temp-frontend/package-lock.json | 4215 +++++++++++++++++ backend/web-bff/temp-frontend/package.json | 29 + backend/web-bff/temp-frontend/public/vite.svg | 1 + backend/web-bff/temp-frontend/src/App.css | 42 + backend/web-bff/temp-frontend/src/App.tsx | 40 + .../temp-frontend/src/assets/react.svg | 1 + backend/web-bff/temp-frontend/src/index.css | 68 + backend/web-bff/temp-frontend/src/main.tsx | 10 + .../web-bff/temp-frontend/src/vite-env.d.ts | 1 + backend/web-bff/temp-frontend/tsconfig.json | 25 + .../web-bff/temp-frontend/tsconfig.node.json | 11 + backend/web-bff/temp-frontend/vite.config.ts | 7 + 21 files changed, 4554 insertions(+), 24 deletions(-) create mode 100644 backend/web-bff/temp-frontend/.eslintrc.cjs create mode 100644 backend/web-bff/temp-frontend/.gitignore create mode 100644 backend/web-bff/temp-frontend/README.md create mode 100644 backend/web-bff/temp-frontend/index.html create mode 100644 backend/web-bff/temp-frontend/package-lock.json create mode 100644 backend/web-bff/temp-frontend/package.json create mode 100644 backend/web-bff/temp-frontend/public/vite.svg create mode 100644 backend/web-bff/temp-frontend/src/App.css create mode 100644 backend/web-bff/temp-frontend/src/App.tsx create mode 100644 backend/web-bff/temp-frontend/src/assets/react.svg create mode 100644 backend/web-bff/temp-frontend/src/index.css create mode 100644 backend/web-bff/temp-frontend/src/main.tsx create mode 100644 backend/web-bff/temp-frontend/src/vite-env.d.ts create mode 100644 backend/web-bff/temp-frontend/tsconfig.json create mode 100644 backend/web-bff/temp-frontend/tsconfig.node.json create mode 100644 backend/web-bff/temp-frontend/vite.config.ts diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index 91fe0c86..11e1109b 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -12,6 +12,7 @@ const rateLimit = require('express-rate-limit') const indexRouter = require('./routes/index'); const usersRouter = require('./routes/users'); const authRouter = require('./routes/auth'); +const apiRouter = require('./routes/api'); /* initialize express */ const app = express(); @@ -21,9 +22,9 @@ const app = express(); * Using cookie-session middleware for persistent user session. */ -connection_string = `mongodb://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}?authSource=admin` +//connection_string = `mongodb://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}?authSource=admin` -console.log(connection_string) +//console.log(connection_string) app.use(session({ name: 'pigeon session', @@ -34,16 +35,15 @@ app.use(session({ cookie: { httpOnly: true, secure: false, // make sure this is true in production - maxAge: 7*24*60*60*1000, + maxAge: 7 * 24 * 60 * 60 * 1000, }, - store: MongoStore.create( - {mongoUrl: connection_string}) +// store: MongoStore.create( +// {mongoUrl: connection_string}) })); - const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, + windowMs: 15 * 60 * 1000, max: 100, }); @@ -55,12 +55,14 @@ app.set('view engine', 'hbs'); app.use(logger('dev')); app.use(express.json()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({extended: false})); app.use(express.static(path.join(__dirname, 'public'))); + app.use('/', indexRouter); app.use('/users', usersRouter); app.use('/auth', authRouter); +app.use('/api', apiRouter) // catch 404 and forward to error handler app.use(function (req, res, next) { diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index c27c9069..e930690a 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -142,6 +142,7 @@ class AuthProvider { req.session.tokenCache = msalInstance.getTokenCache().serialize(); req.session.idToken = tokenResponse.idToken; + req.session.accessToken = tokenResponse.accessToken; req.session.account = tokenResponse.account; req.session.isAuthenticated = true; diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index 2b1de573..9cb15f2a 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -15,13 +15,13 @@ const {BACKEND_API_ENDPOINT} = require("./authConfig"); * @param method The http method for the call. Choice out of 'GET' */ async function fetch(endpoint, accessToken, method) { - let methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] - if (!(method in methods)) { + let methods = ["GET", "POST", "PATCH", "PUT", "DELETE"] + if (!(methods.includes(method))) { throw new Error('Not a valid HTTP method'); } const url = new URL(endpoint, BACKEND_API_ENDPOINT) const headers = { - Authorization: `Bearer ${accessToken}`, + "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", } diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js index 7f8de46f..908eb1d1 100644 --- a/backend/web-bff/App/routes/api.js +++ b/backend/web-bff/App/routes/api.js @@ -17,10 +17,13 @@ function isAuthenticated(req, res, next) { router.all('/*', isAuthenticated, async function(req, res, next) { + console.log("req token: " + req.session.accessToken); try { - const response = await fetch(req.url , req.session.accessToken, req.method) + const response = await fetch( "api" + req.url , req.session.accessToken, req.method) res.send(response) } catch(error) { next(error); } - }) \ No newline at end of file + }) + +module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index 834d59cd..c93c5a63 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -26,16 +26,5 @@ router.get('/id', } ); -router.get('/profile', - isAuthenticated, // check if user is authenticated - async function (req, res, next) { - try { - const response = await fetch("api/test", req.session.accessToken); - res.render('profile', { profile: response }); - } catch (error) { - next(error); - } - } -); module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/.eslintrc.cjs b/backend/web-bff/temp-frontend/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/backend/web-bff/temp-frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/backend/web-bff/temp-frontend/.gitignore b/backend/web-bff/temp-frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/backend/web-bff/temp-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/backend/web-bff/temp-frontend/README.md b/backend/web-bff/temp-frontend/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/backend/web-bff/temp-frontend/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/backend/web-bff/temp-frontend/index.html b/backend/web-bff/temp-frontend/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/backend/web-bff/temp-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/backend/web-bff/temp-frontend/package-lock.json b/backend/web-bff/temp-frontend/package-lock.json new file mode 100644 index 00000000..d2b35f1e --- /dev/null +++ b/backend/web-bff/temp-frontend/package-lock.json @@ -0,0 +1,4215 @@ +{ + "name": "temp-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "temp-frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.6.8", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/core/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/@babel/core/node_modules/caniuse-lite": { + "version": "1.0.30001617", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz", + "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/@babel/core/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/core/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/electron-to-chromium": { + "version": "1.4.763", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.763.tgz", + "integrity": "sha512-k4J8NrtJ9QrvHLRo8Q18OncqBCB7tIUyqxRcJnlonQ0ioHKYB988GcDFF3ZePmnb8eHEopDs/wPHR/iGAFgoUQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/core/node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/core/node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/@babel/core/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/core/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/update-browserslist-db": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", + "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/@babel/core/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", + "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", + "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__core/node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/babel__core/node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/babel__core/node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@types/babel__core/node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/babel__core/node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/babel__generator": { + "dev": true + }, + "node_modules/@types/babel__template": { + "dev": true + }, + "node_modules/@types/babel__traverse": { + "dev": true + }, + "node_modules/@types/prop-types": { + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", + "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/parser/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/type-utils/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/utils/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/eslint/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/eslint/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint/node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/eslint/node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/eslint/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/eslint/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/eslint/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/eslint/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint/node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/eslint/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/eslint/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/eslint/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/eslint/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/eslint/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/eslint/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/eslint/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/eslint/node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/eslint/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/eslint/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/vite/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vite/node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/backend/web-bff/temp-frontend/package.json b/backend/web-bff/temp-frontend/package.json new file mode 100644 index 00000000..88e54830 --- /dev/null +++ b/backend/web-bff/temp-frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "temp-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.8" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/backend/web-bff/temp-frontend/public/vite.svg b/backend/web-bff/temp-frontend/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/backend/web-bff/temp-frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/src/App.css b/backend/web-bff/temp-frontend/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/backend/web-bff/temp-frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx new file mode 100644 index 00000000..15d20997 --- /dev/null +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -0,0 +1,40 @@ +import {useEffect, useState} from 'react' +import axios from 'axios' + +import './App.css' + +function App() { + const [auth, setAuth] + = useState<{nickname: string, otherKey: number} | null >(null) + + useEffect( () => { + axios.get('/auth/current-session').then(({data}) => { + setAuth(data); + }) + }, [] ) + + if (auth === null) { + return ( + <> +

Loading...

+ + ) + } else if (auth) { + return ( + <> +

Logged in!

+

You are logged in as {auth && auth.nickname ? auth.nickname : null}

+ + ) + } else { + return ( + <> +

Welcome

+ + ) + } + + +} + +export default App diff --git a/backend/web-bff/temp-frontend/src/assets/react.svg b/backend/web-bff/temp-frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/backend/web-bff/temp-frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/src/index.css b/backend/web-bff/temp-frontend/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/backend/web-bff/temp-frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/backend/web-bff/temp-frontend/src/main.tsx b/backend/web-bff/temp-frontend/src/main.tsx new file mode 100644 index 00000000..3d7150da --- /dev/null +++ b/backend/web-bff/temp-frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/backend/web-bff/temp-frontend/src/vite-env.d.ts b/backend/web-bff/temp-frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/backend/web-bff/temp-frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/backend/web-bff/temp-frontend/tsconfig.json b/backend/web-bff/temp-frontend/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/backend/web-bff/temp-frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/backend/web-bff/temp-frontend/tsconfig.node.json b/backend/web-bff/temp-frontend/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/backend/web-bff/temp-frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/backend/web-bff/temp-frontend/vite.config.ts b/backend/web-bff/temp-frontend/vite.config.ts new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/backend/web-bff/temp-frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From a9c177460149fb4e286da7a90b04b97e499f3768 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 18:24:07 +0200 Subject: [PATCH 026/130] Improved the code --- frontend/src/@types/requests.d.ts | 2 +- frontend/src/i18n/en/translation.json | 5 +- frontend/src/i18n/nl/translation.json | 5 +- frontend/src/pages/editRole/EditRole.tsx | 229 ++++++++++++----------- 4 files changed, 127 insertions(+), 114 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 7a0c9891..3c4f7c31 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -258,7 +258,7 @@ export type GET_Responses = { url: string email: string role: UserRole - } + }[] [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 8fcb7b58..c7b5f8ff 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -67,10 +67,13 @@ "surname": "Surname", "search": "Search", "emailError": "Please enter a valid email", + "emailTooShort": "Email must be at least 3 characters long", "nameError": "Name must be at least 3 characters long", "surnameError": "Surname must be at least 3 characters long", - "searchTutorial": "Enter a name, surname, or email and press \"Search\" to find users.", + "searchTutorial": "Enter a name, surname, or email to find users.", + "searchTooShort": "The search must be at least 3 characters long", "noUsersFound": "No users found", + "invalidEmail": "Invalid email", "student": "Student", "teacher": "Teacher", "admin": "Admin", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index ec43fe2d..3eb8fab1 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -70,10 +70,13 @@ "surname": "Achternaam", "search": "Zoeken", "emailError": "Vul een geldig email adres in", + "emailTooShort": "Email moet minstens 3 karakters lang zijn", "nameError": "Naam moet minstens 3 karakters lang zijn", "surnameError": "Achternaam moet minstens 3 karakters lang zijn", - "searchTutorial": "Vul een email adres, naam of achternaam in en druk dan op \"Zoeken\" om gebruikers op te zoeken.", + "searchTooShort": "Zoekopdracht moet minstens 3 karakters lang zijn", + "searchTutorial": "Vul een email adres, naam of achternaam in om gebruikers op te zoeken.", "noUsersFound": "Geen gebruikers gevonden", + "invalidEmail": "Ongeldig email adres", "student": "Student", "teacher": "Professor", "admin": "Admin", diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 0ca6f3fe..43346863 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -1,125 +1,132 @@ -import { useEffect, useState, useRef } from "react"; -import { Row, Col, Form, Input, Button, Spin, Select } from "antd"; +import { useEffect, useState, useRef } from "react" +import { Row, Col, Form, Input, Button, Spin, Select, Typography } from "antd" import UserList from "./components/UserList" -import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d"; -import apiCall from "../../util/apiFetch"; -import { useTranslation } from "react-i18next"; -import { UsersListItem } from "./components/UserList"; +import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d" +import apiCall from "../../util/apiFetch" +import { useTranslation } from "react-i18next" +import { UsersListItem } from "./components/UserList" +import { useDebounceValue } from "usehooks-ts" export type UsersType = GET_Responses[ApiRoutes.USERS] - +type SearchType = "name" | "surname" | "email" const ProfileContent = () => { - const [users, setUsers] = useState(null); - const [searchField, setSearchField] = useState("email"); - const searchFieldRef = useRef("email"); - const [searched, setSearched] = useState(false); - const [form] = Form.useForm(); - const { t } = useTranslation(); + const [users, setUsers] = useState(null) + + const [loading, setLoading] = useState(false) + const [form] = Form.useForm() + const searchValue = Form.useWatch("search", form) + const [debouncedSearchValue] = useDebounceValue(searchValue, 250) + const [searchType, setSearchType] = useState("name") + + const { t } = useTranslation() + + useEffect(() => { + onSearch() + }, [debouncedSearchValue]) - function updateRole(user: UsersListItem, role: UserRole) { - //here user is of type User (not UsersListItem), but it seems to work because the needed properties are named the same - console.log(user) - apiCall.patch(ApiRoutes.USER, {role: role}, {id: user.id}).then((res) => { - console.log(res.data); - //replace this user in the userlist with the updated one from res.data - const updatedUsers = users?.map((u) => { - if (u.id === user.id) { - return { ...u, role: res.data.role }; - } - return u; - }); - setUsers(updatedUsers?updatedUsers:null); - }) - } + function updateRole(user: UsersListItem, role: UserRole) { + //here user is of type User (not UsersListItem), but it seems to work because the needed properties are named the same + console.log(user, role) + apiCall.patch(ApiRoutes.USER, { role: role }, { id: user.id }).then((res) => { + console.log(res.data) + //replace this user in the userlist with the updated one from res.data + const updatedUsers = users?.map((u) => { + if (u.userId === user.id) { + return { ...u, role: res.data.role }; + } + return u; + }); + setUsers(updatedUsers?updatedUsers:null); + }) + } - const onSearch = (values: any) => { - setSearched(true); - setUsers(null); - //search operation here - const params = Object.entries(values) - .filter(([key, value]) => value !== undefined) - .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); - const queryString = Object.entries(params) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - console.log(queryString); - apiCall.get(ApiRoutes.USERS,{params:queryString}).then((res) => { - console.log(res.data) - setUsers(res.data); - }) - - }; + const onSearch = async () => { + const value = form.getFieldValue("search") + if (!value || value.length < 3) return + setLoading(true) + const params = new URLSearchParams() + params.append(searchType, form.getFieldValue("search")) + console.log(ApiRoutes.USERS + "?" + params.toString()) + apiCall.get((ApiRoutes.USERS + "?" + params.toString()) as ApiRoutes.USERS).then((res) => { + //FIXME: It's possible that request doesn't come in the same order as they're sent in. So it's possible that it would show the request of an old query + console.log(res.data) + setUsers(res.data) + setLoading(false) + }) + } -return ( -
+ return ( +
- - - - { - if (searchFieldRef.current === 'email') { - // Validate email - const emailRegex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/; - if (!emailRegex.test(value)) { - return Promise.reject(new Error(t("editRole.invalidEmail"))); - } - } else { - // Validate name and surname - if (value.length < 3) { - if(searchFieldRef.current === 'name') return Promise.reject(new Error(t("editRole.nameError"))); - else return Promise.reject(new Error(t("editRole.surnameError"))); - } - } - return Promise.resolve(); - }, - }, - ]}> - {console.log(value); searchFieldRef.current = value}} style={{ width: 120 }} options={[ - { label: t("editRole.email"), value: "email" }, - { label: t("editRole.name"), value: "name" }, - { label: t("editRole.surname"), value: "surname" }, - ]}/> - } - placeholder={t(`editRole.${searchFieldRef.current}`)} - /> - - - -
- {searched ? ( - users === null ? ( -
- { + if (searchType === "email") { + // Validate email + const emailRegex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/ + if (!emailRegex.test(value)) { + return Promise.reject(new Error(t("editRole.invalidEmail"))) + } + } + // Validate name and surname + + + return Promise.resolve() + }, + }, + { + message: t("editRole.searchTooShort"), + min: 3, + } + ]} + > + setSearchType(value)} + style={{ width: 120 }} + options={[ + { label: t("editRole.email"), value: "email" }, + { label: t("editRole.name"), value: "name" }, + { label: t("editRole.surname"), value: "surname" }, + ]} /> -
+ } + /> + + + {users !== null ? ( + <> + {loading ? ( +
+ +
) : ( - - ) - ) : ( -
-

{t("editRole.searchTutorial")}

-
- )} -
- ); -}; + + )} + + ): {t("editRole.searchTutorial")}} +
+ ) +} export function EditRole() { - return ( - - ) -}; + return +} -export default EditRole; \ No newline at end of file +export default EditRole From 427f7114bb92ffcc6ae499add9e4c95172dcb37c Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 13 May 2024 19:31:21 +0200 Subject: [PATCH 027/130] Working api calls to backend api --- backend/web-bff/App/auth/AuthProvider.js | 5 ++--- backend/web-bff/App/fetch.js | 1 - backend/web-bff/App/routes/api.js | 10 ++++++++-- backend/web-bff/App/routes/auth.js | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index e930690a..c73d5938 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -105,13 +105,13 @@ class AuthProvider { req.session.idToken = tokenResponse.idToken; req.session.account = tokenResponse.account; - res.redirect(options.successRedirect); + next(); } catch (error) { if (error instanceof msal.InteractionRequiredAuthError) { return this.login({ scopes: options.scopes || [], redirectUri: options.redirectUri, - successRedirect: options.successRedirect || '/', + successRedirect: '/', })(req, res, next); } @@ -142,7 +142,6 @@ class AuthProvider { req.session.tokenCache = msalInstance.getTokenCache().serialize(); req.session.idToken = tokenResponse.idToken; - req.session.accessToken = tokenResponse.accessToken; req.session.account = tokenResponse.account; req.session.isAuthenticated = true; diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index 9cb15f2a..4ca0c021 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ - const axios = require('axios'); const https = require('https'); const {BACKEND_API_ENDPOINT} = require("./authConfig"); diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js index 908eb1d1..d843ed64 100644 --- a/backend/web-bff/App/routes/api.js +++ b/backend/web-bff/App/routes/api.js @@ -1,9 +1,11 @@ +const authProvider = require('../auth/AuthProvider'); + const express = require('express'); const router = express.Router(); const fetch = require('../fetch'); -const { BACKEND_API_ENDPOINT } = require('../authConfig'); +const { BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig'); // custom middleware to check auth state function isAuthenticated(req, res, next) { @@ -16,8 +18,12 @@ function isAuthenticated(req, res, next) { router.all('/*', isAuthenticated, + authProvider.acquireToken({ + scopes: [msalConfig.auth.clientId + "/.default"], + redirectUri: REDIRECT_URI + }), async function(req, res, next) { - console.log("req token: " + req.session.accessToken); + try { const response = await fetch( "api" + req.url , req.session.accessToken, req.method) res.send(response) diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index 47843e37..eba42917 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -11,7 +11,7 @@ const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, msalConfig} = require('../authCo const router = express.Router(); router.get('/signin', authProvider.login({ - scopes: [], + scopes: [msalConfig.auth.clientId + "/.default"], redirectUri: REDIRECT_URI, successRedirect: '/' })); From e20a17bc25fe6a1fd327787ca0a264ae71a9d341 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 20:14:31 +0200 Subject: [PATCH 028/130] Small bug fixes --- .../src/components/common/saveDockerForm.tsx | 2 +- frontend/src/i18n/en/translation.json | 1 - .../pages/index/components/ProjectCard.tsx | 2 +- .../project/components/SubmissionTab.tsx | 2 +- .../project/components/SubmissionsTab.tsx | 2 +- .../submission/components/SubmissionCard.tsx | 80 +++++++------------ frontend/src/pages/submit/Submit.tsx | 7 +- 7 files changed, 37 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/common/saveDockerForm.tsx b/frontend/src/components/common/saveDockerForm.tsx index 2a616ea4..b52c81e9 100644 --- a/frontend/src/components/common/saveDockerForm.tsx +++ b/frontend/src/components/common/saveDockerForm.tsx @@ -18,7 +18,7 @@ const saveDockerForm = async (form:FormInstance, initialDockerValues: DockerForm return API.POST(ApiRoutes.PROJECT_TESTS, { body: data, pathValues: {id: projectId}}) } - if(data.dockerImage === null || data.dockerImage.length === 0 ) { + if(data.dockerImage == null || data.dockerImage.length === 0 ) { // We do a delete console.log("DELETE", data); return API.DELETE(ApiRoutes.PROJECT_TESTS, { pathValues: {id: projectId} }) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 377a23db..bcd6c716 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -99,7 +99,6 @@ "structureFailed": "Structure tests failed", "passed": "Passed", "uploadDirectory": "Upload directory", - "submission": "Submission", "notSubmitted": "Not submitted", "submitSuccess": "Submission successful", "submissionTime": "Submission time", diff --git a/frontend/src/pages/index/components/ProjectCard.tsx b/frontend/src/pages/index/components/ProjectCard.tsx index 70752eec..7419cae7 100644 --- a/frontend/src/pages/index/components/ProjectCard.tsx +++ b/frontend/src/pages/index/components/ProjectCard.tsx @@ -49,7 +49,7 @@ const ProjectCard: FC<{ courseId?: number }> = ({ courseId }) => { }} > diff --git a/frontend/src/pages/project/components/SubmissionTab.tsx b/frontend/src/pages/project/components/SubmissionTab.tsx index 6d8c50be..03959cf7 100644 --- a/frontend/src/pages/project/components/SubmissionTab.tsx +++ b/frontend/src/pages/project/components/SubmissionTab.tsx @@ -25,7 +25,7 @@ const SubmissionTab: FC<{ projectId: number; courseId: number }> = ({ projectId, return () => { ignore = true } - }, [projectId,courseId,API]) + }, [projectId,courseId]) diff --git a/frontend/src/pages/project/components/SubmissionsTab.tsx b/frontend/src/pages/project/components/SubmissionsTab.tsx index c63ac650..c2e34f32 100644 --- a/frontend/src/pages/project/components/SubmissionsTab.tsx +++ b/frontend/src/pages/project/components/SubmissionsTab.tsx @@ -24,7 +24,7 @@ const SubmissionsTab = () => { return () => { ignore = true } - }, [API,projectId]) + }, [projectId]) const handleDownloadSubmissions = () => { // TODO: implement this! diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index f218535a..b544a312 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -5,7 +5,6 @@ import {ApiRoutes} from "../../../@types/requests" import {ArrowLeftOutlined} from "@ant-design/icons" import {useNavigate} from "react-router-dom" import "@fontsource/jetbrains-mono" -import {useEffect, useState} from "react" import apiCall from "../../../util/apiFetch" export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] @@ -13,13 +12,7 @@ export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) => { const {token} = theme.useToken() const {t} = useTranslation() - const [structureFeedback, setStructureFeedback] = useState(null) - const [dockerFeedback, setDockerFeedback] = useState(null) const navigate = useNavigate() - useEffect(() => { - if (!submission.dockerAccepted) apiCall.get(submission.dockerFeedbackUrl).then((res) => setDockerFeedback(res.data ? res.data : "")) - if (!submission.structureAccepted) apiCall.get(submission.structureFeedbackUrl).then((res) => setStructureFeedback(res.data ? res.data : "")) - }, [submission.dockerFeedbackUrl, submission.structureFeedbackUrl]) const downloadSubmission = async () => { try { @@ -49,6 +42,7 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) } } + const feedback = "TODO: feedback" return ( = ({submission}) {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} {submission.structureAccepted ? null : ( -
- {structureFeedback === null ? ( - - ) : ( - - )} -
- )} +
+ {submission.structureFeedback === null ? ( + + ) : ( + + )} +
+ )} @@ -124,28 +111,21 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission})
  • {submission.dockerAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.dockerAccepted ? null : ( -
    - {dockerFeedback === null ? ( - - ) : ( - - )} -
    - )} + {submission.dockerAccepted ? null : ( +
    + {submission.dockerFeedback === null ? ( + + ) : ( + + )} +
    + )}
  • diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index b5bab401..caf93d60 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -1,15 +1,14 @@ -import {Affix, Button, Card, Col, Form, Row, Typography} from "antd" +import {Button, Card, Col, Form, Row} from "antd" import {useTranslation} from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" import {useNavigate, useParams} from "react-router-dom" -import React, {useState, useRef} from 'react'; +import {useState} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; import JSZip from 'jszip'; -import { Popconfirm, message } from 'antd'; +import { message } from 'antd'; import {AppRoutes} from "../../@types/routes"; -import submission from "../submission/Submission"; const Submit = () => { const {t} = useTranslation() From 78b69d497d105d4393affa857b50ce71af8e6995 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 20:17:42 +0200 Subject: [PATCH 029/130] Fixed submission page from spamming requests --- frontend/src/pages/submission/Submission.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/submission/Submission.tsx b/frontend/src/pages/submission/Submission.tsx index 037b1cea..abeccdf6 100644 --- a/frontend/src/pages/submission/Submission.tsx +++ b/frontend/src/pages/submission/Submission.tsx @@ -21,7 +21,7 @@ const Submission = () => { return () => { ignore = true } - }, [submissionId,API]) + }, [submissionId]) if (submission === null) { return ( From 99dee69da34a7a121aa11811d8b15232313b3fec Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 20:21:32 +0200 Subject: [PATCH 030/130] Fixed more request spamming --- frontend/src/pages/course/components/gradesTab/GradesCard.tsx | 2 +- frontend/src/pages/index/components/ProjectCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/course/components/gradesTab/GradesCard.tsx b/frontend/src/pages/course/components/gradesTab/GradesCard.tsx index e311f29c..0e7c9e32 100644 --- a/frontend/src/pages/course/components/gradesTab/GradesCard.tsx +++ b/frontend/src/pages/course/components/gradesTab/GradesCard.tsx @@ -21,7 +21,7 @@ const GradesCard = () => { return () => { ignore = true } - }, [API]) + }, []) if (feedback === null) return return ( diff --git a/frontend/src/pages/index/components/ProjectCard.tsx b/frontend/src/pages/index/components/ProjectCard.tsx index 7419cae7..b092a439 100644 --- a/frontend/src/pages/index/components/ProjectCard.tsx +++ b/frontend/src/pages/index/components/ProjectCard.tsx @@ -22,7 +22,7 @@ const ProjectCard: FC<{ courseId?: number }> = ({ courseId }) => { setProjects(res.response.data) }) } - }, [courseId, API]) + }, [courseId]) return ( <> From 1acfa6d293b67d33849638bfea9a4c86f81ee354 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Mon, 13 May 2024 20:24:35 +0200 Subject: [PATCH 031/130] Replaced ant message with the app message to match the theme --- frontend/src/pages/submit/Submit.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index caf93d60..d90ccfda 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -7,13 +7,14 @@ import {useState} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; import JSZip from 'jszip'; -import { message } from 'antd'; import {AppRoutes} from "../../@types/routes"; +import useAppApi from "../../hooks/useAppApi" const Submit = () => { const {t} = useTranslation() const [form] = Form.useForm() const {projectId, courseId} = useParams<{ projectId: string, courseId: string}>() + const {message} = useAppApi() const [fileAdded, setFileAdded] = useState(false); const navigate = useNavigate() From b171c489f86d78a815889da87ec1786e4ae8d9b2 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Mon, 13 May 2024 20:47:04 +0200 Subject: [PATCH 032/130] possibly make test more consistent on gh actions --- .../com/ugent/pidgeon/controllers/SubmissionControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index 21b496ae..dabbcf83 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -365,7 +365,7 @@ public void testSubmitFile() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); - Thread.sleep(1000); + Thread.sleep(2000); assertTrue(submission.getDockerAccepted()); assertEquals("dockerFeedback-test", submission.getDockerFeedback()); From d9ce2563c8d3358f9fde864a47a3797b798f6955 Mon Sep 17 00:00:00 2001 From: Wout Verdyck <159531937+usserwoutV2@users.noreply.github.com> Date: Tue, 14 May 2024 11:00:16 +0200 Subject: [PATCH 033/130] Delete startBackend.sh --- startBackend.sh | 9 --------- 1 file changed, 9 deletions(-) delete mode 100755 startBackend.sh diff --git a/startBackend.sh b/startBackend.sh deleted file mode 100755 index ec1fe7ab..00000000 --- a/startBackend.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -export PGU=tristanIs -export PGP=fuckingHeet -open -a Docker -docker compose up db -d -/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dfile.encoding=UTF-8 -classpath /Users/usserwout/Documents/school/3ba/SEL2/UGent-6/backend/app/build/classes/java/main:/Users/usserwout/Documents/school/3ba/SEL2/UGent-6/backend/app/build/resources/main:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/3.2.2/b89d213d9f49c3e6247b2503ac7d94b0ac8260f6/spring-boot-starter-web-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-oauth2-client/3.2.2/cce33514a28968b4f6203cdcce700192d19b1ef0/spring-boot-starter-oauth2-client-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-security/3.2.2/79676d6fe68878890e26be10ecab126f472d7c0f/spring-boot-starter-security-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-oauth2-resource-server/3.2.2/8d71b3dd52be0068cb505de076493c991085a5f1/spring-boot-starter-oauth2-resource-server-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot-starter-active-directory/3.11.0/6e0605d340d45896a875fdbb0bb0da0e1c70d57a/azure-spring-boot-starter-active-directory-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure/2.1.8.RELEASE/ad308707f69f4a66a1e19fdd91389058a9942eba/spring-security-oauth2-autoconfigure-2.1.8.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-config/6.2.1/119953fd2980d50e1119b913a8596e5e6bdc1295/spring-security-config-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-web/6.2.1/4b486977ab1bcdd5dcc6aa5d8a367f1c0814bf56/spring-security-web-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot-starter/3.11.0/776d5a559e1746e11249bac2993349d0f5b65c6/azure-spring-boot-starter-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.auth0/java-jwt/3.18.2/89c1da37cd738d9c3c7176fbf1e291ff2a8b988/java-jwt-3.18.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.auth0/jwks-rsa/0.18.0/cd3977e3234ddd06e2612812308fa621bf27d7e6/jwks-rsa-0.18.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.servlet/javax.servlet-api/4.0.1/a27082684a2ff0bf397666c3943496c44541d1ca/javax.servlet-api-4.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-data-jpa/3.2.2/65cf3aad09f0218b7dfab849c9b0350d0a9e0d81/spring-boot-starter-data-jpa-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tika/tika-core/1.27/79ad0f72558b8fbce947147959e2faff8b7b70a/tika-core-1.27.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-httpclient5/3.3.5/73a916e8931e0d09c8e02ab5e90f38db80dcd2ff/docker-java-transport-httpclient5-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.client5/httpclient5/5.1.2/54d84f95fe7d182a86e6e3064a77b58478e2b5a/httpclient5-5.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java/3.3.5/3f1cdd5f182c53ffd15d7553b8b3d1c96e74ae1b/docker-java-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-devtools/3.2.2/5d4ce10e0c8d4a6cc040a1836280f379893a8213/spring-boot-devtools-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/3.2.2/328f5ce9e10d5f90520e72a3ff8a2586b9e46b37/spring-boot-starter-json-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.2.2/dc04714f9295297f92fa8099eb51edc54dbe67db/spring-boot-starter-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/3.2.2/e22a0ba37910731b382f3fe47ad36aed20fad24d/spring-boot-starter-tomcat-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/6.1.3/f4738a57787add6567e0679eebb1b499a11019cc/spring-webmvc-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/6.1.3/cc3459b4abd436331608ddb6424886875f7086ab/spring-web-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-client/6.2.1/1e78c49fa10f72acb7f40e34e9ced1216753792c/spring-security-oauth2-client-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-jose/6.2.1/9bb10fe87a2dfff27075dd09586075645907b44d/spring-security-oauth2-jose-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-core/6.2.1/b4014a04f217f0f48d15bc7d53906b6911ad855f/spring-security-core-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.1.3/4d9bd4bd9b8bedf9ef151b45c79766b336117b9a/spring-aop-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-resource-server/6.2.1/3d36a1686bdd96d6cd8f90f34bef7b59b4b7feb2/spring-security-oauth2-resource-server-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-webflux/3.2.2/25ae11864604f8f7b504b9feae710b859aed6464/spring-boot-starter-webflux-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-validation/3.2.2/faedd363ffbafde6a23bc7183ce79de11a96e780/spring-boot-starter-validation-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot/3.11.0/761a2ace1da268ad666ac21abc834c5077a3e00/azure-spring-boot-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.microsoft.azure/msal4j/1.11.0/38df9693f67ea1f01f35ebbe9411f0760c9ac77f/msal4j-1.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/nimbus-jose-jwt/9.24.4/29a1f6a00a4daa3e1873f6bf4f16ddf4d6fd6d37/nimbus-jose-jwt-9.24.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.3/a734bc2c47a9453c4efa772461a3aeb273c010d9/jackson-databind-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty/1.1.15/a6a0ff5228472763c9034353711078057c0d8074/reactor-netty-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security.oauth/spring-security-oauth2/2.3.5.RELEASE/7969f5363398d6d3788bef1740b2ab9509043d51/spring-security-oauth2-2.3.5.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.2.2/5c407409f8d260a4bc6e173d16fc3b36e6adec21/spring-boot-autoconfigure-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.2.2/9f274d1bd822c4c57bb5b37ecae2380b980f567/spring-boot-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.3/79baf4e605eb3bbb60b1c475d44a7aecceea1d60/jackson-annotations-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.xml.bind/jaxb-api/2.3.1/8531ad5ac454cc2deb9d4d32c40c4d7451939b5d/jaxb-api-2.3.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-jwt/1.0.10.RELEASE/19a1ca7a83e9d263a31af5f529da460f8f863451/spring-security-jwt-1.0.10.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.1.3/c63f038933701058fd7578460c66dbe2d424915/spring-context-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.1.3/c2df4210e796d3a27efc1f22621aa4e2c6cd985f/spring-beans-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.1.3/a002e96e780954cc3ac4cd70fd3bb16accdc47ed/spring-core-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.1.3/7c35fc3d7525a024fdde8a5d7597a6a8a4e59d7/spring-expression-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-aop/3.2.2/f01ecef0ce5f8d5631890a0c456a88a72323b569/spring-boot-starter-aop-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-jdbc/3.2.2/22ffda6938dca5f584c8b1b64e4e9096e8302c1e/spring-boot-starter-jdbc-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.data/spring-data-jpa/3.2.2/f91a3896c2a6139ac1da1fd8ff4350ca4b0e409e/spring-data-jpa-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.orm/hibernate-core/6.4.1.Final/3dcefddf6609e6491d37208bcc0cab1273598cbd/hibernate-core-6.4.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aspects/6.1.3/c8b5dde3568dc5df6109916d8ad4866efe4e61fd/spring-aspects-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.11/ad96c3f8cf895e696dd35c2bc8e8ebe710be9e6d/slf4j-api-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/commons-io/commons-io/2.13.0/8bb2bc9b4df17e2411533a0708a69f983bf5e83b/commons-io-2.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport/3.3.5/4aa7e97c14ed1f2ca62029bf1ea8467f6ebf48d9/docker-java-transport-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.java.dev.jna/jna/5.13.0/1200e7ebeedbe0d10062093f32925a912020e747/jna-5.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.core5/httpcore5-h2/5.2.4/2872764df7b4857549e2880dd32a6f9009166289/httpcore5-h2-5.2.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.core5/httpcore5/5.2.4/34d8332b975f9e9a8298efe4c883ec43d45b7059/httpcore5-5.2.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/commons-codec/commons-codec/1.16.0/4e3eb3d79888d76b54e28b350915b5dc3919c9de/commons-codec-1.16.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-jersey/3.3.5/5b74264f3cb7af6efa83fa9ba8f6540c4a63d641/docker-java-transport-jersey-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-netty/3.3.5/a416c8c75d216893a11a8167952267876e5977ec/docker-java-transport-netty-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-core/3.3.5/d30fb6b25c0bde6a0f0f25bcb2cde12e23a44422/docker-java-core-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/jcl-over-slf4j/2.0.11/f6226edb8c85f8c9f1f75ec4b0252c02f589478a/jcl-over-slf4j-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.3/4a20a0e104931bfa72f24ef358c2eb63f1ef2aaf/jackson-datatype-jsr310-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.3/8d251b90c5358677e7d8161e0c2488e6f84f49da/jackson-module-parameter-names-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.3/80158cb020c7bd4e4ba94d8d752a65729dc943b2/jackson-datatype-jdk8-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.2.2/3347c3b1cec6cf2d5fa186d1e49d2f378a6b7cae/spring-boot-starter-logging-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/2.2/3af797a25458550a16bf89acc8e4ab2b7f2bfce0/snakeyaml-2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.18/83a3bc6898f2ceed2357ba231a5e83dc2016d454/tomcat-embed-websocket-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/10.1.18/bff6c34649d1dd7b509e819794d73ba795947dcf/tomcat-embed-core-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/10.1.18/b2c4dc05abd363c63b245523bb071727aa2f1046/tomcat-embed-el-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-observation/1.12.2/e082b05a2527fc24ea6fbe4c4b7ae34653aace81/micrometer-observation-1.12.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-core/6.2.1/1927cdcddce10f1f852e25ed3445e1f1b96c17ad/spring-security-oauth2-core-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/oauth2-oidc-sdk/9.43.3/31709dab9f6531cc5c8f0d7e50ed5ccf10127877/oauth2-oidc-sdk-9.43.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-crypto/6.2.1/d7c4f4e8fe5ae84dd1da76094ee8a0a7e214923d/spring-security-crypto-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webflux/6.1.3/426447b8e64765db5c2901f2b33cdb691b845f34/spring-webflux-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-reactor-netty/3.2.2/cf32e5dc15e455a17aa833eeefb9d0f0eb9f6c4e/spring-boot-starter-reactor-netty-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.validator/hibernate-validator/8.0.1.Final/e49e116b3d3928060599b176b3538bb848718e95/hibernate-validator-8.0.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.validation/validation-api/2.0.1.Final/cb855558e6271b1b32e716d24cb85c7f583ce09e/validation-api-2.0.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.3.2/934c04d3cfef185a8008e7bf34331b79730a9d43/javax.annotation-api-1.3.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.stephenc.jcip/jcip-annotations/1.0-1/ef31541dd28ae2cefdd17c7ebf352d93e9058c63/jcip-annotations-1.0-1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.3/60d600567c1862840397bf9ff5a92398edc5797b/jackson-core-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty-http/1.1.15/c79756fa2dfc28ac81fc9d23a14b17c656c3e560/reactor-netty-http-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty-core/1.1.15/3221d405ad55a573cf29875a8244a4217cf07185/reactor-netty-core-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.codehaus.jackson/jackson-mapper-asl/1.9.13/1ee2f2bed0e5dd29d1cb155a166e6f8d50bbddb7/jackson-mapper-asl-1.9.13.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.activation/javax.activation-api/1.2.0/85262acf3ca9816f9537ca47d5adeabaead7cb16/javax.activation-api-1.2.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk15on/1.60/d0c46320fbc07be3a24eb13a56cee4e3d38e0c75/bcpkix-jdk15on-1.60.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.1.3/a715e091ee86243ee94534a03f3c26b4e48de31e/spring-jcl-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.aspectj/aspectjweaver/1.9.21/beaabaea95c7f3330f415c72ee0ffe79b51d172f/aspectjweaver-1.9.21.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jdbc/6.1.3/be4b30cc956b26f13e04ccadc2b0575038c531bb/spring-jdbc-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP/5.0.1/a74c7f0a37046846e88d54f7cb6ea6d565c65f9c/HikariCP-5.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-orm/6.1.3/98572e26c6d011c9710545085358a4e35e27649/spring-orm-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.data/spring-data-commons/3.2.2/9b0b0f5f5bc793463a81171d6889809abc14b19b/spring-data-commons-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-tx/6.1.3/7750337bf46a2ff248685915c7cc88d3bef2f666/spring-tx-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.antlr/antlr4-runtime/4.13.0/5a02e48521624faaf5ff4d99afc88b01686af655/antlr4-runtime-4.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.persistence/jakarta.persistence-api/3.1.0/66901fa1c373c6aff65c13791cc11da72060a8d6/jakarta.persistence-api-3.1.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.transaction/jakarta.transaction-api/2.0.1/51a520e3fae406abb84e2e1148e6746ce3f80a1a/jakarta.transaction-api-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.15.3/71edfaa76deaaa3e8848d8e35e485f132d095f81/jackson-jaxrs-json-provider-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.connectors/jersey-apache-connector/3.1.5/ec06316a19338bcc8236d6d5ac38b273ffbbd5cd/jersey-apache-connector-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpclient/4.5.14/1194890e6f56ec29177673f2f12d0b8e627dec98/httpclient-4.5.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.core/jersey-client/3.1.5/15695e853b7583703aff98e543b95fa0ca4553/jersey-client-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.inject/jersey-hk2/3.1.5/9ecb5339c3de02e5939c72657e74e2c5fdeb71c8/jersey-hk2-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore/4.4.16/51cf043c87253c9f58b539c9f7e44c8894223850/httpcore-4.4.16.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.kohlschutter.junixsocket/junixsocket-common/2.6.1/34151df0d8c8348ddb9f2f387943b3238b5f1a4f/junixsocket-common-2.6.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.kohlschutter.junixsocket/junixsocket-native-common/2.6.1/70a02ed1d3fab753ac436732d4aab877ada8b50f/junixsocket-native-common-2.6.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.105.Final/19a5b78164c6a5a0464586c1e1ac5695cc79844c/netty-handler-proxy-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.105.Final/bc8bc7b5384fb3dcb467d2a44159282935328779/netty-codec-http-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.105.Final/7e997e63d0a445c4b352bcd38474d50f06f2eaf1/netty-handler-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.105.Final/8d20e17cff9ec1aaa3bb133ae7cb339c991bc105/netty-transport-native-epoll-4.1.105.Final-linux-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.105.Final/91e3e877f65e4a485d5cca980e59329034152b43/netty-transport-native-kqueue-4.1.105.Final-osx-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-api/3.3.5/c9cd924da119835a8da0ca43bfa37b740247c029/docker-java-api-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-compress/1.21/4ec95b60d4e86b5c95a0e919cb172a0af98011ef/commons-compress-1.21.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-lang3/3.13.0/b7263237aa89c1f99b327197c41d0669707a462e/commons-lang3-3.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/19.0/6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9/guava-19.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk18on/1.76/10c9cf5c1b4d64abeda28ee32fbade3b74373622/bcpkix-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.14/d98bc162275134cdf1518774da4a2a17ef6fb94d/logback-classic-1.4.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.21.1/d77b2ba81711ed596cd797cc2b5b5bd7409d841c/log4j-to-slf4j-2.21.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.11/279356f8e873b1a26badd8bbb3284b5c3b22c770/jul-to-slf4j-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-commons/1.12.2/b44127d8ec7b3ef11a01912d1e6474e1167f3929/micrometer-commons-1.12.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/content-type/2.2/9a894bce7646dd4086652d85b88013229f23724b/content-type-2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.5.0/57a64f421b472849c40e77d2e7cce3a141b41e99/json-smart-2.5.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/lang-tag/1.7/97c73ecd70bc7e8eefb26c5eea84f251a63f1031/lang-tag-1.7.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor/reactor-core/3.6.2/679ac38d031c154374182748491a177a76c890e1/reactor-core-3.6.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.validation/jakarta.validation-api/3.0.2/92b6631659ba35ca09e44874d3eb936edfeee532/jakarta.validation-api-3.0.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.jboss.logging/jboss-logging/3.5.3.Final/c88fc1d8a96d4c3491f55d4317458ccad53ca663/jboss-logging-3.5.3.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml/classmate/1.6.0/91affab6f84a2182fce5dd72a8d01bc14346dddd/classmate-1.6.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.105.Final/7c558b1ea68a2385b250191ad5ecb0f4afb2d866/netty-codec-http2-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.105.Final/1c3b820b9f44c26a65dec438a731f8c9bf64004/netty-resolver-dns-native-macos-4.1.105.Final-osx-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.105.Final/719fa5bccd87bd3286fc0655a6c3b61cb539e334/netty-resolver-dns-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.codehaus.jackson/jackson-core-asl/1.9.13/3c304d70f42f832e0a86d45bd437f692129299a4/jackson-core-asl-1.9.13.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk15on/1.60/bd47ad3bd14b8e82595c7adaa143501e60842a84/bcprov-jdk15on-1.60.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.15.3/74e8ef60b65b42051258465f06c06195e61e92f2/jackson-module-jaxb-annotations-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.15.3/44b0de22ca9331c21810569154e7d76450be1c6f/jackson-jaxrs-base-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.core/jersey-common/3.1.5/7a9edf47631e6588cf24f777f3e7f183d285a9e1/jersey-common-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.ws.rs/jakarta.ws.rs-api/3.1.0/15ce10d249a38865b58fc39521f10f29ab0e3363/jakarta.ws.rs-api-3.1.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.inject/jakarta.inject-api/2.0.1/4c28afe1991a941d7702fe1362c365f0a8641d1e/jakarta.inject-api-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-locator/3.0.5/ea4a4d2c187dead10c998ebb3c3d6ce5133f5637/hk2-locator-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.29.2-GA/6c32028609e5dd4a1b78e10fbcd122b09b3928b1/javassist-3.29.2-GA.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.105.Final/47570154d4cf9d8d9569a25ec1929d8ad1b0fa68/netty-codec-socks-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.105.Final/4733830b4b2b9111e9bf8136691d07b20027cd51/netty-codec-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.105.Final/5184e1308ed7d853d7061b4e21e47e8de43a28df/netty-transport-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.105.Final/c1c1b4d2c89d2f74c80a2701c124ecfde1ecf067/netty-buffer-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.105.Final/8438fc1de4c10301bb53ceb49ed8db74bd40cc2c/netty-common-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.105.Final/8bb8ab3a8f1e730c6e7d1d1cf8fc24221eacc6cd/netty-transport-native-unix-common-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.105.Final/e69687e013ded60f3f9054164e121c2702200711/netty-resolver-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.105.Final/e006d3b0cfd5b8133c9fcebfa05d9ae80a721e80/netty-transport-classes-epoll-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.105.Final/aadd137239a02aaae8c1de0f4346e8af8898e90f/netty-transport-classes-kqueue-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcutil-jdk18on/1.76/8c7594e651a278bcde18e038d8ab55b1f97f4d31/bcutil-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk18on/1.76/3a785d0b41806865ad7e311162bfa3fa60b3965b/bcprov-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.14/4d3c2248219ac0effeb380ed4c5280a80bf395e8/logback-core-1.4.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.21.1/74c65e87b9ce1694a01524e192d7be989ba70486/log4j-api-2.21.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/2.5.0/aca011492dfe9c26f4e0659028a4fe0970829dd8/accessors-smart-2.5.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.reactivestreams/reactive-streams/1.0.4/3864a1320d97d7b045f729a326e1e077661f31b7/reactive-streams-1.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.105.Final/91c67d50bad110e1430b9dbda0f10a7768e2e13c/netty-resolver-dns-classes-macos-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.105.Final/81a24958441b3f27c3404706202388202690416d/netty-codec-dns-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.xml.bind/jakarta.xml.bind-api/4.0.1/ca2330866cbc624c7e5ce982e121db1125d23e15/jakarta.xml.bind-api-4.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.activation/jakarta.activation-api/2.1.2/640c0d5aff45dbff1e1a1bc09673ff3a02b1ba12/jakarta.activation-api-2.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/osgi-resource-locator/1.0.3/de3b21279df7e755e38275137539be5e2c80dd58/osgi-resource-locator-1.0.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-api/3.0.5/6774367a6780ea4fedc19425981f1b86762a3506/hk2-api-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2.external/aopalliance-repackaged/3.0.5/6a77d3f22a1423322226bff412177addc936b38f/aopalliance-repackaged-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-utils/3.0.5/4d65eff85bd778f66e448be1049be8b9530a028f/hk2-utils-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.3/8e6300ef51c1d801a7ed62d07cd221aca3a90640/asm-9.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.postgresql/postgresql/42.6.0/7614cfce466145b84972781ab0079b8dea49e363/postgresql-42.6.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/4.0.4/7180c50ef8bd127bb1dd645458b906cffcf6c2b5/jaxb-runtime-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/30.0-jre/8ddbc8769f73309fe09b54c5951163f10b0d89fa/guava-30.0-jre.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.31.0/eeefd4af42e2f4221d145c1791582f91868f99ab/checker-qual-3.31.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty.incubator/reactor-netty-incubator-quic/0.1.15/22fee3c4bcb10e725a2c627c5f9f7912ebf450e/reactor-netty-incubator-quic-0.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-core/4.0.4/2d5aadd02af86f1e9d8c6f7e8501673f915d4e25/jaxb-core-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.3.4/dac170e4594de319655ffb62f41cbd6dbb5e601e/error_prone_annotations-2.3.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final/77a5f94b56d49508e0ee334751db5b78e5ccd50c/hibernate-commons-annotations-6.0.6.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.smallrye/jandex/3.1.2/a6c1c89925c7df06242b03dddb353116ceb9584c/jandex-3.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.14.11/725602eb7c8c56b51b9c21f273f9df5c909d9e7d/byte-buddy-1.14.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty.incubator/netty-incubator-codec-native-quic/0.0.55.Final/cb688a13f9867eecc490d489e7cadd06f061eb6a/netty-incubator-codec-native-quic-0.0.55.Final-linux-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.eclipse.angus/angus-activation/2.0.1/eaafaf4eb71b400e4136fc3a286f50e34a68ecb7/angus-activation-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/txw2/4.0.4/cfd2bcf08782673ac370694fdf2cf76dbaa607ef/txw2-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.sun.istack/istack-commons-runtime/4.1.2/18ec117c85f3ba0ac65409136afa8e42bc74e739/istack-commons-runtime-4.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty.incubator/netty-incubator-codec-classes-quic/0.0.55.Final/9e21132ee25bd87de11313d77685fe135fa30fd/netty-incubator-codec-classes-quic-0.0.55.Final.jar com.ugent.pidgeon.PidgeonApplication - -exec $SHELL \ No newline at end of file From 2db48130303fededb2b73d4960783580e4dabd93 Mon Sep 17 00:00:00 2001 From: Wout Verdyck <159531937+usserwoutV2@users.noreply.github.com> Date: Tue, 14 May 2024 11:29:43 +0200 Subject: [PATCH 034/130] Delete startBackend.sh --- startBackend.sh | 9 --------- 1 file changed, 9 deletions(-) delete mode 100755 startBackend.sh diff --git a/startBackend.sh b/startBackend.sh deleted file mode 100755 index ec1fe7ab..00000000 --- a/startBackend.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -export PGU=tristanIs -export PGP=fuckingHeet -open -a Docker -docker compose up db -d -/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -Dfile.encoding=UTF-8 -classpath /Users/usserwout/Documents/school/3ba/SEL2/UGent-6/backend/app/build/classes/java/main:/Users/usserwout/Documents/school/3ba/SEL2/UGent-6/backend/app/build/resources/main:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/3.2.2/b89d213d9f49c3e6247b2503ac7d94b0ac8260f6/spring-boot-starter-web-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-oauth2-client/3.2.2/cce33514a28968b4f6203cdcce700192d19b1ef0/spring-boot-starter-oauth2-client-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-security/3.2.2/79676d6fe68878890e26be10ecab126f472d7c0f/spring-boot-starter-security-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-oauth2-resource-server/3.2.2/8d71b3dd52be0068cb505de076493c991085a5f1/spring-boot-starter-oauth2-resource-server-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot-starter-active-directory/3.11.0/6e0605d340d45896a875fdbb0bb0da0e1c70d57a/azure-spring-boot-starter-active-directory-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure/2.1.8.RELEASE/ad308707f69f4a66a1e19fdd91389058a9942eba/spring-security-oauth2-autoconfigure-2.1.8.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-config/6.2.1/119953fd2980d50e1119b913a8596e5e6bdc1295/spring-security-config-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-web/6.2.1/4b486977ab1bcdd5dcc6aa5d8a367f1c0814bf56/spring-security-web-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot-starter/3.11.0/776d5a559e1746e11249bac2993349d0f5b65c6/azure-spring-boot-starter-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.auth0/java-jwt/3.18.2/89c1da37cd738d9c3c7176fbf1e291ff2a8b988/java-jwt-3.18.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.auth0/jwks-rsa/0.18.0/cd3977e3234ddd06e2612812308fa621bf27d7e6/jwks-rsa-0.18.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.servlet/javax.servlet-api/4.0.1/a27082684a2ff0bf397666c3943496c44541d1ca/javax.servlet-api-4.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-data-jpa/3.2.2/65cf3aad09f0218b7dfab849c9b0350d0a9e0d81/spring-boot-starter-data-jpa-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tika/tika-core/1.27/79ad0f72558b8fbce947147959e2faff8b7b70a/tika-core-1.27.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-httpclient5/3.3.5/73a916e8931e0d09c8e02ab5e90f38db80dcd2ff/docker-java-transport-httpclient5-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.client5/httpclient5/5.1.2/54d84f95fe7d182a86e6e3064a77b58478e2b5a/httpclient5-5.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java/3.3.5/3f1cdd5f182c53ffd15d7553b8b3d1c96e74ae1b/docker-java-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-devtools/3.2.2/5d4ce10e0c8d4a6cc040a1836280f379893a8213/spring-boot-devtools-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/3.2.2/328f5ce9e10d5f90520e72a3ff8a2586b9e46b37/spring-boot-starter-json-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.2.2/dc04714f9295297f92fa8099eb51edc54dbe67db/spring-boot-starter-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/3.2.2/e22a0ba37910731b382f3fe47ad36aed20fad24d/spring-boot-starter-tomcat-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/6.1.3/f4738a57787add6567e0679eebb1b499a11019cc/spring-webmvc-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/6.1.3/cc3459b4abd436331608ddb6424886875f7086ab/spring-web-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-client/6.2.1/1e78c49fa10f72acb7f40e34e9ced1216753792c/spring-security-oauth2-client-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-jose/6.2.1/9bb10fe87a2dfff27075dd09586075645907b44d/spring-security-oauth2-jose-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-core/6.2.1/b4014a04f217f0f48d15bc7d53906b6911ad855f/spring-security-core-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.1.3/4d9bd4bd9b8bedf9ef151b45c79766b336117b9a/spring-aop-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-resource-server/6.2.1/3d36a1686bdd96d6cd8f90f34bef7b59b4b7feb2/spring-security-oauth2-resource-server-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-webflux/3.2.2/25ae11864604f8f7b504b9feae710b859aed6464/spring-boot-starter-webflux-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-validation/3.2.2/faedd363ffbafde6a23bc7183ce79de11a96e780/spring-boot-starter-validation-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.azure.spring/azure-spring-boot/3.11.0/761a2ace1da268ad666ac21abc834c5077a3e00/azure-spring-boot-3.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.microsoft.azure/msal4j/1.11.0/38df9693f67ea1f01f35ebbe9411f0760c9ac77f/msal4j-1.11.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/nimbus-jose-jwt/9.24.4/29a1f6a00a4daa3e1873f6bf4f16ddf4d6fd6d37/nimbus-jose-jwt-9.24.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.3/a734bc2c47a9453c4efa772461a3aeb273c010d9/jackson-databind-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty/1.1.15/a6a0ff5228472763c9034353711078057c0d8074/reactor-netty-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security.oauth/spring-security-oauth2/2.3.5.RELEASE/7969f5363398d6d3788bef1740b2ab9509043d51/spring-security-oauth2-2.3.5.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.2.2/5c407409f8d260a4bc6e173d16fc3b36e6adec21/spring-boot-autoconfigure-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.2.2/9f274d1bd822c4c57bb5b37ecae2380b980f567/spring-boot-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.3/79baf4e605eb3bbb60b1c475d44a7aecceea1d60/jackson-annotations-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.xml.bind/jaxb-api/2.3.1/8531ad5ac454cc2deb9d4d32c40c4d7451939b5d/jaxb-api-2.3.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-jwt/1.0.10.RELEASE/19a1ca7a83e9d263a31af5f529da460f8f863451/spring-security-jwt-1.0.10.RELEASE.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.1.3/c63f038933701058fd7578460c66dbe2d424915/spring-context-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.1.3/c2df4210e796d3a27efc1f22621aa4e2c6cd985f/spring-beans-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.1.3/a002e96e780954cc3ac4cd70fd3bb16accdc47ed/spring-core-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.1.3/7c35fc3d7525a024fdde8a5d7597a6a8a4e59d7/spring-expression-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-aop/3.2.2/f01ecef0ce5f8d5631890a0c456a88a72323b569/spring-boot-starter-aop-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-jdbc/3.2.2/22ffda6938dca5f584c8b1b64e4e9096e8302c1e/spring-boot-starter-jdbc-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.data/spring-data-jpa/3.2.2/f91a3896c2a6139ac1da1fd8ff4350ca4b0e409e/spring-data-jpa-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.orm/hibernate-core/6.4.1.Final/3dcefddf6609e6491d37208bcc0cab1273598cbd/hibernate-core-6.4.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aspects/6.1.3/c8b5dde3568dc5df6109916d8ad4866efe4e61fd/spring-aspects-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.11/ad96c3f8cf895e696dd35c2bc8e8ebe710be9e6d/slf4j-api-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/commons-io/commons-io/2.13.0/8bb2bc9b4df17e2411533a0708a69f983bf5e83b/commons-io-2.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport/3.3.5/4aa7e97c14ed1f2ca62029bf1ea8467f6ebf48d9/docker-java-transport-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.java.dev.jna/jna/5.13.0/1200e7ebeedbe0d10062093f32925a912020e747/jna-5.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.core5/httpcore5-h2/5.2.4/2872764df7b4857549e2880dd32a6f9009166289/httpcore5-h2-5.2.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents.core5/httpcore5/5.2.4/34d8332b975f9e9a8298efe4c883ec43d45b7059/httpcore5-5.2.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/commons-codec/commons-codec/1.16.0/4e3eb3d79888d76b54e28b350915b5dc3919c9de/commons-codec-1.16.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-jersey/3.3.5/5b74264f3cb7af6efa83fa9ba8f6540c4a63d641/docker-java-transport-jersey-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-transport-netty/3.3.5/a416c8c75d216893a11a8167952267876e5977ec/docker-java-transport-netty-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-core/3.3.5/d30fb6b25c0bde6a0f0f25bcb2cde12e23a44422/docker-java-core-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/jcl-over-slf4j/2.0.11/f6226edb8c85f8c9f1f75ec4b0252c02f589478a/jcl-over-slf4j-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.3/4a20a0e104931bfa72f24ef358c2eb63f1ef2aaf/jackson-datatype-jsr310-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.3/8d251b90c5358677e7d8161e0c2488e6f84f49da/jackson-module-parameter-names-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.3/80158cb020c7bd4e4ba94d8d752a65729dc943b2/jackson-datatype-jdk8-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.2.2/3347c3b1cec6cf2d5fa186d1e49d2f378a6b7cae/spring-boot-starter-logging-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/2.2/3af797a25458550a16bf89acc8e4ab2b7f2bfce0/snakeyaml-2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.18/83a3bc6898f2ceed2357ba231a5e83dc2016d454/tomcat-embed-websocket-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/10.1.18/bff6c34649d1dd7b509e819794d73ba795947dcf/tomcat-embed-core-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/10.1.18/b2c4dc05abd363c63b245523bb071727aa2f1046/tomcat-embed-el-10.1.18.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-observation/1.12.2/e082b05a2527fc24ea6fbe4c4b7ae34653aace81/micrometer-observation-1.12.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-oauth2-core/6.2.1/1927cdcddce10f1f852e25ed3445e1f1b96c17ad/spring-security-oauth2-core-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/oauth2-oidc-sdk/9.43.3/31709dab9f6531cc5c8f0d7e50ed5ccf10127877/oauth2-oidc-sdk-9.43.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.security/spring-security-crypto/6.2.1/d7c4f4e8fe5ae84dd1da76094ee8a0a7e214923d/spring-security-crypto-6.2.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webflux/6.1.3/426447b8e64765db5c2901f2b33cdb691b845f34/spring-webflux-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-reactor-netty/3.2.2/cf32e5dc15e455a17aa833eeefb9d0f0eb9f6c4e/spring-boot-starter-reactor-netty-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.validator/hibernate-validator/8.0.1.Final/e49e116b3d3928060599b176b3538bb848718e95/hibernate-validator-8.0.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.validation/validation-api/2.0.1.Final/cb855558e6271b1b32e716d24cb85c7f583ce09e/validation-api-2.0.1.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.annotation/javax.annotation-api/1.3.2/934c04d3cfef185a8008e7bf34331b79730a9d43/javax.annotation-api-1.3.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.stephenc.jcip/jcip-annotations/1.0-1/ef31541dd28ae2cefdd17c7ebf352d93e9058c63/jcip-annotations-1.0-1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.3/60d600567c1862840397bf9ff5a92398edc5797b/jackson-core-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty-http/1.1.15/c79756fa2dfc28ac81fc9d23a14b17c656c3e560/reactor-netty-http-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty/reactor-netty-core/1.1.15/3221d405ad55a573cf29875a8244a4217cf07185/reactor-netty-core-1.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.codehaus.jackson/jackson-mapper-asl/1.9.13/1ee2f2bed0e5dd29d1cb155a166e6f8d50bbddb7/jackson-mapper-asl-1.9.13.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/javax.activation/javax.activation-api/1.2.0/85262acf3ca9816f9537ca47d5adeabaead7cb16/javax.activation-api-1.2.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk15on/1.60/d0c46320fbc07be3a24eb13a56cee4e3d38e0c75/bcpkix-jdk15on-1.60.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.1.3/a715e091ee86243ee94534a03f3c26b4e48de31e/spring-jcl-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.aspectj/aspectjweaver/1.9.21/beaabaea95c7f3330f415c72ee0ffe79b51d172f/aspectjweaver-1.9.21.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jdbc/6.1.3/be4b30cc956b26f13e04ccadc2b0575038c531bb/spring-jdbc-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.zaxxer/HikariCP/5.0.1/a74c7f0a37046846e88d54f7cb6ea6d565c65f9c/HikariCP-5.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-orm/6.1.3/98572e26c6d011c9710545085358a4e35e27649/spring-orm-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework.data/spring-data-commons/3.2.2/9b0b0f5f5bc793463a81171d6889809abc14b19b/spring-data-commons-3.2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.springframework/spring-tx/6.1.3/7750337bf46a2ff248685915c7cc88d3bef2f666/spring-tx-6.1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.antlr/antlr4-runtime/4.13.0/5a02e48521624faaf5ff4d99afc88b01686af655/antlr4-runtime-4.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.persistence/jakarta.persistence-api/3.1.0/66901fa1c373c6aff65c13791cc11da72060a8d6/jakarta.persistence-api-3.1.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.transaction/jakarta.transaction-api/2.0.1/51a520e3fae406abb84e2e1148e6746ce3f80a1a/jakarta.transaction-api-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.15.3/71edfaa76deaaa3e8848d8e35e485f132d095f81/jackson-jaxrs-json-provider-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.connectors/jersey-apache-connector/3.1.5/ec06316a19338bcc8236d6d5ac38b273ffbbd5cd/jersey-apache-connector-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpclient/4.5.14/1194890e6f56ec29177673f2f12d0b8e627dec98/httpclient-4.5.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.core/jersey-client/3.1.5/15695e853b7583703aff98e543b95fa0ca4553/jersey-client-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.inject/jersey-hk2/3.1.5/9ecb5339c3de02e5939c72657e74e2c5fdeb71c8/jersey-hk2-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore/4.4.16/51cf043c87253c9f58b539c9f7e44c8894223850/httpcore-4.4.16.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.kohlschutter.junixsocket/junixsocket-common/2.6.1/34151df0d8c8348ddb9f2f387943b3238b5f1a4f/junixsocket-common-2.6.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.kohlschutter.junixsocket/junixsocket-native-common/2.6.1/70a02ed1d3fab753ac436732d4aab877ada8b50f/junixsocket-native-common-2.6.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.105.Final/19a5b78164c6a5a0464586c1e1ac5695cc79844c/netty-handler-proxy-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.105.Final/bc8bc7b5384fb3dcb467d2a44159282935328779/netty-codec-http-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.105.Final/7e997e63d0a445c4b352bcd38474d50f06f2eaf1/netty-handler-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-epoll/4.1.105.Final/8d20e17cff9ec1aaa3bb133ae7cb339c991bc105/netty-transport-native-epoll-4.1.105.Final-linux-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-kqueue/4.1.105.Final/91e3e877f65e4a485d5cca980e59329034152b43/netty-transport-native-kqueue-4.1.105.Final-osx-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.github.docker-java/docker-java-api/3.3.5/c9cd924da119835a8da0ca43bfa37b740247c029/docker-java-api-3.3.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-compress/1.21/4ec95b60d4e86b5c95a0e919cb172a0af98011ef/commons-compress-1.21.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-lang3/3.13.0/b7263237aa89c1f99b327197c41d0669707a462e/commons-lang3-3.13.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/19.0/6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9/guava-19.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcpkix-jdk18on/1.76/10c9cf5c1b4d64abeda28ee32fbade3b74373622/bcpkix-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.14/d98bc162275134cdf1518774da4a2a17ef6fb94d/logback-classic-1.4.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.21.1/d77b2ba81711ed596cd797cc2b5b5bd7409d841c/log4j-to-slf4j-2.21.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.11/279356f8e873b1a26badd8bbb3284b5c3b22c770/jul-to-slf4j-2.0.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-commons/1.12.2/b44127d8ec7b3ef11a01912d1e6474e1167f3929/micrometer-commons-1.12.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/content-type/2.2/9a894bce7646dd4086652d85b88013229f23724b/content-type-2.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.5.0/57a64f421b472849c40e77d2e7cce3a141b41e99/json-smart-2.5.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.nimbusds/lang-tag/1.7/97c73ecd70bc7e8eefb26c5eea84f251a63f1031/lang-tag-1.7.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor/reactor-core/3.6.2/679ac38d031c154374182748491a177a76c890e1/reactor-core-3.6.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.validation/jakarta.validation-api/3.0.2/92b6631659ba35ca09e44874d3eb936edfeee532/jakarta.validation-api-3.0.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.jboss.logging/jboss-logging/3.5.3.Final/c88fc1d8a96d4c3491f55d4317458ccad53ca663/jboss-logging-3.5.3.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml/classmate/1.6.0/91affab6f84a2182fce5dd72a8d01bc14346dddd/classmate-1.6.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.105.Final/7c558b1ea68a2385b250191ad5ecb0f4afb2d866/netty-codec-http2-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-native-macos/4.1.105.Final/1c3b820b9f44c26a65dec438a731f8c9bf64004/netty-resolver-dns-native-macos-4.1.105.Final-osx-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.105.Final/719fa5bccd87bd3286fc0655a6c3b61cb539e334/netty-resolver-dns-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.codehaus.jackson/jackson-core-asl/1.9.13/3c304d70f42f832e0a86d45bd437f692129299a4/jackson-core-asl-1.9.13.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk15on/1.60/bd47ad3bd14b8e82595c7adaa143501e60842a84/bcprov-jdk15on-1.60.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.15.3/74e8ef60b65b42051258465f06c06195e61e92f2/jackson-module-jaxb-annotations-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.15.3/44b0de22ca9331c21810569154e7d76450be1c6f/jackson-jaxrs-base-2.15.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jersey.core/jersey-common/3.1.5/7a9edf47631e6588cf24f777f3e7f183d285a9e1/jersey-common-3.1.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.ws.rs/jakarta.ws.rs-api/3.1.0/15ce10d249a38865b58fc39521f10f29ab0e3363/jakarta.ws.rs-api-3.1.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.inject/jakarta.inject-api/2.0.1/4c28afe1991a941d7702fe1362c365f0a8641d1e/jakarta.inject-api-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-locator/3.0.5/ea4a4d2c187dead10c998ebb3c3d6ce5133f5637/hk2-locator-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.javassist/javassist/3.29.2-GA/6c32028609e5dd4a1b78e10fbcd122b09b3928b1/javassist-3.29.2-GA.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.105.Final/47570154d4cf9d8d9569a25ec1929d8ad1b0fa68/netty-codec-socks-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.105.Final/4733830b4b2b9111e9bf8136691d07b20027cd51/netty-codec-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.105.Final/5184e1308ed7d853d7061b4e21e47e8de43a28df/netty-transport-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.105.Final/c1c1b4d2c89d2f74c80a2701c124ecfde1ecf067/netty-buffer-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.105.Final/8438fc1de4c10301bb53ceb49ed8db74bd40cc2c/netty-common-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-native-unix-common/4.1.105.Final/8bb8ab3a8f1e730c6e7d1d1cf8fc24221eacc6cd/netty-transport-native-unix-common-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.105.Final/e69687e013ded60f3f9054164e121c2702200711/netty-resolver-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-epoll/4.1.105.Final/e006d3b0cfd5b8133c9fcebfa05d9ae80a721e80/netty-transport-classes-epoll-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport-classes-kqueue/4.1.105.Final/aadd137239a02aaae8c1de0f4346e8af8898e90f/netty-transport-classes-kqueue-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcutil-jdk18on/1.76/8c7594e651a278bcde18e038d8ab55b1f97f4d31/bcutil-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk18on/1.76/3a785d0b41806865ad7e311162bfa3fa60b3965b/bcprov-jdk18on-1.76.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.14/4d3c2248219ac0effeb380ed4c5280a80bf395e8/logback-core-1.4.14.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.21.1/74c65e87b9ce1694a01524e192d7be989ba70486/log4j-api-2.21.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/2.5.0/aca011492dfe9c26f4e0659028a4fe0970829dd8/accessors-smart-2.5.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.reactivestreams/reactive-streams/1.0.4/3864a1320d97d7b045f729a326e1e077661f31b7/reactive-streams-1.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns-classes-macos/4.1.105.Final/91c67d50bad110e1430b9dbda0f10a7768e2e13c/netty-resolver-dns-classes-macos-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.105.Final/81a24958441b3f27c3404706202388202690416d/netty-codec-dns-4.1.105.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.xml.bind/jakarta.xml.bind-api/4.0.1/ca2330866cbc624c7e5ce982e121db1125d23e15/jakarta.xml.bind-api-4.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/jakarta.activation/jakarta.activation-api/2.1.2/640c0d5aff45dbff1e1a1bc09673ff3a02b1ba12/jakarta.activation-api-2.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/osgi-resource-locator/1.0.3/de3b21279df7e755e38275137539be5e2c80dd58/osgi-resource-locator-1.0.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-api/3.0.5/6774367a6780ea4fedc19425981f1b86762a3506/hk2-api-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2.external/aopalliance-repackaged/3.0.5/6a77d3f22a1423322226bff412177addc936b38f/aopalliance-repackaged-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.hk2/hk2-utils/3.0.5/4d65eff85bd778f66e448be1049be8b9530a028f/hk2-utils-3.0.5.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.3/8e6300ef51c1d801a7ed62d07cd221aca3a90640/asm-9.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.postgresql/postgresql/42.6.0/7614cfce466145b84972781ab0079b8dea49e363/postgresql-42.6.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/4.0.4/7180c50ef8bd127bb1dd645458b906cffcf6c2b5/jaxb-runtime-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/30.0-jre/8ddbc8769f73309fe09b54c5951163f10b0d89fa/guava-30.0-jre.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.checkerframework/checker-qual/3.31.0/eeefd4af42e2f4221d145c1791582f91868f99ab/checker-qual-3.31.0.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.projectreactor.netty.incubator/reactor-netty-incubator-quic/0.1.15/22fee3c4bcb10e725a2c627c5f9f7912ebf450e/reactor-netty-incubator-quic-0.1.15.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-core/4.0.4/2d5aadd02af86f1e9d8c6f7e8501673f915d4e25/jaxb-core-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/failureaccess/1.0.1/1dcf1de382a0bf95a3d8b0849546c88bac1292c9/failureaccess-1.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/b421526c5f297295adef1c886e5246c39d4ac629/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/3.0.2/25ea2e8b0c338a877313bd4672d3fe056ea78f0d/jsr305-3.0.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.3.4/dac170e4594de319655ffb62f41cbd6dbb5e601e/error_prone_annotations-2.3.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.3/ba035118bc8bac37d7eff77700720999acd9986d/j2objc-annotations-1.3.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final/77a5f94b56d49508e0ee334751db5b78e5ccd50c/hibernate-commons-annotations-6.0.6.Final.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.smallrye/jandex/3.1.2/a6c1c89925c7df06242b03dddb353116ceb9584c/jandex-3.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.14.11/725602eb7c8c56b51b9c21f273f9df5c909d9e7d/byte-buddy-1.14.11.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty.incubator/netty-incubator-codec-native-quic/0.0.55.Final/cb688a13f9867eecc490d489e7cadd06f061eb6a/netty-incubator-codec-native-quic-0.0.55.Final-linux-x86_64.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.eclipse.angus/angus-activation/2.0.1/eaafaf4eb71b400e4136fc3a286f50e34a68ecb7/angus-activation-2.0.1.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/txw2/4.0.4/cfd2bcf08782673ac370694fdf2cf76dbaa607ef/txw2-4.0.4.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/com.sun.istack/istack-commons-runtime/4.1.2/18ec117c85f3ba0ac65409136afa8e42bc74e739/istack-commons-runtime-4.1.2.jar:/Users/usserwout/.gradle/caches/modules-2/files-2.1/io.netty.incubator/netty-incubator-codec-classes-quic/0.0.55.Final/9e21132ee25bd87de11313d77685fe135fa30fd/netty-incubator-codec-classes-quic-0.0.55.Final.jar com.ugent.pidgeon.PidgeonApplication - -exec $SHELL \ No newline at end of file From 0b7b5ec63fd0962eeac95d024b7186a9222f39cb Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Wed, 15 May 2024 12:36:48 +0200 Subject: [PATCH 035/130] api --- backend/web-bff/App/routes/api.js | 2 +- backend/web-bff/temp-frontend/src/App.tsx | 46 +++++++++++------------ frontend/src/setupProxy.tsx | 5 --- 3 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 frontend/src/setupProxy.tsx diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js index d843ed64..416daf58 100644 --- a/backend/web-bff/App/routes/api.js +++ b/backend/web-bff/App/routes/api.js @@ -23,7 +23,7 @@ router.all('/*', redirectUri: REDIRECT_URI }), async function(req, res, next) { - + try { const response = await fetch( "api" + req.url , req.session.accessToken, req.method) res.send(response) diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx index 15d20997..d1fb3c71 100644 --- a/backend/web-bff/temp-frontend/src/App.tsx +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -4,35 +4,35 @@ import axios from 'axios' import './App.css' function App() { - const [auth, setAuth] - = useState<{nickname: string, otherKey: number} | null >(null) + const [auth, setAuth] + = useState<{ name: string, otherKey: number } | null>(null) - useEffect( () => { - axios.get('/auth/current-session').then(({data}) => { - setAuth(data); - }) - }, [] ) + useEffect(() => { + axios.get('/auth/current-session').then(({data}) => { + setAuth(data); + }) + }, []) - if (auth === null) { - return ( + if (auth === null) { + return ( <>

    Loading...

    - ) - } else if (auth) { - return ( - <> + ) + } else if (auth) { + return ( + <>

    Logged in!

    -

    You are logged in as {auth && auth.nickname ? auth.nickname : null}

    - - ) - } else { - return ( - <> -

    Welcome

    - - ) - } +

    You are logged in as {auth && auth.name ? auth.name : null}

    + + ) + } else { + return ( + <> +

    Welcome

    + + ) + } } diff --git a/frontend/src/setupProxy.tsx b/frontend/src/setupProxy.tsx deleted file mode 100644 index 192825f8..00000000 --- a/frontend/src/setupProxy.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const proxy = require('http-proxy-middleware').createProxyMiddleware; - -module.exports = function (app) { - app.use(proxy(`/auth/**`, {target: 'http://localhost:3000' })); -} \ No newline at end of file From 9804435385225d0298dde994806278a0b0339cea Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Wed, 15 May 2024 14:10:45 +0200 Subject: [PATCH 036/130] user route --- backend/web-bff/App/routes/users.js | 18 +++++++++++++++++- .../web-bff/temp-frontend/package-lock.json | 12 ------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index c93c5a63..740b54c9 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -8,7 +8,8 @@ const router = express.Router(); const fetch = require('../fetch'); -const { BACKEND_API_ENDPOINT } = require('../authConfig'); +const { BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig'); +const authProvider = require("../auth/AuthProvider"); // custom middleware to check auth state function isAuthenticated(req, res, next) { @@ -26,5 +27,20 @@ router.get('/id', } ); +router.get('/isAuthenticated', + isAuthenticated, + authProvider.acquireToken({ + scopes: [msalConfig.auth.clientId + "/.default"], + redirectUri: REDIRECT_URI + }), + async function (req, res, next) { + try { + const response = await fetch( "api/user" , req.session.accessToken, "GET") + res.send(response) + } catch(error) { + next(error); + } + } +); module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/package-lock.json b/backend/web-bff/temp-frontend/package-lock.json index d2b35f1e..0d6094f0 100644 --- a/backend/web-bff/temp-frontend/package-lock.json +++ b/backend/web-bff/temp-frontend/package-lock.json @@ -765,18 +765,6 @@ "node": ">=4" } }, - "node_modules/@types/babel__generator": { - "dev": true - }, - "node_modules/@types/babel__template": { - "dev": true - }, - "node_modules/@types/babel__traverse": { - "dev": true - }, - "node_modules/@types/prop-types": { - "dev": true - }, "node_modules/@types/react": { "version": "18.3.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", From f0d958902955bf1cdb12f5c0e7da69290d3f2e0a Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Wed, 15 May 2024 17:20:56 +0200 Subject: [PATCH 037/130] removing msal from frontend --- backend/web-bff/App/routes/users.js | 40 ++-- backend/web-bff/App/util/isAuthenticated.js | 13 ++ .../web-bff/temp-frontend/package-lock.json | 171 ++++++++---------- backend/web-bff/temp-frontend/src/App.tsx | 23 ++- frontend/src/@types/appTypes.ts | 7 +- frontend/src/App.tsx | 16 +- frontend/src/providers/AuthProvider.tsx | 43 +++++ 7 files changed, 176 insertions(+), 137 deletions(-) create mode 100644 backend/web-bff/App/util/isAuthenticated.js create mode 100644 frontend/src/providers/AuthProvider.tsx diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index 740b54c9..66fd0210 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -6,41 +6,41 @@ const express = require('express'); const router = express.Router(); -const fetch = require('../fetch'); + +const isAuthenticated = require('../util/isAuthenticated') const { BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig'); const authProvider = require("../auth/AuthProvider"); // custom middleware to check auth state -function isAuthenticated(req, res, next) { - if (!req.session.isAuthenticated) { - return res.redirect('/auth/signin'); // redirect to sign-in route - } - next(); -} router.get('/id', - isAuthenticated, // check if user is authenticated + isAuthenticated("/auth/signin"), // check if user is authenticated async function (req, res, next) { res.render('id', { idTokenClaims: req.session.account.idTokenClaims }); } ); -router.get('/isAuthenticated', - isAuthenticated, - authProvider.acquireToken({ - scopes: [msalConfig.auth.clientId + "/.default"], - redirectUri: REDIRECT_URI - }), +router.get('/account', + isAuthenticated("/users/not_signed_in"), async function (req, res, next) { - try { - const response = await fetch( "api/user" , req.session.accessToken, "GET") - res.send(response) - } catch(error) { - next(error); - } + res.send({ + isAuthenticated: true, + account: { + name: req.session.account.name + } + }) } ); +router.get('/not_signed_in', + async function(req, res, next) { + res.send ({ + isAuthenticated: false, + account: null + }) + } +) + module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/util/isAuthenticated.js b/backend/web-bff/App/util/isAuthenticated.js new file mode 100644 index 00000000..1fe0c9af --- /dev/null +++ b/backend/web-bff/App/util/isAuthenticated.js @@ -0,0 +1,13 @@ + + +function isAuthenticated(redirectPath) { + return (req, res, next) => { + if (!req.session.isAuthenticated) { + return res.redirect(redirectPath); // redirect + } + + next(); + } +} + +module.exports = isAuthenticated; \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/package-lock.json b/backend/web-bff/temp-frontend/package-lock.json index 0d6094f0..b68e9a74 100644 --- a/backend/web-bff/temp-frontend/package-lock.json +++ b/backend/web-bff/temp-frontend/package-lock.json @@ -210,24 +210,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/@babel/helper-validator-option": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", @@ -266,18 +248,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/core/node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -313,20 +283,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -597,15 +553,6 @@ "node": ">=4" } }, - "node_modules/@babel/core/node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/core/node_modules/update-browserslist-db": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", @@ -642,6 +589,36 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", @@ -690,6 +667,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -712,58 +703,39 @@ "@types/babel__traverse": "*" } }, - "node_modules/@types/babel__core/node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@types/babel__core/node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__core/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__core/node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/types": "^7.20.7" } }, - "node_modules/@types/babel__core/node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true }, "node_modules/@types/react": { "version": "18.3.2", @@ -3389,6 +3361,15 @@ "node": ">=10" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx index d1fb3c71..02c90509 100644 --- a/backend/web-bff/temp-frontend/src/App.tsx +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -3,27 +3,34 @@ import axios from 'axios' import './App.css' +type Account = { + name: string +} + function App() { - const [auth, setAuth] - = useState<{ name: string, otherKey: number } | null>(null) + const [isAuth, setIsAuth] + = useState(null) + const [account, setAccount] = useState(null) useEffect(() => { - axios.get('/auth/current-session').then(({data}) => { - setAuth(data); + axios.get("/users/account").then(({data}) => { + console.log(data) + setIsAuth(data.isAuthenticated) + setAccount(data.account) }) - }, []) + }, [isAuth, account]) - if (auth === null) { + if (isAuth === null) { return ( <>

    Loading...

    ) - } else if (auth) { + } else if (isAuth) { return ( <>

    Logged in!

    -

    You are logged in as {auth && auth.name ? auth.name : null}

    +

    You are logged in as {account && account.name? account.name : null}

    ) } else { diff --git a/frontend/src/@types/appTypes.ts b/frontend/src/@types/appTypes.ts index b2691ad5..91617758 100644 --- a/frontend/src/@types/appTypes.ts +++ b/frontend/src/@types/appTypes.ts @@ -1,5 +1,10 @@ - +export enum LoginStatus { + LOGIN_IN_PROGRESS = "login_busy", + LOGOUT_IN_PROGRESS = "logout_busy", + LOGGED_IN = "login_done", + LOGGED_OUT = "logout_done" +} export enum Themes { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2444d945..5099df91 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,5 @@ import AppRouter from "./router/AppRouter" -import { IPublicClientApplication } from "@azure/msal-browser" -import { MsalProvider } from "@azure/msal-react" -import { useNavigate } from "react-router-dom" -import CustomNavigation from "./auth/CustomNavigation" + import Layout from "./components/layout/nav/Layout" import "./i18n/config" import ThemeProvider from "./theme/ThemeProvider" @@ -11,21 +8,15 @@ import { UserProvider } from "./providers/UserProvider" import AppApiProvider from "./providers/AppApiProvider" import ErrorProvider from "./providers/ErrorProvider" -type AppProps = { - pca: IPublicClientApplication -} -function App({ pca }: AppProps) { - const navigate = useNavigate() - const navigationClient = new CustomNavigation(navigate) - pca.setNavigationClient(navigationClient) +function App() { + return (
    - @@ -33,7 +24,6 @@ function App({ pca }: AppProps) { - diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx new file mode 100644 index 00000000..3aad5c10 --- /dev/null +++ b/frontend/src/providers/AuthProvider.tsx @@ -0,0 +1,43 @@ +import {createContext, FC, PropsWithChildren, useEffect, useState} from "react" +import {LoginStatus} from "../@types/appTypes"; +import {apiFetch} from "../util/apiFetch"; +import {UserContext} from "./UserProvider"; + + +type Account = { + name: string +} + +export type AuthContextProps = { + isAuthenticated: Boolean, + loginStatus: LoginStatus, + account: Account | null, + updateAccount: () => void +} + +const AuthContext = createContext({} as AuthContextProps) + +const AuthProvider : FC = ({children}) => { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [loginStatus, setLoginStatus] = useState(LoginStatus.LOGGED_OUT) + const [account, setAccount] = useState(null) + + useEffect(() => { + updateAccount() + }, [loginStatus]); + + const updateAccount = async () => { + try { + const res = await apiFetch("GET", "localhost:3000/users/account"); + if (res.data.isAuthenticated) { + setIsAuthenticated(true) + setLoginStatus(LoginStatus.LOGGED_IN) + setAccount(res.data.account) + } + } catch (err) { + console.log(err) + } + } + return {children} +} + From c47965bad1a027ffd32abfe29692349227525f97 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Thu, 16 May 2024 09:03:35 +0200 Subject: [PATCH 038/130] Added course invite page + remove/update invite keys --- frontend/src/@types/requests.d.ts | 18 ++- frontend/src/@types/routes.ts | 2 +- frontend/src/hooks/useApi.tsx | 3 +- frontend/src/i18n/en/translation.json | 12 +- frontend/src/i18n/nl/translation.json | 15 ++- frontend/src/pages/course/Course.tsx | 26 +---- .../informationTab/InformationTab.tsx | 56 ++++++++- .../components/settingsTab/SettingsCard.tsx | 1 + .../components/tabExtraBtn/CourseAdminBtn.tsx | 14 ++- .../components/tabExtraBtn/CourseTagLine.tsx | 36 ++++++ .../tabExtraBtn/InviteModalContent.tsx | 97 ++++++++++++++++ .../src/pages/courseInvite/CourseInvite.tsx | 107 +++++++++++++++++- .../pages/index/components/CourseSection.tsx | 7 +- 13 files changed, 346 insertions(+), 48 deletions(-) create mode 100644 frontend/src/pages/course/components/tabExtraBtn/CourseTagLine.tsx create mode 100644 frontend/src/pages/course/components/tabExtraBtn/InviteModalContent.tsx diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 58762f00..fa8219ef 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -15,6 +15,9 @@ export enum ApiRoutes { COURSE_GRADES = '/api/courses/:id/grades', COURSE_LEAVE = "api/courses/:courseId/leave", COURSE_COPY = "/api/courses/:courseId/copy", + COURSE_JOIN = "/api/courses/:courseId/join/:courseKey", + COURSE_JOIN_WITHOUT_KEY = "/api/courses/:courseId/join", + COURSE_JOIN_LINK = "/api/courses/:courseId/joinKey", PROJECTS = "api/projects", PROJECT = "api/projects/:id", @@ -77,7 +80,8 @@ export type POST_Requests = { }, [ApiRoutes.PROJECT_TESTS]: Omit [ApiRoutes.COURSE_COPY]: undefined - + [ApiRoutes.COURSE_JOIN]: undefined + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: undefined } /** @@ -91,7 +95,8 @@ export type POST_Responses = { [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER], [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] [ApiRoutes.COURSE_COPY]: GET_Responses[ApiRoutes.COURSE] - + [ApiRoutes.COURSE_JOIN]: {name:string, description: string} + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: POST_Responses[ApiRoutes.COURSE_JOIN] } /** @@ -104,6 +109,7 @@ export type DELETE_Requests = { [ApiRoutes.COURSE_LEAVE]: undefined [ApiRoutes.COURSE_MEMBER]: undefined [ApiRoutes.PROJECT_TESTS]: undefined + [ApiRoutes.COURSE_JOIN_LINK]: undefined } @@ -120,6 +126,7 @@ export type PUT_Requests = { [ApiRoutes.CLUSTER_FILL]: { [groupName:string]: number[] /* userId[] */ } + [ApiRoutes.COURSE_JOIN_LINK]: undefined } @@ -131,6 +138,7 @@ export type PUT_Responses = { [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] [ApiRoutes.CLUSTER_FILL]: PUT_Requests[ApiRoutes.CLUSTER_FILL] + [ApiRoutes.COURSE_JOIN_LINK]: ApiRoutes.COURSE_JOIN } @@ -279,7 +287,8 @@ export type GET_Responses = { name: string teacher: CourseTeacher assistents: CourseTeacher[] - joinUrl: string + joinUrl: ApiRoutes.COURSE_JOIN + joinKey: string | null archivedAt: Timestamp | null // null if not archived year: number createdAt: Timestamp @@ -329,4 +338,7 @@ export type GET_Responses = { [ApiRoutes.SUBMISSION_ARTIFACT]: Blob // returned het artifact als zip + + [ApiRoutes.COURSE_JOIN]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: GET_Responses[ApiRoutes.COURSE] } diff --git a/frontend/src/@types/routes.ts b/frontend/src/@types/routes.ts index 499dee49..220dae80 100644 --- a/frontend/src/@types/routes.ts +++ b/frontend/src/@types/routes.ts @@ -16,6 +16,6 @@ export enum AppRoutes { ERROR = "/error", NOT_FOUND = "/not-found", EDIT_ROLE = "/edit-role", - COURSE_INVITE = "/invite/:inviteId", + COURSE_INVITE = "/invite/:courseId", COURSES = "/courses", } diff --git a/frontend/src/hooks/useApi.tsx b/frontend/src/hooks/useApi.tsx index 6336d2c8..52ab30ac 100644 --- a/frontend/src/hooks/useApi.tsx +++ b/frontend/src/hooks/useApi.tsx @@ -98,7 +98,7 @@ const useApi = ():UseApiType => { type Ret = HandleErrorReturn if (typeof options === "string") options = { mode: options } let result: Partial = {} - + try { const response = await apiFetch(method, route, apiOptions.body, apiOptions.pathValues) result.response = response @@ -136,6 +136,7 @@ const useApi = ():UseApiType => { } else if (options.mode === "message") { message.error(errMessage) } else if (options.mode === "page") { + console.log("------"); setError({ status, message: errMessage, diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 8b870ed2..a301fd28 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -229,7 +229,14 @@ "leaveSuccess": "Left course successfully", "leaveFail": "Failed to leave the course", "copy": "Create copy of this course", - "copySuccess": "Course copied successfully" + "copySuccess": "Course copied successfully", + "registerCourse": "Register for this course", + "register": "Register", + "successfullyRegistered": "You have successfully registered for this course", + "invitePeople": "Invite people", + "invitePeopleToCourse": "Invite people to this course", + "allowedInviteText": "Only people with the invite link can join this course", + "regenerateKey": "Regenerate invite key" }, "cancel": "Cancel", "ok": "OK", @@ -240,7 +247,8 @@ "homepage": "Back home", "403_message": "you are not authorized to access this page", "404_message": "Sorry, the page you visited does not exist", - "500_message": "Sorry, something went wrong" + "500_message": "Sorry, something went wrong", + "cannotJoinCourse": "You cannot join this course" }, "goBack": "Go back", "components": { diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 6bb5e83c..a1d8c7c9 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -214,7 +214,7 @@ "score": "Score", "noGroups": "Geen groepen beschikbaar", "searchMember": "Zoek lid", - "inviteLink": "Inschrijvingslink", + "inviteLink": "Uitnodigingslink", "inviteLinkInfo": "Met deze link kunnen studenten zich inschrijven voor dit vak.", "deleteCourse": "Vak verwijderen", "deleteCourseDescription": "Bent u zeker dat u dit vak wilt verwijderen? Alle projecten en indieningen zullen ook verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.", @@ -232,7 +232,14 @@ "leaveSuccess": "Vak succesvol verlaten", "leaveFail": "Het lukte niet om het vak te verlaten", "copy": "Maak kopie van dit vak", - "copySuccess": "Vak succesvol gekopieerd" + "copySuccess": "Vak succesvol gekopieerd", + "registerCourse": "Inschrijven voor dit vak", + "register": "Inschrijven", + "successfullyRegistered": "Je bent succesvol ingeschreven voor dit vak", + "invitePeople": "Mensen uitnodigen", + "invitePeopleToCourse": "Mensen uitnodigen voor dit vak", + "allowedInviteText": "Enkel mensen met een uitnodiging kunnen inschrijven", + "regenerateKey": "Nieuwe uitnodigings sleutel genereren" }, "cancel": "Annuleren", "ok": "Ok", @@ -243,8 +250,8 @@ "homepage": "Naar start", "403_message": "Je bent niet bevoegd om deze pagina te bezoeken", "404_message" : "Sorry, de pagina die je bezocht bestaat niet", - "500_message": "Sorry, er ging iets mis" - + "500_message": "Sorry, er ging iets mis", + "cannotJoinCourse": "Je kan je niet inschrijven voor dit vak" }, "goBack": "Keer terug", "components": { diff --git a/frontend/src/pages/course/Course.tsx b/frontend/src/pages/course/Course.tsx index c487a93e..0928d865 100644 --- a/frontend/src/pages/course/Course.tsx +++ b/frontend/src/pages/course/Course.tsx @@ -1,7 +1,7 @@ import { FC, useMemo } from "react" import { useTranslation } from "react-i18next" import { ApiRoutes, GET_Responses } from "../../@types/requests.d" -import { Space, Tabs, Tag, Typography } from "antd" +import { Tabs, Typography } from "antd" import { TabsProps } from "antd/lib" import ProjectCard from "../index/components/ProjectCard" import GroupsCard from "./components/groupTab/GroupsCard" @@ -12,9 +12,9 @@ import SettingsCard from "./components/settingsTab/SettingsCard" import GradesCard from "./components/gradesTab/GradesCard" import { useLocation, useNavigate } from "react-router-dom" import InformationTab from "./components/informationTab/InformationTab" -import { InboxOutlined, InfoCircleOutlined, ScheduleOutlined, SettingOutlined, TeamOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons" -import PeriodTag from "../../components/common/PeriodTag" +import { InfoCircleOutlined, ScheduleOutlined, SettingOutlined, TeamOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons" import ExtraTabBtn from "./components/tabExtraBtn/ExtraTabBtn" +import CourseTagLine from "./components/tabExtraBtn/CourseTagLine" export type CourseType = GET_Responses[ApiRoutes.COURSE] @@ -84,25 +84,7 @@ const Course: FC = () => { > {course.name} - - - - {course.teacher.name} {course.teacher.surname} - - { - course.archivedAt && } - color="orange">{t("course.archived")} - } - +
    navigate(`#${k}`)} diff --git a/frontend/src/pages/course/components/informationTab/InformationTab.tsx b/frontend/src/pages/course/components/informationTab/InformationTab.tsx index 6c671ce5..41890c8a 100644 --- a/frontend/src/pages/course/components/informationTab/InformationTab.tsx +++ b/frontend/src/pages/course/components/informationTab/InformationTab.tsx @@ -5,11 +5,53 @@ import { InfoCircleOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" import CourseAdminView from "../../../../hooks/CourseAdminView" import useCourseUser from "../../../../hooks/useCourseUser" +import { useLocation } from "react-router-dom" +import { useContext, useMemo } from "react" +import openInviteModal from "../tabExtraBtn/InviteModalContent" +import useAppApi from "../../../../hooks/useAppApi" +import { CourseContext } from "../../../../router/CourseRoutes" + +class UrlBuilder { + private baseUrl: string + private params: Record + + constructor(baseUrl: string) { + this.baseUrl = baseUrl + this.params = {} + } + + addParam(key: string, value: string | number | boolean) { + this.params[key] = value + return this + } + + build() { + const url = new URL(this.baseUrl) + Object.keys(this.params).forEach((key) => { + if (this.params[key] !== null && this.params[key] !== undefined) { + url.searchParams.append(key, this.params[key].toString()) + } + }) + return url.toString() + } +} + +export const generateLink = (courseId:string, joinKey:string|null) => { + const urlBuilder = new UrlBuilder(`${window.location.origin}/invite/${courseId}`) + if (joinKey) { + urlBuilder.addParam("key", joinKey) + } + return urlBuilder.build() +} const InformationTab = () => { const course = useCourse() const { t } = useTranslation() const courseUser = useCourseUser() + const {modal} = useAppApi() + const {setCourse} = useContext(CourseContext) + + const url = useMemo(() => generateLink(course.courseId.toString(), course.joinKey), [course]) return ( @@ -18,7 +60,7 @@ const InformationTab = () => { style={{ height: "100%" }} styles={{ body: { - padding: "0 2rem", + padding: "1rem 2rem", }, }} > @@ -28,11 +70,15 @@ const InformationTab = () => { - + + {t("course.inviteLink")}: - {" "} - {t("course.inviteLink")}:
    - {window.location.host + "/invite/" + course.courseId} +

    + openInviteModal({course, modal, onChange: setCourse, title: t("course.invitePeopleToCourse")}), + }} + >{url}
    diff --git a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx index 8337cfb0..3e889384 100644 --- a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx +++ b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx @@ -63,6 +63,7 @@ const SettingsCard: FC = () => { >
    + = ({courseId}) => { const {t} = useTranslation() const { member } = useContext(CourseContext) const navigate = useNavigate() + const course = useCourse() + const {setCourse} = useContext(CourseContext) const userContext = useContext(UserContext) const API = useApi() + const {modal} = useAppApi() const leaveCourseHandler = async () => { @@ -52,6 +58,12 @@ const CourseAdminBtn:FC<{courseId:string}> = ({courseId}) => { icon: , onClick: makeCopy }, + { + key: '4', + label: t("course.invitePeople"), + icon: , + onClick: () => openInviteModal({modal,course, title: t("course.invitePeopleToCourse"), onChange: setCourse}) + }, { key: '2', label: t("course.leave"), diff --git a/frontend/src/pages/course/components/tabExtraBtn/CourseTagLine.tsx b/frontend/src/pages/course/components/tabExtraBtn/CourseTagLine.tsx new file mode 100644 index 00000000..46f94a66 --- /dev/null +++ b/frontend/src/pages/course/components/tabExtraBtn/CourseTagLine.tsx @@ -0,0 +1,36 @@ +import { FC } from "react" +import { CourseType } from "../../Course" +import { Space, Tag } from "antd" +import PeriodTag from "../../../../components/common/PeriodTag" +import { InboxOutlined } from "@ant-design/icons" +import { useTranslation } from "react-i18next" + +const CourseTagLine: FC<{ course: CourseType }> = ({ course }) => { + const { t } = useTranslation() + + return ( + + + + {course.teacher.name} {course.teacher.surname} + + {course.archivedAt && ( + } + color="orange" + > + {t("course.archived")} + + )} + + ) +} + +export default CourseTagLine diff --git a/frontend/src/pages/course/components/tabExtraBtn/InviteModalContent.tsx b/frontend/src/pages/course/components/tabExtraBtn/InviteModalContent.tsx new file mode 100644 index 00000000..b114a1e5 --- /dev/null +++ b/frontend/src/pages/course/components/tabExtraBtn/InviteModalContent.tsx @@ -0,0 +1,97 @@ +import { Button, Input, Modal, Space, Switch, Tooltip, Typography } from "antd" +import { FC, useMemo, useState } from "react" +import { generateLink } from "../informationTab/InformationTab" +import { CourseType } from "../../Course" +import { HookAPI } from "antd/es/modal/useModal" +import { InfoCircleOutlined, RedoOutlined } from "@ant-design/icons" +import { useTranslation } from "react-i18next" +import useApi from "../../../../hooks/useApi" +import { ApiRoutes } from "../../../../@types/requests.d" + +const InviteModalContent: FC<{ defaultCourse: CourseType; onChange: (course: CourseType) => void }> = ({ defaultCourse, onChange }) => { + const { t } = useTranslation() + const [course, setCourse] = useState(defaultCourse) + const API = useApi() + const [loading, setLoading] = useState(false) + const url = useMemo(() => generateLink(course.courseId.toString(), course.joinKey), [course]) + + const regenerateKey = async () => { + setLoading(true) + const req = await API.PUT(ApiRoutes.COURSE_JOIN_LINK, { body: undefined, pathValues: { courseId: course.courseId } }, "message") + setLoading(false) + if (!req.success) return + console.log(req.response.data) + const newCourse = {...course, joinKey: req.response.data} + onChange(newCourse) + setCourse(newCourse) + } + + const toggleJoinKey = async () => { + if (course.joinKey) { + setLoading(true) + console.log("DELETE"); + const req = await API.DELETE(ApiRoutes.COURSE_JOIN_LINK, { pathValues: { courseId: course.courseId } }, "message") + setLoading(false) + if (!req.success) return + const newCourse = {...course, joinKey: req.response.data} + onChange(newCourse) + setCourse(newCourse) + } else { + await regenerateKey() + } + } + + return ( + + + + + + } + /> + {course.joinKey && ( + + +
    + + ) } export default CourseInvite diff --git a/frontend/src/pages/index/components/CourseSection.tsx b/frontend/src/pages/index/components/CourseSection.tsx index 3bc38a5f..05376280 100644 --- a/frontend/src/pages/index/components/CourseSection.tsx +++ b/frontend/src/pages/index/components/CourseSection.tsx @@ -1,14 +1,9 @@ -import { Button, Select, Space, Typography } from "antd" +import { Select, Typography } from "antd" import useUser from "../../../hooks/useUser" -import CourseCard from "./CourseCard" import { FC, useEffect, useMemo, useState } from "react" import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" import { useTranslation } from "react-i18next" -import { PlusOutlined, RightOutlined } from "@ant-design/icons" import { ProjectsType } from "../Home" -import TeacherView from "../../../hooks/TeacherView" -import { useNavigate } from "react-router-dom" -import { AppRoutes } from "../../../@types/routes" import HorizontalCourseScroll from "./HorizontalCourseScroll" const { Option } = Select From f4c01921d43fb4e73c3d40879c6d8b6756e95573 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Thu, 16 May 2024 09:40:15 +0200 Subject: [PATCH 039/130] Added extra create course btn in nav dropdown --- .../src/components/layout/nav/AuthNav.tsx | 61 ++++++++----- frontend/src/pages/index/Home.tsx | 10 +- .../index/components/CreateCourseModal.tsx | 91 +++++++++++-------- frontend/src/styles.css | 6 ++ 4 files changed, 99 insertions(+), 69 deletions(-) diff --git a/frontend/src/components/layout/nav/AuthNav.tsx b/frontend/src/components/layout/nav/AuthNav.tsx index 96974971..a0d6df1f 100644 --- a/frontend/src/components/layout/nav/AuthNav.tsx +++ b/frontend/src/components/layout/nav/AuthNav.tsx @@ -1,19 +1,22 @@ import { useAccount } from "@azure/msal-react" import { Dropdown, MenuProps, Typography } from "antd" import { useTranslation } from "react-i18next" -import { UserOutlined, BgColorsOutlined, DownOutlined, LogoutOutlined } from "@ant-design/icons" +import { UserOutlined, BgColorsOutlined, DownOutlined, LogoutOutlined, PlusOutlined } from "@ant-design/icons" import { msalInstance } from "../../../index" import { useNavigate } from "react-router-dom" import { Themes } from "../../../@types/appTypes" import { AppRoutes } from "../../../@types/routes" import useApp from "../../../hooks/useApp" - +import createCourseModal from "../../../pages/index/components/CreateCourseModal" +import useIsTeacher from "../../../hooks/useIsTeacher" const AuthNav = () => { const { t } = useTranslation() const app = useApp() const auth = useAccount() + const isTeacher = useIsTeacher() const navigate = useNavigate() + const modal = createCourseModal() const items: MenuProps["items"] = [ { @@ -33,15 +36,23 @@ const AuthNav = () => { { key: Themes.DARK, label: t("nav.dark"), - } - ] - }, - { - key: "logout", - label: t("nav.logout"), - icon: , + }, + ], }, ] + if (isTeacher) { + items.push({ + key: "createCourse", + label: t("home.createCourse"), + icon: , + }) + } + + items.push({ + key: "logout", + label: t("nav.logout"), + icon: , + }) const handleDropdownClick: MenuProps["onClick"] = (menu) => { switch (menu.key) { @@ -57,22 +68,26 @@ const AuthNav = () => { case Themes.LIGHT: app.setTheme(menu.key as Themes) break + case "createCourse": + modal.showModal() } } - - return (<> -
    - - {auth!.name} - -
    - + + return ( + <> +
    + + + {auth!.name} + + +
    ) } diff --git a/frontend/src/pages/index/Home.tsx b/frontend/src/pages/index/Home.tsx index d487e007..a6b1036a 100644 --- a/frontend/src/pages/index/Home.tsx +++ b/frontend/src/pages/index/Home.tsx @@ -10,6 +10,7 @@ import { CalendarOutlined, NodeIndexOutlined, OrderedListOutlined, UnorderedList import ProjectCalander from "../../components/other/ProjectCalander" import CourseSection from "./components/CourseSection" import useApi from "../../hooks/useApi" +import createCourseModal from "./components/CreateCourseModal" export type ProjectsType = GET_Responses[ApiRoutes.COURSE_PROJECTS] @@ -18,9 +19,9 @@ type ProjectView = "table" | "timeline" | "calendar" const Home = () => { const { t } = useTranslation() const [projects, setProjects] = useLocalStorage("__projects_cache",null) - const [open, setOpen] = useState(false) const [projectsViewMode, setProjectsViewMode] = useLocalStorage("projects_view", "table") const API = useApi() + const courseModal = createCourseModal() useEffect(() => { let ignore= false @@ -41,7 +42,7 @@ const Home = () => {
    setOpen(true)} + onOpenNew={() => courseModal.showModal()} />

    @@ -100,10 +101,7 @@ const Home = () => {

    - +
    ) } diff --git a/frontend/src/pages/index/components/CreateCourseModal.tsx b/frontend/src/pages/index/components/CreateCourseModal.tsx index 278edbc4..f11aec97 100644 --- a/frontend/src/pages/index/components/CreateCourseModal.tsx +++ b/frontend/src/pages/index/components/CreateCourseModal.tsx @@ -1,5 +1,5 @@ -import { Alert, Form, Modal } from "antd" -import { FC, useEffect, useState } from "react" +import { Alert, Form } from "antd" +import { useEffect, useState } from "react" import { useTranslation } from "react-i18next" import CourseForm from "../../../components/forms/CourseForm" import { ApiRoutes } from "../../../@types/requests.d" @@ -9,52 +9,63 @@ import { AppRoutes } from "../../../@types/routes" import useUser from "../../../hooks/useUser" import useApi from "../../../hooks/useApi" -const CreateCourseModal: FC<{ open: boolean,setOpen:(b:boolean)=>void }> = ({ open,setOpen }) => { +const createCourseModal = () => { const { t } = useTranslation() const [form] = Form.useForm() const [error, setError] = useState(null) - const [loading,setLoading] = useState(false) - const {message} = useAppApi() + const { message, modal } = useAppApi() const navigate = useNavigate() - const {updateCourses} = useUser() + const { updateCourses } = useUser() const API = useApi() - useEffect(()=> { - form.setFieldValue("year", new Date().getFullYear()-1) - },[]) + useEffect(() => { + form.setFieldValue("year", new Date().getFullYear() - 1) + }, []) - const onFinish = async () => { - await form.validateFields() - setError(null) - - const values:{name:string, description:string} = form.getFieldsValue() - console.log(values); - values.description ??= "" - setLoading(true) - const res = await API.POST(ApiRoutes.COURSES, { body:values}, "message") - if(!res.success) return setLoading(false) - const course= res.response - message.success(t("home.courseCreated")) - await updateCourses() - navigate(AppRoutes.COURSE.replace(":courseId", course.data.courseId.toString())) - + const onFinish = () => { + return new Promise(async (resolve, reject) => { + await form.validateFields() + setError(null) + + const values: { name: string; description: string } = form.getFieldsValue() + console.log(values) + values.description ??= "" + const res = await API.POST(ApiRoutes.COURSES, { body: values }, "message") + if (!res.success) return reject() + const course = res.response + message.success(t("home.courseCreated")) + await updateCourses() + navigate(AppRoutes.COURSE.replace(":courseId", course.data.courseId.toString())) + resolve() + }) } - return ( - setOpen(false)} - onOk={onFinish} - okText={t("course.createCourse")} - okButtonProps={{ loading }} - cancelText={t("cancel")} - - > - {error && } - - - ) + return { + showModal: () => { + modal.info({ + title: t("home.createCourse"), + width: 500, + className: "modal-no-icon", + onOk: onFinish, + okText: t("course.createCourse"), + cancelText: t("cancel"), + okCancel: true, + icon: null, + content: ( + <> + {error && ( + + )} + + + ), + }) + }, + } } -export default CreateCourseModal +export default createCourseModal diff --git a/frontend/src/styles.css b/frontend/src/styles.css index dd90272c..d063ef1b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -106,6 +106,12 @@ html { .ant-table-row > td.ant-table-column-sort, .ant-table-thead > tr > th.ant-table-column-sort { background-color: unset ; } + +.modal-no-icon .ant-modal-confirm-paragraph, .modal-no-icon .ant-modal-confirm-body { + + display: initial; +} + /* *************************** Landing page *************************** */ .landing-page * { From deb2a3030119779bba94fa60c067ad236038f0b1 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Thu, 16 May 2024 09:45:44 +0200 Subject: [PATCH 040/130] Fixed infinite loading when updating course --- .../src/pages/course/components/settingsTab/SettingsCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx index 3e889384..3d1dd86c 100644 --- a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx +++ b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx @@ -41,6 +41,7 @@ const SettingsCard: FC = () => { message.success(t("course.changesSaved")) setCourse(res.response.data) await updateCourses() + setLoading(false) } From 6d2bb3773f304f91be9678ef6a2425d5bf65baad Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Thu, 16 May 2024 09:53:50 +0200 Subject: [PATCH 041/130] Fixed incorrect API route --- frontend/src/pages/course/components/membersTab/MemberCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/course/components/membersTab/MemberCard.tsx b/frontend/src/pages/course/components/membersTab/MemberCard.tsx index d3467dba..23878424 100644 --- a/frontend/src/pages/course/components/membersTab/MemberCard.tsx +++ b/frontend/src/pages/course/components/membersTab/MemberCard.tsx @@ -20,7 +20,7 @@ const MembersCard = () => { let ignore = false - API.GET(ApiRoutes.COURSE_MEMBERS, { pathValues: { id: course.courseId } }, "message").then((res) => { + API.GET(ApiRoutes.COURSE_MEMBERS, { pathValues: { courseId: course.courseId } }, "message").then((res) => { if (!ignore && res.success) setMembers(res.response.data) }) From ef946cf3e5dff810005c595305cdb5aea9eecd8e Mon Sep 17 00:00:00 2001 From: Tristan Verbeken Date: Thu, 16 May 2024 12:47:47 +0200 Subject: [PATCH 042/130] Added docker zip file uploading --- .../DockerSubmissionTestModel.java | 29 ++++++++++++++++++ .../docker/DockerSubmissionTestTest.java | 20 ++++++++++++ .../DockerSubmissionTestTest/helloworld.zip | Bin 0 -> 886 bytes 3 files changed, 49 insertions(+) create mode 100644 backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java index 7e67c969..7722f13d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java @@ -14,6 +14,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -56,6 +57,7 @@ public DockerSubmissionTestModel(String dockerImage) { new File(localMountFolder + "input/").mkdirs(); new File(localMountFolder + "output/").mkdirs(); new File(localMountFolder + "artifacts/").mkdirs(); + new File(localMountFolder + "utils/").mkdirs(); } @@ -86,6 +88,33 @@ public void addInputFiles(File[] files) { } } + public void addUtilFiles(Path pathToZip){ + // first unzip files to the utils folder + try { + ZipFile zipFile = new ZipFile(pathToZip.toFile()); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryDestination = new File(localMountFolder + "utils/", entry.getName()); + if (entry.isDirectory()) { + entryDestination.mkdirs(); + } else { + File parent = entryDestination.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try { + FileUtils.copyInputStreamToFile(zipFile.getInputStream(entry), entryDestination); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + public void addZipInputFiles(ZipFile zipFile) { Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java index e0507156..3f8dac56 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -11,6 +11,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -215,4 +216,23 @@ void isValidTemplate(){ + "bruh\n")); } + @Test + void testDockerReceivesUtilFiles(){ + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + Path zipLocation = Path.of("src/test/test-cases/DockerSubmissionTestTest/d__test.zip"); // simple zip with one file + Path zipLocation2 = Path.of("src/test/test-cases/DockerSubmissionTestTest/helloworld.zip"); // complicated zip with multiple files and folder structure + stm.addUtilFiles(zipLocation); + stm.addUtilFiles(zipLocation2); + DockerTestOutput to = stm.runSubmission("find /shared/utils/"); + List logs = to.logs.stream().map(log -> log.replaceAll("\n", "")).toList(); + assertEquals("/shared/utils/", logs.get(0)); + assertEquals("/shared/utils/helloworld.txt", logs.get(1)); + assertEquals("/shared/utils/helloworld", logs.get(2)); + assertEquals("/shared/utils/helloworld/helloworld2.txt", logs.get(3)); // I don't understand the order of find :sob: but it is important all files are found. + assertEquals("/shared/utils/helloworld/helloworld3.txt", logs.get(4)); + assertEquals("/shared/utils/helloworld/emptyfolder", logs.get(5)); + assertEquals("/shared/utils/helloworld/helloworld1.txt", logs.get(6)); + stm.cleanUp(); + } + } diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip new file mode 100644 index 0000000000000000000000000000000000000000..54fb8fc7ae27c2c4ff6513da0bc005d0ca724a22 GIT binary patch literal 886 zcmWIWW@h1H0D&jT8zR6AD8bDj!;q1hlapVbUzC%g9~#2Rz?}WSHLVGVODnh;7+GF0 zGcbUO0JtHm(F~D7H3VUvkzPqf3D`_Vpm`vS)6BKRnrRF-lND$l2;(#p-LvAT?o7=s zD5*@#&q+xwLU?>1J_A=0>qtYmBmdoZO#@*JM=~uQBj~Q<;^Faaf x7S(*zSR@=cm~jX*927Wqj3}Xv2pnjP0?o%7o2+a=S23^x;W?lQlYp3k0RZ@o!;Js{ literal 0 HcmV?d00001 From fc62123fff0266718df24289318f0eb71beb016e Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Thu, 16 May 2024 16:27:31 +0200 Subject: [PATCH 043/130] gevraagde changes geimplementeerd --- frontend/src/@types/requests.d.ts | 3 ++- frontend/src/pages/editRole/EditRole.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 3c4f7c31..1a1623c7 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -254,7 +254,8 @@ export type GET_Responses = { } [ApiRoutes.USERS]: { name: string - userId: number + surname: string + id: number url: string email: string role: UserRole diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 43346863..e8c02aaf 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -6,6 +6,7 @@ import apiCall from "../../util/apiFetch" import { useTranslation } from "react-i18next" import { UsersListItem } from "./components/UserList" import { useDebounceValue } from "usehooks-ts" +import { User } from "../../providers/UserProvider" export type UsersType = GET_Responses[ApiRoutes.USERS] type SearchType = "name" | "surname" | "email" @@ -25,13 +26,13 @@ const ProfileContent = () => { }, [debouncedSearchValue]) function updateRole(user: UsersListItem, role: UserRole) { - //here user is of type User (not UsersListItem), but it seems to work because the needed properties are named the same console.log(user, role) apiCall.patch(ApiRoutes.USER, { role: role }, { id: user.id }).then((res) => { console.log(res.data) + //onSearch(); //replace this user in the userlist with the updated one from res.data const updatedUsers = users?.map((u) => { - if (u.userId === user.id) { + if (u.id === user.id) { return { ...u, role: res.data.role }; } return u; From d708cf28a4548c90cb44d0e995664ce4e89bfb93 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Thu, 16 May 2024 22:23:26 +0200 Subject: [PATCH 044/130] Fixed course description not rendering markdown --- frontend/src/pages/courseInvite/CourseInvite.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/courseInvite/CourseInvite.tsx b/frontend/src/pages/courseInvite/CourseInvite.tsx index 408f5696..b3fcd904 100644 --- a/frontend/src/pages/courseInvite/CourseInvite.tsx +++ b/frontend/src/pages/courseInvite/CourseInvite.tsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next" import { AppRoutes } from "../../@types/routes" import CourseTagLine from "../course/components/tabExtraBtn/CourseTagLine" import useUser from "../../hooks/useUser" +import MarkdownTextfield from "../../components/input/MarkdownTextfield" const CourseInvite = () => { const { token } = theme.useToken() @@ -92,7 +93,7 @@ const CourseInvite = () => { > {course.name} - {course.description} +
    +
    @@ -34,56 +41,56 @@ const UploadBtn: React.FC<{ form: FormInstance; fieldName: string; textFieldProp } function isValidTemplate(template: string): string { - if(!template?.length) return "" // Template is optional - let atLeastOne = false; // Template should not be empty - const lines = template.split("\n"); - if (lines[0].charAt(0) !== '@') { - return 'Error: The first character of the first line should be "@"'; + if (!template?.length) return "" // Template is optional + let atLeastOne = false // Template should not be empty + const lines = template.split("\n") + if (lines[0].charAt(0) !== "@") { + return 'Error: The first character of the first line should be "@"' } - let isConfigurationLine = false; + let isConfigurationLine = false for (const line of lines) { - if(line.length === 0){ // skip line if empty - continue; + if (line.length === 0) { + // skip line if empty + continue } - if (line.charAt(0) === '@') { - atLeastOne = true; - isConfigurationLine = true; - continue; + if (line.charAt(0) === "@") { + atLeastOne = true + isConfigurationLine = true + continue } if (isConfigurationLine) { - if (line.charAt(0) === '>') { - const isDescription = line.length >= 13 && line.substring(0, 13).toLowerCase() === ">description="; + if (line.charAt(0) === ">") { + const isDescription = line.length >= 13 && line.substring(0, 13).toLowerCase() === ">description=" // option lines - if (line.toLowerCase() !== ">required" && line.toLowerCase() !== ">optional" - && !isDescription) { - return 'Error: Option lines should be either ">Required", ">Optional" or start with ">Description="'; + if (line.toLowerCase() !== ">required" && line.toLowerCase() !== ">optional" && !isDescription) { + return 'Error: Option lines should be either ">Required", ">Optional" or start with ">Description="' } } else { - isConfigurationLine = false; + isConfigurationLine = false } } } if (!atLeastOne) { - return 'Error: Template should not be empty'; + return "Error: Template should not be empty" } - return ''; + return "" } const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() + const {message} = useAppApi() const dockerImage = Form.useWatch("dockerImage", form) + const dockerDir = Form.useWatch("dockerTestDir", form) + console.log(dockerDir); const dockerDisabled = !dockerImage?.length - - return ( <> = ({ form }) => { /> - <> - - - - + + + + - { - const errorMessage = isValidTemplate(value); - return errorMessage === '' ? Promise.resolve() : Promise.reject(new Error(errorMessage)); - }, + { + const errorMessage = isValidTemplate(value) + return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage)) }, - ]} - > - - - + - + + + + + + { + const isPNG = file.type === 'application/zip' + if (!isPNG) { + message.error(`${file.name} is not a zip file`); + return Upload.LIST_IGNORE + } + return false + }} + > + + + ) } diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 87f83b58..66a487fb 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -108,6 +108,9 @@ "groupMembers": "Group members", "newProject": "New project", "scoreTooHigh": "Score is higher than maximum score", + "successfullyDeleted": "Project deleted successfully", + "deleteProject": "Delete project", + "deleteProjectDescription": "Are you sure you want to delete this project? All submissions will be deleted, you cannot undo this action.", "change": { "title": "Create project", "updateTitle": "Update {{name}}", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 4bb8f1a2..1c982566 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -112,6 +112,9 @@ "groupMembers": "Groepsleden", "newProject": "Nieuw project", "scoreTooHigh": "Score is hoger dan maximum score", + "successfullyDeleted": "Project succesvol verwijderd", + "deleteProject": "Project verwijderen", + "deleteProjectDescription": "Bent u zeker dat u dit project wilt verwijderen? Alle indieningen zullen ook verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.", "change": { "title": "Maak project aan", "name": "Naam", diff --git a/frontend/src/pages/editProject/EditProject.tsx b/frontend/src/pages/editProject/EditProject.tsx index 4a846e16..54dff8a9 100644 --- a/frontend/src/pages/editProject/EditProject.tsx +++ b/frontend/src/pages/editProject/EditProject.tsx @@ -59,7 +59,7 @@ const EditProject: React.FC = () => { if (!project) return updateDockerForm() - }, [project]) + }, [project?.projectId]) const handleCreation = async () => { const values: ProjectFormData & DockerFormData = form.getFieldsValue() diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index d3752373..9172ea46 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Tabs, TabsProps, Tooltip, theme } from "antd" +import { Button, Card, Popconfirm, Tabs, TabsProps, Tooltip, theme } from "antd" import { ApiRoutes, GET_Responses } from "../../@types/requests.d" import { useTranslation } from "react-i18next" import { Link, useLocation, useNavigate, useParams } from "react-router-dom" @@ -99,8 +99,15 @@ const Project = () => { const deleteProject = async () => { if (!project || !course) return console.error("project is undefined") - const res = await API.DELETE(ApiRoutes.PROJECT, { pathValues: { id: project.projectId } }, "message") - if(!res.success) return + const res = await API.DELETE( + ApiRoutes.PROJECT, + { pathValues: { id: project.projectId } }, + { + mode: "message", + successMessage: t("project.successfullyDeleted"), + } + ) + if (!res.success) return navigate(AppRoutes.COURSE.replace(":courseId", course.courseId + "")) } @@ -125,7 +132,6 @@ const Project = () => { extra={ courseAdmin ? ( <> - - From 54c42574359df1c5d960d4c3c171106438cf41d4 Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Fri, 17 May 2024 14:21:35 +0200 Subject: [PATCH 058/130] vertalingen --- frontend/src/i18n/en/translation.json | 2 ++ frontend/src/i18n/nl/translation.json | 2 ++ .../src/pages/submission/components/SubmissionCard.tsx | 8 +++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 36521ed1..379302e8 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -191,6 +191,8 @@ "dockertestAborted": "Docker test aborted. Try again later.", "success": "succeeded", "failed": "failed", + "expected": "Expected output:", + "received": "Received output:", "status": { "accepted": "All tests succeeded.", "failed": "Some tests failed." diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index f45ef352..2d63eecd 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -195,6 +195,8 @@ "dockertestAborted": "Dockertest afgebroken. Probeer het later nog eens.", "success": "Geslaagd", "failed": "Niet geslaagd", + "expected": "Vewachte output:", + "received": "Ontvangen output:", "status": { "accepted": "Alle testen geslaagd.", "failed": "Sommige testen niet geslaagd." diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index 9291d56a..e22e325d 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -71,11 +71,9 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) } } - const tmpSubTests = [{testName: "test1", testDescription: "test1", succes: true, correct: "correct", output: "correct"}, {testName: "test2", testDescription: "test2", succes: false, correct: "correct", output: "incorrect"}] - const TestResults: React.FC = ( subTests ) => ( - {tmpSubTests.map((test, index) => { + {subTests.map((test, index) => { const successText = test.succes ? t("submission.success") : t("submission.failed"); const successType = test.succes ? 'success' : 'danger'; return ( @@ -84,9 +82,9 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) header={{`${test.testName}: ${successText}`}} > {test.testDescription} - Expected Output: + {t("submission.expected")} {test.correct} - Actual Output: + {t("submission.received")} {test.output} ); From 87fa0a8acc28f3cd847c078100396e148cc1123f Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 14:55:52 +0200 Subject: [PATCH 059/130] Keep filename of uploaded extratestfiles --- .../ugent/pidgeon/controllers/TestController.java | 7 ++++--- .../com/ugent/pidgeon/model/json/TestJson.java | 14 +++++++++++++- .../ugent/pidgeon/util/EntityToJsonConverter.java | 6 +++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index f8ea2e66..76ce9c6a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -9,6 +9,7 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.io.File; import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; @@ -250,10 +251,10 @@ public ResponseEntity uploadExtraTestFiles( try { Path path = Filehandler.getTestExtraFilesPath(projectId); - Filehandler.saveFile(path, file, Filehandler.EXTRA_TESTFILES_FILENAME); + Filehandler.saveFile(path, file, file.getOriginalFilename()); FileEntity fileEntity = new FileEntity(); - fileEntity.setName(Filehandler.EXTRA_TESTFILES_FILENAME); + fileEntity.setName(file.getOriginalFilename()); fileEntity.setPath(path.resolve(Filehandler.EXTRA_TESTFILES_FILENAME).toString()); fileEntity.setUploadedBy(auth.getUserEntity().getId()); fileEntity = fileRepository.save(fileEntity); @@ -324,7 +325,7 @@ public ResponseEntity getExtraTestFiles( return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No extra files found"); } - return Filehandler.getZipFileAsResponse(Path.of(fileEntity.getPath()), Filehandler.EXTRA_TESTFILES_FILENAME); + return Filehandler.getZipFileAsResponse(Path.of(fileEntity.getPath()), fileEntity.getName()); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java index 80250960..76c9bbb2 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java @@ -1,24 +1,28 @@ package com.ugent.pidgeon.model.json; public class TestJson { + private String projectUrl; private String dockerImage; private String dockerScript; private String dockerTemplate; private String structureTest; private String extraFilesUrl; + private String extraFilesName; + public TestJson() { } public TestJson(String projectUrl, String dockerImage, String dockerScript, - String dockerTemplate, String structureTest, String extraFilesUrl) { + String dockerTemplate, String structureTest, String extraFilesUrl, String extraFilesName) { this.projectUrl = projectUrl; this.dockerImage = dockerImage; this.dockerScript = dockerScript; this.dockerTemplate = dockerTemplate; this.structureTest = structureTest; this.extraFilesUrl = extraFilesUrl; + this.extraFilesName = extraFilesName; } public String getProjectUrl() { @@ -68,4 +72,12 @@ public String getExtraFilesUrl() { public void setExtraFilesUrl(String extraFilesUrl) { this.extraFilesUrl = extraFilesUrl; } + + public String getExtraFilesName() { + return extraFilesName; + } + + public void setExtraFilesName(String extraFilesName) { + this.extraFilesName = extraFilesName; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index 5deb063c..db8923c5 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -39,6 +39,8 @@ public class EntityToJsonConverter { private TestUtil testUtil; @Autowired private TestRepository testRepository; + @Autowired + private FileRepository fileRepository; public GroupJson groupEntityToJson(GroupEntity groupEntity) { @@ -259,13 +261,15 @@ else if (submission.getDockerTestType().equals(DockerTestType.SIMPLE)) { } public TestJson testEntityToTestJson(TestEntity testEntity, long projectId) { + FileEntity extrafiles = testEntity.getExtraFilesId() == null ? null : fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); return new TestJson( ApiRoutes.PROJECT_BASE_PATH + "/" + projectId, testEntity.getDockerImage(), testEntity.getDockerTestScript(), testEntity.getDockerTestTemplate(), testEntity.getStructureTemplate(), - testEntity.getExtraFilesId() == null ? null : ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/extrafiles" + testEntity.getExtraFilesId() == null ? null : ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/extrafiles", + extrafiles == null ? null : extrafiles.getName() ); } } \ No newline at end of file From fe8daf8ae5a54629e80d03c429988ebcae8b4751 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 15:22:04 +0200 Subject: [PATCH 060/130] Updated submissionControllerTests --- .../controllers/SubmissionControllerTest.java | 23 ++++++------------- .../controllers/TestControllerTest.java | 21 +++++++++++++---- .../ugent/pidgeon/util/FileHandlerTest.java | 12 +++++----- .../ugent/pidgeon/util/TestRunnerTest.java | 13 ++++++----- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index 51d24553..4ac934ac 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -276,17 +276,18 @@ public void testSubmitFile() throws Exception { File file = createTestFile(); try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { mockedFileHandler.when(() -> Filehandler.getSubmissionPath(submission.getProjectId(), groupEntity.getId(), submission.getId())).thenReturn(path); - mockedFileHandler.when(() -> Filehandler.saveFile(path, mockMultipartFile)).thenReturn(file); + mockedFileHandler.when(() -> Filehandler.saveFile(path, mockMultipartFile, Filehandler.SUBMISSION_FILENAME)).thenReturn(file); mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(anyLong(), anyLong(), anyLong())).thenReturn(artifactPath); when(testRunner.runStructureTest(any(), eq(testEntity), any())).thenReturn(null); - when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any())).thenReturn(null); + when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId()))).thenReturn(null); when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); when(testRepository.findByProjectId(submission.getProjectId())).thenReturn(Optional.of(testEntity)); when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) .file(mockMultipartFile)) .andExpect(status().isOk()) @@ -295,7 +296,7 @@ public void testSubmitFile() throws Exception { /* assertEquals(DockerTestState.running, submission.getDockerTestState()); */ // This executes too quickly so we can't test this - Thread.sleep(1000); + Thread.sleep(2000); // File repository needs to save again after setting path verify(fileRepository, times(1)).save(argThat( @@ -356,7 +357,7 @@ public void testSubmitFile() throws Exception { testEntity.setDockerImage("dockerImage"); testEntity.setDockerTestScript("dockerTestScript"); DockerOutput dockerOutput = new DockerTestOutput( List.of("dockerFeedback-test"), true); - when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any())).thenReturn(dockerOutput); + when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId()))).thenReturn(dockerOutput); submission.setDockerAccepted(false); submission.setDockerFeedback("dockerFeedback-test"); mockMvc.perform(MockMvcRequestBuilders.multipart(url) @@ -380,7 +381,7 @@ public void testSubmitFile() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); verify(testRunner, times(0)).runStructureTest(any(), eq(testEntity), any()); - verify(testRunner, times(0)).runDockerTest(any(), eq(testEntity), eq(artifactPath), any()); + verify(testRunner, times(0)).runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId())); /* Unexpected error */ reset(fileRepository); @@ -416,6 +417,7 @@ public void testGetSubmissionFile() throws Exception { when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.of(fileEntity)); mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenReturn(mockedResource); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(path, fileEntity.getName())).thenCallRealMethod(); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) @@ -424,23 +426,12 @@ public void testGetSubmissionFile() throws Exception { HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileEntity.getName())) .andExpect(content().bytes(mockedResource.getInputStream().readAllBytes())); - /* Resource not found */ - mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenReturn(null); - mockMvc.perform(MockMvcRequestBuilders.get(url)) - .andExpect(status().isNotFound()); /* file not found */ when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.empty()); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isNotFound()); - /* Unexpected error */ - when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.of(fileEntity)); - mockedFileHandler.reset(); - mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.get(url)) - .andExpect(status().isInternalServerError()); - /* User can't get submission */ when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); mockMvc.perform(MockMvcRequestBuilders.get(url)) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java index a360aaff..a21c3c63 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java @@ -106,7 +106,10 @@ public void setup() { test.getDockerImage(), test.getDockerTestScript(), test.getDockerTestTemplate(), - test.getStructureTemplate() + test.getStructureTemplate(), + "extraFilesUrl", + "extraFilesName" + ); } @@ -132,7 +135,9 @@ public void testUpdateTest() throws Exception { dockerImage, dockerTestScript, dockerTestTemplate, - structureTemplate + structureTemplate, + "extraFilesUrl", + "extraFilesName" ); /* All checks succeed */ when(testUtil.checkForTestUpdate( @@ -174,7 +179,9 @@ public void testUpdateTest() throws Exception { null, null, null, - null + null, + "extraFilesUrl", + "extraFilesName" ); testUpdateJson = new TestUpdateJson( dockerImageBlank, @@ -285,7 +292,9 @@ public void testPutTest() throws Exception { dockerImage, dockerTestScript, dockerTestTemplate, - structureTemplate + structureTemplate, + "extraFilesUrl", + "extraFilesName" ); /* All checks succeed */ when(testUtil.checkForTestUpdate( @@ -337,7 +346,9 @@ public void testPutTest() throws Exception { null, null, null, - null + null, + "extraFilesUrl", + "extraFilesName" ); reset(testUtil); when(testUtil.checkForTestUpdate( diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java index 83392fef..e7cd6668 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -66,7 +66,7 @@ public void setUp() throws IOException { @Test public void testSaveSubmission() throws Exception { - File savedFile = Filehandler.saveFile(tempDir, file); + File savedFile = Filehandler.saveFile(tempDir, file, Filehandler.SUBMISSION_FILENAME); assertTrue(savedFile.exists()); assertEquals(Filehandler.SUBMISSION_FILENAME, savedFile.getName()); @@ -77,7 +77,7 @@ public void testSaveSubmission() throws Exception { @Test public void testSaveSubmission_dirDoesntExist() throws Exception { - File savedFile = Filehandler.saveFile(tempDir.resolve("nonexistent"), file); + File savedFile = Filehandler.saveFile(tempDir.resolve("nonexistent"), file, Filehandler.SUBMISSION_FILENAME); assertTrue(savedFile.exists()); assertEquals(Filehandler.SUBMISSION_FILENAME, savedFile.getName()); @@ -88,7 +88,7 @@ public void testSaveSubmission_dirDoesntExist() throws Exception { @Test public void testSaveSubmission_errorWhileCreatingDir() throws Exception { - assertThrows(IOException.class, () -> Filehandler.saveFile(Path.of(""), file)); + assertThrows(IOException.class, () -> Filehandler.saveFile(Path.of(""), file, Filehandler.SUBMISSION_FILENAME)); } @Test @@ -96,7 +96,7 @@ public void testSaveSubmission_notAZipFile() { MockMultipartFile notAZipFile = new MockMultipartFile( "notAZipFile.txt", "This is not a zip file".getBytes() ); - assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, notAZipFile)); + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, notAZipFile, Filehandler.SUBMISSION_FILENAME)); } @Test @@ -104,12 +104,12 @@ public void testSaveSubmission_fileEmpty() { MockMultipartFile emptyFile = new MockMultipartFile( "emptyFile.txt", new byte[0] ); - assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, emptyFile)); + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, emptyFile, Filehandler.SUBMISSION_FILENAME)); } @Test public void testSaveSubmission_fileNull() { - assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, null)); + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, null, Filehandler.SUBMISSION_FILENAME)); } @Test diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java index 828a0f7d..d9a7434f 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java @@ -49,6 +49,7 @@ public class TestRunnerTest { private SubmissionResult submissionResult; private DockerTestOutput dockerTestOutput; private DockerTemplateTestOutput dockerTemplateTestOutput; + private final long projectId = 876L; @BeforeEach public void setUp() { @@ -108,7 +109,7 @@ public void testRunDockerTest() throws IOException { .thenReturn(dockerTemplateTestOutput); when(dockerModel.getArtifacts()).thenReturn(artifacts); - DockerOutput result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel); + DockerOutput result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(1)).addZipInputFiles(file); @@ -117,7 +118,7 @@ public void testRunDockerTest() throws IOException { /* artifacts are empty */ when(dockerModel.getArtifacts()).thenReturn(Collections.emptyList()); - result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(2)).addZipInputFiles(file); verify(dockerModel, times(2)).cleanUp(); @@ -125,7 +126,7 @@ public void testRunDockerTest() throws IOException { /* aritifacts are null */ when(dockerModel.getArtifacts()).thenReturn(null); - result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(3)).addZipInputFiles(file); verify(dockerModel, times(3)).cleanUp(); @@ -134,19 +135,19 @@ public void testRunDockerTest() throws IOException { /* No template */ testEntity.setDockerTestTemplate(null); when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenReturn(dockerTestOutput); - result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); assertEquals(dockerTestOutput, result); verify(dockerModel, times(4)).addZipInputFiles(file); verify(dockerModel, times(4)).cleanUp(); /* Error gets thrown */ when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenThrow(new RuntimeException("Error")); - assertThrows(Exception.class, () -> new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel)); + assertThrows(Exception.class, () -> new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId)); verify(dockerModel, times(5)).cleanUp(); /* No script */ testEntity.setDockerTestScript(null); - result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); assertNull(result); } From f0224e1c42be7fa077a2144be18e530f270bdaed Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 15:59:24 +0200 Subject: [PATCH 061/130] TestControllTest updated --- .../pidgeon/controllers/TestController.java | 15 +- .../controllers/TestControllerTest.java | 213 ++++++++++++++++++ 2 files changed, 220 insertions(+), 8 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index 76ce9c6a..e614fc5b 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -251,7 +251,7 @@ public ResponseEntity uploadExtraTestFiles( try { Path path = Filehandler.getTestExtraFilesPath(projectId); - Filehandler.saveFile(path, file, file.getOriginalFilename()); + Filehandler.saveFile(path, file, Filehandler.EXTRA_TESTFILES_FILENAME); FileEntity fileEntity = new FileEntity(); fileEntity.setName(file.getOriginalFilename()); @@ -283,20 +283,19 @@ public ResponseEntity deleteExtraTestFiles( try { - FileEntity fileEntity = fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); + FileEntity fileEntity = testEntity.getExtraFilesId() == null ? + null : fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); if (fileEntity == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No extra files found"); } - Path path = Path.of(fileEntity.getPath()); - Filehandler.deleteLocation(path.toFile()); - testEntity.setExtraFilesId(null); testEntity = testRepository.save(testEntity); - fileRepository.delete(fileEntity); - - + CheckResult delResult = fileUtil.deleteFileById(fileEntity.getId()); + if (!delResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(delResult.getStatus()).body(delResult.getMessage()); + } return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); } catch (Exception e) { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java index a21c3c63..7a7cfb6a 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java @@ -4,6 +4,7 @@ import com.ugent.pidgeon.CustomObjectMapper; import com.ugent.pidgeon.model.json.TestJson; import com.ugent.pidgeon.model.json.TestUpdateJson; +import com.ugent.pidgeon.postgre.models.FileEntity; import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.TestEntity; @@ -13,20 +14,28 @@ import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.CommonDatabaseActions; import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.FileUtil; import com.ugent.pidgeon.util.Filehandler; import com.ugent.pidgeon.util.Pair; import com.ugent.pidgeon.util.ProjectUtil; import com.ugent.pidgeon.util.TestUtil; +import java.io.File; +import java.io.FileOutputStream; import java.time.OffsetDateTime; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -45,6 +54,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -61,12 +71,17 @@ public class TestControllerTest extends ControllerTest{ private TestRepository testRepository; @Mock private ProjectRepository projectRepository; + @Mock + private FileRepository fileRepository; @Mock private EntityToJsonConverter entityToJsonConverter; @Mock private CommonDatabaseActions commonDatabaseActions; + @Mock + private FileUtil fileUtil; + @InjectMocks private TestController testController; @@ -74,6 +89,8 @@ public class TestControllerTest extends ControllerTest{ private ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + private MockMultipartFile mockMultipartFile; + private FileEntity fileEntity; private ProjectEntity project; private TestEntity test; private TestJson testJson; @@ -112,6 +129,12 @@ public void setup() { ); + byte[] fileContent = "Your file content".getBytes(); + mockMultipartFile = new MockMultipartFile("file", "filename.txt", + MediaType.TEXT_PLAIN_VALUE, fileContent); + + fileEntity = new FileEntity("name", "dir/name", 1L); + fileEntity.setId(77L); } @Test @@ -683,4 +706,194 @@ public void testDeleteTest() throws Exception { mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isIAmATeapot()); } + + public static File createTestFile() throws IOException { + // Create a temporary directory + File tempDir = Files.createTempDirectory("test-dir").toFile(); + + // Create a temporary file within the directory + File tempFile = File.createTempFile("test-file", ".zip", tempDir); + + // Create some content to write into the zip file + String content = "Hello, this is a test file!"; + byte[] bytes = content.getBytes(); + + // Write the content into a file inside the zip file + try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempFile))) { + ZipEntry entry = new ZipEntry("test.txt"); + zipOut.putNextEntry(entry); + zipOut.write(bytes); + zipOut.closeEntry(); + } + + // Return the File object representing the zip file + return tempFile; + } + + @Test + public void testUploadExtraTestFiles() throws IOException { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + Path savePath = Path.of("savePath"); + File file = createTestFile(); + + try (MockedStatic mockedFilehandler = mockStatic(Filehandler.class)) { + mockedFilehandler.when(() -> Filehandler.getTestExtraFilesPath(project.getId())).thenReturn(savePath); + mockedFilehandler.when(() -> Filehandler.saveFile(savePath, mockMultipartFile, Filehandler.EXTRA_TESTFILES_FILENAME)) + .thenReturn(file); + + when(fileRepository.save(argThat( + fileEntity -> fileEntity.getName().equals(mockMultipartFile.getOriginalFilename()) && + fileEntity.getPath() + .equals(savePath.resolve(Filehandler.EXTRA_TESTFILES_FILENAME).toString()) && + fileEntity.getUploadedBy() == getMockUser().getId() + ))).thenReturn(fileEntity); + + when(testRepository.save(test)).thenReturn(test); + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(testRepository, times(1)).save(test); + assertEquals(fileEntity.getId(), test.getExtraFilesId()); + + /* Unexpected error */ + mockedFilehandler.when(() -> Filehandler.saveFile(savePath, mockMultipartFile, Filehandler.EXTRA_TESTFILES_FILENAME)) + .thenThrow(new IOException("Unexpected error")); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isInternalServerError()); + + /* Check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isIAmATeapot()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testDeleteExtraFiles() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + test.setExtraFilesId(fileEntity.getId()); + + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.of(fileEntity)); + when(testRepository.save(test)).thenReturn(test); + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(testRepository, times(1)).save(test); + verify(fileUtil, times(1)).deleteFileById(fileEntity.getId()); + assertNull(test.getExtraFilesId()); + + /* Unexpected error when deleting file */ + test.setExtraFilesId(fileEntity.getId()); + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Unexpected error", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + + /* Error thrown */ + test.setExtraFilesId(fileEntity.getId()); + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenThrow(new RuntimeException("Error thrown")); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isInternalServerError()); + + /* No extra files */ + test.setExtraFilesId(null); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isNotFound()); + + /* Check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void getExtraTestFiles() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + + ResponseEntity mockResponseEntity = ResponseEntity.ok().build(); + test.setExtraFilesId(fileEntity.getId()); + + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.of(fileEntity)); + + try (MockedStatic mockedFilehandler = mockStatic(Filehandler.class)) { + mockedFilehandler.when(() -> Filehandler.getZipFileAsResponse(argThat( + path -> path.toString().equals(fileEntity.getPath()) + ), eq(fileEntity.getName()))) + .thenReturn(mockResponseEntity); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()); + + /* Files not found */ + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.empty()); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* No extra files */ + test.setExtraFilesId(null); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } From af3fe07223686d13c3d4bf512f4391a2d42def16 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 16:27:51 +0200 Subject: [PATCH 062/130] All test updated now DockerSubmissionTestTest still fails for some reason --- .../pidgeon/util/EntityToJsonConverter.java | 2 +- .../com/ugent/pidgeon/util/Filehandler.java | 26 ++++----- .../docker/DockerSubmissionTestTest.java | 16 +++--- .../util/EntityToJsonConverterTest.java | 16 ++++++ .../ugent/pidgeon/util/FileHandlerTest.java | 54 ++++++++++++++++-- .../ugent/pidgeon/util/TestRunnerTest.java | 11 +++- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 7 files changed, 94 insertions(+), 31 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index db8923c5..4babf139 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -268,7 +268,7 @@ public TestJson testEntityToTestJson(TestEntity testEntity, long projectId) { testEntity.getDockerTestScript(), testEntity.getDockerTestTemplate(), testEntity.getStructureTemplate(), - testEntity.getExtraFilesId() == null ? null : ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/extrafiles", + testEntity.getExtraFilesId() == null ? null : ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/extrafiles", extrafiles == null ? null : extrafiles.getName() ); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index 7f1cc483..c7b6b332 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -195,22 +195,18 @@ public static void copyFilesAsZip(List files, Path path) throws IOExceptio public static ResponseEntity getZipFileAsResponse(Path path, String filename) { // Get the file from the server - try { - Resource zipFile = Filehandler.getFileAsResource(path); - if (zipFile == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found."); - } + Resource zipFile = Filehandler.getFileAsResource(path); + if (zipFile == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found."); + } - // Set headers for the response - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); - headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); + // Set headers for the response + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); + headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); - return ResponseEntity.ok() - .headers(headers) - .body(zipFile); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); - } + return ResponseEntity.ok() + .headers(headers) + .body(zipFile); } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java index 3f8dac56..38945741 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -223,15 +223,15 @@ void testDockerReceivesUtilFiles(){ Path zipLocation2 = Path.of("src/test/test-cases/DockerSubmissionTestTest/helloworld.zip"); // complicated zip with multiple files and folder structure stm.addUtilFiles(zipLocation); stm.addUtilFiles(zipLocation2); - DockerTestOutput to = stm.runSubmission("find /shared/utils/"); + DockerTestOutput to = stm.runSubmission("find /shared/extra/"); List logs = to.logs.stream().map(log -> log.replaceAll("\n", "")).toList(); - assertEquals("/shared/utils/", logs.get(0)); - assertEquals("/shared/utils/helloworld.txt", logs.get(1)); - assertEquals("/shared/utils/helloworld", logs.get(2)); - assertEquals("/shared/utils/helloworld/helloworld2.txt", logs.get(3)); // I don't understand the order of find :sob: but it is important all files are found. - assertEquals("/shared/utils/helloworld/helloworld3.txt", logs.get(4)); - assertEquals("/shared/utils/helloworld/emptyfolder", logs.get(5)); - assertEquals("/shared/utils/helloworld/helloworld1.txt", logs.get(6)); + assertEquals("/shared/extra/", logs.get(0)); + assertEquals("/shared/extra/helloworld.txt", logs.get(1)); + assertEquals("/shared/extra/helloworld", logs.get(2)); + assertEquals("/shared/extra/helloworld/helloworld2.txt", logs.get(3)); // I don't understand the order of find :sob: but it is important all files are found. + assertEquals("/shared/extra/helloworld/helloworld3.txt", logs.get(4)); + assertEquals("/shared/extra/helloworld/emptyfolder", logs.get(5)); + assertEquals("/shared/extra/helloworld/helloworld1.txt", logs.get(6)); stm.cleanUp(); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index fa1a2daa..45f5fb0b 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -76,6 +76,9 @@ public class EntityToJsonConverterTest { @Mock private SubmissionRepository submissionRepository; + @Mock + private FileRepository fileRepository; + @Spy @InjectMocks private EntityToJsonConverter entityToJsonConverter; @@ -558,12 +561,25 @@ public void testGetSubmissionJson() { @Test public void testTestEntityToTestJson() { + testEntity.setExtraFilesId(5L); + when(fileRepository.findById(testEntity.getExtraFilesId())) + .thenReturn(Optional.of(new FileEntity("nameoffiles", "path", 5L))); + TestJson result = entityToJsonConverter.testEntityToTestJson(testEntity, projectEntity.getId()); + + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(), result.getProjectUrl()); assertEquals(testEntity.getDockerImage(), result.getDockerImage()); assertEquals(testEntity.getDockerTestScript(), result.getDockerScript()); assertEquals(testEntity.getDockerTestTemplate(), result.getDockerTemplate()); assertEquals(testEntity.getStructureTemplate(), result.getStructureTest()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/tests/extrafiles", result.getExtraFilesUrl()); + assertEquals("nameoffiles", result.getExtraFilesName()); + + testEntity.setExtraFilesId(null); + result = entityToJsonConverter.testEntityToTestJson(testEntity, projectEntity.getId()); + assertNull(result.getExtraFilesUrl()); + assertNull(result.getExtraFilesName()); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java index e7cd6668..67376b28 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -29,6 +29,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; @ExtendWith(MockitoExtension.class) @@ -65,7 +66,7 @@ public void setUp() throws IOException { } @Test - public void testSaveSubmission() throws Exception { + public void testSaveFile() throws Exception { File savedFile = Filehandler.saveFile(tempDir, file, Filehandler.SUBMISSION_FILENAME); assertTrue(savedFile.exists()); @@ -76,7 +77,7 @@ public void testSaveSubmission() throws Exception { } @Test - public void testSaveSubmission_dirDoesntExist() throws Exception { + public void testSaveFile_dirDoesntExist() throws Exception { File savedFile = Filehandler.saveFile(tempDir.resolve("nonexistent"), file, Filehandler.SUBMISSION_FILENAME); assertTrue(savedFile.exists()); @@ -87,12 +88,12 @@ public void testSaveSubmission_dirDoesntExist() throws Exception { } @Test - public void testSaveSubmission_errorWhileCreatingDir() throws Exception { + public void testSaveFile_errorWhileCreatingDir() throws Exception { assertThrows(IOException.class, () -> Filehandler.saveFile(Path.of(""), file, Filehandler.SUBMISSION_FILENAME)); } @Test - public void testSaveSubmission_notAZipFile() { + public void testSaveFile_notAZipFile() { MockMultipartFile notAZipFile = new MockMultipartFile( "notAZipFile.txt", "This is not a zip file".getBytes() ); @@ -100,7 +101,7 @@ public void testSaveSubmission_notAZipFile() { } @Test - public void testSaveSubmission_fileEmpty() { + public void testSaveFile_fileEmpty() { MockMultipartFile emptyFile = new MockMultipartFile( "emptyFile.txt", new byte[0] ); @@ -108,7 +109,7 @@ public void testSaveSubmission_fileEmpty() { } @Test - public void testSaveSubmission_fileNull() { + public void testSaveFile_fileNull() { assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, null, Filehandler.SUBMISSION_FILENAME)); } @@ -255,6 +256,12 @@ public void testGetSubmissionArtifactPath() { assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", "2", "3", "artifacts.zip"), submissionArtifactPath); } + @Test + public void testGetTextExtraFilesPath() { + Path textExtraFilesPath = Filehandler.getTestExtraFilesPath(88); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", String.valueOf(88)), textExtraFilesPath); + } + @Test public void testGetFileAsResource_FileExists() { try { @@ -380,4 +387,39 @@ public void testCopyFilesAsZip_zipFileAlreadyExistNonWriteable() throws IOExcept } } + @Test + public void testGetZipFileAsResponse() throws IOException { + List files = new ArrayList<>(); + File tempFile1 = Files.createTempFile("tempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("tempFile2", ".txt").toFile(); + + try { + files.add(tempFile1); + files.add(tempFile2); + + File zipFile = tempDir.resolve("files.zip").toFile(); + Filehandler.copyFilesAsZip(files, zipFile.toPath()); + + assertTrue(zipFile.exists()); + + ResponseEntity response = Filehandler.getZipFileAsResponse(zipFile.toPath(), "customfilename.zip"); + + assertNotNull(response); + assertEquals(200, response.getStatusCodeValue()); + assertEquals("attachment; filename=customfilename.zip", response.getHeaders().get("Content-Disposition").get(0)); + assertEquals("application/zip", response.getHeaders().get("Content-Type").get(0)); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testGetZipFileAsResponse_fileDoesNotExist() { + ResponseEntity response = Filehandler.getZipFileAsResponse(Path.of("nonexistent"), "customfilename.zip"); + + assertNotNull(response); + assertEquals(404, response.getStatusCodeValue()); + } + } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java index d9a7434f..04a4a42f 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java @@ -96,8 +96,12 @@ public void testRunStructureTest() throws IOException { @Test public void testRunDockerTest() throws IOException { + Path outputPath = Path.of("outputPath"); + Path extraFilesPath = Path.of("extraFilesPath"); + Path extraFilesPathResolved = extraFilesPath.resolve(Filehandler.EXTRA_TESTFILES_FILENAME); + try (MockedStatic filehandler = org.mockito.Mockito.mockStatic(Filehandler.class)) { - Path outputPath = Path.of("outputPath"); + AtomicInteger filehandlerCalled = new AtomicInteger(); filehandlerCalled.set(0); filehandler.when(() -> Filehandler.copyFilesAsZip(artifacts, outputPath)).thenAnswer( @@ -105,6 +109,7 @@ public void testRunDockerTest() throws IOException { filehandlerCalled.getAndIncrement(); return null; }); + filehandler.when(() -> Filehandler.getTestExtraFilesPath(projectId)).thenReturn(extraFilesPath); when(dockerModel.runSubmissionWithTemplate(testEntity.getDockerTestScript(), testEntity.getDockerTestTemplate())) .thenReturn(dockerTemplateTestOutput); when(dockerModel.getArtifacts()).thenReturn(artifacts); @@ -114,6 +119,7 @@ public void testRunDockerTest() throws IOException { verify(dockerModel, times(1)).addZipInputFiles(file); verify(dockerModel, times(1)).cleanUp(); + verify(dockerModel, times(1)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* artifacts are empty */ @@ -122,6 +128,7 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(2)).addZipInputFiles(file); verify(dockerModel, times(2)).cleanUp(); + verify(dockerModel, times(2)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* aritifacts are null */ @@ -130,6 +137,7 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTemplateTestOutput, result); verify(dockerModel, times(3)).addZipInputFiles(file); verify(dockerModel, times(3)).cleanUp(); + verify(dockerModel, times(3)).addUtilFiles(extraFilesPathResolved); assertEquals(1, filehandlerCalled.get()); /* No template */ @@ -139,6 +147,7 @@ public void testRunDockerTest() throws IOException { assertEquals(dockerTestOutput, result); verify(dockerModel, times(4)).addZipInputFiles(file); verify(dockerModel, times(4)).cleanUp(); + verify(dockerModel, times(4)).addUtilFiles(extraFilesPathResolved); /* Error gets thrown */ when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenThrow(new RuntimeException("Error")); diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 4e389ca3afc1b890a7685fc0d8db9af2ea9be82b..b6b88bf08700b995cf09e5f2a14a83eeb91444a0 100644 GIT binary patch delta 28 hcmZ3)xQLNAz?+#xgn@&DgMq1e<3wJ6W)Kzc3; Date: Fri, 17 May 2024 17:06:55 +0200 Subject: [PATCH 063/130] Updated tests --- .../postgre/models/SubmissionEntity.java | 6 +++ .../controllers/SubmissionControllerTest.java | 47 +++++++++++++++++- .../util/CommonDataBaseActionsTest.java | 2 +- .../util/EntityToJsonConverterTest.java | 9 +++- .../ugent/pidgeon/util/FileHandlerTest.java | 16 +++++- .../com/ugent/pidgeon/util/GroupUtilTest.java | 12 +++++ .../pidgeon/util/SubmissionUtilTest.java | 34 +++++++++---- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 8 files changed, 109 insertions(+), 17 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java index 1edc07b8..0b14e27e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java @@ -60,6 +60,10 @@ public Long getGroupId() { return groupId; } + public void setGroupId(Long groupId) { + this.groupId = groupId; + } + public long getFileId() { return fileId; } @@ -155,4 +159,6 @@ public DockerTestType getDockerTestType() { public void setDockerType(DockerTestType dockerType) { this.dockerType = dockerType.toString(); } + + } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index 21b496ae..cd2b8dc9 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -137,7 +137,7 @@ public static File createTestFile() throws IOException { public void setup() { setUpController(submissionController); - submission = new SubmissionEntity(22, 45, 99L, OffsetDateTime.MIN, true, true); + submission = new SubmissionEntity(22L, 45L, 99L, OffsetDateTime.MIN, true, true); submission.setId(56L); groupIds = List.of(45L); submissionJson = new SubmissionJson( @@ -527,4 +527,49 @@ public void testGetSubmissionsForGroup() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } + + @Test + public void testGetAdminSubmissions() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/adminsubmissions"; + + /* all checks succeed */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(submissionRepository.findAdminSubmissionsByProjectId(submission.getProjectId())) + .thenReturn(List.of(submission)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(submissionJson)))); + } catch (Exception e) { + e.printStackTrace(); + } + + /* No submissions */ + when(submissionRepository.findAdminSubmissionsByProjectId(submission.getProjectId())) + .thenReturn(List.of()); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + } catch (Exception e) { + e.printStackTrace(); + } + + /* User can't get project */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + e.printStackTrace(); + } + } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java index 8fedae77..f0cabe86 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java @@ -144,7 +144,7 @@ public void setUp() { submissionEntity = new SubmissionEntity( 22, - 45, + 45L, 99L, OffsetDateTime.MIN, true, diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index fa1a2daa..3143b958 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -209,7 +209,7 @@ public void setUp() { submissionEntity = new SubmissionEntity( 22, - 45, + 45L, 99L, OffsetDateTime.MIN, true, @@ -414,7 +414,7 @@ public void testProjectEntityToProjectResponseJsonWithStatus() { @Test public void testProjectEntityToProjectResponseJson() { GroupEntity secondGroup = new GroupEntity("secondGroup", groupClusterEntity.getId()); - SubmissionEntity secondSubmission = new SubmissionEntity(22, 232, 90L, OffsetDateTime.MIN, true, true); + SubmissionEntity secondSubmission = new SubmissionEntity(22, 232L, 90L, OffsetDateTime.MIN, true, true); CourseUserEntity courseUser = new CourseUserEntity(projectEntity.getCourseId(), userEntity.getId(), CourseRelation.creator); when(projectRepository.findGroupIdsByProjectId(projectEntity.getId())).thenReturn(List.of(groupEntity.getId(), secondGroup.getId())); @@ -554,6 +554,11 @@ public void testGetSubmissionJson() { assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); assertFalse(result.getDockerFeedback().allowed()); + + /* Group id is null */ + submissionEntity.setGroupId(null); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertNull(result.getGroupUrl()); } @Test diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java index 89efd857..af29bee8 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -245,16 +245,28 @@ public void testDeleteLocation_parentDirIsNull() throws IOException { @Test public void testGetSubmissionPath() { - Path submissionPath = Filehandler.getSubmissionPath(1, 2, 3); + Path submissionPath = Filehandler.getSubmissionPath(1, 2L, 3); assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", "2", "3"), submissionPath); } + @Test + public void testGetSubmissionPath_groupIdIsNull() { + Path submissionPath = Filehandler.getSubmissionPath(1, null, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", Filehandler.ADMIN_SUBMISSION_FOLDER, "3"), submissionPath); + } + @Test public void testGetSubmissionArtifactPath() { - Path submissionArtifactPath = Filehandler.getSubmissionArtifactPath(1, 2, 3); + Path submissionArtifactPath = Filehandler.getSubmissionArtifactPath(1, 2L, 3); assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", "2", "3", "artifacts.zip"), submissionArtifactPath); } + @Test + public void testGetSubmissionArtifactPath_groupIdIsNull() { + Path submissionArtifactPath = Filehandler.getSubmissionArtifactPath(1, null, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", Filehandler.ADMIN_SUBMISSION_FOLDER, "3", "artifacts.zip"), submissionArtifactPath); + } + @Test public void testGetFileAsResource_FileExists() { try { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java index 145b1cc9..0870b292 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java @@ -317,6 +317,18 @@ public void testCanGetProjectGroupData() throws Exception { when(projectUtil.getProjectIfExists(project.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Check if groupId is null (eg: adminsubmission) */ + /* User is admin of project */ + when(projectUtil.getProjectIfExists(project.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", project)); + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + result = groupUtil.canGetProjectGroupData(null, project.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User is not admin of project */ + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + result = groupUtil.canGetProjectGroupData(null, project.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java index 0875693c..e0040ff8 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java @@ -55,7 +55,7 @@ public class SubmissionUtilTest { public void setUp() { submissionEntity = new SubmissionEntity( 22, - 45, + 45L, 99L, OffsetDateTime.MIN, true, @@ -84,7 +84,7 @@ public void setUp() { groupEntity = new GroupEntity( "groupName", - 52L + projectEntity.getGroupClusterId() ); groupEntity.setId(4L); @@ -149,16 +149,27 @@ public void testCheckOnSubmit() { CheckResult result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.OK, result.getStatus()); + /* User not part of group but admin */ + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertNull(result.getData()); + + /* User not part of group and not admin */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "User is not part of a group for this project", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(groupEntity.getId()); + /* Deadline passed */ projectEntity.setDeadline(OffsetDateTime.now().minusDays(1)); result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - /* Project not found */ - when(projectUtil.getProjectIfExists(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Project not found", null)); - result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); - assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); - /* GroupCluster in archived course */ when(groupClusterRepository.inArchivedCourse(groupEntity.getClusterId())).thenReturn(true); result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); @@ -169,15 +180,16 @@ public void testCheckOnSubmit() { result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); - /* User not part of group */ - when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); - result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); /* User not part of project */ when(projectUtil.userPartOfProject(projectEntity.getId(), userEntity.getId())).thenReturn(false); result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Project not found */ + when(projectUtil.getProjectIfExists(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Project not found", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); } diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 4e389ca3afc1b890a7685fc0d8db9af2ea9be82b..3da4a2af46dec4215c2d51ba9bb08013feeaed40 100644 GIT binary patch delta 28 hcmZ3)xQLNAz?+#xgn@&DgJDO<#)-WC%pfY>8318W2qypl delta 28 hcmZ3)xQLNAz?+#xgn@&DgTW?w^+aBOW)Kzc3; Date: Fri, 17 May 2024 17:54:24 +0200 Subject: [PATCH 064/130] visible_after field works --- .../controllers/ProjectController.java | 20 +++++++++++++++++++ .../pidgeon/model/ProjectResponseJson.java | 3 ++- .../ugent/pidgeon/model/json/ProjectJson.java | 9 +++++++++ .../pidgeon/postgre/models/ProjectEntity.java | 11 ++++++++++ .../pidgeon/util/EntityToJsonConverter.java | 3 ++- backend/database/start_database.sql | 4 +++- 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 24bf639d..0cefb37c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -10,6 +10,7 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.time.OffsetDateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -74,6 +75,10 @@ public ResponseEntity getProjects(Auth auth) { CourseRelation relation = courseCheck.getData().getSecond(); if (relation.equals(CourseRelation.enrolled)) { + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + projectRepository.save(project); + } if (project.isVisible()) { enrolledProjects.add(entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(project, course, user)); } @@ -113,6 +118,11 @@ public ResponseEntity getProjectById(@PathVariable Long projectId, Auth auth) } CourseEntity course = courseCheck.getData().getFirst(); CourseRelation relation = courseCheck.getData().getSecond(); + Logger.getGlobal().info("project visible after: " + project.getVisibleAfter().toInstant() + " now: " + OffsetDateTime.now().toInstant()); + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + projectRepository.save(project); + } if (!project.isVisible() && relation.equals(CourseRelation.enrolled)) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Project not found"); } @@ -161,6 +171,8 @@ public ResponseEntity createProject( projectJson.getGroupClusterId(), null, projectJson.isVisible(), projectJson.getMaxScore(), projectJson.getDeadline()); + project.setVisibleAfter(projectJson.getVisibleAfter()); + // Save the project entity ProjectEntity savedProject = projectRepository.save(project); CourseEntity courseEntity = checkAcces.getData(); @@ -180,6 +192,10 @@ private ResponseEntity doProjectUpdate(ProjectEntity project, ProjectJson pro project.setDeadline(projectJson.getDeadline()); project.setMaxScore(projectJson.getMaxScore()); project.setVisible(projectJson.isVisible()); + project.setVisibleAfter(projectJson.getVisibleAfter()); + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + } projectRepository.save(project); return ResponseEntity.ok(entityToJsonConverter.projectEntityToProjectResponseJson(project, courseRepository.findById(project.getCourseId()).get(), user)); } @@ -261,6 +277,10 @@ public ResponseEntity patchProjectById(@PathVariable Long projectId, @Request projectJson.setVisible(project.isVisible()); } + if (projectJson.getVisibleAfter() == null) { + projectJson.setVisibleAfter(project.getVisibleAfter()); + } + CheckResult checkProject = projectUtil.checkProjectJson(projectJson, project.getCourseId()); if (checkProject.getStatus() != HttpStatus.OK) { return ResponseEntity.status(checkProject.getStatus()).body(checkProject.getMessage()); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java index 88e5e0fe..1b4b0ca3 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java @@ -21,5 +21,6 @@ public record ProjectResponseJson( boolean visible, ProjectProgressJson progress, Long groupId, - Long clusterId + Long clusterId, + OffsetDateTime visibleAfter ) {} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java index abbf1b22..11c49711 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java @@ -15,6 +15,7 @@ public class ProjectJson { private Long groupClusterId; private Boolean visible; private Integer maxScore; + private OffsetDateTime visibleAfter; @JsonSerialize(using = OffsetDateTimeSerializer.class) private OffsetDateTime deadline; @@ -79,4 +80,12 @@ public Integer getMaxScore() { public void setMaxScore(Integer maxScore) { this.maxScore = maxScore; } + + public OffsetDateTime getVisibleAfter() { + return visibleAfter; + } + + public void setVisibleAfter(OffsetDateTime visibleAfter) { + this.visibleAfter = visibleAfter; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java index 67dc778a..0735d649 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java @@ -38,6 +38,9 @@ public class ProjectEntity { @Column(name="max_score") private Integer maxScore; + @Column(name = "visible_after") + private OffsetDateTime visibleAfter; + public ProjectEntity(long courseId, String name, String description, long groupClusterId, Long testId, Boolean visible, Integer maxScore, OffsetDateTime deadline) { this.courseId = courseId; this.name = name; @@ -124,4 +127,12 @@ public OffsetDateTime getDeadline() { public void setDeadline(OffsetDateTime deadline) { this.deadline = deadline; } + + public OffsetDateTime getVisibleAfter() { + return visibleAfter; + } + + public void setVisibleAfter(OffsetDateTime visibleAfter) { + this.visibleAfter = visibleAfter; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index 716ae1bf..b3d6d525 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -215,7 +215,8 @@ public ProjectResponseJson projectEntityToProjectResponseJson(ProjectEntity proj project.isVisible(), new ProjectProgressJson(completed, total), groupId, - clusterId + clusterId, + project.getVisibleAfter() ); } diff --git a/backend/database/start_database.sql b/backend/database/start_database.sql index 94cf30e6..4b7f936d 100644 --- a/backend/database/start_database.sql +++ b/backend/database/start_database.sql @@ -9,6 +9,7 @@ CREATE TABLE users ( email VARCHAR(100) UNIQUE NOT NULL, azure_id VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL, + studentnumber VARCHAR(50), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); @@ -73,7 +74,8 @@ CREATE TABLE projects ( deadline TIMESTAMP WITH TIME ZONE NOT NULL, test_id INT REFERENCES tests(test_id), visible BOOLEAN DEFAULT false NOT NULL, - max_score INT + max_score INT, + visible_after TIMESTAMP WITH TIME ZONE DEFAULT NULL ); From 4f94fb40d32e6b4de36728ef3f1ec30886b40290 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 18:04:11 +0200 Subject: [PATCH 065/130] Added studentnumber fields --- .../ugent/pidgeon/auth/JwtAuthenticationFilter.java | 7 +++++-- .../java/com/ugent/pidgeon/auth/RolesInterceptor.java | 2 +- .../src/main/java/com/ugent/pidgeon/model/Auth.java | 1 + .../src/main/java/com/ugent/pidgeon/model/User.java | 4 +++- .../com/ugent/pidgeon/postgre/models/UserEntity.java | 11 ++++++++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java index a4ba6cfc..cf8cd406 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java @@ -83,6 +83,7 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res String lastName; String email; String oid; + String studentnumber; String version = jwt.getClaim("ver").asString(); @@ -92,21 +93,23 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res lastName = jwt.getClaim("family_name").asString(); email = jwt.getClaim("unique_name").asString(); oid = jwt.getClaim("oid").asString(); + studentnumber = jwt.getClaim("studentnumber").asString(); } else if (version.startsWith("2.0")) { displayName = jwt.getClaim("name").asString(); lastName = jwt.getClaim("surname").asString(); firstName = displayName.replace(lastName, "").strip(); email = jwt.getClaim("mail").asString(); oid = jwt.getClaim("oid").asString(); + studentnumber = jwt.getClaim("studentnumber").asString(); } else { throw new JwkException("Invalid OAuth version"); } // print full object - // logger.info(jwt.getClaims()); + logger.info(jwt.getClaims()); - User user = new User(displayName, firstName,lastName, email, oid); + User user = new User(displayName, firstName,lastName, email, oid, studentnumber); Auth authUser = new Auth(user, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authUser); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java index 17952d89..b3f5950a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java @@ -61,7 +61,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if(userEntity == null) { System.out.println("User does not exist, creating new one. user_id: " + auth.getOid()); - userEntity = new UserEntity(auth.getUser().firstName,auth.getUser().lastName, auth.getEmail(), UserRole.student, auth.getOid()); + userEntity = new UserEntity(auth.getUser().firstName,auth.getUser().lastName, auth.getEmail(), UserRole.student, auth.getOid(), auth.getStudentNumber()); OffsetDateTime now = OffsetDateTime.now(); userEntity.setCreatedAt(now); userEntity = userRepository.save(userEntity); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java b/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java index 4add2b68..2ebd970e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java @@ -29,6 +29,7 @@ public String getName(){ public String getEmail(){ return user.email; } + public String getStudentNumber() { return user.studentnumber; } public String getOid(){ return user.oid; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/User.java b/backend/app/src/main/java/com/ugent/pidgeon/model/User.java index 330c74e7..0ce04625 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/User.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/User.java @@ -9,12 +9,14 @@ public class User { public String lastName; public String email; public String oid; + public String studentnumber; - public User (String name, String firstName, String lastName, String email, String oid) { + public User (String name, String firstName, String lastName, String email, String oid, String studentnumber) { this.name = name; this.email = email; this.oid = oid; this.firstName = firstName; this.lastName = lastName; + this.studentnumber = studentnumber; } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java index d39ac2cc..0addda2d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java @@ -34,12 +34,17 @@ public class UserEntity { @Column(name = "created_at") private OffsetDateTime createdAt; - public UserEntity(String name, String surname, String email, UserRole role, String azureId) { + @Column(name = "studentnumber") + private String studentNumber; + + public UserEntity(String name, String surname, String email, UserRole role, String azureId, + String studentNumber) { this.name = name; this.surname = surname; this.email = email; this.role = role.toString(); this.azureId = azureId; + this.studentNumber = studentNumber; } public UserEntity() { @@ -110,5 +115,9 @@ public OffsetDateTime getCreatedAt() { public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public String getStudentNumber() { + return studentNumber; + } } From e106cd885e9d9136c4590fee6993e51351ba9d78 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 18:42:25 +0200 Subject: [PATCH 066/130] Update tests --- .../controllers/ProjectController.java | 2 +- .../pidgeon/controllers/ControllerTest.java | 5 +- .../controllers/CourseControllerTest.java | 9 +- .../GroupMembersControllerTest.java | 4 +- .../controllers/ProjectControllerTest.java | 115 ++++++++++++++++-- .../controllers/UserControllerTest.java | 6 +- .../com/ugent/pidgeon/model/AuthTest.java | 2 +- .../com/ugent/pidgeon/model/UserTest.java | 2 +- .../ugent/pidgeon/util/ClusterUtilTest.java | 2 +- .../ugent/pidgeon/util/CourseUtilTest.java | 2 +- .../util/EntityToJsonConverterTest.java | 13 +- .../pidgeon/util/GroupFeedbackUtilTest.java | 2 +- .../com/ugent/pidgeon/util/GroupUtilTest.java | 4 +- .../ugent/pidgeon/util/ProjectUtilTest.java | 2 +- .../pidgeon/util/SubmissionUtilTest.java | 3 +- .../com/ugent/pidgeon/util/TestUtilTest.java | 3 +- .../com/ugent/pidgeon/util/UserUtilTest.java | 2 +- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 18 files changed, 143 insertions(+), 35 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 0cefb37c..6d3f5742 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -118,7 +118,7 @@ public ResponseEntity getProjectById(@PathVariable Long projectId, Auth auth) } CourseEntity course = courseCheck.getData().getFirst(); CourseRelation relation = courseCheck.getData().getSecond(); - Logger.getGlobal().info("project visible after: " + project.getVisibleAfter().toInstant() + " now: " + OffsetDateTime.now().toInstant()); + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { project.setVisible(true); projectRepository.save(project); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java index b506e8b6..c0e6a073 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java @@ -47,7 +47,7 @@ public class ControllerTest { public void testSetUp() { MockitoAnnotations.openMocks(this); - User user = new User("displayName", "firstName", "lastName", "email", "test"); + User user = new User("displayName", "firstName", "lastName", "email", "test", "studentnummer"); Auth authUser = new Auth(user, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authUser); @@ -57,7 +57,8 @@ public void testSetUp() { user.lastName, user.email, UserRole.teacher, - user.oid + user.oid, + "studentnummer" ); mockUser.setId(1L); authUser.setUserEntity(mockUser); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java index f80af245..6fe060c0 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java @@ -626,7 +626,8 @@ public void testGetProjectsByCourseId() throws Exception { true, new ProjectProgressJson(1, 1), 1L, - 1L + 1L, + OffsetDateTime.now() ); /* If user is in course, return projects */ when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(),getMockUser())) @@ -831,7 +832,7 @@ public void testAddCourseMember() throws Exception { String requestStringAdmin = "{\"userId\": 1, \"relation\": \"course_admin\"}"; String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members"; CourseUserEntity courseUser = new CourseUserEntity(activeCourse.getId(), 1, CourseRelation.enrolled); - UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id"); + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); /* If all checks succeed, return 201 */ when(courseUtil.canUpdateUserInCourse( @@ -907,7 +908,7 @@ public void testUpdateCourseMember() throws Exception { String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members/" + userId; String request = "{\"relation\": \"enrolled\"}"; String adminRequest = "{\"relation\": \"course_admin\"}"; - UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id"); + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); CourseUserEntity enrolledUser = new CourseUserEntity(activeCourse.getId(), userId, CourseRelation.enrolled); CourseUserEntity adminUser = new CourseUserEntity(activeCourse.getId(), userId, CourseRelation.course_admin); /* If all checks succeed, 200 gets returned */ @@ -1004,7 +1005,7 @@ public void testUpdateCourseMember() throws Exception { @Test public void testGetCourseMembers() throws Exception { CourseUserEntity courseUserEntity = new CourseUserEntity(1L, 1L, CourseRelation.enrolled); - UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id"); + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); UserReferenceWithRelation userJson = new UserReferenceWithRelation( new UserReferenceJson("name", "surname", 1L), ""+CourseRelation.enrolled diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java index 70fd1ac9..8793a7dd 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java @@ -54,9 +54,9 @@ public class GroupMembersControllerTest extends ControllerTest { @BeforeEach public void setup() { setUpController(groupMemberController); - userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); userEntity.setId(5L); - userEntity2 = new UserEntity("name2", "surname2", "email2", UserRole.student, "azureid2"); + userEntity2 = new UserEntity("name2", "surname2", "email2", UserRole.student, "azureid2", ""); userEntity2.setId(6L); userReferenceJson = new UserReferenceJson(userEntity.getName(), userEntity.getEmail(), userEntity.getId()); userReferenceJson2 = new UserReferenceJson(userEntity2.getName(), userEntity2.getEmail(), userEntity2.getId()); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java index e95b0b5b..5e0545c4 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java @@ -35,6 +35,8 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -118,7 +120,8 @@ void setUp() { projectEntity.isVisible(), new ProjectProgressJson(0, 0), 1L, - groupClusterId + groupClusterId, + OffsetDateTime.now() ); projectEntity2 = new ProjectEntity( @@ -144,7 +147,8 @@ void setUp() { projectEntity2.isVisible(), new ProjectProgressJson(0, 0), 1L, - groupClusterId + groupClusterId, + OffsetDateTime.now() ); } @@ -180,17 +184,46 @@ void testGetProjects() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); - /* If project is visible and role enrolled, don't return it */ + /* If project isn't visible and role enrolled, don't return it */ + projectEntity2.setVisible(false); + userProjectsJson = new UserProjectsJson( + Collections.emptyList(), + List.of(projectResponseJson) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + /* If project isn't visible but visibleAfter is passed, update visibility */ + projectEntity2.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + userProjectsJson = new UserProjectsJson( + List.of(projectJsonWithStatus), + List.of(projectResponseJson) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + verify(projectRepository, times(1)).save(projectEntity2); + assertTrue(projectEntity2.isVisible()); + + /* If project isn't visible and visibleAfter is in the future, don't return it */ projectEntity2.setVisible(false); + projectEntity2.setVisibleAfter(OffsetDateTime.now().plusDays(1)); userProjectsJson = new UserProjectsJson( Collections.emptyList(), List.of(projectResponseJson) ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + assertFalse(projectEntity2.isVisible()); + /* If a coursecheck fails, return corresponding status */ when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())).thenReturn( new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) @@ -222,6 +255,22 @@ void testGetProject() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isNotFound()); + /* if visibleAfter is passed, update visibility */ + projectEntity.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()); + + verify(projectRepository, times(1)).save(projectEntity); + assertTrue(projectEntity.isVisible()); + + /* If visibleAfter is in the future, return 404 */ + projectEntity.setVisible(false); + projectEntity.setVisibleAfter(OffsetDateTime.now().plusDays(1)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + assertFalse(projectEntity.isVisible()); + /* If user is not enrolled and project not visible, return project */ when(courseUtil.getCourseIfUserInCourse(projectEntity.getCourseId(), getMockUser())).thenReturn( new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.course_admin)) @@ -249,13 +298,15 @@ void testGetProject() throws Exception { @Test public void testCreateProject() throws Exception { String url = ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId() + "/projects"; + projectEntity.setVisibleAfter(OffsetDateTime.now().plusDays(1)); String request = "{\n" + " \"name\": \"" + projectEntity.getName() + "\",\n" + " \"description\": \"" + projectEntity.getDescription() + "\",\n" + " \"groupClusterId\": " + projectEntity.getGroupClusterId() + ",\n" + " \"visible\": " + projectEntity.isVisible() + ",\n" + " \"maxScore\": " + projectEntity.getMaxScore() + ",\n" + - " \"deadline\": \"" + projectEntity.getDeadline() + "\"\n" + + " \"deadline\": \"" + projectEntity.getDeadline() + "\",\n" + + " \"visibleAfter\": \"" + projectEntity.getVisibleAfter() + "\"\n" + "}"; /* If all checks succeed, create course */ @@ -287,6 +338,7 @@ public void testCreateProject() throws Exception { && project.isVisible().equals(projectEntity.isVisible()) && project.getMaxScore().equals(projectEntity.getMaxScore()) && project.getDeadline().toInstant().equals(projectEntity.getDeadline().toInstant()) + && project.getVisibleAfter().toInstant().equals(projectEntity.getVisibleAfter().toInstant()) )); /* If groupClusterId is not provided, use invalid groupClusterId */ @@ -387,7 +439,8 @@ void testPutProjectById() throws Exception { false, new ProjectProgressJson(0, 0), 1L, - groupClusterId * 4 + groupClusterId * 4, + OffsetDateTime.now() ); /* If all checks pass, update and return the project */ when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( @@ -426,6 +479,48 @@ void testPutProjectById() throws Exception { projectEntity.setMaxScore(orginalMaxScore); projectEntity.setDeadline(orginalDeadline); + /* If visible after is passed, update visibility */ + projectEntity.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().minusDays(1) + "\"\n" + + "}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)); + + verify(projectRepository, times(2)).save(projectEntity); + assertTrue(projectEntity.isVisible()); + + /* If visible after isn't passed, don't update visibility */ + projectEntity.setVisible(false); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().plusDays(1) + "\"\n" + + "}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)); + + assertFalse(projectEntity.isVisible()); + + projectEntity.setName(orginalName); + projectEntity.setDescription(orginalDescription); + projectEntity.setGroupClusterId(orginalGroupClusterId); + projectEntity.setVisible(orginalVisible); + projectEntity.setMaxScore(orginalMaxScore); + projectEntity.setDeadline(orginalDeadline); + /* If groupClusterId is not provided, use invalid groupClusterId */ reset(projectUtil); request = "{\n" + @@ -461,9 +556,11 @@ void testPutProjectById() throws Exception { assertEquals(projectEntity.isVisible(), false); assertEquals(projectEntity.getMaxScore(), orginalMaxScore + 33); assertEquals(projectEntity.getDeadline().toInstant(), newDeadline.toInstant()); - verify(projectRepository, times(2)).save(projectEntity); + verify(projectRepository, times(4)).save(projectEntity); projectEntity.setGroupClusterId(orginalGroupClusterId); + + /* If project json is invalid, return corresponding status */ reset(projectUtil); when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( @@ -504,7 +601,8 @@ void testPatchProjectById() throws Exception { " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + " \"visible\": " + false + ",\n" + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + - " \"deadline\": \"" + newDeadline + "\"\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().plusDays(1) + "\"\n" + "}"; String orginalName = projectEntity.getName(); String orginalDescription = projectEntity.getDescription(); @@ -524,7 +622,8 @@ void testPatchProjectById() throws Exception { false, new ProjectProgressJson(0, 0), 1L, - groupClusterId * 4 + groupClusterId * 4, + OffsetDateTime.now() ); /* If all checks pass, update and return the project */ when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java index a60a6cf2..53d3c904 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java @@ -49,7 +49,7 @@ public class UserControllerTest extends ControllerTest { @BeforeEach public void setup() { setUpController(userController); - userEntity = new UserEntity("Bob", "Testman", "email", UserRole.student, "azureId"); + userEntity = new UserEntity("Bob", "Testman", "email", UserRole.student, "azureId", ""); userEntity.setId(74L); mockUserJson = new UserJson(getMockUser()); userJson = new UserJson(userEntity); @@ -260,7 +260,7 @@ public void testUpdateUserById() throws Exception { setMockUserRoles(UserRole.admin); String url = ApiRoutes.USERS_BASE_PATH + "/" + userEntity.getId(); String request = "{\"name\":\"John\",\"surname\":\"Doe\",\"email\":\"john@example.com\",\"role\":\"teacher\"}"; - UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId"); + UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId", ""); updateUserEntity.setId(userEntity.getId()); UserJson updatedUserJson = new UserJson(updateUserEntity); @@ -311,7 +311,7 @@ public void testPatchUserById() throws Exception { setMockUserRoles(UserRole.admin); String url = ApiRoutes.USERS_BASE_PATH + "/" + userEntity.getId(); String request = "{\"name\":\"John\",\"surname\":\"Doe\",\"email\":\"john@example.com\",\"role\":\"teacher\"}"; - UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId"); + UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId", ""); updateUserEntity.setId(userEntity.getId()); UserJson updatedUserJson = new UserJson(updateUserEntity); String originalName = userEntity.getName(); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java index 6f415d42..738f922f 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java @@ -10,7 +10,7 @@ public class AuthTest { - private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456"); + private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456", ""); private final List authLijst = List.of(new SimpleGrantedAuthority("READ_AUTHORITY")); private final Auth auth = new Auth(testUser, authLijst); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java index 38e15043..e410fca3 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java @@ -5,7 +5,7 @@ public class UserTest { - private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456"); + private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456", ""); @Test public void isNotNull() { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java index 37588085..b2450230 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java @@ -54,7 +54,7 @@ public class ClusterUtilTest { public void setUp() { clusterEntity = new GroupClusterEntity(1L, 20, "clustername", 5); clusterEntity.setId(4L); - mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); } @Test diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java index 089851cb..f5593dad 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java @@ -56,7 +56,7 @@ public class CourseUtilTest { @BeforeEach public void setUp() { - user = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + user = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); user.setId(44L); course = new CourseEntity("name", "description",2024); course.setId(9L); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index fa1a2daa..de5c788f 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -131,7 +131,8 @@ public void setUp() { "surname", "email", UserRole.student, - "azureId" + "azureId", + "" ); userEntity.setId(44L); userReferenceJson = new UserReferenceJson( @@ -145,7 +146,8 @@ public void setUp() { "otherSurname", "otherEmail", UserRole.student, - "otherAzureId" + "otherAzureId", + "" ); otherUserReferenceJson = new UserReferenceJson( otherUser.getName() + " " + otherUser.getSurname(), @@ -190,7 +192,8 @@ public void setUp() { projectEntity.isVisible(), new ProjectProgressJson(44, 60), groupEntity.getId(), - groupClusterEntity.getId() + groupClusterEntity.getId(), + OffsetDateTime.now() ); groupFeedbackEntity = new GroupFeedbackEntity( @@ -416,7 +419,7 @@ public void testProjectEntityToProjectResponseJson() { GroupEntity secondGroup = new GroupEntity("secondGroup", groupClusterEntity.getId()); SubmissionEntity secondSubmission = new SubmissionEntity(22, 232, 90L, OffsetDateTime.MIN, true, true); CourseUserEntity courseUser = new CourseUserEntity(projectEntity.getCourseId(), userEntity.getId(), CourseRelation.creator); - + projectEntity.setVisibleAfter(OffsetDateTime.now()); when(projectRepository.findGroupIdsByProjectId(projectEntity.getId())).thenReturn(List.of(groupEntity.getId(), secondGroup.getId())); when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), groupEntity.getId())).thenReturn(Optional.of(submissionEntity)); when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), secondGroup.getId())).thenReturn(Optional.of(secondSubmission)); @@ -443,6 +446,8 @@ public void testProjectEntityToProjectResponseJson() { assertEquals(2, result.progress().total()); assertNull(result.groupId()); // User is a creator/course_admin -> no group assertEquals(groupClusterEntity.getId(), result.clusterId()); + assertEquals(projectEntity.getVisibleAfter(), result.visibleAfter()); + /* TestId is null */ projectEntity.setTestId(null); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java index 6eb7581c..506d1607 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java @@ -52,7 +52,7 @@ public void setup() { 10.0f, "Good job!" ); - mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); mockUser.setId(2L); projectEntity = new ProjectEntity( 13L, diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java index 145b1cc9..f02bcc17 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java @@ -50,7 +50,7 @@ public class GroupUtilTest { public void setup() { group = new GroupEntity("Groupname", 12L); group.setId(54L); - mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); mockUser.setId(10L); groupCluster = new GroupClusterEntity(9L, 5, "cluster test", 20); groupCluster.setId(12L); @@ -137,7 +137,7 @@ public void testCanUpdateGroup() { @Test public void TestCanAddUserToGroup() { long otherUserId = 5L; - UserEntity otherUser = new UserEntity("othername", "othersurname", "otheremail", UserRole.student, "otherazureid"); + UserEntity otherUser = new UserEntity("othername", "othersurname", "otheremail", UserRole.student, "otherazureid", ""); /* All checks succeed */ /* Trying to add yourself to the group */ when(groupRepository.findById(group.getId())).thenReturn(Optional.of(group)); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java index 85ad1db1..347b36a3 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java @@ -52,7 +52,7 @@ public void setUp() { ); projectEntity.setId(64); - mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); mockUser.setId(10L); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java index 0875693c..3930c212 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java @@ -78,7 +78,8 @@ public void setUp() { "surname", "email", UserRole.student, - "azureId" + "azureId", + "" ); userEntity.setId(44L); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java index 3f132ec0..c3ae300b 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java @@ -64,7 +64,8 @@ public void setUp() { "surname", "email", UserRole.student, - "azureId" + "azureId", + "" ); userEntity.setId(44L); testEntity = new TestEntity( diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java index bd9a059f..b9706a56 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java @@ -34,7 +34,7 @@ public class UserUtilTest { @BeforeEach public void setUp() { - user = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); + user = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); user.setId(87L); } diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 4e389ca3afc1b890a7685fc0d8db9af2ea9be82b..d50e8b5a1ff565803ba5881494ffcdb3e354bad5 100644 GIT binary patch delta 28 hcmZ3)xQLNAz?+#xgn@&DgCT3m#)-WC%pfY>830^T2crN0 delta 28 hcmZ3)xQLNAz?+#xgn@&DgTW?w^+aBOW)Kzc3; Date: Fri, 17 May 2024 20:14:23 +0200 Subject: [PATCH 067/130] Add studentnumber to user(reference)json Hide if the user requesting it is not an admin of the course --- .../pidgeon/auth/JwtAuthenticationFilter.java | 6 ++--- .../controllers/ClusterController.java | 20 +++++++++----- .../pidgeon/controllers/CourseController.java | 4 ++- .../pidgeon/controllers/GroupController.java | 16 ++++++++++-- .../controllers/GroupMemberController.java | 14 +++++++--- .../controllers/ProjectController.java | 10 ++++--- .../controllers/SubmissionController.java | 2 +- .../ugent/pidgeon/model/json/UserJson.java | 9 +++++++ .../pidgeon/model/json/UserReferenceJson.java | 11 +++++++- .../postgre/repository/GroupRepository.java | 3 ++- .../pidgeon/util/EntityToJsonConverter.java | 26 ++++++++++++------- 11 files changed, 89 insertions(+), 32 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java index cf8cd406..1ad279e6 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java @@ -93,22 +93,20 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res lastName = jwt.getClaim("family_name").asString(); email = jwt.getClaim("unique_name").asString(); oid = jwt.getClaim("oid").asString(); - studentnumber = jwt.getClaim("studentnumber").asString(); + studentnumber = jwt.getClaim("ugentStudentID").asString(); } else if (version.startsWith("2.0")) { displayName = jwt.getClaim("name").asString(); lastName = jwt.getClaim("surname").asString(); firstName = displayName.replace(lastName, "").strip(); email = jwt.getClaim("mail").asString(); oid = jwt.getClaim("oid").asString(); - studentnumber = jwt.getClaim("studentnumber").asString(); + studentnumber = jwt.getClaim("ugentStudentID").asString(); } else { throw new JwkException("Invalid OAuth version"); } // print full object logger.info(jwt.getClaims()); - - User user = new User(displayName, firstName,lastName, email, oid, studentnumber); Auth authUser = new Auth(user, new ArrayList<>()); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java index 6508be3c..55f01119 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java @@ -65,10 +65,15 @@ public ResponseEntity getClustersForCourse(@PathVariable("courseid") Long cou if (checkResult.getStatus() != HttpStatus.OK) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + + CourseRelation courseRelation = checkResult.getData().getSecond(); + boolean hideStudentNumber = courseRelation.equals(CourseRelation.enrolled); + // Get the clusters for the course List clusters = groupClusterRepository.findClustersWithoutInvidualByCourseId(courseid); List clusterJsons = clusters.stream().map( - entityToJsonConverter::clusterEntityToClusterJson).toList(); + g -> entityToJsonConverter.clusterEntityToClusterJson(g, hideStudentNumber) + ).toList(); // Return the clusters return ResponseEntity.ok(clusterJsons); } @@ -113,7 +118,7 @@ public ResponseEntity createClusterForCourse(@PathVariable("courseid") Long c groupRepository.save(new GroupEntity("Group " + (i + 1), cluster.getId())); } - GroupClusterJson clusterJsonResponse = entityToJsonConverter.clusterEntityToClusterJson(clusterEntity); + GroupClusterJson clusterJsonResponse = entityToJsonConverter.clusterEntityToClusterJson(clusterEntity, false); // Return the cluster return ResponseEntity.status(HttpStatus.CREATED).body(clusterJsonResponse); @@ -138,8 +143,11 @@ public ResponseEntity getCluster(@PathVariable("clusterid") Long clusterid, A return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } GroupClusterEntity cluster = checkResult.getData(); + + CheckResult courseAdmin = courseUtil.getCourseIfAdmin(cluster.getCourseId(), auth.getUserEntity()); + boolean hideStudentNumber = !courseAdmin.getStatus().equals(HttpStatus.OK); // Return the cluster - return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(cluster)); + return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(cluster, hideStudentNumber)); } @@ -175,7 +183,7 @@ public ResponseEntity doGroupClusterUpdate(GroupClusterEntity clusterEntity, clusterEntity.setMaxSize(clusterJson.getCapacity()); clusterEntity.setName(clusterJson.getName()); clusterEntity = groupClusterRepository.save(clusterEntity); - return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(clusterEntity)); + return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(clusterEntity, false)); } /** @@ -226,7 +234,7 @@ public ResponseEntity fillCluster(@PathVariable("clusterid") Long clusterid, groupCluster.setGroupAmount(clusterFillJson.getClusterGroupMembers().size()); groupClusterRepository.save(groupCluster); - return ResponseEntity.status(HttpStatus.OK).body(entityToJsonConverter.clusterEntityToClusterJson(groupCluster)); + return ResponseEntity.status(HttpStatus.OK).body(entityToJsonConverter.clusterEntityToClusterJson(groupCluster, false)); } catch (Exception e) { Logger.getGlobal().severe(e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Something went wrong"); @@ -317,6 +325,6 @@ public ResponseEntity createGroupForCluster(@PathVariable("clusterid") Long c cluster.setGroupAmount(cluster.getGroupAmount() + 1); groupClusterRepository.save(cluster); - return ResponseEntity.status(HttpStatus.CREATED).body(entityToJsonConverter.groupEntityToJson(group)); + return ResponseEntity.status(HttpStatus.CREATED).body(entityToJsonConverter.groupEntityToJson(group, false)); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java index fa912b5f..8dfc5072 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java @@ -591,6 +591,8 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + boolean hideStudentNumber = checkResult.getData().getSecond().equals(CourseRelation.enrolled); + List members = courseUserRepository.findAllMembers(courseId); List memberJson = members.stream(). map(cue -> { @@ -598,7 +600,7 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId if (user == null) { return null; } - return entityToJsonConverter.userEntityToUserReferenceWithRelation(user, cue.getRelation()); + return entityToJsonConverter.userEntityToUserReferenceWithRelation(user, cue.getRelation(), hideStudentNumber); }). filter(Objects::nonNull).toList(); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java index bdae9bec..bd579216 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java @@ -9,7 +9,9 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.ClusterUtil; import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.GroupUtil; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +30,10 @@ public class GroupController { private EntityToJsonConverter entityToJsonConverter; @Autowired private CommonDatabaseActions commonDatabaseActions; + @Autowired + private ClusterUtil clusterUtil; + @Autowired + private CourseUtil courseUtil; /** @@ -50,8 +56,14 @@ public ResponseEntity getGroupById(@PathVariable("groupid") Long groupid, Aut return ResponseEntity.status(checkResult1.getStatus()).body(checkResult1.getMessage()); } + boolean hideStudentNumber = true; + CheckResult adminCheck = groupUtil.isAdminOfGroup(groupid, auth.getUserEntity()); + if (adminCheck.getStatus().equals(HttpStatus.OK)) { + hideStudentNumber = false; + } + // Return the group - GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group); + GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group, hideStudentNumber); return ResponseEntity.ok(groupJson); } @@ -113,7 +125,7 @@ private ResponseEntity doGroupNameUpdate(Long groupid, NameRequest nameReques groupRepository.save(group); // Return the updated group - GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group); + GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group, false); return ResponseEntity.ok(groupJson); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java index a3b0161b..e7e9397c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java @@ -111,7 +111,9 @@ public ResponseEntity addMemberToGroup(@PathVariable("groupid") long gro try { groupMemberRepository.addMemberToGroup(groupId, memberid); List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map(entityToJsonConverter::userEntityToUserReference).toList(); + List response = members.stream().map( + u -> entityToJsonConverter.userEntityToUserReference(u, false) + ).toList(); return ResponseEntity.ok(response); } catch (Exception e) { Logger.getGlobal().severe(e.getMessage()); @@ -143,7 +145,9 @@ public ResponseEntity addMemberToGroupInferred(@PathVariable("groupid") try { groupMemberRepository.addMemberToGroup(groupId,user.getId()); List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map(entityToJsonConverter::userEntityToUserReference).toList(); + List response = members.stream().map( + u -> entityToJsonConverter.userEntityToUserReference(u, true) + ).toList(); return ResponseEntity.ok(response); } catch (Exception e) { Logger.getGlobal().severe(e.getMessage()); @@ -171,8 +175,12 @@ public ResponseEntity findAllMembersByGroupId(@PathVariable("groupid") l return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + boolean hideStudentnumber = !groupUtil.isAdminOfGroup(groupId, user).getStatus().equals(HttpStatus.OK); + List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map((UserEntity e) -> entityToJsonConverter.userEntityToUserReference(e)).toList(); + List response = members.stream().map( + (UserEntity e) -> entityToJsonConverter.userEntityToUserReference(e, hideStudentnumber)) + .toList(); return ResponseEntity.ok(response); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 6d3f5742..3e68a627 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -312,11 +312,15 @@ public ResponseEntity getGroupsOfProject(@PathVariable Long projectId, Auth a "No groups for this project: use " + memberUrl + " to get the members of the course"); } + boolean hideStudentNumber; + CheckResult adminCheck = projectUtil.isProjectAdmin(projectId, auth.getUserEntity()); + hideStudentNumber = !adminCheck.getStatus().equals(HttpStatus.OK); + List groups = projectRepository.findGroupIdsByProjectId(projectId); List groupjsons = groups.stream() - .map((Long id) -> { - return groupRepository.findById(id).orElse(null); - }).filter(Objects::nonNull).map(entityToJsonConverter::groupEntityToJson).toList(); + .map((Long id) -> groupRepository.findById(id).orElse(null)).filter(Objects::nonNull).map( + g -> entityToJsonConverter.groupEntityToJson(g, hideStudentNumber)) + .toList(); return ResponseEntity.ok(groupjsons); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index 39bdca89..4edec9f9 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -115,7 +115,7 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti if (group == null) { throw new RuntimeException("Group not found"); } - GroupJson groupjson = entityToJsonConverter.groupEntityToJson(group); + GroupJson groupjson = entityToJsonConverter.groupEntityToJson(group, false); GroupFeedbackEntity groupFeedbackEntity = groupFeedbackRepository.getGroupFeedback(groupId, projectid); GroupFeedbackJson groupFeedbackJson; if (groupFeedbackEntity == null) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java index bcba3900..3f8940e3 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java @@ -14,6 +14,7 @@ public class UserJson { private String surname; private String email; private UserRole role; + private String studentNumber; private OffsetDateTime createdAt; @@ -29,6 +30,7 @@ public UserJson(UserEntity entity) { this.email = entity.getEmail(); this.role = entity.getRole(); this.createdAt = entity.getCreatedAt(); + this.studentNumber = entity.getStudentNumber(); // this.courses = new ArrayList<>(); } @@ -96,4 +98,11 @@ public String getProjectUrl() { } public void setProjectUrl(String s){} + public String getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java index b14d4068..1d486fae 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java @@ -4,11 +4,13 @@ public class UserReferenceJson { private String name; private String email; private Long userId; + private String studentNumber; - public UserReferenceJson(String name, String email, Long userId) { + public UserReferenceJson(String name, String email, Long userId, String studentNumber) { this.name = name; this.email = email; this.userId = userId; + this.studentNumber = studentNumber; } public String getEmail() { @@ -39,4 +41,11 @@ public void setName(String name) { } + public String getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java index 1a3e1d07..65af7bc0 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java @@ -36,9 +36,10 @@ public interface UserReference { Long getUserId(); String getName(); String getEmail(); + String getStudentNumber(); } @Query(value= """ - SELECT gu.userId as userId, u.name, CONCAT(u.name, ' ', u.surname) as name, u.email as email + SELECT gu.userId as userId, u.name, CONCAT(u.name, ' ', u.surname) as name, u.email as email, u.studentNumber as studentNumber FROM GroupUserEntity gu JOIN UserEntity u ON u.id = gu.userId WHERE gu.groupId = ?1""") List findGroupUsersReferencesByGroupId(long id); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index b3d6d525..152b2594 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -41,7 +41,7 @@ public class EntityToJsonConverter { private TestRepository testRepository; - public GroupJson groupEntityToJson(GroupEntity groupEntity) { + public GroupJson groupEntityToJson(GroupEntity groupEntity, boolean hideStudentNumber) { GroupClusterEntity cluster = groupClusterRepository.findById(groupEntity.getClusterId()).orElse(null); if (cluster == null) { throw new RuntimeException("Cluster not found"); @@ -54,7 +54,7 @@ public GroupJson groupEntityToJson(GroupEntity groupEntity) { } // Get the members of the group List members = groupRepository.findGroupUsersReferencesByGroupId(groupEntity.getId()).stream().map(user -> - new UserReferenceJson(user.getName(), user.getEmail(), user.getUserId()) + new UserReferenceJson(user.getName(), user.getEmail(), user.getUserId(), hideStudentNumber ? null : user.getStudentNumber()) ).toList(); // Return the group with its members @@ -63,9 +63,9 @@ public GroupJson groupEntityToJson(GroupEntity groupEntity) { } - public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster) { + public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster, boolean hideStudentNumber) { List groups = groupRepository.findAllByClusterId(cluster.getId()).stream().map( - this::groupEntityToJson + g -> groupEntityToJson(g, hideStudentNumber) ).toList(); return new GroupClusterJson( cluster.getId(), @@ -78,20 +78,26 @@ public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster) { ); } - public UserReferenceJson userEntityToUserReference(UserEntity user) { - return new UserReferenceJson(user.getName() + " " + user.getSurname(), user.getEmail(), user.getId()); + public UserReferenceJson userEntityToUserReference(UserEntity user, boolean hideStudentNumber) { + return new UserReferenceJson( + user.getName() + " " + user.getSurname(), + user.getEmail(), user.getId(), + hideStudentNumber ? null : user.getStudentNumber() + ); } - public UserReferenceWithRelation userEntityToUserReferenceWithRelation(UserEntity user, CourseRelation relation) { - return new UserReferenceWithRelation(userEntityToUserReference(user), relation.toString()); + public UserReferenceWithRelation userEntityToUserReferenceWithRelation(UserEntity user, CourseRelation relation, boolean hideStudentNumber) { + return new UserReferenceWithRelation(userEntityToUserReference(user, hideStudentNumber), relation.toString()); } public CourseWithInfoJson courseEntityToCourseWithInfo(CourseEntity course, String joinLink, boolean hideKey) { UserEntity teacher = courseRepository.findTeacherByCourseId(course.getId()); - UserReferenceJson teacherJson = userEntityToUserReference(teacher); + UserReferenceJson teacherJson = userEntityToUserReference(teacher, true); List assistants = courseRepository.findAssistantsByCourseId(course.getId()); - List assistantsJson = assistants.stream().map(this::userEntityToUserReference).toList(); + List assistantsJson = assistants.stream().map( + u -> userEntityToUserReference(u, true) + ).toList(); return new CourseWithInfoJson( course.getId(), From 93f5e827ee891819ed242dea21585a4ff81a9c78 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Fri, 17 May 2024 21:16:43 +0200 Subject: [PATCH 068/130] updated tests --- .../pidgeon/postgre/models/UserEntity.java | 4 ++ .../controllers/ClusterControllerTest.java | 42 +++++++++-- .../controllers/CourseControllerTest.java | 26 +++++-- .../controllers/GroupControllerTest.java | 19 ++++- .../GroupMembersControllerTest.java | 35 +++++++--- .../controllers/ProjectControllerTest.java | 18 ++++- .../controllers/SubmissionControllerTest.java | 4 +- .../pidgeon/global/RolesInterceptorTest.java | 1 + .../util/EntityToJsonConverterTest.java | 65 ++++++++++++++---- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 10 files changed, 173 insertions(+), 41 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java index 0addda2d..0f3a04ca 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java @@ -119,5 +119,9 @@ public void setCreatedAt(OffsetDateTime createdAt) { public String getStudentNumber() { return studentNumber; } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java index 2bd77265..bc3392e3 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java @@ -105,12 +105,26 @@ public void testGetClustersForCourse() throws Exception { when(courseUtil.getCourseIfUserInCourse(courseId, getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.enrolled))); when(groupClusterRepository.findClustersWithoutInvidualByCourseId(courseId)).thenReturn(List.of(groupClusterEntity)); - when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity)).thenReturn(groupClusterJson); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true)).thenReturn(groupClusterJson); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupClusterJson)))); + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, true); + + + /* If user is course_admin, studentnumber isn't hidden */ + when(courseUtil.getCourseIfUserInCourse(courseId, getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.course_admin))); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupClusterJson)))); + + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, false); + /* If a certain check fails, the corresponding status code is returned */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); @@ -129,7 +143,7 @@ public void testCreateClusterForCourse() throws Exception { json -> json.name().equals("test") && json.capacity().equals(20) && json.groupCount().equals(5) ))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupClusterRepository.save(any())).thenReturn(groupClusterEntity); - when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity)).thenReturn(groupClusterJson); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) @@ -158,13 +172,27 @@ public void testGetCluster() throws Exception { String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(); /* If the user has acces to the cluster and it isn't an individual cluster, the cluster is returned */ - when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity)).thenReturn(groupClusterJson); + /* User is not an admin, studentNumber should be hidden */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", courseEntity)); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true)).thenReturn(groupClusterJson); when(clusterUtil.getGroupClusterEntityIfNotIndividual(groupClusterEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, true); + + /* User is an admin, studentNumber should be visible */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", courseEntity)); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, false); + /* If any check fails, the corresponding status code is returned */ when(clusterUtil.getGroupClusterEntityIfNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); mockMvc.perform(MockMvcRequestBuilders.get(url)) @@ -188,7 +216,7 @@ public void testUpdateCluster() throws Exception { copy.setName("newclustername"); GroupClusterJson updated = new GroupClusterJson(1L, "newclustername", 20, 5, OffsetDateTime.now(), Collections.emptyList(), ""); when(groupClusterRepository.save(groupClusterEntity)).thenReturn(copy); - when(entityToJsonConverter.clusterEntityToClusterJson(copy)).thenReturn(updated); + when(entityToJsonConverter.clusterEntityToClusterJson(copy, false)).thenReturn(updated); mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) @@ -318,7 +346,7 @@ public void testPatchCluster() throws Exception { argThat(json -> json.getName() == groupClusterEntity.getName() && json.getCapacity() == groupClusterEntity.getMaxSize()) )).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupClusterRepository.save(groupClusterEntity)).thenReturn(groupClusterEntity); - when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity)).thenReturn(groupClusterJson); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) @@ -339,7 +367,7 @@ public void testPatchCluster() throws Exception { )).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); GroupClusterJson updated = new GroupClusterJson(1L, "newclustername", 22, 5, OffsetDateTime.now(), Collections.emptyList(), ""); when(groupClusterRepository.save(groupClusterEntity)).thenReturn(copy); - when(entityToJsonConverter.clusterEntityToClusterJson(copy)).thenReturn(updated); + when(entityToJsonConverter.clusterEntityToClusterJson(copy, false)).thenReturn(updated); mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) @@ -397,7 +425,7 @@ public void testCreateGroupForCluster() throws Exception { when(groupRepository.save(argThat( group -> group.getName().equals("test") && group.getClusterId() == groupClusterEntity.getId() ))).thenReturn(groupEntity); - when(entityToJsonConverter.groupEntityToJson(groupEntity)).thenReturn(groupJson); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java index 6fe060c0..a712c982 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java @@ -112,7 +112,7 @@ public void setup() { activeCourse.getId(), activeCourse.getName(), activeCourse.getDescription(), - new UserReferenceJson("", "", 0L), + new UserReferenceJson("", "", 0L, ""), new ArrayList<>(), "", "", @@ -308,7 +308,7 @@ public void testUpdateCourse() throws Exception { activeCourse.getId(), "test", "description", - new UserReferenceJson("", "", 0L), + new UserReferenceJson("", "", 0L, ""), new ArrayList<>(), "", "", @@ -403,7 +403,7 @@ public void testPatchCourse() throws Exception { activeCourse.getId(), "test", "description2", - new UserReferenceJson("", "", 0L), + new UserReferenceJson("", "", 0L, ""), new ArrayList<>(), "", "", @@ -1007,21 +1007,33 @@ public void testGetCourseMembers() throws Exception { CourseUserEntity courseUserEntity = new CourseUserEntity(1L, 1L, CourseRelation.enrolled); UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); UserReferenceWithRelation userJson = new UserReferenceWithRelation( - new UserReferenceJson("name", "surname", 1L), + new UserReferenceJson("name", "surname", 1L, ""), ""+CourseRelation.enrolled ); List userList = List.of(courseUserEntity); String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members"; /* If user is in course, return members */ when(courseUtil.getCourseIfUserInCourse(activeCourseJson.courseId(), getMockUser())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.course_admin))); + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.enrolled))); when(courseUserRepository.findAllMembers(activeCourseJson.courseId())).thenReturn(userList); when(userUtil.getUserIfExists(courseUserEntity.getUserId())).thenReturn(user); - when(entityToJsonConverter.userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled)).thenReturn(userJson); + /* User is enrolled so studentNumber should be hidden */ + when(entityToJsonConverter.userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, true)).thenReturn(userJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + verify(entityToJsonConverter, times(1)).userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, true); + + /* If user is admin studentNumber should be visible */ + when(courseUtil.getCourseIfUserInCourse(activeCourseJson.courseId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.course_admin))); + when(entityToJsonConverter.userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, false)).thenReturn(userJson); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + verify(entityToJsonConverter, times(1)).userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, false); /* If user doesn't get found it gets filtered out */ when(userUtil.getUserIfExists(anyLong())).thenReturn(null); @@ -1107,7 +1119,7 @@ public void testCopyCourse() throws Exception { 2L, "name", "description", - new UserReferenceJson("", "", 0L), + new UserReferenceJson("", "", 0L, ""), new ArrayList<>(), "", "", diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java index 798e0d89..76ef9110 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java @@ -29,6 +29,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -74,11 +76,26 @@ public void testGetGroupById() throws Exception { .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); when(groupUtil.canGetGroup(groupEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(entityToJsonConverter.groupEntityToJson(groupEntity)).thenReturn(groupJson); + /* User is admin, student number should not be hidden */ + when(groupUtil.isAdminOfGroup(groupEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(objectMapper.writeValueAsString(groupJson))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + + /* User is not admin, student number should be hidden */ + when(groupUtil.isAdminOfGroup(groupEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, true)).thenReturn(groupJson); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(groupJson))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); + /* If the user doesn't have acces to group, return forbidden */ when(groupUtil.canGetGroup(anyLong(), any())) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java index 8793a7dd..b3074b26 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java @@ -58,8 +58,8 @@ public void setup() { userEntity.setId(5L); userEntity2 = new UserEntity("name2", "surname2", "email2", UserRole.student, "azureid2", ""); userEntity2.setId(6L); - userReferenceJson = new UserReferenceJson(userEntity.getName(), userEntity.getEmail(), userEntity.getId()); - userReferenceJson2 = new UserReferenceJson(userEntity2.getName(), userEntity2.getEmail(), userEntity2.getId()); + userReferenceJson = new UserReferenceJson(userEntity.getName(), userEntity.getEmail(), userEntity.getId(), ""); + userReferenceJson2 = new UserReferenceJson(userEntity2.getName(), userEntity2.getEmail(), userEntity2.getId(), ""); } @Test @@ -117,8 +117,8 @@ public void testAddMemberToGroup() throws Exception { .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupMemberRepository.findAllMembersByGroupId(groupId)) .thenReturn(List.of(userEntity, userEntity2)); - when(entityToJsonConverter.userEntityToUserReference(userEntity)).thenReturn(userReferenceJson); - when(entityToJsonConverter.userEntityToUserReference(userEntity2)).thenReturn(userReferenceJson2); + when(entityToJsonConverter.userEntityToUserReference(userEntity, false)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, false)).thenReturn(userReferenceJson2); mockMvc.perform(MockMvcRequestBuilders.post(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -141,15 +141,15 @@ public void testAddMemberToGroup() throws Exception { @Test public void testAddMemberToGroupInferred() throws Exception { String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId); - UserReferenceJson mockUserJson = new UserReferenceJson(getMockUser().getName(), getMockUser().getEmail(), getMockUser().getId()); + UserReferenceJson mockUserJson = new UserReferenceJson(getMockUser().getName(), getMockUser().getEmail(), getMockUser().getId(), getMockUser().getStudentNumber()); /* If all checks succeed, the user is added to the group */ when(groupUtil.canAddUserToGroup(groupId, getMockUser().getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupMemberRepository.findAllMembersByGroupId(groupId)) .thenReturn(List.of(getMockUser(), userEntity2)); - when(entityToJsonConverter.userEntityToUserReference(getMockUser())).thenReturn(mockUserJson); - when(entityToJsonConverter.userEntityToUserReference(userEntity2)).thenReturn(userReferenceJson2); + when(entityToJsonConverter.userEntityToUserReference(getMockUser(), true)).thenReturn(mockUserJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, true)).thenReturn(userReferenceJson2); mockMvc.perform(MockMvcRequestBuilders.post(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -174,8 +174,10 @@ public void testFindAllMembersByGroupId() throws Exception { List members = List.of(userEntity, userEntity2); List userReferenceJsons = List.of(userReferenceJson, userReferenceJson2); when(groupMemberRepository.findAllMembersByGroupId(groupId)).thenReturn(members); - when(entityToJsonConverter.userEntityToUserReference(userEntity)).thenReturn(userReferenceJson); - when(entityToJsonConverter.userEntityToUserReference(userEntity2)).thenReturn(userReferenceJson2); + /* User is admin of group so don't hide studentNumbers */ + when(groupUtil.isAdminOfGroup(groupId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.userEntityToUserReference(userEntity, false)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, false)).thenReturn(userReferenceJson2); /* If user can get group return list of members */ when(groupUtil.canGetGroup(groupId, getMockUser())) @@ -185,6 +187,21 @@ public void testFindAllMembersByGroupId() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(userReferenceJsons))); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, false); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity2, false); + + /* If user isn't admin, studentNumbers should be hidden */ + when(groupUtil.isAdminOfGroup(groupId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.userEntityToUserReference(userEntity, true)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, true)).thenReturn(userReferenceJson2); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userReferenceJsons))); + + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, true); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity2, true); + /* If use can't get group return corresponding status */ when(groupUtil.canGetGroup(groupId, getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java index 5e0545c4..e17ae6ae 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java @@ -746,7 +746,7 @@ void testPatchProjectById() throws Exception { } @Test - void getGroupsOfProject() throws Exception { + void testGetGroupsOfProject() throws Exception { String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/groups"; GroupEntity groupEntity = new GroupEntity("groupName", 1L); long groupId = 83L; @@ -760,12 +760,26 @@ void getGroupsOfProject() throws Exception { when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(false); when(projectRepository.findGroupIdsByProjectId(projectEntity.getId())).thenReturn(List.of(groupId)); when(grouprRepository.findById(groupId)).thenReturn(Optional.of(groupEntity)); - when(entityToJsonConverter.groupEntityToJson(groupEntity)).thenReturn(groupJson); + /* User is admin so studentNumber shouldn't be hidden */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupJson)))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + + /* If user is not admin, studentNumber should be hidden */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, true)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupJson)))); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); + /* If inidividual cluster return no content */ when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(true); mockMvc.perform(MockMvcRequestBuilders.get(url)) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index 21b496ae..aada0734 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -209,7 +209,7 @@ public void testGetSubmissions() throws Exception { when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(projectRepository.findGroupIdsByProjectId(submission.getProjectId())).thenReturn(groupIds); when(groupRepository.findById(groupIds.get(0))).thenReturn(Optional.of(groupEntity)); - when(entityToJsonConverter.groupEntityToJson(groupEntity)).thenReturn(groupJson); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); when(groupFeedbackRepository.getGroupFeedback(groupEntity.getId(), submission.getProjectId())).thenReturn(groupFeedbackEntity); when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.of(submission)); @@ -219,6 +219,8 @@ public void testGetSubmissions() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(List.of(lastGroupSubmissionJson)))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + /* no submission */ when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.empty()); lastGroupSubmissionJson.setSubmission(null); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java b/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java index bff3e4e1..76b25d2e 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java @@ -76,6 +76,7 @@ void testUserDoesntExistYet() throws Exception { user.getName().equals(getMockUser().getName()) && user.getSurname().equals(getMockUser().getSurname()) && user.getEmail().equals(getMockUser().getEmail()) && + user.getStudentNumber().equals(getMockUser().getStudentNumber()) && duration.getSeconds() < 5; } ))).thenReturn(getMockUser()); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index de5c788f..f4806083 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -138,7 +138,8 @@ public void setUp() { userReferenceJson = new UserReferenceJson( userEntity.getName() + " " + userEntity.getSurname(), userEntity.getEmail(), - userEntity.getId() + userEntity.getId(), + "" ); otherUser = new UserEntity( @@ -152,7 +153,8 @@ public void setUp() { otherUserReferenceJson = new UserReferenceJson( otherUser.getName() + " " + otherUser.getSurname(), otherUser.getEmail(), - otherUser.getId() + otherUser.getId(), + "" ); @@ -222,6 +224,7 @@ public void setUp() { @Test public void testGroupEntityToJson() { + userEntity.setStudentNumber("studentNumber"); when(groupClusterRepository.findById(groupEntity.getClusterId())).thenReturn(Optional.of(groupClusterEntity)); when(groupRepository.findGroupUsersReferencesByGroupId(anyLong())).thenReturn( List.of(new UserReference[]{ @@ -240,11 +243,16 @@ public String getName() { public String getEmail() { return userEntity.getEmail(); } + + @Override + public String getStudentNumber() { + return userEntity.getStudentNumber(); + } } }) ); - GroupJson result = entityToJsonConverter.groupEntityToJson(groupEntity); + GroupJson result = entityToJsonConverter.groupEntityToJson(groupEntity, false); assertEquals(groupClusterEntity.getMaxSize(), result.getCapacity()); assertEquals(groupEntity.getId(), result.getGroupId()); assertEquals(groupEntity.getName(), result.getName()); @@ -254,25 +262,32 @@ public String getEmail() { assertEquals(userEntity.getId(), userReferenceJson.getUserId()); assertEquals(userEntity.getName() + " " + userEntity.getSurname(), userReferenceJson.getName()); assertEquals(userEntity.getEmail(), userReferenceJson.getEmail()); + assertEquals(userEntity.getStudentNumber(), userReferenceJson.getStudentNumber()); /* Cluster is individual */ groupClusterEntity.setMaxSize(1); - result = entityToJsonConverter.groupEntityToJson(groupEntity); + result = entityToJsonConverter.groupEntityToJson(groupEntity, false); assertEquals(1, result.getCapacity()); assertNull(result.getGroupClusterUrl()); + /* StudentNumber gets hidden correctly */ + result = entityToJsonConverter.groupEntityToJson(groupEntity, true); + assertNull(result.getMembers().get(0).getStudentNumber()); + /* Issue when groupClusterEntity is null */ when(groupClusterRepository.findById(groupEntity.getClusterId())).thenReturn(Optional.empty()); - assertThrows(RuntimeException.class, () -> entityToJsonConverter.groupEntityToJson(groupEntity)); + assertThrows(RuntimeException.class, () -> entityToJsonConverter.groupEntityToJson(groupEntity, false)); } @Test public void testClusterEntityToClusterJson() { when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenReturn(List.of(groupEntity)); - doReturn(groupJson).when(entityToJsonConverter).groupEntityToJson(groupEntity); + doReturn(groupJson).when(entityToJsonConverter).groupEntityToJson(groupEntity, false); + + GroupClusterJson result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false); - GroupClusterJson result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); assertEquals(groupClusterEntity.getId(), result.clusterId()); assertEquals(groupClusterEntity.getName(), result.name()); @@ -282,27 +297,49 @@ public void testClusterEntityToClusterJson() { assertEquals(1, result.groups().size()); assertEquals(groupJson, result.groups().get(0)); assertEquals(ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId(), result.courseUrl()); + + /* Hide studentNumber */ + doReturn(groupJson).when(entityToJsonConverter).groupEntityToJson(groupEntity, true); + + result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); } @Test public void testUserEntityToUserReference() { - UserReferenceJson result = entityToJsonConverter.userEntityToUserReference(userEntity); + userEntity.setStudentNumber("studentNumber"); + UserReferenceJson result = entityToJsonConverter.userEntityToUserReference(userEntity, false); assertEquals(userEntity.getId(), result.getUserId()); assertEquals(userEntity.getName() + " " + userEntity.getSurname(), result.getName()); assertEquals(userEntity.getEmail(), result.getEmail()); + assertEquals(userEntity.getStudentNumber(), result.getStudentNumber()); + + /* Hide studentnumber */ + result = entityToJsonConverter.userEntityToUserReference(userEntity, true); + assertNull(result.getStudentNumber()); } @Test public void testUserEntityToUserReferenceWithRelation() { - doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity); - UserReferenceWithRelation result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.creator); + + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, false); + UserReferenceWithRelation result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.creator, false); assertEquals(userReferenceJson, result.getUser()); assertEquals(CourseRelation.creator.toString(), result.getRelation()); - result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.course_admin); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, false); + + /* Hide studentnumber */ + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, true); + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.creator, true); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, true); + + /* Different relations */ + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.course_admin, false); assertEquals(CourseRelation.course_admin.toString(), result.getRelation()); - result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.enrolled); + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.enrolled, false); assertEquals(CourseRelation.enrolled.toString(), result.getRelation()); } @@ -315,8 +352,8 @@ public void testCourseEntityToCourseWithInfo() { when(courseRepository.findTeacherByCourseId(courseEntity.getId())).thenReturn(userEntity); when(courseRepository.findAssistantsByCourseId(courseEntity.getId())).thenReturn(List.of(otherUser)); - doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity); - doReturn(otherUserReferenceJson).when(entityToJsonConverter).userEntityToUserReference(otherUser); + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, true); + doReturn(otherUserReferenceJson).when(entityToJsonConverter).userEntityToUserReference(otherUser, true); CourseWithInfoJson result = entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, joinLink, false); assertEquals(courseEntity.getId(), result.courseId()); diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index d50e8b5a1ff565803ba5881494ffcdb3e354bad5..9ad7ca11633f3de8c62a501ead1805e577a40c7e 100644 GIT binary patch delta 26 gcmZ3)xQLM_z?+#xgn@&DgW>SXi97*JKr+q+08m{A8vp Date: Fri, 17 May 2024 21:21:00 +0200 Subject: [PATCH 069/130] Some small UI changes + fixed submission status --- frontend/src/i18n/en/translation.json | 6 +- frontend/src/i18n/nl/translation.json | 8 +- .../components/SubmissionStatusTag.tsx | 2 +- .../submission/components/SubmissionCard.tsx | 235 +++++------------- .../components/SubmissionCardContent.tsx | 100 ++++++++ 5 files changed, 173 insertions(+), 178 deletions(-) create mode 100644 frontend/src/pages/submission/components/SubmissionCardContent.tsx diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 379302e8..86212f5b 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -185,10 +185,12 @@ "submission": { "submission": "Submission", + "running": "Running tests", "submittedFiles": "Submitted files:", "structuretest": "Structure test results:", - "dockertest": "Docker test results:", - "dockertestAborted": "Docker test aborted. Try again later.", + "dockertest": "Test results:", + "downloadSubmission": "Download submission", + "dockertestAborted": "Tests aborted. Try again later.", "success": "succeeded", "failed": "failed", "expected": "Expected output:", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 2d63eecd..45220312 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -190,16 +190,18 @@ "submission": { "submission": "Indiening", "submittedFiles": "Ingediende bestanden:", + "running": "Testen uitvoeren...", "structuretest": "Resultaten structuurtest:", "dockertest": "Resultaten dockertest:", - "dockertestAborted": "Dockertest afgebroken. Probeer het later nog eens.", + "downloadSubmission": "Download indiening", + "dockertestAborted": "Testen afgebroken. Probeer het later nog eens.", "success": "Geslaagd", "failed": "Niet geslaagd", "expected": "Vewachte output:", "received": "Ontvangen output:", "status": { - "accepted": "Alle testen geslaagd.", - "failed": "Sommige testen niet geslaagd." + "accepted": "Alle testen zijn geslaagd.", + "failed": "Sommige testen zijn niet geslaagd." } }, diff --git a/frontend/src/pages/project/components/SubmissionStatusTag.tsx b/frontend/src/pages/project/components/SubmissionStatusTag.tsx index 7c3beefd..5f455822 100644 --- a/frontend/src/pages/project/components/SubmissionStatusTag.tsx +++ b/frontend/src/pages/project/components/SubmissionStatusTag.tsx @@ -18,7 +18,7 @@ export function createStatusBitVector(submission: GET_Responses[ApiRoutes.SUBMIS if(!submission.structureAccepted){ status |= SubmissionStatus.STRUCTURE_REJECTED } - if(!submission.dockerAccepted){ + if(!submission.dockerFeedback.allowed){ status |= SubmissionStatus.DOCKER_REJECTED } if(status === 0){ diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index e22e325d..f99ff053 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -1,189 +1,80 @@ -import {Card, Spin, theme, Input, Button, Typography} from "antd" -import {useTranslation} from "react-i18next" -import {GET_Responses} from "../../../@types/requests" -import {ApiRoutes} from "../../../@types/requests" -import {ArrowLeftOutlined} from "@ant-design/icons" -import {useNavigate} from "react-router-dom" +import { Card, Spin, theme, Input, Button, Typography } from "antd" +import { useTranslation } from "react-i18next" +import { GET_Responses } from "../../../@types/requests" +import { ApiRoutes } from "../../../@types/requests" +import { ArrowLeftOutlined, DownloadOutlined } from "@ant-design/icons" +import { useNavigate } from "react-router-dom" import "@fontsource/jetbrains-mono" import apiCall from "../../../util/apiFetch" -import {Collapse} from "antd" -import { SubTest } from "../../../@types/requests" -import { useEffect, useState } from "react" +import SubmissionContent from "./SubmissionCardContent" export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] -const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({submission}) => { - const {token} = theme.useToken() - const {t} = useTranslation() - const navigate = useNavigate() - const [filename, setFilename] = useState(null) +const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission }) => { + const { token } = theme.useToken() + const { t } = useTranslation() + const navigate = useNavigate() - useEffect(() => { - const getFileName = async () => { - //mss is er een betere manier om de filename te krijgen, dit werkt maar kan mss langzaam zijn met hele grote files/directories - try { - const response = await apiCall.get(submission.fileUrl, undefined, undefined, { - responseType: 'blob', - transformResponse: [(data) => data], - }); - const contentDisposition = response.headers['content-disposition']; - if (contentDisposition) { - const fileNameMatch = contentDisposition.match(/filename=([^;]+)/); - console.log(fileNameMatch); - if (fileNameMatch && fileNameMatch[1]) { - setFilename(fileNameMatch[1]); // use the filename from the headers - } - } - } catch (err) { - console.log(err); - setFilename(null); - } - }; - - getFileName(); - }, [submission]); - - const downloadSubmission = async () => { - try { - const response = await apiCall.get(submission.fileUrl, undefined, undefined, { - responseType: 'blob', - transformResponse: [(data) => data], - }); - console.log(response); - const url = window.URL.createObjectURL(new Blob([response.data])); - const link = document.createElement('a'); - link.href = url; - const contentDisposition = response.headers['content-disposition']; - console.log(contentDisposition); - let fileName = 'file.zip'; // default filename - if (contentDisposition) { - const fileNameMatch = contentDisposition.match(/filename=([^;]+)/); - console.log(fileNameMatch); - if (fileNameMatch && fileNameMatch[1]) { - fileName = fileNameMatch[1]; // use the filename from the headers - } - } - link.setAttribute('download', fileName); - document.body.appendChild(link); - link.click(); - } catch (err) { - console.error(err); + + const downloadSubmission = async () => { + try { + const response = await apiCall.get(submission.fileUrl, undefined, undefined, { + responseType: "blob", + transformResponse: [(data) => data], + }) + console.log(response) + const url = window.URL.createObjectURL(new Blob([response.data])) + const link = document.createElement("a") + link.href = url + const contentDisposition = response.headers["content-disposition"] + console.log(contentDisposition) + let fileName = "file.zip" // default filename + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename=([^;]+)/) + console.log(fileNameMatch) + if (fileNameMatch && fileNameMatch[1]) { + fileName = fileNameMatch[1] // use the filename from the headers } + } + link.setAttribute("download", fileName) + document.body.appendChild(link) + link.click() + } catch (err) { + console.error(err) } + } - const TestResults: React.FC = ( subTests ) => ( - - {subTests.map((test, index) => { - const successText = test.succes ? t("submission.success") : t("submission.failed"); - const successType = test.succes ? 'success' : 'danger'; - return ( - {`${test.testName}: ${successText}`}} - > - {test.testDescription} - {t("submission.expected")} - {test.correct} - {t("submission.received")} - {test.output} - - ); - })} - - ); - return ( - + return ( + - {t("submission.submission")} + {t("submission.submission")} - } - > - {t("submission.submittedFiles")} - -
      -
    • - -
    • -
    - - {t("submission.structuretest")} - -
      -
    • - {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.structureAccepted ? null : ( -
      - {submission.structureFeedback === null ? ( - - ) : ( - - )} -
      - )} -
    • -
    - - {submission.dockerStatus !== "running" && submission.dockerFeedback.type === "NONE" ? null : (<> - - {t("submission.dockertest")} - -
      -
    • - {submission.dockerStatus === "running" ? ( - - ) : (submission.dockerStatus === "aborted" ? t("submission.dockertestAborted") : <> - {submission.dockerFeedback.allowed ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.dockerFeedback.type === "SIMPLE" ? ( -
      - -
      - ) : (submission.dockerFeedback.type === "TEMPLATE" ? ( - TestResults(submission.dockerFeedback.feedback.subtests) - ) : null)} - )} -
    • -
    - )} -
    - ) + } + extra={ + + } + > + +
    + ) } export default SubmissionCard diff --git a/frontend/src/pages/submission/components/SubmissionCardContent.tsx b/frontend/src/pages/submission/components/SubmissionCardContent.tsx new file mode 100644 index 00000000..f3bcf3ed --- /dev/null +++ b/frontend/src/pages/submission/components/SubmissionCardContent.tsx @@ -0,0 +1,100 @@ +import { Collapse, Flex, Input, Spin, Typography } from "antd" +import { useTranslation } from "react-i18next" +import { SubTest } from "../../../@types/requests" +import { FC } from "react" +import { SubmissionType } from "./SubmissionCard" + +const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) => { + const { t } = useTranslation() + + const TestResults: React.FC = (subTests) => ( + + {subTests.map((test, index) => { + const successText = test.succes ? t("submission.success") : t("submission.failed") + const successType = test.succes ? "success" : "danger" + return ( + {`${test.testName}: ${successText}`}} + > + {test.testDescription} + +
    + {t("submission.expected")} + +
    +
    + {t("submission.received")} + + +
    +
    +
    + ) + })} +
    + ) + if (submission.dockerStatus === "aborted") return {t("submission.dockertestAborted")} + if (submission.dockerStatus === "running") + return ( +
    +
    + +
    +
    + {t("submission.running")} +
    +
    +
    + ) + return ( + <> + {t("submission.structuretest")} + + {submission.dockerStatus === "no_test" &&
      +
    • + {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.structureAccepted ? null : ( +
      + {submission.structureFeedback === null ? ( + + ) : ( + + )} +
      + )} +
    • +
    } + + {submission.dockerStatus === "finished" && ( +
      +
    • + <> + {submission.dockerFeedback.allowed ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.dockerFeedback.type === "SIMPLE" ? ( +
      + +
      + ) : submission.dockerFeedback.type === "TEMPLATE" ? ( + TestResults(submission.dockerFeedback.feedback.subtests) + ) : null} + +
    • +
    + )} + + ) +} + +export default SubmissionContent From 52547489fb4ca98479d53f8ca3454c6d27835999 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 17 May 2024 21:54:18 +0200 Subject: [PATCH 070/130] Updated types --- frontend/src/@types/requests.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 1e0b6399..2b7ecb99 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -68,6 +68,7 @@ export type POST_Requests = { visible: boolean; maxScore: number; deadline: Date | null; + visibleAfter: Date | null; } [ApiRoutes.GROUP_MEMBERS]: { @@ -264,6 +265,7 @@ export type GET_Responses = { email: string name: string userId: number + studentNumber: string | null // Null in case of enrolled/student } [ApiRoutes.USERS]: { name: string @@ -359,6 +361,7 @@ export type GET_Responses = { id: number name: string surname: string + studentNumber: string | null // Null in case of enrolled/student }, [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], [ApiRoutes.USER_COURSES]: { From ef2cb35f3521c1c3790bff679235c601547a93dc Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 17 May 2024 22:03:26 +0200 Subject: [PATCH 071/130] Fixed styles with tabel header --- frontend/src/styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d063ef1b..f5592199 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -103,8 +103,8 @@ html { font-size: 28px; } -.ant-table-row > td.ant-table-column-sort, .ant-table-thead > tr > th.ant-table-column-sort { - background-color: unset ; +.ant-table-row > td.ant-table-column-sort { + background: unset ; } .modal-no-icon .ant-modal-confirm-paragraph, .modal-no-icon .ant-modal-confirm-body { From 9b495e7b096cb85a4e7e020fddc502ee5d9f450f Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 17 May 2024 22:07:10 +0200 Subject: [PATCH 072/130] Fixed more merge conflicts --- frontend/src/@types/requests.d.ts | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index e6dd88d5..22991ec4 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -15,14 +15,10 @@ export enum ApiRoutes { COURSE_GRADES = '/api/courses/:id/grades', COURSE_LEAVE = "api/courses/:courseId/leave", COURSE_COPY = "/api/courses/:courseId/copy", -<<<<<<< HEAD - -======= COURSE_JOIN = "/api/courses/:courseId/join/:courseKey", COURSE_JOIN_WITHOUT_KEY = "/api/courses/:courseId/join", COURSE_JOIN_LINK = "/api/courses/:courseId/joinKey", ->>>>>>> frontend PROJECTS = "api/projects", PROJECT = "api/projects/:id", PROJECT_CREATE = "api/courses/:courseId/projects", @@ -73,11 +69,7 @@ export type POST_Requests = { visible: boolean; maxScore: number; deadline: Date | null; -<<<<<<< HEAD -} -======= } ->>>>>>> frontend [ApiRoutes.GROUP_MEMBERS]: { id: number @@ -93,12 +85,8 @@ export type POST_Requests = { }, [ApiRoutes.PROJECT_TESTS]: Omit [ApiRoutes.COURSE_COPY]: undefined -<<<<<<< HEAD - -======= [ApiRoutes.COURSE_JOIN]: undefined [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: undefined ->>>>>>> frontend } /** @@ -113,12 +101,8 @@ export type POST_Responses = { [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER], [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] [ApiRoutes.COURSE_COPY]: GET_Responses[ApiRoutes.COURSE] -<<<<<<< HEAD - -======= [ApiRoutes.COURSE_JOIN]: {name:string, description: string} [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: POST_Responses[ApiRoutes.COURSE_JOIN] ->>>>>>> frontend } /** @@ -131,11 +115,8 @@ export type DELETE_Requests = { [ApiRoutes.COURSE_LEAVE]: undefined [ApiRoutes.COURSE_MEMBER]: undefined [ApiRoutes.PROJECT_TESTS]: undefined -<<<<<<< HEAD -======= [ApiRoutes.COURSE_JOIN_LINK]: undefined [ApiRoutes.PROJECT_TESTS_UPLOAD]: undefined ->>>>>>> frontend } @@ -148,14 +129,13 @@ export type PUT_Requests = { [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } [ApiRoutes.PROJECT_SCORE]: { score: number | null , feedback: string}, [ApiRoutes.PROJECT_TESTS]: POST_Requests[ApiRoutes.PROJECT_TESTS] -<<<<<<< HEAD [ApiRoutes.USER]: { name: string surname: string email: string role: UserRole } -======= + [ApiRoutes.CLUSTER_FILL]: { [groupName:string]: number[] /* userId[] */ @@ -164,7 +144,6 @@ export type PUT_Requests = { [ApiRoutes.PROJECT_TESTS_UPLOAD]: { file: FormData } ->>>>>>> frontend } @@ -175,12 +154,9 @@ export type PUT_Responses = { [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] -<<<<<<< HEAD -======= [ApiRoutes.CLUSTER_FILL]: PUT_Requests[ApiRoutes.CLUSTER_FILL] [ApiRoutes.COURSE_JOIN_LINK]: ApiRoutes.COURSE_JOIN [ApiRoutes.PROJECT_TESTS_UPLOAD]: undefined ->>>>>>> frontend } @@ -210,11 +186,7 @@ type SubTest = { } type DockerFeedback = { -<<<<<<< HEAD - type: "SIMPLE", -======= type: "SIMPLE", ->>>>>>> frontend feedback: string, // de logs van de dockerrun allowed: boolean // vat samen of de test geslaagd is of niet } | { From 7907a85e0580a159a7390564246ba252f98c736e Mon Sep 17 00:00:00 2001 From: Tristan Verbeken Date: Fri, 17 May 2024 22:46:41 +0200 Subject: [PATCH 073/130] Fixed test to work on all os, by making the find consistent. --- .../pidgeon/docker/DockerSubmissionTestTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java index 38945741..e99e185f 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -224,14 +224,14 @@ void testDockerReceivesUtilFiles(){ stm.addUtilFiles(zipLocation); stm.addUtilFiles(zipLocation2); DockerTestOutput to = stm.runSubmission("find /shared/extra/"); - List logs = to.logs.stream().map(log -> log.replaceAll("\n", "")).toList(); + List logs = to.logs.stream().map(log -> log.replaceAll("\n", "")).sorted().toList(); assertEquals("/shared/extra/", logs.get(0)); - assertEquals("/shared/extra/helloworld.txt", logs.get(1)); - assertEquals("/shared/extra/helloworld", logs.get(2)); - assertEquals("/shared/extra/helloworld/helloworld2.txt", logs.get(3)); // I don't understand the order of find :sob: but it is important all files are found. - assertEquals("/shared/extra/helloworld/helloworld3.txt", logs.get(4)); - assertEquals("/shared/extra/helloworld/emptyfolder", logs.get(5)); - assertEquals("/shared/extra/helloworld/helloworld1.txt", logs.get(6)); + assertEquals("/shared/extra/helloworld", logs.get(1)); + assertEquals("/shared/extra/helloworld.txt", logs.get(2)); + assertEquals("/shared/extra/helloworld/emptyfolder", logs.get(3)); + assertEquals("/shared/extra/helloworld/helloworld1.txt", logs.get(4)); + assertEquals("/shared/extra/helloworld/helloworld2.txt", logs.get(5)); // I don't understand the order of find :sob: but it is important all files are found. + assertEquals("/shared/extra/helloworld/helloworld3.txt", logs.get(6)); stm.cleanUp(); } From 5487fb74d3c27fc09706ba667ee90c581145bcda Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Fri, 17 May 2024 23:40:22 +0200 Subject: [PATCH 074/130] Fixed table header color, fixed bugs with joining groups --- .../course/components/groupTab/GroupList.tsx | 51 ++++++++----------- .../src/pages/project/components/GroupTab.tsx | 3 ++ frontend/src/theme/themes/dark.ts | 6 ++- frontend/src/theme/themes/light.ts | 3 ++ 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/frontend/src/pages/course/components/groupTab/GroupList.tsx b/frontend/src/pages/course/components/groupTab/GroupList.tsx index ca97981b..08760a10 100644 --- a/frontend/src/pages/course/components/groupTab/GroupList.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupList.tsx @@ -38,7 +38,6 @@ const Group: FC<{ group: GroupType; canJoin: boolean; canLeave: boolean; onClick size="small" disabled={!canJoin} onClick={onJoin} - style={{ width: "130px" }} > {t("course.joinGroup")} @@ -94,7 +93,7 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType return () => { ignore = true } - }, [groups, project, courseId]) + }, [project, courseId]) const handleModalClick = (group: GroupType) => { setSelectedGroup(group.groupId) @@ -102,21 +101,16 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType } const removeUserFromGroup = async (userId: number, groupId: number) => { - try { - setLoading(true) - const response = await API.DELETE(ApiRoutes.GROUP_MEMBER, { pathValues: { id: groupId, userId: userId } }, "message") - if (!response.success) return - - if(onChanged) await onChanged() - - setGroupId(null) - message.success(t("course.leftGroup")) - } catch (err) { - console.error(err) - // TODO: handle error - } finally { - setLoading(false) - } + setLoading(true) + const response = await API.DELETE(ApiRoutes.GROUP_MEMBER, { pathValues: { id: groupId, userId: userId } }, "message") + if (!response.success) return setLoading(false) + + setGroupId(null) + if (onChanged) await onChanged() + + message.success(t("course.leftGroup")) + + setLoading(false) } const onLeave = async (group: GroupType) => { @@ -125,22 +119,19 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType } const onJoin = async (group: GroupType) => { - // TODO: join group request if (!user) return - try { - setLoading(true) - const response = await API.POST(ApiRoutes.GROUP_MEMBERS, { body:{id: user.id},pathValues: { id: group.groupId } }, "message") - if(!response.success) return - if(onChanged) await onChanged() - - message.success(t("course.joinedGroup")) - setGroupId(group.groupId) - } catch (err) { - console.error(err) - } + setLoading(true) + const response = await API.POST(ApiRoutes.GROUP_MEMBERS, { body: { id: user.id }, pathValues: { id: group.groupId } }, "message") + if (!response.success) return setLoading(false) + if (onChanged) await onChanged() + + message.success(t("course.joinedGroup")) + setGroupId(group.groupId) + setLoading(false) } + console.log("Group: ", groupId); return ( <> @@ -154,7 +145,7 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType renderItem={(g) => ( handleModalClick(g)} - canJoin={g.members.length < g.capacity || groupId !== null} + canJoin={g.members.length < g.capacity && groupId === null} canLeave={groupId === g.groupId} group={g} loading={loading} diff --git a/frontend/src/pages/project/components/GroupTab.tsx b/frontend/src/pages/project/components/GroupTab.tsx index 56e6d3a4..4e93bee2 100644 --- a/frontend/src/pages/project/components/GroupTab.tsx +++ b/frontend/src/pages/project/components/GroupTab.tsx @@ -3,12 +3,14 @@ import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" import GroupList from "../../course/components/groupTab/GroupList" import { useParams } from "react-router-dom" import useApi from "../../../hooks/useApi" +import useProject from "../../../hooks/useProject" export type GroupType = GET_Responses[ApiRoutes.PROJECT_GROUPS][number] const GroupTab: FC<{}> = () => { const [groups, setGroups] = useState(null) const { projectId } = useParams() + const project = useProject() const API = useApi() useEffect(() => { @@ -27,6 +29,7 @@ const GroupTab: FC<{}> = () => { ) } diff --git a/frontend/src/theme/themes/dark.ts b/frontend/src/theme/themes/dark.ts index f13b3e6d..b4e92ab1 100644 --- a/frontend/src/theme/themes/dark.ts +++ b/frontend/src/theme/themes/dark.ts @@ -22,6 +22,10 @@ export const darkTheme: ThemeConfig = { }, Calendar: { itemActiveBg: "#002b60" + }, + Table: { + headerSortActiveBg:"#252525" } - } + }, + }; diff --git a/frontend/src/theme/themes/light.ts b/frontend/src/theme/themes/light.ts index d9fe347c..2fbae02e 100644 --- a/frontend/src/theme/themes/light.ts +++ b/frontend/src/theme/themes/light.ts @@ -15,6 +15,9 @@ export const lightTheme: ThemeConfig = { Layout: { headerBg: "#1D64C7", headerHeight: 48, + }, + Table: { + headerSortActiveBg: "#f9f6fa" } } From 7437706da0197a38ec1f2d032271cf8aae69a2af Mon Sep 17 00:00:00 2001 From: Tristan Verbeken Date: Fri, 17 May 2024 23:45:24 +0200 Subject: [PATCH 075/130] Added tryTemplate function --- .../SubmissionTemplateModel.java | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java index 13bdd40b..913181d1 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java @@ -52,7 +52,6 @@ public void parseSubmissionTemplate(String templateString) { mostSpaces = spaceAmount; } lines[i] = "\t".repeat(tabsPerSpaces.get(spaceAmount)) + line.replaceAll(" ", ""); - ; } // Create folder stack for keeping track of all the folders while exploring the insides @@ -175,4 +174,63 @@ public SubmissionResult checkSubmission(String file) throws IOException { return checkSubmission(new ZipFile(file)); } + // will throw error if there are errors in the template + public void tryTemplate(String template) throws Exception { + List lines = List.of(template.split("\n")); + // check if the template is valid, control if every line contains a file parsable string + // check if the file is in a valid folder location (indentation is correct) + // check if the first file has indentation 0 + List indentionAmounts = new ArrayList<>(); + indentionAmounts.add(0); + if(getIndentation(lines.get(0)) != 0){ + throw new Exception("First file should not have any spaces or tabs."); + } + boolean newFolder = false; + for(int line_index = 0; line_index < lines.size(); line_index++){ + String line = lines.get(line_index); + int indentation = getIndentation(line); + if(line.isEmpty()){ + throw new Exception("Empty file name in template, remove blank lines"); + } + if(newFolder && indentation > indentionAmounts.getLast()){ + // since the indentation is larger than the previous, we are dealing with the first file in a new folder + indentionAmounts.add(indentation); + }else{ + // we are dealing with a file in a folder, thus the indentation should be equal to one of the previous folders + for(int i = indentionAmounts.size() - 1; i >= 0; i--){ + if(indentionAmounts.get(i) == indentation){ + break; + } + if(i == 0){ + throw new Exception("File at line "+ line_index + " is not in a valid folder location (indentation is incorrect)"); + } + } + // check if file is correct, since location is correct + + // first check if file contains valid file names + if(line.substring(0,line.length() - 1).contains("/")){ + throw new Exception("File at line "+ line_index + " contains invalid characters"); + } + // check if file is a folder + if(line.charAt(line.length() - 1) == '/') { + newFolder = true; + } + + } + + + } + } + + private int getIndentation(String line){ + int length = line.length(); + // one space is equal to a tab + for(int i = 0; i < length; i++){ + if(line.charAt(i) != ' ' || line.charAt(i) != '\t'){ + return i - 1; + } + } + return length - 1; + } + } From 29031748763c88b2c0d2acf6b1be34510e2e156d Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sat, 18 May 2024 00:19:23 +0200 Subject: [PATCH 076/130] Fixed project tabs bugs --- .../course/components/groupTab/GroupList.tsx | 5 ++- frontend/src/pages/project/Project.tsx | 38 +++++++++++-------- .../src/pages/project/components/GroupTab.tsx | 12 +++++- .../src/pages/project/components/ScoreTab.tsx | 6 +-- .../project/components/SubmissionTab.tsx | 6 ++- frontend/src/pages/submit/Submit.tsx | 2 +- frontend/src/router/ProjectRoutes.tsx | 11 ++---- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/frontend/src/pages/course/components/groupTab/GroupList.tsx b/frontend/src/pages/course/components/groupTab/GroupList.tsx index 08760a10..dedc9ca7 100644 --- a/frontend/src/pages/course/components/groupTab/GroupList.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupList.tsx @@ -59,7 +59,7 @@ const Group: FC<{ group: GroupType; canJoin: boolean; canLeave: boolean; onClick ) } -const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType | null; onChanged?: () => Promise }> = ({ groups, project, onChanged }) => { +const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType | null; onChanged?: () => Promise, onGroupIdChange?: (groupId: number|null) => void }> = ({ groups, project, onChanged,onGroupIdChange }) => { const [modalOpened, setModalOpened] = useState(false) const [selectedGroup, setSelectedGroup] = useState(null) const [groupId, setGroupId] = useState(null) @@ -106,6 +106,7 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType if (!response.success) return setLoading(false) setGroupId(null) + if(onGroupIdChange) onGroupIdChange(null) if (onChanged) await onChanged() message.success(t("course.leftGroup")) @@ -127,7 +128,7 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType message.success(t("course.joinedGroup")) setGroupId(group.groupId) - + if(onGroupIdChange) onGroupIdChange(group.groupId) setLoading(false) } diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index 9172ea46..8abb2c9f 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -61,23 +61,29 @@ const Project = () => { }) } - items.push({ - key: "submissions", - label: t("project.submissions"), - icon: , - children: courseAdmin ? ( - - - - ) : ( - - ), - }) + // if we work without groups -> always show submissions & score + // if we work with groups -> only show submissions if we are in a group + // if we are course admin -> always show submissions but not score + if((project?.groupId || !project?.clusterId) || courseAdmin) { - if (!courseAdmin) { + items.push({ + key: "submissions", + label: t("project.submissions"), + icon: , + children: courseAdmin ? ( + + + + ) : ( + + ), + }) + } + + if ((project?.groupId || !project?.clusterId) && !courseAdmin) { items.push({ key: "score", label: t("course.score"), diff --git a/frontend/src/pages/project/components/GroupTab.tsx b/frontend/src/pages/project/components/GroupTab.tsx index 4e93bee2..db2f5781 100644 --- a/frontend/src/pages/project/components/GroupTab.tsx +++ b/frontend/src/pages/project/components/GroupTab.tsx @@ -1,9 +1,10 @@ -import { FC, useEffect, useState } from "react" +import { FC, useContext, useEffect, useState } from "react" import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" import GroupList from "../../course/components/groupTab/GroupList" import { useParams } from "react-router-dom" import useApi from "../../../hooks/useApi" import useProject from "../../../hooks/useProject" +import { ProjectContext } from "../../../router/ProjectRoutes" export type GroupType = GET_Responses[ApiRoutes.PROJECT_GROUPS][number] @@ -11,6 +12,7 @@ const GroupTab: FC<{}> = () => { const [groups, setGroups] = useState(null) const { projectId } = useParams() const project = useProject() + const { updateProject } = useContext(ProjectContext) const API = useApi() useEffect(() => { @@ -25,11 +27,19 @@ const GroupTab: FC<{}> = () => { setGroups(res.response.data) } + const handleGroupIdChange = async (groupId: number | null) => { + if (!project) return console.error("No projectId found") + let newProject = { ...project } + newProject.groupId = groupId + updateProject(newProject) + } + return ( ) } diff --git a/frontend/src/pages/project/components/ScoreTab.tsx b/frontend/src/pages/project/components/ScoreTab.tsx index 7366b3ed..593f4b9c 100644 --- a/frontend/src/pages/project/components/ScoreTab.tsx +++ b/frontend/src/pages/project/components/ScoreTab.tsx @@ -24,11 +24,11 @@ const ScoreCard = () => { // /projects/{projectid}/groups/{groupid}/score if (!projectId) return console.error("No project id") if (project?.groupId === undefined) return setScore(null) // Means you aren't in a group yet - + if(!project.groupId) return console.error("No groupId found") let ignore = false - API.GET(ApiRoutes.PROJECT_SCORE, { pathValues: { id: projectId, groupId: projectId } }).then((res) => { + API.GET(ApiRoutes.PROJECT_SCORE, { pathValues: { id: projectId, groupId: project.groupId } }).then((res) => { if (ignore) return if (!res.success) return setScore(null) setScore(res.response.data) @@ -37,7 +37,7 @@ const ScoreCard = () => { return () => { ignore = true } - }, []) + }, [project?.groupId]) // don't show the card if no score is available if (score === undefined) return null diff --git a/frontend/src/pages/project/components/SubmissionTab.tsx b/frontend/src/pages/project/components/SubmissionTab.tsx index 6146af3e..ecb032f1 100644 --- a/frontend/src/pages/project/components/SubmissionTab.tsx +++ b/frontend/src/pages/project/components/SubmissionTab.tsx @@ -15,9 +15,11 @@ const SubmissionTab: FC<{ projectId: number; courseId: number }> = ({ projectId, if(!project) return if(!project.submissionUrl) return setSubmissions([]) //TODO: fix me, project.submissionUrl can be null + if(!project.groupId) return console.error("No groupId found"); console.log(project); let ignore = false - API.GET(project.submissionUrl, {}).then((res) => { + console.log("Sending request to: ", project.submissionUrl); + API.GET(ApiRoutes.PROJECT_GROUP_SUBMISSIONS, {pathValues: {projectId: project.projectId, groupId: project.groupId}}).then((res) => { console.log(res); if (!res.success || ignore) return setSubmissions(res.response.data.sort((a, b) => b.submissionId - a.submissionId)) @@ -27,7 +29,7 @@ const SubmissionTab: FC<{ projectId: number; courseId: number }> = ({ projectId, return () => { ignore = true } - }, [projectId,courseId]) + }, [projectId,courseId,project?.groupId]) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index d90ccfda..43f5f8af 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -91,7 +91,7 @@ const Submit = () => { - - ), + <> + - - - + form={form} + onFinishFailed={onInvalid} + onFinish={handleCreation} + layout="vertical" + requiredMark="optional" + > +
    + + + + ), + }} + /> +
    + + ) } diff --git a/frontend/src/pages/projectCreate/ProjectCreate.tsx b/frontend/src/pages/projectCreate/ProjectCreate.tsx index a775b93c..bcd32cbc 100644 --- a/frontend/src/pages/projectCreate/ProjectCreate.tsx +++ b/frontend/src/pages/projectCreate/ProjectCreate.tsx @@ -74,6 +74,7 @@ const ProjectCreate: React.FC = () => { description: "", groupClusterId: undefined, visible: false, // Stel de standaardwaarde in op false + visibleAfter: null, maxScore: 20, deadline: null, }} From a97a5f8c753a214578a0cc7b575c94a78b5bc85c Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sat, 18 May 2024 16:27:16 +0200 Subject: [PATCH 087/130] Added test submission for course admins, added lock to groups component --- frontend/public/favicon.ico | Bin 3870 -> 100805 bytes frontend/src/@types/requests.d.ts | 5 +- frontend/src/components/forms/ClusterForm.tsx | 73 +++++++++++++----- .../forms/projectFormTabs/GroupsFormTab.tsx | 10 --- frontend/src/i18n/en/translation.json | 5 +- frontend/src/i18n/nl/translation.json | 6 +- .../course/components/groupTab/GroupList.tsx | 14 +++- .../course/components/groupTab/GroupsCard.tsx | 7 +- frontend/src/pages/project/Project.tsx | 25 +++++- .../src/pages/project/components/GroupTab.tsx | 1 + .../project/components/SubmissionTab.tsx | 8 +- .../components/GroupClusterDropdown.tsx | 1 + .../components/GroupClusterModalContent.tsx | 1 + 13 files changed, 111 insertions(+), 45 deletions(-) diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..0ca3c013bd3b2c4275b856a6082ae1aa1e0aed9c 100644 GIT binary patch literal 100805 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 953fdda1..6a26d835 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -28,6 +28,7 @@ export enum ApiRoutes { PROJECT_GROUP = "api/projects/:id/groups/:groupId", PROJECT_GROUPS = "api/projects/:id/groups", PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", + PROJECT_TEST_SUBMISSIONS = "/api/projects/:projectId/adminsubmissions", PROJECT_TESTS_UPLOAD = "api/projects/:id/tests/extrafiles", PROJECT_SUBMIT = "api/projects/:id/submit", @@ -213,6 +214,7 @@ export type GET_Responses = { submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet }[], [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] + [ApiRoutes.PROJECT_TEST_SUBMISSIONS]: GET_Responses[ApiRoutes.PROJECT_GROUP_SUBMISSIONS] [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] [ApiRoutes.SUBMISSION]: { submissionId: number @@ -300,7 +302,8 @@ export type GET_Responses = { groupCount: number; createdAt: Timestamp; groups: GET_Responses[ApiRoutes.GROUP][] - courseUrl: ApiRoutes.COURSE + courseUrl: ApiRoutes.COURSE, + lockGroupsAfter: Timestamp | null // means students can't join or leave the group } [ApiRoutes.COURSE]: { description: string diff --git a/frontend/src/components/forms/ClusterForm.tsx b/frontend/src/components/forms/ClusterForm.tsx index 78f3364d..ffd77fc7 100644 --- a/frontend/src/components/forms/ClusterForm.tsx +++ b/frontend/src/components/forms/ClusterForm.tsx @@ -1,26 +1,57 @@ -import { Form, Input, InputNumber } from "antd" +import { DatePicker, Form, Input, InputNumber } from "antd" import { useTranslation } from "react-i18next" - - const ClusterForm = () => { - const {t} = useTranslation() - - - return <> - - - - - - - - - - - - - + const { t } = useTranslation() + + return ( + <> + + + + + + + + + + + + + + + + + ) } -export default ClusterForm \ No newline at end of file +export default ClusterForm diff --git a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx index 8a0f9633..91efd873 100644 --- a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx @@ -51,16 +51,6 @@ const GroupsFormTab: FC<{ form: FormInstance }> = ({ form }) => { <> {selectedCluster ? ( <> - - - - - Promise, onGroupIdChange?: (groupId: number|null) => void }> = ({ groups, project, onChanged,onGroupIdChange }) => { +const GroupList: FC<{ locked:ClusterType["lockGroupsAfter"] ,groups: GroupType[] | null; project?: number | ProjectType | null; onChanged?: () => Promise, onGroupIdChange?: (groupId: number|null) => void }> = ({ groups, project, onChanged,onGroupIdChange,locked }) => { const [modalOpened, setModalOpened] = useState(false) const [selectedGroup, setSelectedGroup] = useState(null) const [groupId, setGroupId] = useState(null) @@ -68,8 +70,14 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType const { message } = useAppApi() const { user } = useUser() const { courseId } = useParams<{ courseId: string }>() + const isCourseAdmin = useIsCourseAdmin() const API = useApi() + const isLocked = useMemo(()=> { + if(!locked) return false + return new Date(locked).getTime() < Date.now() + }, [locked]) + useEffect(() => { if (typeof project === "number") return setGroupId(project) if (project !== undefined) return setGroupId(project?.groupId ?? null) @@ -146,8 +154,8 @@ const GroupList: FC<{ groups: GroupType[] | null; project?: number | ProjectType renderItem={(g) => ( handleModalClick(g)} - canJoin={g.members.length < g.capacity && groupId === null} - canLeave={groupId === g.groupId} + canJoin={g.members.length < g.capacity && groupId === null && !isCourseAdmin && !isLocked} + canLeave={groupId === g.groupId && !isLocked} group={g} loading={loading} onJoin={() => onJoin(g)} diff --git a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx index e7de7e25..3c8615a2 100644 --- a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx @@ -1,5 +1,5 @@ import { Card, Collapse, CollapseProps, Spin, Typography } from "antd" -import { FC, useEffect, useState } from "react" +import { FC, useEffect, useMemo, useState } from "react" import { ApiRoutes, GET_Responses } from "../../../../@types/requests.d" import GroupList from "./GroupList" import { CardProps } from "antd/lib" @@ -29,16 +29,17 @@ const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ co // // - const items: CollapseProps["items"] = groups?.map((cluster) => ({ + const items: CollapseProps["items"] = useMemo(()=> groups?.map((cluster) => ({ key: cluster.clusterId.toString(), label: cluster.name, children: ( ), - })) + })), [groups]) if (Array.isArray(items) && !items.length) return ( diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index 8abb2c9f..12848216 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -66,7 +66,7 @@ const Project = () => { // if we are course admin -> always show submissions but not score if((project?.groupId || !project?.clusterId) || courseAdmin) { - items.push({ + items.push({ key: "submissions", label: t("project.submissions"), icon: , @@ -81,6 +81,21 @@ const Project = () => { /> ), }) + + if(courseAdmin) { + items.push({ + key: "testSubmissions", + label: t("project.testSubmissions"), + icon: , + children: + + }) + } + } if ((project?.groupId || !project?.clusterId) && !courseAdmin) { @@ -138,6 +153,14 @@ const Project = () => { extra={ courseAdmin ? ( <> + + - - - - - - - ) + return ( + <> + {isCourseAdmin && ( + +
    + +
    +
    + )} + + {isCourseAdmin ? ( + + ) : ( + + )} + + + ) } export default ProjectCard diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx new file mode 100644 index 00000000..861c0c66 --- /dev/null +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -0,0 +1,138 @@ +import { Button, Table, TableProps, Tag, Tooltip } from "antd" +import { FC, useMemo } from "react" +import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" +import { useTranslation } from "react-i18next" +import i18n from 'i18next' +import useAppApi from "../../../hooks/useAppApi" +import ProjectStatusTag from "./ProjectStatusTag" +import GroupProgress from "./GroupProgress" +import { Link } from "react-router-dom" +import { AppRoutes } from "../../../@types/routes" +import { ClockCircleOutlined } from "@ant-design/icons" +import useIsCourseAdmin from "../../../hooks/useIsCourseAdmin"; + +export type ProjectType = GET_Responses[ApiRoutes.PROJECT] + +const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: string[] }> = ({ projects, ignoreColumns }) => { + const { t } = useTranslation() + const { modal } = useAppApi() + const isCourseAdmin = useIsCourseAdmin() + + const columns: TableProps["columns"] = useMemo( + () => { + let columns: TableProps["columns"] = [ + { + title: t("home.projects.name"), + key: "name", + render: (project: ProjectType) => ( + + + + ) + }, + { + title: t("home.projects.course"), + dataIndex: "course", + key: "course", + sorter: (a: ProjectType, b: ProjectType) => a.course.name.localeCompare(b.course.name), + sortDirections: ['ascend', 'descend'], + render: (course: ProjectType["course"]) => course.name + }, + { + title: t("home.projects.deadline"), + dataIndex: "deadline", + key: "deadline", + sorter: (a: ProjectType, b: ProjectType) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime(), + sortDirections: ['ascend', "descend"], + defaultSortOrder: "ascend", + filters: [{ text: t('home.projects.deadlineNotPassed'), value: 'notPassed' }], + onFilter: (value: any, record: any) => { + const currentTimestamp = new Date().getTime(); + const deadlineTimestamp = new Date(record.deadline).getTime(); + return value === 'notPassed' ? deadlineTimestamp >= currentTimestamp : true; + }, + defaultFilteredValue: ["notPassed"], + render: (text: string) => + new Date(text).toLocaleString(i18n.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + }, + { + title: t("home.projects.groupProgress"), + key: "progress", + render: (project: ProjectType) => ( + + ), + } + ] + + if (ignoreColumns) { + columns = columns.filter((c) => !ignoreColumns.includes(c.key as string)) + } + + if (isCourseAdmin) { + columns = columns.filter((c) => c.key !== "status") + columns.push({ + title: t("home.projects.visibility"), + key: "visible", + render: (project: ProjectType) => { + if (project.visible) { + return {t("home.projects.visibleStatus.visible")} + } else if (project.visibleAfter) { + return ( + + } color="default">{t("home.projects.visibleStatus.scheduled")} + + ) + } else { + return {t("home.projects.visibleStatus.invisible")} + } + } + }) + } else { + columns.push({ + title: t("home.projects.projectStatus"), + key: "status", + render: (project: ProjectType) => + project.status && , + }) + } + + return columns + }, + [t, modal, projects, isCourseAdmin] + ) + + return ( + project.projectId} + /> + ) +} + +export default ProjectTableCourse From 851bdac6feaa2e8fa9a140228ddcaf21b24bbf96 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Sat, 18 May 2024 18:37:10 +0200 Subject: [PATCH 090/130] =?UTF-8?q?env=20builder=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env-template | 8 ++++++++ .gitignore | 1 + envBuilder.bat | 28 ++++++++++++++++++++++++++++ envBuilder.sh | 18 ++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 .env-template create mode 100644 envBuilder.bat create mode 100644 envBuilder.sh diff --git a/.env-template b/.env-template new file mode 100644 index 00000000..6f466741 --- /dev/null +++ b/.env-template @@ -0,0 +1,8 @@ +backend/app/src/main/resources/application-secrets.properties,spring.datasource.username= +backend/app/src/main/resources/application-secrets.properties,spring.datasource.password= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.client-id= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.b2c.client-secret= +backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.tenant-id= +docker.env,PGU= +docker.env,PGP= +docker.env,POSTGRES_USER=${PGU} diff --git a/.gitignore b/.gitignore index 9d406eb9..9a182e36 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ docker.env ./startBackend.sh startBackend.sh +/.env diff --git a/envBuilder.bat b/envBuilder.bat new file mode 100644 index 00000000..fa93db80 --- /dev/null +++ b/envBuilder.bat @@ -0,0 +1,28 @@ +@echo off +setlocal EnableDelayedExpansion + +set "prevFile=" + +for /F "tokens=1,2 delims=," %%a in (.env) do ( + echo Processing line: %%a,%%b + for /F "tokens=1,2 delims==" %%c in ("%%b") do ( + echo File: %%a + echo Variable: %%c + echo Value: %%d + + if not "%%a"=="!prevFile!" ( + if exist %%a ( + del %%a + echo Deleted file: %%a + ) + type nul > %%a + echo Created file: %%a + ) + + echo. >> %%a + echo %%c=%%d >> %%a + echo Added variable to file + + set "prevFile=%%a" + ) +) diff --git a/envBuilder.sh b/envBuilder.sh new file mode 100644 index 00000000..99413a31 --- /dev/null +++ b/envBuilder.sh @@ -0,0 +1,18 @@ +ENV_FILE=".env" + +while IFS= read -r line +do + echo "Processing line: $line" + IFS=',' read -r full_addr var <<< "$line" + IFS='=' read -r file env <<< "$full_addr" + echo "File: $file" + echo "Variable: $var" + echo "Value: $env" + touch "$file" + if ! grep -q "${var}=" "$file"; then + echo "Variable not set, appending to file..." + echo "${var}=${env}" >> "$file" + else + echo "Variable already set in file." + fi +done < "$ENV_FILE" \ No newline at end of file From b03284c2e01624448924422de6774c6e6326cac7 Mon Sep 17 00:00:00 2001 From: Arne Dierick Date: Sun, 19 May 2024 00:07:51 +0200 Subject: [PATCH 091/130] Implemented requested changes --- .../forms/projectFormTabs/GeneralFormTab.tsx | 26 +- .../src/pages/editProject/EditProject.tsx | 272 +++++++++--------- .../src/pages/projectCreate/ProjectCreate.tsx | 181 ++++++------ 3 files changed, 235 insertions(+), 244 deletions(-) diff --git a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx index 91bb231f..f81fd4c7 100644 --- a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx @@ -1,31 +1,12 @@ import { DatePicker, Form, FormInstance, Input, Switch, Typography } from "antd" import { useTranslation } from "react-i18next" -import { FC, useEffect, useState } from "react" +import { FC } from "react" import MarkdownEditor from "../../input/MarkdownEditor" const GeneralFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() const description = Form.useWatch("description", form) const visible = Form.useWatch("visible", form) - const [isVisible, setIsVisible] = useState(visible) - const [savedVisibleAfter, setSavedVisibleAfter] = useState(null) - - useEffect(() => { - setIsVisible(visible) - if (visible && savedVisibleAfter) { - form.setFieldsValue({ visibleAfter: null }) - } - }, [visible]) - - const handleVisibleChange = (checked: boolean) => { - setIsVisible(checked) - if (checked) { - setSavedVisibleAfter(form.getFieldValue("visibleAfter")) - form.setFieldsValue({ visibleAfter: null }) - } else { - form.setFieldsValue({ visibleAfter: savedVisibleAfter }) - } - } return ( <> @@ -48,10 +29,10 @@ const GeneralFormTab: FC<{ form: FormInstance }> = ({ form }) => { name="visible" valuePropName="checked" > - + - {!isVisible && ( + {!visible && ( = ({ form }) => { )} diff --git a/frontend/src/pages/editProject/EditProject.tsx b/frontend/src/pages/editProject/EditProject.tsx index fcd4d358..ef7c6b62 100644 --- a/frontend/src/pages/editProject/EditProject.tsx +++ b/frontend/src/pages/editProject/EditProject.tsx @@ -15,155 +15,159 @@ import useApi from "../../hooks/useApi" import saveDockerForm, { DockerFormData } from "../../components/common/saveDockerForm" const EditProject: React.FC = () => { - const [form] = Form.useForm() - const { t } = useTranslation() - const { courseId, projectId } = useParams() - const [loading, setLoading] = useState(false) - const API = useApi() - const [error, setError] = useState(null) - const navigate = useNavigate() - const project = useProject() - const { updateProject } = useContext(ProjectContext) - const [initialDockerValues, setInitialDockerValues] = useState(null) - const location = useLocation() - - const updateDockerForm = async () => { - if (!projectId) return - const response = await API.GET(ApiRoutes.PROJECT_TESTS, { pathValues: { id: projectId } }) - if (!response.success) return setInitialDockerValues(null) - - let formVals: POST_Requests[ApiRoutes.PROJECT_TESTS] = { - structureTest: null, - dockerTemplate: null, - dockerScript: null, - dockerImage: null, - } - if (response.success) { - const tests = response.response.data - console.log(tests) - - if (tests.extraFilesName) { - const downloadLink = AppRoutes.DOWNLOAD_PROJECT_TESTS.replace(":projectId", projectId).replace(":courseId", courseId!) - - const uploadVal: UploadProps["defaultFileList"] = [{ - uid: '1', - name: tests.extraFilesName, - status: 'done', - url: downloadLink, - type: "file", - }] - - form.setFieldValue("dockerTestDir", uploadVal) - } - - formVals = { - structureTest: tests.structureTest ?? "", - dockerTemplate: tests.dockerTemplate ?? "", - dockerScript: tests.dockerScript ?? "", - dockerImage: tests.dockerImage ?? "", - } + const [form] = Form.useForm() + const { t } = useTranslation() + const { courseId, projectId } = useParams() + const [loading, setLoading] = useState(false) + const API = useApi() + const [error, setError] = useState(null) + const navigate = useNavigate() + const project = useProject() + const { updateProject } = useContext(ProjectContext) + const [initialDockerValues, setInitialDockerValues] = useState(null) + const location = useLocation() + + const updateDockerForm = async () => { + if (!projectId) return + const response = await API.GET(ApiRoutes.PROJECT_TESTS, { pathValues: { id: projectId } }) + if (!response.success) return setInitialDockerValues(null) + + let formVals: POST_Requests[ApiRoutes.PROJECT_TESTS] = { + structureTest: null, + dockerTemplate: null, + dockerScript: null, + dockerImage: null, + } + if (response.success) { + const tests = response.response.data + console.log(tests) + + if (tests.extraFilesName) { + const downloadLink = AppRoutes.DOWNLOAD_PROJECT_TESTS.replace(":projectId", projectId).replace(":courseId", courseId!) + + const uploadVal: UploadProps["defaultFileList"] = [{ + uid: '1', + name: tests.extraFilesName, + status: 'done', + url: downloadLink, + type: "file", + }] + + form.setFieldValue("dockerTestDir", uploadVal) + } + + formVals = { + structureTest: tests.structureTest ?? "", + dockerTemplate: tests.dockerTemplate ?? "", + dockerScript: tests.dockerScript ?? "", + dockerImage: tests.dockerImage ?? "", + } + } + + form.setFieldsValue(formVals) + + setInitialDockerValues(formVals) } - form.setFieldsValue(formVals) + console.log(initialDockerValues) - setInitialDockerValues(formVals) - } + useEffect(() => { + if (!project) return - console.log(initialDockerValues) + updateDockerForm() + }, [project?.projectId]) - useEffect(() => { - if (!project) return + const handleCreation = async () => { + const values: ProjectFormData & DockerFormData = form.getFieldsValue() + if (values.visible) { + values.visibleAfter = null + } - updateDockerForm() - }, [project?.projectId]) + console.log(values) - const handleCreation = async () => { - const values: ProjectFormData & DockerFormData = form.getFieldsValue() - console.log(values) + if (!courseId || !projectId) return console.error("courseId or projectId is undefined") + setLoading(true) - if (!courseId || !projectId) return console.error("courseId or projectId is undefined") - setLoading(true) + const response = await API.PUT( + ApiRoutes.PROJECT, + { + body: values, + pathValues: { id: projectId }, + }, + "alert" + ) + if (!response.success) { + setError(response.alert || null) + setLoading(false) + return + } - const response = await API.PUT( - ApiRoutes.PROJECT, - { - body: values, - pathValues: { id: projectId }, - }, - "alert" - ) - if (!response.success) { - setError(response.alert || null) - setLoading(false) - return - } + let promises = [] - let promises = [] + promises.push(saveDockerForm(form, initialDockerValues, API, projectId)) - promises.push(saveDockerForm(form, initialDockerValues, API, projectId)) + if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { + promises.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + } - if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { - promises.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + await Promise.all(promises) + + const result = response.response.data + updateProject(result) + navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project } - await Promise.all(promises) - - const result = response.response.data - updateProject(result) - navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project - } - - const onInvalid: FormProps["onFinishFailed"] = (e) => { - const errField = e.errorFields[0].name[0] - if (errField === "groupClusterId") navigate("#groups") - else if (errField === "structureTest") navigate("#structure") - else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") - else navigate("#general") - } - - if (!project) return <> - return ( - <> -
    -
    - - - - ), + const onInvalid: FormProps["onFinishFailed"] = (e) => { + const errField = e.errorFields[0].name[0] + if (errField === "groupClusterId") navigate("#groups") + else if (errField === "structureTest") navigate("#structure") + else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") + else navigate("#general") + } + + if (!project) return <> + return ( + <> + -
    - - - ) + form={form} + onFinishFailed={onInvalid} + onFinish={handleCreation} + layout="vertical" + requiredMark="optional" + > +
    + + + + ), + }} + /> +
    + + + ) } export default EditProject diff --git a/frontend/src/pages/projectCreate/ProjectCreate.tsx b/frontend/src/pages/projectCreate/ProjectCreate.tsx index bcd32cbc..2abc3bea 100644 --- a/frontend/src/pages/projectCreate/ProjectCreate.tsx +++ b/frontend/src/pages/projectCreate/ProjectCreate.tsx @@ -13,102 +13,107 @@ import useApi from "../../hooks/useApi" import { ApiRoutes } from "../../@types/requests.d" const ProjectCreate: React.FC = () => { - const [form] = Form.useForm() - const { t } = useTranslation() - const navigate = useNavigate() - const { courseId } = useParams<{ courseId: string }>() - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) // Gebruik ProjectError type voor error state - const API = useApi() - const { message } = useAppApi() + const [form] = Form.useForm() + const { t } = useTranslation() + const navigate = useNavigate() + const { courseId } = useParams<{ courseId: string }>() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) // Gebruik ProjectError type voor error state + const API = useApi() + const { message } = useAppApi() - const handleCreation = async () => { - const values: ProjectFormData & DockerFormData = form.getFieldsValue() - const project: Omit = { - name: values.name, - description: values.description, - groupClusterId: values.groupClusterId, - deadline: values.deadline, - maxScore: values.maxScore, - testId: values.testId, - visible: values.visible, - visibleAfter: values.visibleAfter, - } - console.log(values) + const handleCreation = async () => { + const values: ProjectFormData & DockerFormData = form.getFieldsValue() + if (values.visible) { + values.visibleAfter = null + } - if (!courseId) return console.error("courseId is undefined") - setLoading(true) + const project: Omit = { + name: values.name, + description: values.description, + groupClusterId: values.groupClusterId, + deadline: values.deadline, + maxScore: values.maxScore, + testId: values.testId, + visible: values.visible, + visibleAfter: values.visibleAfter, + } - const response = await API.POST(ApiRoutes.PROJECT_CREATE, { body: project, pathValues: { courseId } }, "alert") - if (!response.success) { - setError(response.alert || null) - return setLoading(false) - } - const result = response.response.data - let promisses: Promise[] = [] + console.log(values) - promisses.push(saveDockerForm(form, null, API, result.projectId.toString())) - if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { - promisses.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) - } + if (!courseId) return console.error("courseId is undefined") + setLoading(true) + + const response = await API.POST(ApiRoutes.PROJECT_CREATE, { body: project, pathValues: { courseId } }, "alert") + if (!response.success) { + setError(response.alert || null) + return setLoading(false) + } + const result = response.response.data + let promises: Promise[] = [] - await Promise.all(promisses) + promises.push(saveDockerForm(form, null, API, result.projectId.toString())) + if (form.isFieldTouched("groups") && values.groupClusterId && values.groups) { + promises.push(API.PUT(ApiRoutes.CLUSTER_FILL, { body: values.groups, pathValues: { id: values.groupClusterId } }, "message")) + } - message.success(t("project.change.success")) // Toon een succesbericht - navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project - } + await Promise.all(promises) - const onInvalid: FormProps["onFinishFailed"] = (e) => { - const errField = e.errorFields[0].name[0] - if (errField === "groupClusterId") navigate("#groups") - else if (errField === "structureTest") navigate("#structure") - else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") - else navigate("#general") - } + message.success(t("project.change.success")) // Toon een succesbericht + navigate(AppRoutes.PROJECT.replace(":projectId", result.projectId.toString()).replace(":courseId", courseId)) // Navigeer naar het nieuwe project + } + + const onInvalid: FormProps["onFinishFailed"] = (e) => { + const errField = e.errorFields[0].name[0] + if (errField === "groupClusterId") navigate("#groups") + else if (errField === "structureTest") navigate("#structure") + else if (errField === "dockerScript" || errField === "dockerImage" || errField === "dockerTemplate") navigate("#tests") + else navigate("#general") + } - return ( - <> -
    -
    - - - - ), - }} - /> -
    - - - ) + return ( + <> +
    +
    + + + + ), + }} + /> +
    + + + ) } export default ProjectCreate From e66fc4a7b843f6dbeeeebce8478d923c3880db58 Mon Sep 17 00:00:00 2001 From: Arne Dierick Date: Sun, 19 May 2024 10:35:30 +0200 Subject: [PATCH 092/130] Test works now --- .../app/src/main/java/com/ugent/pidgeon/util/TestUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java index 08d909ce..13c28bc0 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java @@ -80,9 +80,9 @@ public CheckResult> checkForTestUpdate( } // This returns false if the image isn't pullt yet! FIX PLS -// if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { -// return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); -// } + if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); + } if (!httpMethod.equals(HttpMethod.PATCH) && dockerTemplate != null && dockerImage == null) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script and image are required in a docker template test.", null); From c05c21fa01600a770594dfc39d4f3f5162791a3c Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 11:32:56 +0200 Subject: [PATCH 093/130] Make score no longer required --- .../main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java index 03e21232..753a13a9 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java @@ -100,8 +100,8 @@ public CheckResult checkGroupFeedbackUpdateJson(UpdateGroupScoreRequest re return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); } Integer maxScore = projectCheck.getData().getMaxScore(); - if ((request.getScore() == null && maxScore != null) || request.getFeedback() == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score and feedback need to be provided", null); + if (request.getFeedback() == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "Feedbacks need to be provided", null); } if (request.getScore() != null && request.getScore() < 0) { From b84efdbea77ce65ba8561831ff4172eb8cc15861 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 11:33:54 +0200 Subject: [PATCH 094/130] Updated getAllSubmissionJson Now works with a second function to get the submissions this way the logic to get the submissions can be reused elsewhere --- .../controllers/SubmissionController.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index f410d2c5..bd6aecf6 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -15,8 +15,13 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -89,6 +94,15 @@ public ResponseEntity getSubmission(@PathVariable("submissionid") long submis return ResponseEntity.ok(submissionJson); } + private Map> getLatestSubmissionsForProject(long projectId) { + List groupIds = projectRepository.findGroupIdsByProjectId(projectId); + return groupIds.stream() + .collect(Collectors.toMap( + groupId -> groupId, + groupId -> submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectId, groupId) + )); + } + /** * Function to get all submissions * @@ -109,28 +123,29 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - List projectGroupIds = projectRepository.findGroupIdsByProjectId(projectid); - List res = projectGroupIds.stream().map(groupId -> { - GroupEntity group = groupRepository.findById(groupId).orElse(null); + Map> submissions = getLatestSubmissionsForProject(projectid); + List res = new ArrayList<>(); + for (Map.Entry> entry : submissions.entrySet()) { + GroupEntity group = groupRepository.findById(entry.getKey()).orElse(null); if (group == null) { throw new RuntimeException("Group not found"); } GroupJson groupjson = entityToJsonConverter.groupEntityToJson(group, false); - GroupFeedbackEntity groupFeedbackEntity = groupFeedbackRepository.getGroupFeedback(groupId, projectid); + GroupFeedbackEntity groupFeedbackEntity = groupFeedbackRepository.getGroupFeedback(entry.getKey(), projectid); GroupFeedbackJson groupFeedbackJson; if (groupFeedbackEntity == null) { groupFeedbackJson = null; } else { groupFeedbackJson = entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity); } - SubmissionEntity submission = submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectid, groupId).orElse(null); + SubmissionEntity submission = entry.getValue().orElse(null); if (submission == null) { - return new LastGroupSubmissionJson(null, groupjson, groupFeedbackJson); + res.add(new LastGroupSubmissionJson(null, groupjson, groupFeedbackJson)); + continue; } + res.add(new LastGroupSubmissionJson(entityToJsonConverter.getSubmissionJson(submission), groupjson, groupFeedbackJson)); + } - return new LastGroupSubmissionJson(entityToJsonConverter.getSubmissionJson(submission), groupjson, groupFeedbackJson); - - }).toList(); return ResponseEntity.ok(res); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); @@ -138,6 +153,8 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti } + + /** * Function to submit a file * From 145e5a129d26a9ef6745b624a047d5ed1075ade5 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 12:36:03 +0200 Subject: [PATCH 095/130] Add route to download all submission files --- .../controllers/SubmissionController.java | 58 ++++++++++++++++++- .../com/ugent/pidgeon/util/Filehandler.java | 11 ++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index bd6aecf6..2311ac23 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -15,6 +15,10 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Map; import java.util.Objects; @@ -22,6 +26,8 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -152,9 +158,6 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti } } - - - /** * Function to submit a file * @@ -321,6 +324,55 @@ public ResponseEntity getSubmissionFile(@PathVariable("submissionid") long su return Filehandler.getZipFileAsResponse(Path.of(file.getPath()), file.getName()); } + @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submissions/files") + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity getSubmissionsFiles(@PathVariable("projectid") long projectid, @RequestParam(value = "artifacts", required = false) Boolean artifacts, Auth auth) { + try { + CheckResult checkResult = projectUtil.isProjectAdmin(projectid, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + + Path tempDir = Files.createTempDirectory("allsubmissions"); + Path mainZipPath = tempDir.resolve("main.zip"); + try (ZipOutputStream mainZipOut = new ZipOutputStream(Files.newOutputStream(mainZipPath))) { + Map> submissions = getLatestSubmissionsForProject(projectid); + for (Map.Entry> entry : submissions.entrySet()) { + SubmissionEntity submission = entry.getValue().orElse(null); + if (submission == null) { + continue; + } + FileEntity file = fileRepository.findById(submission.getFileId()).orElse(null); + if (file == null) { + continue; + } + + // Create the group-specific zip file in a temporary location + Path groupZipPath = tempDir.resolve("group-" + submission.getGroupId() + ".zip"); + try (ZipOutputStream groupZipOut = new ZipOutputStream(Files.newOutputStream(groupZipPath))) { + File submissionZip = Path.of(file.getPath()).toFile(); + Filehandler.addExistingZip(groupZipOut, "files.zip", submissionZip); + + if (artifacts != null && artifacts) { + Path artifactPath = Filehandler.getSubmissionArtifactPath(projectid, submission.getGroupId(), submission.getId()); + File artifactZip = artifactPath.toFile(); + if (artifactZip.exists()) { + Filehandler.addExistingZip(groupZipOut, "artifacts.zip", artifactZip); + } + } + + } + + Filehandler.addExistingZip(mainZipOut, "group-" + submission.getGroupId() + ".zip", groupZipPath.toFile()); + } + } + + return Filehandler.getZipFileAsResponse(mainZipPath, "allsubmissions.zip"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + } + @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/artifacts") //Route to get a submission @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getSubmissionArtifacts(@PathVariable("submissionid") long submissionid, Auth auth) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index a06e5399..93ab4fae 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -213,4 +213,15 @@ public static ResponseEntity getZipFileAsResponse(Path path, String filename) .headers(headers) .body(zipFile); } + + + public static void addExistingZip(ZipOutputStream groupZipOut, String zipFileName, File zipFile) throws IOException { + ZipEntry zipEntry = new ZipEntry(zipFileName); + groupZipOut.putNextEntry(zipEntry); + + // Read the content of the zip file and write it to the group zip output stream + Files.copy(zipFile.toPath(), groupZipOut); + + groupZipOut.closeEntry(); + } } From 1adb9f0634fd8c80b03eed5dc92577ec48d6d37b Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 14:06:13 +0200 Subject: [PATCH 096/130] Fixed tests Still need to at some tests for newly added Filehandler & SubmissionController methods --- .../main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java | 2 +- .../java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java index 753a13a9..b1dc7e52 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java @@ -108,7 +108,7 @@ public CheckResult checkGroupFeedbackUpdateJson(UpdateGroupScoreRequest re return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score can't be lower than 0", null); } - if (maxScore != null && request.getScore() > maxScore) { + if (maxScore != null && request.getScore() != null && request.getScore() > maxScore) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score can't be higher than the defined max score (" + maxScore + ")", null); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java index 506d1607..21bb85fc 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java @@ -200,13 +200,7 @@ public void testCheckGroupFeedbackUpdateJson() { /* Score is null */ updateGroupScoreRequest.setScore(null); result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - - /* Score is null but so is maxScore */ - projectEntity.setMaxScore(null); - result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); assertEquals(HttpStatus.OK, result.getStatus()); - projectEntity.setMaxScore(34); /* Feedback is null */ updateGroupScoreRequest.setScore(Float.valueOf(projectEntity.getMaxScore())); From c4c0c427506912d0636ca465a52b79cb2c91be52 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 14:30:12 +0200 Subject: [PATCH 097/130] Added test for addToExistingZip --- .../ugent/pidgeon/util/FileHandlerTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java index a7dcc193..a76170d9 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -12,16 +12,22 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -435,4 +441,44 @@ public void testGetZipFileAsResponse_fileDoesNotExist() { assertEquals(404, response.getStatusCodeValue()); } + @Test + public void testAddExistingZip() throws IOException { + // Create zip file + String zipFileName = "existingZipFile.zip"; + File tempZipFile = Files.createTempFile("existingZip", ".zip").toFile(); + + // Populate the zip file with some content + try (ZipOutputStream tempZipOutputStream = new ZipOutputStream(new FileOutputStream(tempZipFile))) { + ZipEntry entry = new ZipEntry("testFile.txt"); + tempZipOutputStream.putNextEntry(entry); + tempZipOutputStream.write("Test content".getBytes()); + tempZipOutputStream.closeEntry(); + Filehandler.addExistingZip(tempZipOutputStream, zipFileName, tempZipFile); + } + + + + + + // Check if the zip file contains the entry + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(tempZipFile))) { + ZipEntry entry; + boolean found = false; + boolean originalFound = false; + while ((entry = zis.getNextEntry()) != null) { + Logger.getGlobal().info("Entry: " + entry.getName()); + if (entry.getName().equals(zipFileName)) { + found = true; + } else if (entry.getName().equals("testFile.txt")) { + originalFound = true; + } + } + assertTrue(found); + assertTrue(originalFound); + } + } + + + + } From fda84f24a30accc3cc4b411896be46d54eae0bee Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sun, 19 May 2024 14:40:08 +0200 Subject: [PATCH 098/130] Improved/fixed submissions table --- frontend/package-lock.json | 29 ++++++ frontend/package.json | 4 + frontend/public/favicon.ico | Bin 100805 -> 102987 bytes frontend/public/favicon1.ico | Bin 0 -> 103868 bytes frontend/public/logo192.png | Bin 5347 -> 11151 bytes frontend/public/logo512.png | Bin 9664 -> 44786 bytes frontend/src/@types/requests.d.ts | 8 +- .../src/components/other/ProjectCalander.tsx | 1 + .../src/components/other/ProjectTimeline.tsx | 1 + frontend/src/i18n/en/translation.json | 15 ++- frontend/src/i18n/nl/translation.json | 15 ++- frontend/src/pages/editRole/EditRole.tsx | 2 +- .../pages/index/components/CourseSection.tsx | 2 + .../index/components/ProjectStatusTag.tsx | 6 +- frontend/src/pages/profile/Profile.tsx | 2 +- frontend/src/pages/project/Project.tsx | 8 +- .../project/components/SubmissionTab.tsx | 27 ++--- .../project/components/SubmissionsTab.tsx | 85 ++++++++++++++-- .../project/components/SubmissionsTable.tsx | 94 +++++++++++++----- .../src/pages/project/components/createCsv.ts | 63 ++++++++++++ frontend/src/pages/submission/Submission.tsx | 4 +- .../submission/components/SubmissionCard.tsx | 66 ++++++------ .../pages/submit/components/SubmitForm.tsx | 4 +- 23 files changed, 344 insertions(+), 92 deletions(-) create mode 100644 frontend/public/favicon1.ico create mode 100644 frontend/src/pages/project/components/createCsv.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 323dd4dc..66ef0a47 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,11 +24,13 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.2", "axios": "^1.6.7", + "file-saver": "^2.0.5", "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", "jszip": "^3.10.1", "lowlight": "^3.1.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -43,6 +45,8 @@ }, "devDependencies": { "@testing-library/react": "^14.2.2", + "@types/file-saver": "^2.0.7", + "@types/papaparse": "^5.3.14", "@types/react-syntax-highlighter": "^15.5.11", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", @@ -2153,6 +2157,12 @@ "@types/estree": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2240,6 +2250,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -4172,6 +4191,11 @@ "bser": "2.1.1" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -8335,6 +8359,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parse-entities": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index d0d0e81f..15deafcc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,11 +19,13 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.2", "axios": "^1.6.7", + "file-saver": "^2.0.5", "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", "i18next-localstorage-cache": "^1.1.1", "jszip": "^3.10.1", "lowlight": "^3.1.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -77,6 +79,8 @@ }, "devDependencies": { "@testing-library/react": "^14.2.2", + "@types/file-saver": "^2.0.7", + "@types/papaparse": "^5.3.14", "@types/react-syntax-highlighter": "^15.5.11", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 0ca3c013bd3b2c4275b856a6082ae1aa1e0aed9c..7b8b318650d8946fe502bfb393e7658e10b56272 100644 GIT binary patch literal 102987 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 literal 100805 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
    UA10S5*N;2 zP!jE1wp9|dr_WLn7cS8I0vCyvxRh{7Ni1KsTuD?Y`I3?-U#twx>87B4D+*trL#uX5 zV$r;XN@DczQA#3jcG7Q@NTu&+=7;1J=zA>>x_^@QpgsKe@L!ezSMshum$DC9%06f* z`=CAYuj@N|l;*Z< z-l8PlY4)~~kbZ$qxOmY7%{#Ymj~+RyBw|j-C<%8xJbL7J@-GEbCw;FZ^!aor;{y}F zJp4D8ClCMKD-lxmL3`xiEYVW-K}*>O?XHuu4_eATXpj7xb5z#-pk>_;+Ef3#EAKhW zkPe3fe%`sSNl+c&))+rmW}5F8c&T_(DkdnBI{J%!;2&C{5e05bCB=12>-h;l2 z@J;1I@*cG0J!r{$(5{^1J!r{$(31C{|7jCFAx9! z$ui*Kzo-6}W#HkLf8QxIuj_!j{e80GQW6DxpHmXsw`@}qu4FT8ZJ3hi)4jKnICu7( zl5pkUy*|I4oUA04%wMb|W=x%?B-%7>r6k(EMf!dM9b30k5{3K04(-%ssd`u#u2d(e{ipe64?yK<8Epe64?OWuRN-%j!#wB$W#$$QX}_n;;3 zK}+6)mb?coc@Nsde~U)p;n>qBj@t^v8^CeQxAEiIv ztJ}ZK3YnoK%(<&nQc{u$$ux&55fv4sBubYktt8H#Ijbae|J}VmdX)6{1nShOqaR!;conX3SFiobo(h?-t~K(pOV+kX#IWNKmFdre{=beyaz3L587QP zc@Nsde~F?|Q>GvM_|98tj?XP~s1Q+8y-9ES2Gqhiyz#k|7 zP!f85w{+1`C9x@dlagrLwyl!Lm?@)@Sg~@2lF1h2?^b`$RT+@H2Q7II+Le>M2Q7II z+QWZWIcR<^WgoPZebBD>>F#q6|IPU?>weI(?guUFe$cY+2QBM<(C#`9|Mm9X*GhU} z0`86X+q_BoZ34G%-&PVjf21xX=*=59n~*bSP9>qsTgQ$x-zuQ{@9KWls#Qut_y6y{ z&yXR5l8|#7pxt$Y`wdbO*)6m`l0d5VK2>`zdF_n$@c-{D1CsZkJ^VM@!+7N1y&6Eu zK4>ZXpgr=RetycjAN1cT>(2ao_~k#ey{g0b^q`8edo@dmBg92GfG0Pg@D%GLfS7# zLe~$cxjd6jrjz<0{qv7g^%uCwUKA@*cG0J!r{$&^kXP??Fr6gLdUS z{7)tCdU^23zng1*dKpdcUdldbDf^&3^6#ntWf{=xGg;FE6Tbg;vXW@muAP!_H@`P+BK-k@Teog0 z34OgMhV~Pb`0~pyl|;UL`ILma@p1Kj)23w4B(QnICM7Y4&bX1#^D|-_=@SY_*$3@f z{&jxq_a6Rx_@8zekg^ZjBmeI8|D^1Lma-38%06h_N!bVOk$+G9FUx@2^}n0?U;23* z6GqW_I|6!nRd?iOZL1|385dBSt6***5^)sT0{3 z3S`clSxLAG-_`RhS+XdJEn8^6VgWhV1KO2+)2ihwl?2Y5mvG5{o!|Psi}`;K|J|$a zCGSB?-h=k=-y{FB40z<xm6N;&?cu+9KMpDTpr!1C zma-4}Ugg`>_a6ScD$(ZWvhD}%Dt|rozj;1hudns`T(9rl-Ty((HGs}fd%8*-Idnux zn7QlvZt>#9N+M%Mvi}osRUY)`nKETk68iJ|eLr^WSS68=kf0=XY@_{x1oRxz*MlpT z4p0($|1UNFwf&s#asLkw|J`fs0uUmj)M5Bh%B z_qzWNyZp<&A3;<9z_eea-k#|9`hHXs=*(>iy*_jIJ~%j7NqBjADG5ElAN0L$-ywAg zKucRl(C)gqv<4`lkH2+(==VB5b$;vj`unwo<`--=}_C<)!(%gkN(@7Atet0W2)Dx@SR zG557hNZka``h27NDKdX?@xlet0Z+jjuQpH;IiH~OfCO}Yq~_QCJ^#bQfAe~inVat4 zN#28&yaz3L58A_jkNnFr;E{h%{V&Ua-kzHpdnY&gd7- z>;1@KBb0=0Z!cxt8J(J65BB_55C8A21W4Y4_VC{$|FR4?lX>}{NB+(BRkH2}?WzBr zH2UfMMP+M*gzQ^@mSqn#%^#bfp9dTC#Xu#Y*OvNx!93bCMnc!0iXaJ1 z^Vk8`#9PYTtesPnBk+9MA+K(O2U=@3+FCS63_bNRTBDqy6g7d z%n!*c(0A3yr}L8TpX5Dg5C1*O?U8?Te(86f`achy&zl0h zKGy5&bgK5h^eAt(T%{yP-eV+W+(5gk5A^Y(oErcw&-w%Xce}9lVM=25%-Krf+O=yc zJ?Qbdc#-z*6ZmrQ|CEHDUVVN<&o3wWY0htrAAgCdZhRzp4_fjbw3GOn%7ES;==tH{ zzsvERhyS|J-NU_bagLv5kR|g2^YzJPhi~0u}Y$5rRqvTo{bJ# zZ~rK-OzoqS{4(XI%xlm(KOXeG9v{hj&`#20O0S+D9{#8F?U8qSe(L%Cp!uzbr^iQ+ zuZREHm8=8w@=p0>k_RXG<&l5U2FSV}w5R^pCGwxWC;h?FX5`*UW!wQL<=a&Lb$QU| z!+bJkQxY!~FRLVae9%ovbbIe(B{3^xrjm%-6{RHf^K5#&PyPwWz6fY3+o1LKOHYq} z?<7AQd1cEtbAJA_+;ttgnfHi$3hzNXNw0_hrujT`e(Cw?ngjpr8+v>_{5QAnlpm(@ z?nE9u@-OnpQ~&?F>VLiVp|gB_B&ht4kRW~4s}fG+!6Yxb{OILF-=9`LA2ZjJK1xEL zkJI(nb^rA6^!VuadU|A9LCd@VE%OSro}d4=dp$jRdiDI!^GnZ9{a)vX&My!D|Jf4b z;lFu4M=$TXJm~rL&vNhHL)X)zr&rGpkNlh4%X{a*!|j8gw_jNaDj&fnc=?|3mk@nJ zde5W_fwF`D+=1TyyZY8;`IK=5t))k%74*Z+o%Jp6z3d3-M^eCDZ~&r9h06;u5^PlBEw)^LUHQ{e65{B!xPjW+ow z!vd|xN5&QOy`XNu5Ys6R~}qW+iV=Wgyr1pZR{Ap%5j zG6HvZ@9@C(dWr{kdvEuEUuO^Q>b?YJS53lTLIz7g`XyJ>_`R<`+^^&D9FODZp2u&t zF}prqOBtUV?}_g^!!P#tzDE9U@SNlMxTK%w`5g26yiQ`DpXYgedTyHEcXglYyA*$o zbtDe=Z(!LydBO2`T+*M@LhG1>T$!I90g#>BRevZD{9L>*v|Hd!Og2oC@&E zO5qSl-~K9)D{~HtM~)H1MCjDqcfFd&HT@3#&un>i`V-0)DXoA~1xgqwmanh^s#mCb zkElZ51MH9OzI?G46;Qfh2?K@w3n+m9FXCTtCy+CDFKD57PcPrJwf$pmxo$OV`_D zscQ+E?VngrFRSHA1=Olk!vLQh)v{3w1q}SWzky-@_mu*^8Tz$>v9u2p@T4V^0oL1m zIuqGj5wcmHvLh>9vnp>(76WXrLH#aefa7i7N5L(o)60)*`gxxb-hV{ryWVfhMeRJ0 zop=ERlqhGwCd;$cRqIs)7tUW$K>L<$4e;Jz7w9YmfX_#`lyFG_%a<)TP@&{Y3MgN! zjDhy8=zBtkR_zQdnzv8^qlb?&kT*N&md#rf@J_S04e)yjw1)!V${js&l=gi+s$x#Z7)a}V z-Wzu6r0*5L?UbkwjC8r;|Kpl|>fe+BNN@8RpvyVm=gLfa_5`-gJNElg1?XP$9?IH zl$PA{ekKLzTxekXmTd-feuk~3eU}J*y7xA4?kw%QOmNrTrad5mCG!^>m@##l0@^ff zWuWa_)bAs7Y~9X4A-@6&U^`90ccUz%q|27h)ck)u)6eHb()istA5_mb9@q2#6Go4t zb3n(X$>i#@x>F0UMyfi-taJu=*{pu!l8!5n*>(r)`0#5yT%7Cl) z`MK3}q1S57CVo3H#ozsJ`{J^iGIf5d=R?s+_4 z>=~NZBm8ml4+EUesJtz!}e- z{a3qoZ3W!8LFY9PbeEV&=SUH1{HLk`?yoXG9al~_Umw@>d;3~xtV?h%_if&!{uY7H zyXE}me)#5%n-xIzUpiFn@>&gc1bRs+E2xkN@rDj;jttOl5mMT!(rz<~n?4Dk1S z<@$o(clX-4b7uwcK0(|LvHuHcP6*)f0M+xx`jjXKw~3bUq+iql%zvJT=X~XPy{((J zD4=!g)&|nz-W&64>pe^Me3x$Nnn+ zAN}-m9cZrqb7b(MGwQtTYcjJrTh*1zR}6gr?PLYCYuC;|TKw9yiTVcwKEHw6{TP}f z1o%w#eEISzAT59N|M@(~%^Nl;V9u;L1~{S-+o+#NNSpthZoWRI=})8o!|ON`MvYg1 z!(lH?0*}-8?%k^Znm0F~^J&TyI_HmY`7+J_5k`y{VSvj+r%p6pBxKH<*?|7NexD^v z76okCLi5T5uJ`rv=Bni@6~KB1spOy2%~w(Pp^bQ4(x2A&hsQlFXpI+0zLjjiQ4Tbs zO`A3f(D}vVcs@^k&z?OB=-Rcb0sVbf_mwMGRsf$DwP4OX1?VoYXI}+yJ;v=mkMlU) zeC7Z7y~ic}Y0dv|y_uipdI3JaSI=j@U%Ys+0y1W#@gE^AK4!|4Ndaj+clH0+v11jG zkU;zO5_W8(c_9Kj?gvyX9iRY~f99)Wp2x-f&*PGQSM#4dkH+iwoX=@pgM)(=;N|6I zz!g7TJ?HTr_Y?GSs2&g2KVkW0zVnsj-I(X*bn}(}=lA&fsHdOu-*7Y@o&xCgAg}XQ zEK@-NJnqx;UB6$ucC7*m6)I$a0&z>`xj(_}hVH+9&-w#zyxKqkIiJX3fccn`F89`d z9-s85zy8be#Am+q{_4yJZXftecRtIV&v57dI-lFCmj^zZozG;AR%-$yu2z}jeK4s zpVP?aGqQht9wVQ_$m@AG+Ue9juz)2aDxJ4cbzl@{Ome}0eJLgPG4QGSg1 z!ACv)oDZ(_A2}bmUf}xRZrA;opL|XapU4u{M2QEI;1^;Ykb{^z5f zel7>B|4jL7oL7*RJn%d_@1xH9`Sbid^HIO*{Rn=}XR+`ZEPVC~ug7!#JK?(#PmVXI zKKIVEkU_Nj?%zOE>o*?ht&wKXsUj4jBKd&?Kp8Q;nnBQE! zI2^u;cg!3Wbi8s`Vk zr}BC%>t}F1&i!kCkH6!5(n4T#g6Fi{r^xP6wy!-=2OwU+>la zk@?U0#n)8%`H0$o{@c^9mxssA{`udYeq&=B-XFoZB1Pzh*{{Yn3cP;jvBVnbNx29{+oP(@Gcr?<#*dANk7p?6?{p34ZAOVm>;q z^n&0w_K7G@=tywv6H%0I{ziv7A+MKjO{_IheTTgjHxRJT1NQB^L0~qhqO zegto)nR*w0+Tu>*CbH`6L=ZZS)_DdCbAx7Bj z``txUQ@vu0&%JVwX4`Cp<*-YyRWFrII_%MBOquSmMgMWSW^2wi=)(Sd)Yl^&w&zXj zHFnsY*Q;Jf+MJI@TvA0ajX!99&6|*iQ7TWc*~DH7g~?`$_S3jRpg80ngE%2>X++!c zxBWfioOr4$uAsi4?+tw`9@}AV4)Zh^`*>$~%3%-wQRffIhG@KD2mVo~4;*&jm@|SM zIPAY0)~WBX|2Erxr`Ubx$mnI9(?@LvY`)Q!#8A9SA#RR{Iw3H}OY^(kF~_UbbB;O? zcD)FfZJIyn`jMdv2YY7NF|sZ5kp6=e=D>9u=DoZ3I_#GNUMT0VU6yvsKakxb?3N9C z~pNWU>^7P!@sMq4cq)-YQ&e{7=FMX z0Q%osHvg)A`DwXPK3}1*P=1LIu@sMB#4*kI)7$~Ydy5`-#2;}-eUI|lx$V0Sn_}yg zE5_6yY>CH?80)Yl-WV31t{pLKhfhYIbZR>c9%5Yz_QLNpYh&1s*~`)viVOCXF*$oZ zY1qipbF}r2@&X#R29PJrdBG+YYtrma96Ro?hy6M9r@v$ii#*@CW2eLJwIkWU!|oOK zu4rdOd=NLbb#>IMRJN!;<4b*lJa~>im!r&}+~mvVry5e7iM<&Rca#aZ=;P5w+=C~4 zr$QGkA-lePPHitZt;Usf1TnXJX#YXAZ_hrp`?o0d9o12g`_bQwQbBY;>KnjJAi6Ojm5u`9j=BbRsl7kxnaVB|_Nd=a40hO~Vr|-$L)_W^ zbROxJvh6AAQ?@&;MQ4Ju%_+j?{6x97m2E@UpfTbPJrj!OP(nPvhb!XF_pmM8v2A;* zsKK6c{K)ZlZBGf^%(G|DI_xJWj+x-FojiT&w8Q`1zwLI|Oy;HfN5;h-H|WX?wZ%Pm z4U!G?ffG5txI$Kowk0R%uyppj#NE+ z_EZD=4Nw!uf2$TRSft?h71AMPcP1{5%GGZ5Dz$B;s4YXgBjaYomEuf&1H`>E)tAel z|0UnS{Brr?FQ#kT2z$m^A+ucCGa~M6!&t6dIfwn?#EBErwOxe0Vl~>!2D(NvZgyQY z8b280rup=2zg}lz-W2vZY@5e z5$dx+Ukot%>3N>Y<&Xn=Tu&Nthn}!)TxakIaYtJP+qj_vhd6BG4j(-1u!|crdW^#+ zu5G)vPVM0kceaBoR;-vQ-}Q9B?(K&ken?gK!rl$_8*JXt1|bci9G$25Q``+3CyY&v zXOLSiJ6KDHUL9QgxpPL_4x6rzKK#gG(^bBFd8hVV(C@ihJ1*$*eel5tMwyiP4|>D; zaEHB?CdU+KBkq_dNH)cv&Wy&o(#oG#7;Oz~&e5L3R&hJkqaS_rk;L_ZO9Pv#k3as{VN*r@L5DpR(!g%Xk|jnN<2bsy1_T5+?53_z zp9=K^+yUxau)P%8GL&z7ToGrUpGg&e*vIVKv)Awgwm~a?`o&=v1f4EDKClHke*C!c zK7N)jU+$J25ctS;KWkU5aoGG|e;Bs+LEN3h-&}sEtw8=8@uz+OWOvuDU25*!x$4zdUsWYalyH7g?v}jSo-gP$Z zaklE0l}5Qi*fFQlj6cde#a)wMBmOq~9Q5nqV4Jg*`p0aSgEol6ZSQlT{{ov5 zwErmg9Cut{OR{zAR);Og=FOXpw7`a>LWK(H+K|9*6#cI;W5%e86)T$J-=akeHF`AdV?lcv zKz2hzL)FryOVvv+y`-Lb=9zSL;|mrnsQLx=Q~USrH)IYvcCe{|i==S@_Q}Xe{h8eM z`bV~3#1m8hf$M+RcFdbK*QssCqD6~R*>u33qh-sM3VOM);{ZQk%OQD`77e?NQKLo~ zwi~e7I6&!Ow{G=XhrNccy&q@pKbZS(C}U`UVVf|ZPk+Ncf&Jp^!Gi}KHVLS^^?1P! zAw63K{eAS0Hf-47ush(sE^H1k2gCLTh!^TkbN?-M|JjfedhfjsUDXO|!*mxPAFq&x z#*G`Rk|j%~D%*&=vvkRL<9Xr2g^l`Y+qP|n>|eir-QJhBjj2&@_WiVvF~^9wBVLF< zArXCk^q!G zHh#w(xowXzN~dG~gzF;cmWQtkckmc-hh8~!%C~RXt}t##e-mQ@v_Xg`(!jhx+|lQN z4*Az#f32WHo}NQn3B7Uj1F)tb%FZq5izBX>^FiKANAIPYzkSn|GfRzZN@kU|FeVMbF=zQ zjX5hRv#2u(PUo*U4Qb6E!XNCX+puNQg|_G9iD()p#4Gf_ zmMvLo#CzG2WmL{0X^eTs=u@6NaZ>$CV~7Q_=NsweekPCanm1^wpyP!+=laGLch~cW zm>VToPBnik^8s_RNCP|68;AqSEav^7kE}cN=b(p-as02FepS#xhW>FheZvl8Q;e4| zXN9<eZ_p1*cc&(f+jbCj~uPp3CL= zJJk2kpM~yh<2r8``7QhS_PF2m{JA~;ru7T*!?6Z{Gytg6>eYB%jrnG@nm>D_~+6LkyHjW|2gy|m{Q_6FWb;ZxYD>q!~M^l19l+m?ph-N-*$*D&g@n?xrg9OM_u zE7D=?`Aze6)oHGy+RIgpwi9#ry*~NO*jKgz$qe=mM>=UO)A$bG0``2tcL1EQIMQUVr7eyZQr&KgSbS#8ssIf#?s&bVA31;z9kTyVKvG_+xGZ zeG<%({p0!@GA$2-{*1a~u>?_13+JHu$m?Lz8=`x9OfRHd4Sl5=7K=?CDdl<+?`5%A z^y^^!pTTxsjlAOThsg~_WSqqFB>s1jEVX*7K@VN_o-U6Sq;7Z>Gx){VqM}GwE9Dw#5Z5*Eu zz`7t*0Z&huXLyvIEH`v^;q1BHh%q9a^{k21%5rSl%?yf-@bMR&FzI`4fh`=f&HHoT&A!~c=Qi)Pt?jXGjFmYLC{K(vdd_q7y?D=P zP6zK1jqzYwj@RFL?VZ=%dCi^ux8<|%6ohY|r;6ui+%Lh}x^%opvHVZcyazGwJ&b*a z+3`NZ>^H(9Iv!!8PL*rSD67u)&E-9E*-v@JI27|`y#KAUeQ$Y>TTZ7DrhT1QmOID~ zuMJ~di!m^-31eKTJKl?xvXWYL8+h$kzRP>8ri5?k4`7}52>%B;=e5_o{^7`u>#Ztz%*y68R<_?=R^H z-?m2M(5vC~J$%D^Ch}g12%q;yWIvHFGR->8`yKLJ3a{6Ra>(m+M*4Yu3;v_7s0peZR=AAA8S**E+xa~z2U%n-VZLVec*WAiPxOu zdXp`D3R{#vUSC4^G^uFpS;l*naU6JWGVDu+c?aH$jGyzB_Z&ms^O_N_7di5u!Y10- zOBwM&`QyE_j5yeTAzL+IQXY9zik=&S=OT&;3h3Fz1i_wedZr{v|&! z&+lk|(B|^ozWg3E&*wV}7yt46z5X4~*~|HQgo&`>^!69=#N0N|5%HWh&u8;oHhpTy zKhI&OHGj=>){eB>=cgCL>tWW(wJAroHWlz^ISB~L$iJX&p)HxV262U z^gB?tY7-6O5?DLDp+#%?&!_1$oHxfzYABTp-T_LMH`tfO7s1aTn<}=KN$Bu4v3t(f7u@ znCzb*9y}Mu^I(t{{KtIeaCzSAUgo@9jX%L-Ev;0q8TK>xH2&1{=YEa9)dsQ!qk4qq z8EV|HSPEde<|NHbY#^gOU@%P|02VhLf49Nz&q?wVej^Nog)k8|<|!~OQIO!6r+9*H zTsX?}0Lo|Rm!2eG9u9!+9sYCZ0PEgR+4hFb4Qy?k+1S8#1~xOWmCI(Fz+S~}LyF+kOT`*0){4O&)W!6^qx|4|F2DF6@RF0AU1po>_ z=Chcpr~W@ZR2XTW+e@3bxyF)?cNMiQrVc3%l1Xs$2izb41R}CE-ol2EA|aEIXCp9I zJ7MsKeGr6VjhMk9jZmT*2ll*|CavW#JduN=a{WhX)xcEY?olY{rKZOvEWFC)J9ahV z?oXbTyjrTER<&kuI$NeumEYK^2!5cGK?5EPz-h_%Jdb76GP#<21}>@XaPPaYUE60^ zzO0wV_8sCqdJ{e~&zWy_4X1v(kfN&`-D&OUl!KRU{wqp(wFz|7XN#e#6N)?FMQ1+{ zWi*F}(0c@+#rR!+!PsA*t?CP)K$k0QAiE1fJI7qA!!&kkA4>yN| zjgD=^%zvd4v<@2mbVYnHo0Eu2Iz*75(^jbPfDtT2ao}6@?g{|i<3fU>GyDK;1ZtTv z9^wN1`fXfcflK_YM37X^ku)$o#xAK+LA{>5NYtgV25*skB`Wnow_eT)*MEh}`2NSi zd7ArrJSs*ZCfx{JMt!D(JiyXnD|jF1#b@-RS;;%84@lq&HeC4nN!1uY;L^x}qv(p% z#l^GXhUF-bGAgQolVBT0?bvwdw}8uoqYiwl6?V z6w+dKhw%g;M+%F)6?Li2iItHdGN16*rcPw#H2eOF|4327 z;Aw#gF&KNm1xb{~t0OICp|%JnKN5ubX*WH;dDrlhp_@qdDJY6o**=d02>cZc)4u0d z27rDAM-1qN!q*jN7%4pfJPJd~LA@FAFn2;y$8F5Qn`=#qqyKMVFQhLdqD3|af`?XN z?mM5b;VCY@;dxZ6k~+d6G2BuxuOXyZJ8!x7?n5Pj1@C|bTFOog>=*eymf+q12!I_# zk=PO*o(zL`H;jvF5&QRmr@*rDuh%d9o{W#+9p)Vzgu4OSg#P$0HMW_AW)F%C?3h!= z6uzhDAh1hgGPTh@AN^@ZeA)r`YXuIdjzs*!!(j|8jECU@{O`W6iI3buXbkAo0U+de zj--V#U|J-}KJ8gY-z>vqyVQ5NkvWY>Ku?!$Xp(^I1(#8^bwW2}6CEx@-sibz+KEOM zti{9rljtNkXD(9m6VgR%J|BcWRCDmUDt5$NHG7kUW{QJ1SX5`c19eLqd z6#9k6?T)5!eE>ZG4GvyoIm)qOr1;5hjvEo4MYFf~m7xviF*dTC160G~``jX~u0KAN zcApx5?Z)jPp%PxG80%Le6Qk0kRR8JP5i{;rVt7BZpkVynS&)rCwBu$i694O-*^_1!ZvZy1mENyHxh&+y!yWF zo^ci2b;ls`+LyqhKoF~dprJ55G06|M%BP%O&eEmie<*#2-8VvRF7O^L?0rVrmp!~B zG){}b6WZx7uRpNRyL>rVALizBQGqzE8F;l>3Y(R)T%&xoBqDPJw6-eZ{RK)Of;?D_ z$^|xS>v=sX)L-L@rW5**Gvq@hXJ++HRxP*He}r^mQa|W{fmDHUVr3!RbT9D}Inpfu z044}>3)-3KHs_F7Gnd= zAfj1(HWVi+2_w}HeNF?TCMuM2s58_y6f|Z}7klZl?v$3EmasPd>pn{rJv;2*(;X_D zm?9hOd$J!L@U_I3o1zU@k!>!QV2*(0ua%t1aB73Y5oFfcM+i~{wok2DRFvJo*+Md@ z5q3tkNF|yyFWB@ZqlS*x{@8>>BQ5N9-Ur&h7YYy?&-s)6A~(4G=HoDVi#And%FaIr zxC3$2echxUu{((DHaQ7{oYMVg3+(PuSRreLzVM9huM3$6N2?85MH}@ zCN2181W0Ec46C!%UJtcSYE~7)b~%m%T)U@C#9?Tgrs!4VTKi4^ftGJ(%i&0*`XB@C zAP0LfX?T%vB=oDC1Tqe-HZBS<=M6k|1aQ$xH6$`x5fUyD&YslxjV`%3P~*zb#|MTM zet9zLBfntwx;SZ?^I#wC$#^P_2xu+YBvULbNco0Ea3=PrNGQXd>@r!+ETY3kU2ikUhM&kswg9xZre#YxoN-&qzU8{@KBo) zS@}xHiD9;C;MRUwo=4!)p;p`Onl~+8B1CGmyIA2%+ZiilrAR`s%YT%b^D69PBnAwr zRq20B7zAz3W*Pz;=f2k3ecg@*PQVXgidE@xOYdi7?If2c$$DayVER$9C?r>2 z=Wz@+6QR#|C*~jjoO+coe@tSno*1pysb9J+yhiU(O|-4Jvs)^Ocw|`?DUM@%|C9OY;Op(* zQ!qro`3J5z%mcv+Q8pwCe8E zLEU-xG;AxXoJYT4vYy&Epr0SYk{-Hf$A#l8P>?C&E3lKuAy4~2^dEYw*smSLQiR}3 zuSBQa*_7=S^!Ep4RTB3`faT{3PNKBxZ=7mowkwU9>4Ickt*ru4QBlpM9pT2a-upTT?&HEcc zEK#ltvU&Uca(&pU1=n*aLN~sKeoWn%NWw2;T5CM&`25`HA>T*@9vg^`uON;J_~kKE z(CI%XsRN(welQkJod$@&z=xxw3Y2z z`=V6?@TcUi$w4pFVC}AK@ZZ-A4Ay~Y9k4g_veTKYvi$*#&IGF-ql?;rVpcr60-siW z$*)-RF4})!RJ3h&PMCOC*i;RG!gb>e5i46>240T+ciU+|475Lt+Hd1-Nsg{q0=#_aZ#q;dpOHx&0e#O+ zMB1kU9=?@PH(y_2E;)VwXSd_}X~h#pZ|07j7b(ZNzZPEQdP~-`@W8}wY=bgGkTYs! zBdP0znCD5$zO0~*i1u=|5qxWFtBbOXZS#9@}=P#VAt%2+z;s-vb zX{my;YRdB@J6AKJA=6r85F|lFNND6?eP#v^r%3g^h&aI=2}v5qCoKXQ+!|{Dk{Zir z_YeFG(8{tZipDc>iye@7fMHV;5y2UTHR4IwoTFPX#vTMtB;6qyA`;}eT1(^1M~a15 zxcLLe$WD3(_g^t-yU*`ZJh)keu+))&p>`>O-0!s5Rsdnh+FjiQV2b8+jx~o;T2WzE zRMrm&7WKaGb2i_ka-qiyFa@$b8PV=pGrR;DeImGlD0v)!V!ey8sK#p;GT}@fqx{*d zkmbSY4@1$$n)fNVJ`9|Y{!^YFnq~;u@c58Rz@qI%*Yn*z%OcAkiG10&2|}k(u3o?| z3~t{RLD6@cRnu&C%)vraSM$;w)F)pj@jd!$UDV7$pcktwc?>!@VfIxLsl)L8*CDlG zjx~wd$Bd%c9!TBfd2XEH*o%5Y9Zjhi)1)m_FrP`jO!9n$yf9k-bd)C+(@RnzZ376N zg3MlYE*|?}E@z|;d5tA%ENf&<@|#N4vYrori#RM|!i$I})>Z;k(^fDTc^#O1Xt)P- z%>Y;u2(SW0n^jJ)w=jzhMikiBqH0HFAkia0y;D|#lwO~UR>$0WE_$y@kYZyA`h^~k z>-f(g-xHV5zB;cZ+f$4pHaRSKHGaam6n6g=KH7nosX$DL1TFKbfrFUHQTbhMA62h| zvi?&fN9FCLk)GbKZ1Z$xzG#`I@qEWe&6+r!vlK$0|3e|Yy-38VPaZjh1(a=V>5#+s z10N!rH*WTEQ^)Vz#&(%E>eS~EZLc(j2O1QPEqM``bkO@<3U}>!S&+4upN$v~_@T&{ z#7G<=uZ2HXF}!!g(6p!3;!Hz7F$t+XS+vN4SaBLB zT0%Lc@jP9!6E|`2(SZwbZUw9UQmmj^U<+wh-+NLJ^diCsus7{!z~3Kzsy6OJJ8oRI zx3sY_3Gff_{R9kRMUPEi(Oh6kK8Qd%DP*K=w&QR(dF}q8+uHw#BR|EO#qTkr?%soN znH(TBNHU2C2UuSxkpQ_$Z$d;rYg_a9XA4mV;ijRJ3Lrrx<_kWFi{O;#FZU^&T(H+D*G0yND@e%5HB5Km3 zm+@-URL?`5nd}8+Q%1kAOd>!hRNYUyo#DEzf4@u_yhOBwjhh8}J^D6n7n{T~qXBT} zpp8^QU<|N(kEE1t_sQ6V>|@W2|HV&L~F1D@WZoK=%i3Er~%v21++^f+S9ubB^px)ZQ(&$715eCtFaOdE&F41M=_qhH#*)_Tff<Y#v2iC527kDm>Hl5@VN& zY@qh%Cpl)XvS0)@U_uq_Jqimq&3%t5*|Q~;@==VgJ}tsZP$4=WDSIY_%VI1lO}_et z5HzvKnY8;PGb39Gg3wOL)FW%w1h&ajAq57~Vsqrq^k$tRs!Z~&=+u*Q$Bpf=(p$6C zJhG2C4_feFA|QTzr0$o1c<$@(2Zr-AqDclCg& zqyaLt0TmAn@Czbm2e|ThNT}OT3=z*cJX6#H^aHtD>djHa*=1IY=;4L7bR5yF*^JVG54rwP^Rc7Qn;NBUlgw1H2s;bkf#ve|8YmyIy00^4O@<@GtjA2QKLHQq`8taWk@h7bGT_ zn|7vaQh26i=te7G|C5k06+*l zW}+|1;HCl&D31S2*vo13LUY=M>ImrY3?9A3pT_oaL_PxrV4vWs)S?0B}&>7c{ost&{{J zz_L3^X{394oaI)I^V@ov6g|pYy0?^E5QMkfSFBCMeUc75KZJufMH-f!~48 zCJl60$^MarCFF`p_Oed1Hb6SD3Z(!+7*Mf^Z3Gnx?% z$j{jO#f{ap>J@p!P$Z}GZ}eXr57D$oW(N(0T6znA*eO1EvBc8%dfpubpq#an!wF*6 z_(~X4Ojr>ArN23UGpFx%JMK586VEst zAUxf6exXxkde~~_H$l8{zn?X5yjZ6fO*7J)SmpXBks*NFsoM|P_;CJ9WX7sRsk2iQ;LeM{c{{znn1Ude92rc8g>C=BZ4qIQU&;Z9<$^} z1+)$Y^%ypu%s}yRV#GV&W2!cj%e@{9No{EEg&H&PBOvzq{Hu@iP3M?b37pMSUQ9K&maYK6_FzUJA(R3NS$|ySV84jw~FVMs}w1sRL#3TbdpA=^2r{IQg zC%?Xl9<(LWMsKIKcTZt1gYqc};eWkKOWQ=x5u5rS8gz38N*P6b4)SeYIpvppE}aC7K!%Qvak~Q!R<=FOX~sTzGuqo~NYI=dN?ygEfgJQ8Dw{AX z0O9B>TB*MpJfLm(fXs(b2?|a-PMZQ-CE7T_>AX6g?IkD3fQOGU2J-X%`>28e(BU36 zVR*YI)H8SJHH1{kAraUp_D-&H;uC&*OUNz`_7(EGM+8Mr3UTCOX5mgLCU;7mMr?(x z$+I)|WeXGm#fOdh5%03K-=N122m;@v2YuLZ8>+0ip@z)&>5onJz`vHjhaWP`ZJ*O{ z05}Gg5YTsO0sbhjL&j^le8t<^K;#eoMr8jzo_5AQ-{!#oIneV%HN`R{b&O-Gq_e_K zq9*c{IH|?MvxyCkJb6FNz+eN{G9s^04rcSGi;Y{wl{5c-eSJOBPXKvmzxML-0%<6y zS)DyC@6UL~4E{nF^RTa`7?n1TUUhtUHwC;k0Kp>PMHsr30Eo=6JbZC&gvbRyVzF7f zYs!P$zy}XgguAP0r6w8)27(~ZyKx0kjgAI15N89_|AfSp^2 zOHf+NwpxSNeyy9rp_DeUlq{ls=RIwP^Wj@&ivifQ*vkkGr&z)Egr6_q+O7zgo<&}F z@~dK*eGy*oLi|HR@o=QGzsP=!K3ILlH>sHglF$GS_9>)Sw`u>anQ0O7AJNPD>pv#} zt3_XTYr0-s^HkfNXoeaw{k)IEv3%F}cP2_3=$A?%U;ZL<3vm5>^EN)!xYwfsd@EnQ)WC^XlsX z!Hh?NpC6%ba?(1&r|wH6wr6J*3)yc3@x&E_?XR}cbUpL_w3PUw-?r-B^f29j4uesa zCFy~mIZIqP48jpU(~rQNYuAQ|fvVuB64el%+5rU97s^XL{lxvP_%@pAB-5HD*3#0_CL9gYeJ<1;-r)3gP6ehxtmB;g0-T}2<&e4G-x;P? zD%(!rQftR#81ZC!4tdIirm^a_7BnNSV>?~7M}J!3Xu1y~Py|p5EDLgQHo{x+AZ8Lp zpMF3QG(GvbhrO?DDp1o0NmfpQVaVjtS$5cEx(q0#Dsn%PJ4Ws)w93(k=JpljuXl8`W4~j3Z^N&sn7ZZylP*{Q1s?5dn2r`R`B41 z7(!5AKky|X8axGk62)!tPn*N@X%Pz&^VXD@w$l|p#Yq3CJwz=)k4_t(0Cdy+QvAVl zHHGThm)5glYh`yF*qbln5kQY;ql)TmNN0u!OlVzAnbTmb2tn{UdBIh~>4x*{`7?~* zt7AqxR;%YSSHQ6>uOlat0bz5MGx*%}MkKIk%OU2}n84Ke?+DpsbYQl9S)D(XJa#c2 zK=>#FG+**1D*hzl86fqpw{iIqZ2;ftGlnSr2Xzn?(O>_b?rB)dg54!BaAuHIg&D9~>CsO!31JI$EhVBP7?wSQcM91@#5$ z1}K4y5OGyFlqeS}A%3`LUmZU2b@GR_UKau%qOOj8zb`@dqITpY{b!ck7 zWyVZk8gV88gRGZdMpVrYq+&$?)-%y%CQRM;`fy#qmKh_J@>Hd$|7J(1+me4k8)Vut z6&P0j0dR$U}0e7Z*V+ zc%m)mqD%{Nrr_MrA%)_K5;O0h;E}q&GCQQn_|G8E&a~K=V-(bom%#@(2u+n}5>6ce zpbP(>1$Z%UJa0qTTM<9L8-Ne<>Np9tnxO8)jQ6TG;$MgqAQMo}WIPrQwO>v+xN!Z` zspihE$%1TT50is`?P<_B3NT{qpU&fd4ZK{9n;5n}#*tQ<=035@C78`I>bbY5OVA|C zPBBkes(PN}Av@5}#E;ew$+2B|xz#QpybneMIQWjX)pJT2Y!@3-FLnRx_zo+koro9x1R_$v_$e;!j zXNZbE&Hk2~`)g*3@n!Qa;l6}X;{Dm)vO``|mFPB04sUuPH>5xqj=H^R?_aKK2VQok zeC7;Fc!-c=LQH)eGkl3Fnqa?Y=`BU4zkw&i%TW=Zd8WGAxp*xS1C10fPlVSBt_nQf z1!_Y6nPUwe61~dQa3j#YXps?we5yC1sKo&H3D@AiN~h8VQgLf3OcC(6L&1eFvoN+$ z-hC@8y!1w-l&#%#0=+;Tf3LO^O{!-p9pgX6`#-J3vZrXG2?5&B@KF)F#HzLvYx@~X z;xVDyw-PFR|6jtnsNCOjs5{{LW9C0U^IKSs(eyE;k!G2Md52zGBN9S_xI6NzUUrLhwgeuKjmPoTm09rZN?T9 zAr<{Sei{ejQI;)f?T|0_?`M#{_oOW$%whT*Jd*|+hZ7fF+<9qJ&EoMopfjW;+KCUL zo3x+Gb>|7AGKAD;wxlB8aCj@oO3XQQe|_*GJI$wHDbT_()<(G4Gk|Bke-K}BzGV5? zfwe(h%!ULVjSTs+M`Mk1H)0_Q4hhzftHVD(FxlIRLR36OwK=ebWpV_0e)m8JXM^Sq zUQPDIa(Mr5sWzXb<~m5%6Fe_}JTc-dZ)WU!EebGrdeUI7G$&cHZEhGLOxH6)Xa8H8*~cns|U#wmH3t3SL`hY0+99?h9&*r6ld}($nN{N-NC# z9`Kl9#k-p}5N^w0!FOF~jRd3jx;9u-p|slXb=xc& zKN+>wX!quHaF%Fx2mupr659BGdp|ACy>^v!oE;hTCuhGC6AltAgWxV)yVd#4r%h2& zf~kM~YO;nQJQjiW7yvpZN$Qqq?LV0GW8jL9U@Ke3?%Gg4qFK3xFIQSmzY!0GP;!RX?1)))0 zb#an^28Ospn(s_WGqUaSDJ|3gMz}CNv{_1dd^HCKg%iGue2#P##8Ea_4sKl%n2Y>` zOdcFY?#sT-^UpT(kII44V|(mgeVGVs@ee5GOYcm<(F$PZ zt03Rfe(}EP5<9?fqyTH6w@6Ax=&j$r1#n|tJ!j5V_vFS`Nu-0FUjOp$(Va(Z4dbae3t zwelE13I>yhlgXNI2#z@i2rn>6fm7}o&xxmB-j*nC$x?e05yvgsN+!y6!MI|(zclLH zL%l_GcvNb#QwPxer7EcPytt%VVLwqUUzqMk_yJ3iZUX!qt~|%GZVY^uX4Hj;9{)sZ zfs(PN*ZIBaxtq=cs)r|_Y`WsKGm1fhI1?b4F_+`q(h2Ht%rKrX&&C_j?#x(B-IPY(7DTSSHGfK zOZYEdKMG#atn?UgT$(h7kLlS=l)%l)_}1bKh8EY1v!*7Jvl!X1scR-57mwm!)9vG_ z?^}p;)rD8~!Ujkq`(^AW90{#_+k{Yv`l3K}#5;yWp2czN9XnThn{#=qmMS7_aNGLwbklb9GFj^IJ{(lP zkq*G~v!xoo^+|R5xD)cAiv%=hVRXr3m5KNpj>&F3HCdW5?w}_O{$(R$*`{gk{y`vk zte-l08PO`uLXR*RKC#Ot-|yw|Sm*Dz^wsf7rTuj;x^y_4*hQt^zv}7R29~0xDARWA zcM;&iE8Z87Y^NU|-nkjNyFjYzy%b2c8$R^PuyuL`u>X-fQv1D!a;Gq?(E00{pTcYI zBVNeyH+3$nt=rujdLJdFdCj_*aH3My6L7+j_)jH<|Jc104Ri?kVmO1LF1kDqe)$T{ zb7ve+H&OBSRYEfMt36(-V1vifVx&Qw8bk+y8;-W zI7aTux_F_P({zzgpuJ7>kw41(4o4Fi98eCSP^|zy^OFoX($)(xCDhg6d7CYuyzvyt fbuTGhGp`_)^Y<1J32^goge@Q=srb24+%WKe6Z`M@ literal 5347 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 literal 9664 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 00a843f1..3d4cc141 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -31,6 +31,7 @@ export enum ApiRoutes { PROJECT_TEST_SUBMISSIONS = "/api/projects/:projectId/adminsubmissions", PROJECT_TESTS_UPLOAD = "api/projects/:id/tests/extrafiles", PROJECT_SUBMIT = "api/projects/:id/submit", + PROJECT_DOWNLOAD_ALL_SUBMISSIONS = "api/projects/:id/submissions/files", SUBMISSION = "api/submissions/:id", SUBMISSION_FILE = "api/submissions/:id/file", @@ -89,6 +90,7 @@ export type POST_Requests = { [ApiRoutes.COURSE_COPY]: undefined [ApiRoutes.COURSE_JOIN]: undefined [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: undefined + [ApiRoutes.PROJECT_SCORE]: Omit } /** @@ -96,7 +98,7 @@ export type POST_Requests = { */ export type POST_Responses = { - [ApiRoutes.PROJECT_SUBMIT]: GET_Responses[ApiRoutes.SUBMISSION] + [ApiRoutes.PROJECT_SUBMIT]: GET_Responses[ApiRoutes.SUBMISSION] [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] @@ -105,6 +107,7 @@ export type POST_Responses = { [ApiRoutes.COURSE_COPY]: GET_Responses[ApiRoutes.COURSE] [ApiRoutes.COURSE_JOIN]: {name:string, description: string} [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: POST_Responses[ApiRoutes.COURSE_JOIN] + [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] } /** @@ -172,7 +175,7 @@ type Course = { } export type DockerStatus = "no_test" | "running" | "finished" | "aborted" -export type ProjectStatus = "correct" | "incorrect" | "not started" +export type ProjectStatus = "correct" | "incorrect" | "not started" | "no group" export type CourseRelation = "enrolled" | "course_admin" | "creator" export type UserRole = "student" | "teacher" | "admin" @@ -416,5 +419,6 @@ export type GET_Responses = { [ApiRoutes.COURSE_JOIN]: GET_Responses[ApiRoutes.COURSE] [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: GET_Responses[ApiRoutes.COURSE] [ApiRoutes.PROJECT_TESTS_UPLOAD]: Blob + [ApiRoutes.PROJECT_DOWNLOAD_ALL_SUBMISSIONS]: Blob } diff --git a/frontend/src/components/other/ProjectCalander.tsx b/frontend/src/components/other/ProjectCalander.tsx index 5a862159..e38004a1 100644 --- a/frontend/src/components/other/ProjectCalander.tsx +++ b/frontend/src/components/other/ProjectCalander.tsx @@ -13,6 +13,7 @@ const projectStatusToBadge:Record = { "not started": "default", correct: "success", incorrect: "error", + "no group": "warning", } type ProjectWithDeadlineDay = ProjectType & { deadlineDay: Dayjs }; diff --git a/frontend/src/components/other/ProjectTimeline.tsx b/frontend/src/components/other/ProjectTimeline.tsx index 33bb9b86..b49fd473 100644 --- a/frontend/src/components/other/ProjectTimeline.tsx +++ b/frontend/src/components/other/ProjectTimeline.tsx @@ -11,6 +11,7 @@ const colorByProjectStatus: Record = { "correct": "green", "incorrect": "red", "not started": "gray", + "no group": "warning" } const ProjectTimeline: FC<{ projects: ProjectType[] | null }> = ({ projects }) => { diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index ff2a53c7..4c199eea 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -59,7 +59,8 @@ "status": { "completed": "Completed", "failed": "Failed", - "notStarted": "Not started" + "notStarted": "Not started", + "noGroup": "Not in group" }, "groupProgress": "Group progress", "completeProgress": "{{count}} / {{total}} completed", @@ -109,9 +110,13 @@ "submit": "Submit", "back": "Cancel", "uploadAreaTitle": "Click or drag file to this area to upload", - "uploadAreaSubtitle": "Maximum file size is 100MB", + "uploadAreaSubtitle": "Maximum file size is 50MB", "deadlinePassed": "Deadline passed", "downloadSubmissions": "Download all submissions", + "exportToCSV": "Export to CSV", + "exportToUfora": "Export to Ufora", + "withArtifacts": "with artifacts", + "withoutArtifacts": "without artifacts", "downloadingFile": "File is being downloaded...", "group": "Group", "status": "Status", @@ -199,7 +204,10 @@ "fileStructurePreview": "File structure preview" }, "noScore": "No score available", - "noFeedback": "No feedback provided" + "noFeedback": "No feedback provided", + "noScoreLabel": "No score", + "noFeedbackLabel": "No feedback", + "noSubmissionDownload": "No submission found" }, "group": { "removeUserFromGroup": "Remove {{name}} from group" @@ -219,6 +227,7 @@ "structuretest": "Structure test results:", "dockertest": "Test results:", "downloadSubmission": "Download submission", + "downloadArtifacts": "Download artifacts", "dockertestAborted": "Tests aborted. Try again later.", "success": "succeeded", "failed": "failed", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 79201370..f20c558c 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -58,7 +58,8 @@ "status": { "completed": "Voltooid", "failed": "Verkeerd", - "notStarted": "Niet begonnen" + "notStarted": "Niet begonnen", + "noGroup": "Niet in groep" }, "groupProgress": "Voortgang groep", "completeProgress": "{{count}} / {{total}} voltooid", @@ -111,10 +112,14 @@ "addFiles": "Bestanden toevoegen", "submit": "Indienen", "uploadAreaTitle": "Bestanden slepen of klikken om bestanden toe te voegen", - "uploadAreaSubtitle": "Maximum bestandsgrootte is 100MB", + "uploadAreaSubtitle": "Maximum bestandsgrootte is 50MB", "deadlinePassed": "Deadline verstreken", "uploadDirectory": "Folder uploaden", "downloadSubmissions": "Download alle indieningen", + "exportToCSV": "Exporteer naar CSV", + "exportToUfora": "Exporteer naar Ufora", + "withArtifacts": "Met artifacts", + "withoutArtifacts": "Zonder artifacts", "group": "Groep", "status": "Status", "feedback": "Feedback", @@ -202,7 +207,10 @@ "fileStructurePreview": "Voorbeeld van bestandsstructuur" }, "noScore": "Nog geen score beschikbaar", - "noFeedback": "Geen feedback gegeven" + "noFeedback": "Geen feedback gegeven", + "noScoreLabel": "Geen score", + "noFeedbackLabel": "Geen feedback", + "noSubmissionDownload": "Geen indiening gevonden" }, "group": { "removeUserFromGroup": "Verwijder {{name}} uit deze groep" @@ -222,6 +230,7 @@ "structuretest": "Resultaten structuurtest:", "dockertest": "Resultaten dockertest:", "downloadSubmission": "Download indiening", + "downloadArtifacts": "Download artifacts", "dockertestAborted": "Testen afgebroken. Probeer het later nog eens.", "success": "Geslaagd", "failed": "Niet geslaagd", diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index e8c02aaf..3e215c5f 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -112,7 +112,7 @@ const ProfileContent = () => { + > ) : ( void } return () => (ignore = true) }, [courses, projects]) + console.log(courseProjects); + const [filteredCourseProjects, filteredAdminCourseProjects, courseProjectsList, adminCourseProjectsList, yearOptions]: [CourseProjectList, CourseProjectList, CourseProjectList, CourseProjectList, number[] | null] = useMemo(() => { // Filter courses based on selected year if (courseProjects === null || adminCourseProjects === null) return [null, null, [], [], null] diff --git a/frontend/src/pages/index/components/ProjectStatusTag.tsx b/frontend/src/pages/index/components/ProjectStatusTag.tsx index 6f1e0bcd..1baa0527 100644 --- a/frontend/src/pages/index/components/ProjectStatusTag.tsx +++ b/frontend/src/pages/index/components/ProjectStatusTag.tsx @@ -1,4 +1,4 @@ -import { CheckCircleOutlined, CloseCircleOutlined, MinusCircleOutlined } from "@ant-design/icons" +import { CheckCircleOutlined, CloseCircleOutlined, MinusCircleOutlined, UserOutlined } from "@ant-design/icons" import { Tag } from "antd" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -15,6 +15,8 @@ const ProjectStatusTag: FC<{ status: ProjectStatus,icon?:boolean }> = ({ status, return } color="volcano">{t("home.projects.status.failed")} } else if (status === "not started") { return } color="default">{t("home.projects.status.notStarted")} + }else if (status === "no group") { + return } color="warning">{t("home.projects.status.noGroup")} } else return null } @@ -24,6 +26,8 @@ const ProjectStatusTag: FC<{ status: ProjectStatus,icon?:boolean }> = ({ status, return {t("home.projects.status.failed")} } else if (status === "not started") { return {t("home.projects.status.notStarted")} + }else if (status === "no group") { + return {t("home.projects.status.noGroup")} } else return null } diff --git a/frontend/src/pages/profile/Profile.tsx b/frontend/src/pages/profile/Profile.tsx index d26ed512..ff794b58 100644 --- a/frontend/src/pages/profile/Profile.tsx +++ b/frontend/src/pages/profile/Profile.tsx @@ -11,7 +11,7 @@ const ProfileContent = () => { + > ) } diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index fdaf6c87..d327e3ba 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -203,13 +203,7 @@ const Project = () => { activeKey={activeTab} onChange={changeTab} items={items} - tabBarExtraContent={ - activeTab === "submissions" ? ( - - - - ) : null - } + /> diff --git a/frontend/src/pages/project/components/SubmissionTab.tsx b/frontend/src/pages/project/components/SubmissionTab.tsx index d1bba2ea..1c8df176 100644 --- a/frontend/src/pages/project/components/SubmissionTab.tsx +++ b/frontend/src/pages/project/components/SubmissionTab.tsx @@ -6,34 +6,35 @@ import useApi from "../../../hooks/useApi" export type GroupSubmissionType = GET_Responses[ApiRoutes.PROJECT_GROUP_SUBMISSIONS][number] -const SubmissionTab: FC<{ projectId: number; courseId: number,testSubmissions?:boolean }> = ({ projectId, courseId,testSubmissions }) => { +const SubmissionTab: FC<{ projectId: number; courseId: number; testSubmissions?: boolean }> = ({ projectId, courseId, testSubmissions }) => { const [submissions, setSubmissions] = useState(null) const project = useProject() const API = useApi() useEffect(() => { - - if(!project) return - if(!project.submissionUrl) return setSubmissions([]) - if(!project.groupId && !testSubmissions) return console.error("No groupId found"); - console.log(project); + if (!project) return + if (!project.submissionUrl) return setSubmissions([]) + if (!project.groupId && !testSubmissions) return console.error("No groupId found") + console.log(project) let ignore = false - console.log("Sending request to: ", project.submissionUrl); - API.GET(testSubmissions ? ApiRoutes.PROJECT_TEST_SUBMISSIONS : ApiRoutes.PROJECT_GROUP_SUBMISSIONS, {pathValues: {projectId: project.projectId, groupId: project.groupId??""}}).then((res) => { - console.log(res); + console.log("Sending request to: ", project.submissionUrl) + API.GET(testSubmissions ? ApiRoutes.PROJECT_TEST_SUBMISSIONS : ApiRoutes.PROJECT_GROUP_SUBMISSIONS, { pathValues: { projectId: project.projectId, groupId: project.groupId ?? "" } }).then((res) => { + console.log(res) if (!res.success || ignore) return setSubmissions(res.response.data.sort((a, b) => b.submissionId - a.submissionId)) }) - return () => { ignore = true } - }, [projectId,courseId,project?.groupId]) - + }, [projectId, courseId, project?.groupId]) + return ( + <> + - return ( + + ) } diff --git a/frontend/src/pages/project/components/SubmissionsTab.tsx b/frontend/src/pages/project/components/SubmissionsTab.tsx index c2e34f32..176f7881 100644 --- a/frontend/src/pages/project/components/SubmissionsTab.tsx +++ b/frontend/src/pages/project/components/SubmissionsTab.tsx @@ -3,6 +3,11 @@ import { ApiRoutes, GET_Responses } from "../../../@types/requests.d" import SubmissionsTable from "./SubmissionsTable" import { useParams } from "react-router-dom" import useApi from "../../../hooks/useApi" +import { exportSubmissionStatusToCSV, exportToUfora } from "./createCsv" +import { Button, Space, Switch } from "antd" +import { DownloadOutlined, ExportOutlined } from "@ant-design/icons" +import { useTranslation } from "react-i18next" +import useProject from "../../../hooks/useProject" export type ProjectSubmissionsType = GET_Responses[ApiRoutes.PROJECT_SUBMISSIONS][number] @@ -11,10 +16,12 @@ const SubmissionsTab = () => { const [submissions, setSubmissions] = useState(null) const { projectId } = useParams() const API = useApi() - + const { t } = useTranslation() + const project = useProject() + const [withArtifacts, setWithArtifacts] = useState(true) useEffect(() => { - if(!projectId) return + if (!projectId) return let ignore = false API.GET(ApiRoutes.PROJECT_SUBMISSIONS, { pathValues: { id: projectId } }).then((res) => { if (!res.success || ignore) return @@ -26,14 +33,78 @@ const SubmissionsTab = () => { } }, [projectId]) - const handleDownloadSubmissions = () => { - // TODO: implement this! + const handleDownloadSubmissions = async () => { + if (!project) return + const apiRoute = ApiRoutes.PROJECT_DOWNLOAD_ALL_SUBMISSIONS + "?artifacts=true" + const response = await API.GET( + apiRoute as ApiRoutes.PROJECT_DOWNLOAD_ALL_SUBMISSIONS, + { + config: { + responseType: "blob", + transformResponse: [(data) => data], + }, + pathValues: { id: project.projectId }, + }, + "message" + ) + if (!response.success) return + console.log(response) + const url = window.URL.createObjectURL(new Blob([response.response.data])) + const link = document.createElement("a") + link.href = url + const fileName = `${project.name}-submissions.zip` + link.setAttribute("download", fileName) + document.body.appendChild(link) + link.click() + link.parentNode!.removeChild(link) + } + + const handleExportToUfora = () => { + if (!submissions || !project) return + exportToUfora(submissions, project.maxScore ?? 0) + } + + const exportStatus = () => { + if (!submissions) return + exportSubmissionStatusToCSV(submissions) } return ( - <> - - + + + + + + + + + ) } diff --git a/frontend/src/pages/project/components/SubmissionsTable.tsx b/frontend/src/pages/project/components/SubmissionsTable.tsx index c9feb8c2..65a1e0b6 100644 --- a/frontend/src/pages/project/components/SubmissionsTable.tsx +++ b/frontend/src/pages/project/components/SubmissionsTable.tsx @@ -1,4 +1,4 @@ -import { Button, Input, List, Table, Typography } from "antd" +import { Button, Input, List, Table, Tooltip, Typography } from "antd" import { FC, useMemo } from "react" import { ProjectSubmissionsType } from "./SubmissionsTab" import { TableProps } from "antd/lib" @@ -16,17 +16,32 @@ const GroupMember = ({ name }: ProjectSubmissionsType["group"]["members"][number return {name} } -const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onChange: (s: ProjectSubmissionsType[]) => void }> = ({ submissions, onChange }) => { +const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onChange: (s: ProjectSubmissionsType[]) => void, withArtifacts?:boolean }> = ({ submissions, onChange,withArtifacts }) => { const { t } = useTranslation() const project = useProject() const { courseId, projectId } = useParams() const { message } = useAppApi() const API = useApi() - - const updateTable = async (groupId: number, feedback: Partial) => { + const updateTable = async (groupId: number, feedback: Partial, usePost: boolean) => { if (!projectId || submissions === null || !groupId) return console.error("No projectId or submissions or groupId found") - const res = await API.PATCH(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message") + let res + if (usePost) { + res = await API.POST( + ApiRoutes.PROJECT_SCORE, + { + body: { + score: 0, + feedback: "", + ...feedback, + }, + pathValues: { id: projectId, groupId }, + }, + "message" + ) + } else { + res = await API.PATCH(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message") + } if (!res.success) return const data = res.response.data @@ -54,15 +69,44 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha else score = parseFloat(scoreStr) if (isNaN(score as number)) score = null if (score !== null && score > project.maxScore) return message.error(t("project.scoreTooHigh")) - await updateTable(s.group.groupId, { score }) + await updateTable(s.group.groupId, { score }, s.feedback === null) } const updateFeedback = async (s: ProjectSubmissionsType, feedback: string) => { - await updateTable(s.group.groupId, { feedback }) + await updateTable(s.group.groupId, { feedback }, s.feedback === null) } - const downloadFile = async (s: ProjectSubmissionsType) => { - // TODO: implement this + const downloadFile = async (route: ApiRoutes.SUBMISSION_FILE | ApiRoutes.SUBMISSION_ARTIFACT, filename: string) => { + const response = await API.GET( + route, + { + config: { + responseType: "blob", + transformResponse: [(data) => data], + }, + }, + "message" + ) + if (!response.success) return + console.log(response) + const url = window.URL.createObjectURL(new Blob([response.response.data])) + const link = document.createElement("a") + link.href = url + let fileName = filename+".zip" // default filename + link.setAttribute("download", fileName) + document.body.appendChild(link) + link.click() + link.parentNode!.removeChild(link) + + } + + + const downloadSubmission = async (submission: ProjectSubmissionsType) => { + if (!submission.submission) return console.error("No submission found") + downloadFile(submission.submission.fileUrl, submission.group.name+".zip") + if(withArtifacts && submission.submission.artifactUrl && submission.submission.dockerFeedback.type !== "NONE") { + downloadFile(submission.submission.artifactUrl, submission.group.name+"-artifacts.zip") + } } const columns: TableProps["columns"] = useMemo(() => { @@ -79,7 +123,7 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha { title: t("project.submission"), key: "submissionId", - render: (s: ProjectSubmissionsType) => ( + render: (s: ProjectSubmissionsType) => s.submission ? ( - ), + ) : null, }, { title: t("project.status"), @@ -106,9 +150,9 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha render: (time: ProjectSubmissionsType["submission"]) => time?.submissionTime && {new Date(time.submissionTime).toLocaleString()}, sorter: (a: ProjectSubmissionsType, b: ProjectSubmissionsType) => { // Implement sorting logic for submissionTime column - const timeA: any = a.submission?.submissionTime || 0; - const timeB: any = b.submission?.submissionTime || 0; - return timeA - timeB; + const timeA: any = a.submission?.submissionTime || 0 + const timeB: any = b.submission?.submissionTime || 0 + return timeA - timeB }, }, ] @@ -119,10 +163,10 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha key: "score", render: (s: ProjectSubmissionsType) => ( updateScore(s, e), maxLength: 10 }} > - {s.feedback?.score ?? "-"} + {s.feedback?.score ?? t("project.noScoreLabel")} ), }) @@ -132,11 +176,14 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha title: "Download", key: "download", render: (s: ProjectSubmissionsType) => ( -
    updateFeedback(g, value), }} + type={g.feedback?.feedback ? undefined : "secondary"} > - {g.feedback?.feedback || "-"} + {g.feedback?.feedback || t('project.noFeedbackLabel')} diff --git a/frontend/src/pages/project/components/createCsv.ts b/frontend/src/pages/project/components/createCsv.ts new file mode 100644 index 00000000..1724abf6 --- /dev/null +++ b/frontend/src/pages/project/components/createCsv.ts @@ -0,0 +1,63 @@ +import { ProjectSubmissionsType } from "./SubmissionsTab" +import { saveAs } from "file-saver" +import { unparse } from "papaparse" + +function exportSubmissionStatusToCSV(submissions: ProjectSubmissionsType[]): void { + const csvData = submissions.map((submission) => { + const groupId = submission.group.groupId + const groupName = submission.group.name + let submissionTime = "Not submitted" + let structureStatus = "Not submitted" + let dockerStatus = "Not submitted" + if (submission.submission) { + submissionTime = submission.submission.submissionTime + structureStatus = submission.submission?.structureAccepted ? "Accepted" : "Rejected" + dockerStatus = submission.submission?.dockerStatus || "Unknown" + } + + const students = submission.group.members.map((member) => `${member.name} (${member.studentNumber ?? "N/A"})`).join("; ") + + return { + groupId, + groupName, + structureStatus, + dockerStatus, + submissionTime, + students, + } + }) + + const csvString = unparse(csvData) + + const blob = new Blob([csvString], { type: "text/csv;charset=utf-8;" }) + saveAs(blob, "project_submissions.csv") +} + + + +function exportToUfora(submissions: ProjectSubmissionsType[],maxScore:number): void { + + const evaluationHeader = `Evaluation 1 Exercise 1 Points Grade `; + + const csvData = submissions.flatMap(submission => + submission.group.members.map(member => ({ + OrgDefinedId: `#${member.studentNumber}`, + LastName: member.name.split(' ').slice(-1)[0], + FirstName: member.name.split(' ').slice(0, -1).join(' '), + Email: member.email, + [evaluationHeader]: submission.feedback?.score ?? "", + "End-of-Line Indicator": "#" + })) + ); + console.log(submissions, csvData); + + const csvString = unparse(csvData, { + quotes: true, + header: true + }); + + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + saveAs(blob, 'ufora_submissions.csv'); +} + +export { exportSubmissionStatusToCSV,exportToUfora } diff --git a/frontend/src/pages/submission/Submission.tsx b/frontend/src/pages/submission/Submission.tsx index 8fedef7c..6fabeb27 100644 --- a/frontend/src/pages/submission/Submission.tsx +++ b/frontend/src/pages/submission/Submission.tsx @@ -43,7 +43,9 @@ const Submission = () => { + > + + ) } diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index ec7a95ee..411d32ba 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -1,4 +1,4 @@ -import { Card, Spin, theme, Input, Button, Typography } from "antd" +import { Card, Spin, theme, Input, Button, Typography, Space } from "antd" import { useTranslation } from "react-i18next" import { GET_Responses } from "../../../@types/requests" import { ApiRoutes } from "../../../@types/requests" @@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom" import "@fontsource/jetbrains-mono" import apiCall from "../../../util/apiFetch" import SubmissionContent from "./SubmissionCardContent" +import useApi from "../../../hooks/useApi" export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] @@ -14,34 +15,40 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission } const { token } = theme.useToken() const { t } = useTranslation() const navigate = useNavigate() + const API = useApi() + + + const downloadFile = async (route: ApiRoutes.SUBMISSION_FILE | ApiRoutes.SUBMISSION_ARTIFACT, filename: string) => { + const response = await API.GET( + route, + { + config: { + responseType: "blob", + transformResponse: [(data) => data], + }, + }, + "message" + ) + if (!response.success) return + console.log(response) + const url = window.URL.createObjectURL(new Blob([response.response.data])) + const link = document.createElement("a") + link.href = url + let fileName = filename+".zip" // default filename + link.setAttribute("download", fileName) + document.body.appendChild(link) + link.click() + link.parentNode!.removeChild(link) + +} const downloadSubmission = async () => { - try { - const response = await apiCall.get(submission.fileUrl, undefined, undefined, { - responseType: "blob", - transformResponse: [(data) => data], - }) - console.log(response) - const url = window.URL.createObjectURL(new Blob([response.data])) - const link = document.createElement("a") - link.href = url - const contentDisposition = response.headers["content-disposition"] - console.log(contentDisposition) - let fileName = "file.zip" // default filename - if (contentDisposition) { - const fileNameMatch = contentDisposition.match(/filename=([^;]+)/) - console.log(fileNameMatch) - if (fileNameMatch && fileNameMatch[1]) { - fileName = fileNameMatch[1] // use the filename from the headers - } - } - link.setAttribute("download", fileName) - document.body.appendChild(link) - link.click() - } catch (err) { - console.error(err) - } + downloadFile(submission.fileUrl, t("project.submission")) + } + + const downloadSubmissionArtifacts = async () => { + downloadFile(submission.artifactUrl, t("project.submissionArtifacts")) } return ( @@ -67,8 +74,11 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission } {t("submission.submission")} } - extra={ - + extra={ + + {submission.fileUrl && } + {submission.artifactUrl && submission.dockerFeedback.type !== "NONE" && } + } > diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 0cdd2993..26c5ce1d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -178,7 +178,7 @@ const SubmitForm: FC<{ name="files" valuePropName="fileList" getValueFromEvent={normFile} - style={{height: "100%"}} + > {t("project.uploadAreaTitle")}

    {t("project.uploadAreaSubtitle")}

    + {t("project.uploadDirectory")} - ) } From 312f172f59048d4e0daba8e40da0a02400b50ebe Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sun, 19 May 2024 14:52:52 +0200 Subject: [PATCH 099/130] Added limit to amount of MB you can upload in frontend --- frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/nl/translation.json | 2 +- .../index/components/ProjectTableCourse.tsx | 4 +-- .../pages/submit/components/SubmitForm.tsx | 25 ++++++++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 4c199eea..66d5d613 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -132,6 +132,7 @@ "submissionTime": "Submission time", "noSubmissions": "No submissions", "loadingSubmissions": "Loading submissions...", + "fileTooLarge": "File is too large (max 50MB)", "options": "Update", "groupMembers": "Group members", "newProject": "New project", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index f20c558c..4b6c5a99 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -137,7 +137,7 @@ "options": "Aanpassen", "groupMembers": "Groepsleden", "newProject": "Nieuw project", - + "fileTooLarge": "Bestand is te groot (max 50 MB)", "scoreTooHigh": "Score is hoger dan maximum score", "successfullyDeleted": "Project succesvol verwijderd", "deleteProject": "Project verwijderen", diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx index 861c0c66..d9ebaf0e 100644 --- a/frontend/src/pages/index/components/ProjectTableCourse.tsx +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -89,7 +89,7 @@ const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: s key: "visible", render: (project: ProjectType) => { if (project.visible) { - return {t("home.projects.visibleStatus.visible")} + return {t("home.projects.visibleStatus.visible")} } else if (project.visibleAfter) { return ( ) } else { - return {t("home.projects.visibleStatus.invisible")} + return {t("home.projects.visibleStatus.invisible")} } } }) diff --git a/frontend/src/pages/submit/components/SubmitForm.tsx b/frontend/src/pages/submit/components/SubmitForm.tsx index 26c5ce1d..7c7b157d 100644 --- a/frontend/src/pages/submit/components/SubmitForm.tsx +++ b/frontend/src/pages/submit/components/SubmitForm.tsx @@ -6,6 +6,7 @@ import {Button} from "antd"; import {Tree} from 'antd'; import {CloseOutlined} from '@ant-design/icons'; import { DataNode } from "antd/es/tree"; +import useAppApi from "../../../hooks/useAppApi"; type TreeNode = { type: string; @@ -24,6 +25,7 @@ const SubmitForm: FC<{ const {t} = useTranslation() const directoryInputRef = useRef(null); const [directoryTree, setDirectoryTree] = useState([]); + const {message} = useAppApi() const normFile = (e: any) => { console.log("Upload event:", e) @@ -92,6 +94,20 @@ const SubmitForm: FC<{ const onDirectoryUpload = (event: React.ChangeEvent) => { const files = event.target.files; if (files) { + + // + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isLt50M = file.size / 1024 / 1024 < 50; + if (!isLt50M) { + message.error(t("project.fileTooLarge")); + return; + } + } + + + const currentFileList = form.getFieldValue('files') || []; const newDirectoryTree: TreeNode[] = [...directoryTree]; @@ -182,7 +198,14 @@ const SubmitForm: FC<{ > false} + beforeUpload={(file) => { + const fileSize = file.size / 1024 / 1024; + if (fileSize > 50) { + message.error(t("project.fileTooLarge")); + return Upload.LIST_IGNORE + } + return false + }} multiple={true} style={{height: "100%"}} showUploadList={false} From 10b8c6fbfe6967175bb89ebc98d04b2e26c1f673 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sun, 19 May 2024 15:36:40 +0200 Subject: [PATCH 100/130] added docs link --- frontend/src/@types/requests.d.ts | 2 +- frontend/src/components/layout/sidebar/Sidebar.tsx | 12 +++++++----- frontend/src/i18n/en/translation.json | 3 ++- frontend/src/i18n/nl/translation.json | 3 ++- frontend/src/pages/index/landing/Navbar.tsx | 5 +++++ frontend/src/pages/project/Project.tsx | 7 +++++-- .../pages/project/components/SubmissionStatusTag.tsx | 4 ++-- .../pages/submission/components/SubmissionCard.tsx | 7 +++---- 8 files changed, 27 insertions(+), 16 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 3d4cc141..a2f80805 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -231,7 +231,7 @@ export type GET_Responses = { fileUrl: ApiRoutes.SUBMISSION_FILE structureFeedback: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK dockerFeedback: DockerFeedback, - artifactUrl: ApiRoutes.SUBMISSION_ARTIFACT + artifactUrl: ApiRoutes.SUBMISSION_ARTIFACT | null } [ApiRoutes.SUBMISSION_FILE]: BlobPart [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] diff --git a/frontend/src/components/layout/sidebar/Sidebar.tsx b/frontend/src/components/layout/sidebar/Sidebar.tsx index bacb262a..ce770a47 100644 --- a/frontend/src/components/layout/sidebar/Sidebar.tsx +++ b/frontend/src/components/layout/sidebar/Sidebar.tsx @@ -63,19 +63,21 @@ const Sidebar: FC = () => { + } diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 66d5d613..8e6a104e 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -218,7 +218,8 @@ "title-2": "submission platform", "subtitle-1": "Effortlessly manage courses and projects,", "subtitle-2": "automatically test and assess code.", - "getStarted": "Get started" + "getStarted": "Get started", + "docs": "Documentation" }, "submission": { diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 4b6c5a99..81b4afa0 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -220,7 +220,8 @@ "title-2": "indien platform", "subtitle-1": "Eenvoudig cursussen en projecten beheren,", "subtitle-2": "automatisch code testen en beoordelen.", - "getStarted": "Aan de slag" + "getStarted": "Aan de slag", + "docs": "Documentatie" }, "submission": { diff --git a/frontend/src/pages/index/landing/Navbar.tsx b/frontend/src/pages/index/landing/Navbar.tsx index 2d2f4bda..c4aa81b2 100644 --- a/frontend/src/pages/index/landing/Navbar.tsx +++ b/frontend/src/pages/index/landing/Navbar.tsx @@ -21,6 +21,11 @@ const Navbar: FC<{ onLogin: () => void }> = ({ onLogin }) => {
    +
    + window.open("https://github.com/SELab-2/UGent-6/wiki", "_blank")} style={{ cursor: "pointer" }} >{t("landingPage.docs")} + +
    +
    diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index d327e3ba..d18afdb0 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -6,8 +6,7 @@ import SubmissionCard from "./components/SubmissionTab" import useCourse from "../../hooks/useCourse" import useProject from "../../hooks/useProject" import ScoreCard from "./components/ScoreTab" -import CourseAdminView from "../../hooks/CourseAdminView" -import { DeleteOutlined, DownloadOutlined, FileDoneOutlined, HeatMapOutlined, InfoCircleOutlined, PlusOutlined, SendOutlined, SettingFilled, TeamOutlined } from "@ant-design/icons" +import { DeleteOutlined, FileDoneOutlined, InfoCircleOutlined, PlusOutlined, SendOutlined, SettingFilled, TeamOutlined } from "@ant-design/icons" import { useMemo, useState } from "react" import useIsCourseAdmin from "../../hooks/useIsCourseAdmin" import GroupTab from "./components/GroupTab" @@ -174,6 +173,10 @@ const Project = () => { title={t("project.deleteProject")} description={t("project.deleteProjectDescription")} onConfirm={deleteProject} + okButtonProps={{ + danger: true, + }} + okText={t("course.confirmDelete")} > } - {submission.artifactUrl && submission.dockerFeedback.type !== "NONE" && } + {submission.artifactUrl && } } > From 824a9c68a8668f2751ffb04fcc1bbd5f6d445e3f Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 15:59:06 +0200 Subject: [PATCH 101/130] Test for getSubmissionFiles --- .../controllers/SubmissionControllerTest.java | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index 269e7c77..d9705dc9 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -19,16 +19,24 @@ import com.ugent.pidgeon.postgre.models.types.DockerTestType; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; +import java.util.logging.Logger; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,6 +51,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.time.OffsetDateTime; @@ -54,6 +63,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -565,4 +575,355 @@ public void testGetAdminSubmissions() { e.printStackTrace(); } } + + @Test + public void testGetSubmissionsFiles() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/submissions/files"; + + /* Create temp zip file for submission */ + File file = null; + try { + file = createTestFile(); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(file); + fileEntity.setPath(file.getAbsolutePath()); + + + /* All checks succeed */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + when(projectRepository.findGroupIdsByProjectId(submission.getProjectId())).thenReturn(groupIds); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.of(submission)); + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.of(fileEntity)); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } + } + } + } + } + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* With arifact */ + url += "?artifacts=true"; + // Create artifact tempfile + File artifactFile = null; + try { + artifactFile = createTestFile(); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(artifactFile); + + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler. + getSubmissionArtifactPath(submission.getProjectId(), groupEntity.getId(), submission.getId())) + .thenReturn(Path.of(artifactFile.getAbsolutePath())); + mockedFileHandler.when(() -> Filehandler.addExistingZip(any(), any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(any())) + .thenCallRealMethod(); + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + assertTrue(artifactzipfound); + } catch (Exception e) { + e.printStackTrace(); + } + + /* With artifact but no artifact file, should just return the zip without an artifacts.zip */ + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler. + getSubmissionArtifactPath(submission.getProjectId(), groupEntity.getId(), submission.getId())) + .thenReturn(Path.of("nonexistent")); + mockedFileHandler.when(() -> Filehandler.addExistingZip(any(), any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(any())) + .thenCallRealMethod(); + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + assertFalse(artifactzipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* With artifact parameter false */ + url = url.replace("?artifacts=true", "?artifacts=false"); + + try { + + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* File not found, should return empty zip */ + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.empty()); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean zipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + zipfound = true; + } + } + assertFalse(zipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* Submission not found, should return empty zip */ + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.empty()); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean zipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + zipfound = true; + } + } + assertFalse(zipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* Not admin */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + e.printStackTrace(); + } + + /* Unexecpted error */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenThrow(new RuntimeException()); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isInternalServerError()); + } catch (Exception e) { + e.printStackTrace(); + } + } } \ No newline at end of file From 413b0cd6f1515717c9badd13bef4da8b731fd747 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sun, 19 May 2024 16:05:44 +0200 Subject: [PATCH 102/130] artifacts are downloadable if exist --- frontend/src/pages/project/components/SubmissionsTab.tsx | 1 + frontend/src/pages/project/components/SubmissionsTable.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/project/components/SubmissionsTab.tsx b/frontend/src/pages/project/components/SubmissionsTab.tsx index 176f7881..d2729b3d 100644 --- a/frontend/src/pages/project/components/SubmissionsTab.tsx +++ b/frontend/src/pages/project/components/SubmissionsTab.tsx @@ -103,6 +103,7 @@ const SubmissionsTab = () => { ) diff --git a/frontend/src/pages/project/components/SubmissionsTable.tsx b/frontend/src/pages/project/components/SubmissionsTable.tsx index 65a1e0b6..5bca0673 100644 --- a/frontend/src/pages/project/components/SubmissionsTable.tsx +++ b/frontend/src/pages/project/components/SubmissionsTable.tsx @@ -104,7 +104,7 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha const downloadSubmission = async (submission: ProjectSubmissionsType) => { if (!submission.submission) return console.error("No submission found") downloadFile(submission.submission.fileUrl, submission.group.name+".zip") - if(withArtifacts && submission.submission.artifactUrl && submission.submission.dockerFeedback.type !== "NONE") { + if(withArtifacts && submission.submission.artifactUrl) { downloadFile(submission.submission.artifactUrl, submission.group.name+"-artifacts.zip") } } From e9b3a170f03019172dce9280c6ad96dbdfb92cc8 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 16:18:45 +0200 Subject: [PATCH 103/130] Added prefix to tempfiles --- .../controllers/SubmissionController.java | 2 +- .../com/ugent/pidgeon/util/Filehandler.java | 2 +- .../controllers/SubmissionControllerTest.java | 4 +- .../controllers/TestControllerTest.java | 4 +- .../ugent/pidgeon/util/FileHandlerTest.java | 36 +++++++++--------- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index 2311ac23..ac9729b5 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -333,7 +333,7 @@ public ResponseEntity getSubmissionsFiles(@PathVariable("projectid") long pro return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - Path tempDir = Files.createTempDirectory("allsubmissions"); + Path tempDir = Files.createTempDirectory("SELAB6CANDELETEallsubmissions"); Path mainZipPath = tempDir.resolve("main.zip"); try (ZipOutputStream mainZipOut = new ZipOutputStream(Files.newOutputStream(mainZipPath))) { Map> submissions = getLatestSubmissionsForProject(projectid); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index 93ab4fae..94e6a031 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -39,7 +39,7 @@ public static File saveFile(Path directory, MultipartFile file, String filename) try { // Create a temporary file and save the uploaded file to it - File tempFile = File.createTempFile("uploaded-zip-", ".zip"); + File tempFile = File.createTempFile("SELAB6CANDELETEuploaded-zip-", ".zip"); file.transferTo(tempFile); // Check if the file is a ZIP file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index d9705dc9..5de0fbe5 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -122,10 +122,10 @@ public class SubmissionControllerTest extends ControllerTest { public static File createTestFile() throws IOException { // Create a temporary directory - File tempDir = Files.createTempDirectory("test-dir").toFile(); + File tempDir = Files.createTempDirectory("SELAB6CANDELETEtest-dir").toFile(); // Create a temporary file within the directory - File tempFile = File.createTempFile("test-file", ".zip", tempDir); + File tempFile = File.createTempFile("SELAB6CANDELETEtest-file", ".zip", tempDir); // Create some content to write into the zip file String content = "Hello, this is a test file!"; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java index 7a7cfb6a..686e9144 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java @@ -709,10 +709,10 @@ public void testDeleteTest() throws Exception { public static File createTestFile() throws IOException { // Create a temporary directory - File tempDir = Files.createTempDirectory("test-dir").toFile(); + File tempDir = Files.createTempDirectory("SELAB6CANDELETEtest-dir").toFile(); // Create a temporary file within the directory - File tempFile = File.createTempFile("test-file", ".zip", tempDir); + File tempFile = File.createTempFile("SELAB6CANDELETEtest-file", ".zip", tempDir); // Create some content to write into the zip file String content = "Hello, this is a test file!"; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java index a76170d9..94b7a153 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -64,7 +64,7 @@ public void cleanup() throws Exception { @BeforeEach public void setUp() throws IOException { - tempDir = Files.createTempDirectory("test"); + tempDir = Files.createTempDirectory("SELAB6CANDELETEtest"); fileContent = Files.readAllBytes(testFilePath.resolve(basicZipFileName)); file = new MockMultipartFile( basicZipFileName, fileContent @@ -121,17 +121,17 @@ public void testSaveFile_fileNull() { @Test public void testDeleteLocation() throws Exception { - Path testDir = Files.createTempDirectory("test"); - Path tempFile = Files.createTempFile(testDir, "test", ".txt"); + Path testDir = Files.createTempDirectory("SELAB6CANDELETEtest"); + Path tempFile = Files.createTempFile(testDir, "SELAB6CANDELETEtest", ".txt"); Filehandler.deleteLocation(new File(tempFile.toString())); assertFalse(Files.exists(testDir)); } @Test public void testDeleteLocation_parentDirNotEmpty() throws Exception { - Path testDir = Files.createTempDirectory("test"); - Path tempFile = Files.createTempFile(testDir, "test", ".txt"); - Files.createTempFile(testDir, "test2", ".txt"); + Path testDir = Files.createTempDirectory("SELAB6CANDELETEtest"); + Path tempFile = Files.createTempFile(testDir, "SELAB6CANDELETEtest", ".txt"); + Files.createTempFile(testDir, "SELAB6CANDELETEtest2", ".txt"); Filehandler.deleteLocation(new File(tempFile.toString())); assertTrue(Files.exists(testDir)); } @@ -284,7 +284,7 @@ public void testGetSubmissionArtifactPath_groupIdIsNull() { @Test public void testGetFileAsResource_FileExists() { try { - File tempFile = Files.createTempFile("testFile", ".txt").toFile(); + File tempFile = Files.createTempFile("SELAB6CANDELETEtestFile", ".txt").toFile(); Resource resource = Filehandler.getFileAsResource(tempFile.toPath()); @@ -308,8 +308,8 @@ public void testGetFileAsResource_FileDoesNotExist() { @Test public void testCopyFilesAsZip() throws IOException { List files = new ArrayList<>(); - File tempFile1 = Files.createTempFile("tempFile1", ".txt").toFile(); - File tempFile2 = Files.createTempFile("tempFile2", ".txt").toFile(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); try { files.add(tempFile1); @@ -337,9 +337,9 @@ public void testCopyFilesAsZip() throws IOException { @Test public void testCopyFilesAsZip_zipFileAlreadyExist() throws IOException { List files = new ArrayList<>(); - File tempFile1 = Files.createTempFile("tempFile1", ".txt").toFile(); - File tempFile2 = Files.createTempFile("tempFile2", ".txt").toFile(); - File zipFile = Files.createTempFile(tempDir, "files", ".zip").toFile(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + File zipFile = Files.createTempFile(tempDir, "SELAB6CANDELETEfiles", ".zip").toFile(); try { files.add(tempFile1); @@ -379,9 +379,9 @@ private static File createTempFileWithContent(String prefix, String suffix, int @Test public void testCopyFilesAsZip_zipFileAlreadyExistNonWriteable() throws IOException { List files = new ArrayList<>(); - File tempFile1 = createTempFileWithContent("tempFile1", ".txt", 4095); - File tempFile2 = Files.createTempFile("tempFile2", ".txt").toFile(); - File zipFile = Files.createTempFile(tempDir, "files", ".zip").toFile(); + File tempFile1 = createTempFileWithContent("SELAB6CANDELETEtempFile1", ".txt", 4095); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + File zipFile = Files.createTempFile(tempDir, "SELAB6CANDELETEfiles", ".zip").toFile(); zipFile.setWritable(false); try { @@ -409,8 +409,8 @@ public void testCopyFilesAsZip_zipFileAlreadyExistNonWriteable() throws IOExcept @Test public void testGetZipFileAsResponse() throws IOException { List files = new ArrayList<>(); - File tempFile1 = Files.createTempFile("tempFile1", ".txt").toFile(); - File tempFile2 = Files.createTempFile("tempFile2", ".txt").toFile(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); try { files.add(tempFile1); @@ -445,7 +445,7 @@ public void testGetZipFileAsResponse_fileDoesNotExist() { public void testAddExistingZip() throws IOException { // Create zip file String zipFileName = "existingZipFile.zip"; - File tempZipFile = Files.createTempFile("existingZip", ".zip").toFile(); + File tempZipFile = Files.createTempFile("SELAB6CANDELETEexistingZip", ".zip").toFile(); // Populate the zip file with some content try (ZipOutputStream tempZipOutputStream = new ZipOutputStream(new FileOutputStream(tempZipFile))) { diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 9ad7ca11633f3de8c62a501ead1805e577a40c7e..05ec10e712e6637397c71fd53d05bc6a59bdffc6 100644 GIT binary patch delta 28 hcmZ3)xQLNAz?+#xgn@&DgW+o9=83%i%pfY>831D*2v-0A delta 28 hcmZ3)xQLNAz?+#xgn@&DgW>SXjT3qOnL$*%GXP{#2!8+o From cd2020acfc40b87cb3bb5eef508faa06f1da4a2b Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Sun, 19 May 2024 16:30:11 +0200 Subject: [PATCH 104/130] artifacts URL null if no artifacts --- .../pidgeon/util/EntityToJsonConverter.java | 12 +- .../util/EntityToJsonConverterTest.java | 133 ++++++++++-------- .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index ba304571..961f335f 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -9,6 +9,8 @@ import com.ugent.pidgeon.postgre.models.types.DockerTestState; import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.repository.*; +import java.io.File; +import java.nio.file.Path; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -252,6 +254,14 @@ else if (submission.getDockerTestType().equals(DockerTestType.SIMPLE)) { } else { feedback = new DockerTestFeedbackJson(DockerTestType.TEMPLATE, submission.getDockerFeedback(), submission.getDockerAccepted()); } + + boolean artifactsExist; + if (submission.getGroupId() != null) { + Path artifactPath = Filehandler.getSubmissionArtifactPath(submission.getProjectId(), submission.getGroupId(), submission.getId()); + artifactsExist = new File(artifactPath.toString()).exists(); + } else { + artifactsExist = false; + } return new SubmissionJson( submission.getId(), ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId(), @@ -264,7 +274,7 @@ else if (submission.getDockerTestType().equals(DockerTestType.SIMPLE)) { submission.getStructureFeedback(), feedback, submission.getDockerTestState().toString(), - ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/artifacts" + artifactsExist ? ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/artifacts" : null ); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index f02c545a..2fcc809b 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -24,6 +24,8 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.postgre.repository.GroupRepository.UserReference; +import java.io.File; +import java.io.IOException; import java.time.OffsetDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -31,6 +33,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; @@ -47,6 +50,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -550,62 +554,79 @@ public void testCourseEntityToCourseReference() { @Test public void testGetSubmissionJson() { - submissionEntity.setDockerTestState(DockerTestState.running); - submissionEntity.setSubmissionTime(OffsetDateTime.now()); - submissionEntity.setStructureAccepted(true); - submissionEntity.setStructureFeedback("feedback"); - SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(submissionEntity.getId(), result.getSubmissionId()); - assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + submissionEntity.getProjectId(), result.getProjectUrl()); - assertEquals(ApiRoutes.GROUP_BASE_PATH + "/" + submissionEntity.getGroupId(), result.getGroupUrl()); - assertEquals(submissionEntity.getProjectId(), result.getProjectId()); - assertEquals(submissionEntity.getGroupId(), result.getGroupId()); - assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/file", result.getFileUrl()); - assertTrue(result.getStructureAccepted()); - assertEquals(submissionEntity.getSubmissionTime(), result.getSubmissionTime()); - assertEquals(submissionEntity.getStructureFeedback(), result.getStructureFeedback()); - assertNull(result.getDockerFeedback()); - assertEquals(DockerTestState.running.toString(), result.getDockerStatus()); - assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/artifacts", result.getArtifactUrl()); - - /* Docker finished running */ - submissionEntity.setDockerTestState(DockerTestState.finished); - /* No docker test */ - submissionEntity.setDockerType(DockerTestType.NONE); - result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(DockerTestState.finished.toString(), result.getDockerStatus()); - assertEquals(DockerTestType.NONE, result.getDockerFeedback().type()); - - /* Simple docker test */ - submissionEntity.setDockerFeedback("dockerFeedback - simple"); - submissionEntity.setDockerAccepted(true); - submissionEntity.setDockerType(DockerTestType.SIMPLE); - result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(DockerTestType.SIMPLE, result.getDockerFeedback().type()); - assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); - assertTrue(result.getDockerFeedback().allowed()); - - /* Template docker test */ - submissionEntity.setDockerFeedback("dockerFeedback - template"); - submissionEntity.setDockerAccepted(false); - submissionEntity.setDockerType(DockerTestType.TEMPLATE); - result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); - assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); - assertFalse(result.getDockerFeedback().allowed()); - - /* Docker aborted */ - submissionEntity.setDockerTestState(DockerTestState.aborted); - result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(DockerTestState.aborted.toString(), result.getDockerStatus()); - assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); - assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); - assertFalse(result.getDockerFeedback().allowed()); - - /* Group id is null */ - submissionEntity.setGroupId(null); - result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertNull(result.getGroupUrl()); + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + /* Create temp file for artifacts */ + File file = File.createTempFile("SELAB2CANDELETEtest", "zip"); + mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(submissionEntity.getProjectId(), submissionEntity.getGroupId(), submissionEntity.getId())) + .thenReturn(file.toPath()); + submissionEntity.setDockerTestState(DockerTestState.running); + submissionEntity.setSubmissionTime(OffsetDateTime.now()); + submissionEntity.setStructureAccepted(true); + submissionEntity.setStructureFeedback("feedback"); + SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(submissionEntity.getId(), result.getSubmissionId()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + submissionEntity.getProjectId(), + result.getProjectUrl()); + assertEquals(ApiRoutes.GROUP_BASE_PATH + "/" + submissionEntity.getGroupId(), + result.getGroupUrl()); + assertEquals(submissionEntity.getProjectId(), result.getProjectId()); + assertEquals(submissionEntity.getGroupId(), result.getGroupId()); + assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/file", + result.getFileUrl()); + assertTrue(result.getStructureAccepted()); + assertEquals(submissionEntity.getSubmissionTime(), result.getSubmissionTime()); + assertEquals(submissionEntity.getStructureFeedback(), result.getStructureFeedback()); + assertNull(result.getDockerFeedback()); + assertEquals(DockerTestState.running.toString(), result.getDockerStatus()); + assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/artifacts", + result.getArtifactUrl()); + + /* No artifacts */ + file.delete(); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertNull(result.getArtifactUrl()); + + /* Docker finished running */ + submissionEntity.setDockerTestState(DockerTestState.finished); + /* No docker test */ + submissionEntity.setDockerType(DockerTestType.NONE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestState.finished.toString(), result.getDockerStatus()); + assertEquals(DockerTestType.NONE, result.getDockerFeedback().type()); + + /* Simple docker test */ + submissionEntity.setDockerFeedback("dockerFeedback - simple"); + submissionEntity.setDockerAccepted(true); + submissionEntity.setDockerType(DockerTestType.SIMPLE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestType.SIMPLE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertTrue(result.getDockerFeedback().allowed()); + + /* Template docker test */ + submissionEntity.setDockerFeedback("dockerFeedback - template"); + submissionEntity.setDockerAccepted(false); + submissionEntity.setDockerType(DockerTestType.TEMPLATE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertFalse(result.getDockerFeedback().allowed()); + + /* Docker aborted */ + submissionEntity.setDockerTestState(DockerTestState.aborted); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestState.aborted.toString(), result.getDockerStatus()); + assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertFalse(result.getDockerFeedback().allowed()); + + /* Group id is null */ + submissionEntity.setGroupId(null); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertNull(result.getGroupUrl()); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Test diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 05ec10e712e6637397c71fd53d05bc6a59bdffc6..00b2b6bfdef119624eab2ce0a67bdbe128332895 100644 GIT binary patch delta 26 gcmZ3)xQLM_z?+#xgn@&DgJDkdM4kX9AQ|TZ087~hfdBvi delta 26 gcmZ3)xQLM_z?+#xgn@&DgW+o9M4kX9AQ|TZ08dQ@?*IS* From 9667598b188a083523d99524fd1f48b4b4490988 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sun, 19 May 2024 17:05:12 +0200 Subject: [PATCH 105/130] Removed broken tests --- frontend/src/test/mocks.tsx | 17 ++++++++++------- frontend/src/test/pages/home/Home.test.tsx | 14 +++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/frontend/src/test/mocks.tsx b/frontend/src/test/mocks.tsx index ba369053..3fce596c 100644 --- a/frontend/src/test/mocks.tsx +++ b/frontend/src/test/mocks.tsx @@ -39,13 +39,16 @@ jest.mock('react-router-dom', () => ({ jest.mock("../hooks/useUser", () => ({ __esModule: true, // this property makes it work default: () => { - const user: GET_Responses[ApiRoutes.USER] = { courseUrl: "/api/courses", projects_url: "/api/projects/1", url: "/api/users/12", role: "teacher", email: "test@gmail.com", id: 12, name: "Bob", surname: "test" } - const courses: UserCourseType[] = [{courseId:1,name:"Course 1", relation: "enrolled", url:"/api/courses/1"}] - return { - user, - setUser: () => {}, - courses - } + + + + // const user: GET_Responses[ApiRoutes.USER] = { courseUrl: "/api/courses", projects_url: "/api/projects/1", url: "/api/users/12", role: "teacher", email: "test@gmail.com", id: 12, name: "Bob", surname: "test" } + // const courses: UserCourseType[] = [{courseId:1,name:"Course 1", relation: "enrolled", url:"/api/courses/1"}] + // return { + // user, + // setUser: () => {}, + // courses + // } }, })) diff --git a/frontend/src/test/pages/home/Home.test.tsx b/frontend/src/test/pages/home/Home.test.tsx index 3fcd593f..0d909d1d 100644 --- a/frontend/src/test/pages/home/Home.test.tsx +++ b/frontend/src/test/pages/home/Home.test.tsx @@ -48,13 +48,13 @@ jest.mock('react-router-dom', () => ({ jest.mock("../../../hooks/useUser", () => ({ __esModule: true, // this property makes it work default: () => { - const user: GET_Responses[ApiRoutes.USER] = { courseUrl: "/api/courses", projects_url: "/api/projects/1", url: "/api/users/12", role: "teacher", email: "test@gmail.com", id: 12, name: "Bob", surname: "test" } - const courses: UserCourseType[] = [{courseId:1,name:"Course 1", relation: "enrolled", url:"/api/courses/1"}] - return { - user, - setUser: () => {}, - courses - } + // const user: GET_Responses[ApiRoutes.USER] = { courseUrl: "/api/courses", projects_url: "/api/projects/1", url: "/api/users/12", role: "teacher", email: "test@gmail.com", id: 12, name: "Bob", surname: "test" } + // const courses: UserCourseType[] = [{courseId:1,name:"Course 1", relation: "enrolled", url:"/api/courses/1"}] + // return { + // user, + // setUser: () => {}, + // courses + // } }, })) From 40810de42399be47245a567bca438afd8ef8e881 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Sat, 18 May 2024 11:03:18 +0200 Subject: [PATCH 106/130] idk --- frontend/src/pages/submission/Submission.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/pages/submission/Submission.tsx b/frontend/src/pages/submission/Submission.tsx index 367f2904..d569afc3 100644 --- a/frontend/src/pages/submission/Submission.tsx +++ b/frontend/src/pages/submission/Submission.tsx @@ -1,6 +1,5 @@ -import { Card, Typography, Spin } from "antd" +import { Spin } from "antd" import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" import SubmissionCard from "./components/SubmissionCard" import { SubmissionType } from "./components/SubmissionCard" import { useParams } from "react-router-dom" From e4aedb2338d13d3deb5308872291131b171e4e64 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 19 May 2024 18:17:57 +0200 Subject: [PATCH 107/130] Login working in test frontend --- backend/web-bff/App/app.js | 16 +- backend/web-bff/App/package-lock.json | 21 ++ backend/web-bff/App/package.json | 1 + backend/web-bff/App/routes/auth.js | 2 +- backend/web-bff/App/routes/users.js | 21 +- .../web-bff/temp-frontend/package-lock.json | 212 ++++++++++-------- backend/web-bff/temp-frontend/package.json | 3 +- backend/web-bff/temp-frontend/src/App.tsx | 11 +- backend/web-bff/temp-frontend/src/main.tsx | 7 +- 9 files changed, 178 insertions(+), 116 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index 11e1109b..afdf6b5f 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -9,6 +9,8 @@ const logger = require('morgan'); const rateLimit = require('express-rate-limit') +const cors = require('cors') + const indexRouter = require('./routes/index'); const usersRouter = require('./routes/users'); const authRouter = require('./routes/auth'); @@ -49,6 +51,14 @@ const limiter = rateLimit({ app.use(limiter); +const corsOptions = { + origin: /localhost/, + optionsSuccessStatus: 200, + credentials: true, +} + +app.use('*', cors(corsOptions)); + // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); @@ -60,9 +70,9 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); -app.use('/users', usersRouter); -app.use('/auth', authRouter); -app.use('/api', apiRouter) +app.use('/web/users', usersRouter); +app.use('/web/auth', authRouter); +app.use('/web/api', apiRouter) // catch 404 and forward to error handler app.use(function (req, res, next) { diff --git a/backend/web-bff/App/package-lock.json b/backend/web-bff/App/package-lock.json index 054f7a67..210260a2 100644 --- a/backend/web-bff/App/package-lock.json +++ b/backend/web-bff/App/package-lock.json @@ -13,6 +13,7 @@ "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", + "cors": "^2.8.5", "csurf": "^1.11.0", "debug": "^4.3.4", "dotenv": "^16.4.1", @@ -408,6 +409,18 @@ "node": ">= 0.8" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -1440,6 +1453,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", diff --git a/backend/web-bff/App/package.json b/backend/web-bff/App/package.json index 37a778d3..2808e361 100644 --- a/backend/web-bff/App/package.json +++ b/backend/web-bff/App/package.json @@ -11,6 +11,7 @@ "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", "cookie-session": "^2.1.0", + "cors": "^2.8.5", "csurf": "^1.11.0", "debug": "^4.3.4", "dotenv": "^16.4.1", diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index eba42917..80316d64 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -13,7 +13,7 @@ const router = express.Router(); router.get('/signin', authProvider.login({ scopes: [msalConfig.auth.clientId + "/.default"], redirectUri: REDIRECT_URI, - successRedirect: '/' + successRedirect: 'http://localhost:5173' })); router.get('/acquireToken', authProvider.acquireToken({ diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index 740b54c9..9cc84255 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -14,7 +14,7 @@ const authProvider = require("../auth/AuthProvider"); // custom middleware to check auth state function isAuthenticated(req, res, next) { if (!req.session.isAuthenticated) { - return res.redirect('/auth/signin'); // redirect to sign-in route + return res.redirect('/web/auth/signin'); // redirect to sign-in route } next(); @@ -28,15 +28,20 @@ router.get('/id', ); router.get('/isAuthenticated', - isAuthenticated, - authProvider.acquireToken({ - scopes: [msalConfig.auth.clientId + "/.default"], - redirectUri: REDIRECT_URI - }), + async function (req, res, next) { try { - const response = await fetch( "api/user" , req.session.accessToken, "GET") - res.send(response) + if (req.session.isAuthenticated) { + res.send({ + isAuthenticated: true, + name: req.session.account.name + }); + } else { + res.send({ + isAuthenticated: false, + name: "" + }) + } } catch(error) { next(error); } diff --git a/backend/web-bff/temp-frontend/package-lock.json b/backend/web-bff/temp-frontend/package-lock.json index 0d6094f0..9000963f 100644 --- a/backend/web-bff/temp-frontend/package-lock.json +++ b/backend/web-bff/temp-frontend/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "axios": "^1.6.8", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.66", @@ -210,24 +211,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/@babel/helper-validator-option": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", @@ -266,18 +249,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/core/node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -313,20 +284,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -597,15 +554,6 @@ "node": ">=4" } }, - "node_modules/@babel/core/node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/core/node_modules/update-browserslist-db": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", @@ -642,6 +590,36 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", @@ -690,6 +668,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -699,6 +691,14 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -712,58 +712,39 @@ "@types/babel__traverse": "*" } }, - "node_modules/@types/babel__core/node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@types/babel__core/node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__core/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__core/node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/types": "^7.20.7" } }, - "node_modules/@types/babel__core/node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true }, "node_modules/@types/react": { "version": "18.3.2", @@ -3369,6 +3350,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3389,6 +3400,15 @@ "node": ">=10" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", diff --git a/backend/web-bff/temp-frontend/package.json b/backend/web-bff/temp-frontend/package.json index 88e54830..af51b155 100644 --- a/backend/web-bff/temp-frontend/package.json +++ b/backend/web-bff/temp-frontend/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --cors true", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" @@ -12,6 +12,7 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1", "axios": "^1.6.8" }, "devDependencies": { diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx index d1fb3c71..bb265a18 100644 --- a/backend/web-bff/temp-frontend/src/App.tsx +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -1,14 +1,16 @@ import {useEffect, useState} from 'react' import axios from 'axios' +import {Link} from "react-router-dom" import './App.css' function App() { const [auth, setAuth] - = useState<{ name: string, otherKey: number } | null>(null) + = useState<{ isAuthenticated:boolean, name: string} | null>(null) useEffect(() => { - axios.get('/auth/current-session').then(({data}) => { + axios.get('http://localhost:3000/web/users/isAuthenticated', {withCredentials: true}).then(({data}) => { + console.log(data) setAuth(data); }) }, []) @@ -19,7 +21,7 @@ function App() {

    Loading...

    ) - } else if (auth) { + } else if (auth.isAuthenticated) { return ( <>

    Logged in!

    @@ -29,7 +31,8 @@ function App() { } else { return ( <> -

    Welcome

    +

    Welcome, please login

    + Login ) } diff --git a/backend/web-bff/temp-frontend/src/main.tsx b/backend/web-bff/temp-frontend/src/main.tsx index 3d7150da..a561b2f8 100644 --- a/backend/web-bff/temp-frontend/src/main.tsx +++ b/backend/web-bff/temp-frontend/src/main.tsx @@ -1,10 +1,11 @@ -import React from 'react' + import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' +import {BrowserRouter} from "react-router-dom"; ReactDOM.createRoot(document.getElementById('root')!).render( - + - , + , ) From b2cdb68997d8f08b1ddc39f43c2d5b535f4317a7 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 19 May 2024 19:26:26 +0200 Subject: [PATCH 108/130] change port nmber --- backend/web-bff/App/routes/auth.js | 2 +- frontend/package-lock.json | 1028 +++++++++++++++------------- 2 files changed, 559 insertions(+), 471 deletions(-) diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index 80316d64..62c5ee63 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -13,7 +13,7 @@ const router = express.Router(); router.get('/signin', authProvider.login({ scopes: [msalConfig.auth.clientId + "/.default"], redirectUri: REDIRECT_URI, - successRedirect: 'http://localhost:5173' + successRedirect: 'http://localhost:3001' })); router.get('/acquireToken', authProvider.acquireToken({ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4c867e4..216ad184 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@fontsource/jetbrains-mono": "^5.0.19", "@fontsource/roboto-mono": "^5.0.17", + "@mdx-js/react": "^3.0.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", @@ -25,6 +26,7 @@ "axios": "^1.6.7", "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", + "http-proxy-middleware": "^3.0.0", "i18next-localstorage-cache": "^1.1.1", "lowlight": "^3.1.0", "react": "^18.2.0", @@ -972,351 +974,6 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@fontsource/jetbrains-mono": { "version": "5.0.19", "integrity": "sha512-SdwUuvdfuAvGWRRc4LOFRSmDrpkE+vFUpCtOIOUl1PpXdLfeU//93BZiGf7j/oFGSZJbHAurfux2uLT38/NIjw==" @@ -2062,9 +1719,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rc-component/color-picker": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.2.tgz", + "node_modules/@mdx-js/react": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", + "integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.2.tgz", "integrity": "sha512-YJXujYzYFAEtlXJXy0yJUhwzUWPTcniBZto+wZ/vnACmFnUTNR7dH+NOeqSwMMsssh74e9H5Jfpr5LAH2PYqUw==", "dependencies": { "@babel/runtime": "^7.23.6", @@ -2653,6 +2326,14 @@ "@types/unist": "*" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2705,6 +2386,11 @@ "@types/unist": "*" } }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -3244,7 +2930,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -3860,149 +3545,494 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.751", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", + "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=12" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.751", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", - "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=12" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/esbuild": { + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" } }, "node_modules/escalade": { @@ -4082,6 +4112,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4304,7 +4339,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4766,6 +4800,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -4780,6 +4827,33 @@ "node": ">= 6" } }, + "node_modules/http-proxy-middleware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "dependencies": { + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -5052,6 +5126,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5070,6 +5152,17 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -5094,7 +5187,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -8204,7 +8296,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -8531,7 +8622,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -9613,8 +9703,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", @@ -10075,7 +10164,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, From 78a4b7986ae353cf98f03b82cdca88e198da9d54 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 19 May 2024 21:59:00 +0200 Subject: [PATCH 109/130] react integration --- backend/web-bff/App/routes/users.js | 6 +- backend/web-bff/temp-frontend/src/App.tsx | 4 +- frontend/src/@types/requests.d.ts | 76 +++++++++---------- frontend/src/App.tsx | 3 + .../src/components/layout/nav/AuthNav.tsx | 13 ++-- frontend/src/components/layout/nav/Layout.tsx | 15 ++-- .../src/components/layout/nav/UnauthNav.tsx | 11 +-- frontend/src/hooks/useAuth.tsx | 8 ++ frontend/src/index.tsx | 31 ++------ frontend/src/pages/apiTest/ApiTest.tsx | 13 +--- frontend/src/pages/index/HomeAuthCheck.tsx | 14 ++-- .../src/pages/index/landing/LandingPage.tsx | 11 +-- frontend/src/providers/AuthProvider.tsx | 37 +++++++-- frontend/src/providers/UserProvider.tsx | 21 ++--- frontend/src/router/AuthenticatedRoute.tsx | 19 +++-- frontend/src/test/pages/home/Home.test.tsx | 10 +-- frontend/src/util/apiFetch.ts | 46 +---------- 17 files changed, 146 insertions(+), 192 deletions(-) create mode 100644 frontend/src/hooks/useAuth.tsx diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index 646c1d83..d90985d6 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -28,12 +28,14 @@ router.get('/isAuthenticated', if (req.session.isAuthenticated) { res.send({ isAuthenticated: true, - name: req.session.account.name + account: { + name: req.session.account?.name + } }); } else { res.send({ isAuthenticated: false, - name: "" + account: null }) } } catch(error) { diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx index 58136d8e..3e8022d9 100644 --- a/backend/web-bff/temp-frontend/src/App.tsx +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -8,7 +8,7 @@ import './App.css' function App() { const [auth, setAuth] - = useState<{ isAuthenticated:boolean, name: string} | null>(null) + = useState<{ isAuthenticated:boolean, account: { name:string } | null} | null>(null) useEffect(() => { axios.get('http://localhost:3000/web/users/isAuthenticated', {withCredentials: true}).then(({data}) => { @@ -28,7 +28,7 @@ function App() { return ( <>

    Logged in!

    -

    You are logged in as {auth && auth.name? auth.name : null}

    +

    You are logged in as {auth && auth.account?.name ? auth.account.name : null}

    ) } else { diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 63f6846a..1ab473b6 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -4,45 +4,45 @@ import type {ProjectFormData} from "../pages/projectCreate/components/ProjectCre * Routes used to make API calls */ export enum ApiRoutes { - USER_COURSES = "api/courses", - COURSES = "api/courses", + USER_COURSES = "/web/api/courses", + COURSES = "/web/api/courses", - COURSE = "api/courses/:courseId", - COURSE_MEMBERS = "api/courses/:courseId/members", - COURSE_MEMBER = "api/courses/:courseId/members/:userId", - COURSE_PROJECTS = "api/courses/:id/projects", - COURSE_CLUSTERS = "api/courses/:id/clusters", - COURSE_GRADES = '/api/courses/:id/grades', - COURSE_LEAVE = "api/courses/:courseId/leave", - COURSE_COPY = "/api/courses/:courseId/copy", - - PROJECTS = "api/projects", - PROJECT = "api/projects/:id", - PROJECT_CREATE = "api/courses/:courseId/projects", - PROJECT_TESTS = "api/projects/:id/tests", - PROJECT_SUBMISSIONS = "api/projects/:id/submissions", - PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", - PROJECT_GROUP = "api/projects/:id/groups/:groupId", - PROJECT_GROUPS = "api/projects/:id/groups", - PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", - - SUBMISSION = "api/submissions/:id", - SUBMISSION_FILE = "api/submissions/:id/file", - SUBMISSION_STRUCTURE_FEEDBACK= "/api/submissions/:id/structurefeedback", - SUBMISSION_DOCKER_FEEDBACK= "/api/submissions/:id/dockerfeedback", - SUBMISSION_ARTIFACT="/api/submissions/:id/artifacts", - - - CLUSTER = "api/clusters/:id", - - GROUP = "api/groups/:id", - GROUP_MEMBERS = "api/groups/:id/members", - GROUP_MEMBER = "api/groups/:id/members/:userId", - GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", - - USER = "api/users/:id", - USERS = "api/users", - USER_AUTH = "api/user", + COURSE = "/web/api/courses/:courseId", + COURSE_MEMBERS = "/web/api/courses/:courseId/members", + COURSE_MEMBER = "/web/api/courses/:courseId/members/:userId", + COURSE_PROJECTS = "/web/api/courses/:id/projects", + COURSE_CLUSTERS = "/web/api/courses/:id/clusters", + COURSE_GRADES = '/web/api/courses/:id/grades', + COURSE_LEAVE = "/web/api/courses/:courseId/leave", + COURSE_COPY = "/web/api/courses/:courseId/copy", + + PROJECTS = "/web/api/projects", + PROJECT = "/web/api/projects/:id", + PROJECT_CREATE = "/web/api/courses/:courseId/projects", + PROJECT_TESTS = "/web/api/projects/:id/tests", + PROJECT_SUBMISSIONS = "/web/api/projects/:id/submissions", + PROJECT_SCORE = "/web/api/projects/:id/groups/:groupId/score", + PROJECT_GROUP = "/web/api/projects/:id/groups/:groupId", + PROJECT_GROUPS = "/web/api/projects/:id/groups", + PROJECT_GROUP_SUBMISSIONS = "/web/api/projects/:projectId/submissions/:groupId", + + SUBMISSION = "/web/api/submissions/:id", + SUBMISSION_FILE = "/web/api/submissions/:id/file", + SUBMISSION_STRUCTURE_FEEDBACK= "/web/api/submissions/:id/structurefeedback", + SUBMISSION_DOCKER_FEEDBACK= "/web/api/submissions/:id/dockerfeedback", + SUBMISSION_ARTIFACT="/web/api/submissions/:id/artifacts", + + + CLUSTER = "/web/api/clusters/:id", + + GROUP = "/web/api/groups/:id", + GROUP_MEMBERS = "/web/api/groups/:id/members", + GROUP_MEMBER = "/web/api/groups/:id/members/:userId", + GROUP_SUBMISSIONS = "/web/api/projects/:id/groups/:id/submissions", + + USER = "/web/api/users/:id", + USERS = "/web/api/users", + USER_AUTH = "/web/api/user", } export type Timestamp = string diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5099df91..2499f4a1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import Layout from "./components/layout/nav/Layout" import "./i18n/config" import ThemeProvider from "./theme/ThemeProvider" import { AppProvider } from "./providers/AppProvider" +import {AuthProvider} from "./providers/AuthProvider" import { UserProvider } from "./providers/UserProvider" import AppApiProvider from "./providers/AppApiProvider" import ErrorProvider from "./providers/ErrorProvider" @@ -17,6 +18,7 @@ function App() { + @@ -24,6 +26,7 @@ function App() { + diff --git a/frontend/src/components/layout/nav/AuthNav.tsx b/frontend/src/components/layout/nav/AuthNav.tsx index 96974971..8d9befbf 100644 --- a/frontend/src/components/layout/nav/AuthNav.tsx +++ b/frontend/src/components/layout/nav/AuthNav.tsx @@ -1,18 +1,18 @@ -import { useAccount } from "@azure/msal-react" + import { Dropdown, MenuProps, Typography } from "antd" import { useTranslation } from "react-i18next" import { UserOutlined, BgColorsOutlined, DownOutlined, LogoutOutlined } from "@ant-design/icons" -import { msalInstance } from "../../../index" import { useNavigate } from "react-router-dom" import { Themes } from "../../../@types/appTypes" import { AppRoutes } from "../../../@types/routes" import useApp from "../../../hooks/useApp" +import useAuth from "../../../hooks/useAuth" const AuthNav = () => { const { t } = useTranslation() const app = useApp() - const auth = useAccount() + const auth = useAuth() const navigate = useNavigate() const items: MenuProps["items"] = [ @@ -49,9 +49,8 @@ const AuthNav = () => { navigate(AppRoutes.PROFILE) break case "logout": - msalInstance.logoutPopup({ - account: auth, - }) + auth.logout() + navigate('http://localhost:3000/web/auth/signout') break case Themes.DARK: case Themes.LIGHT: @@ -69,7 +68,7 @@ const AuthNav = () => { }} > - {auth!.name} + {auth!.account?.name}
    diff --git a/frontend/src/components/layout/nav/Layout.tsx b/frontend/src/components/layout/nav/Layout.tsx index 2e1ed3a2..4b6afcdc 100644 --- a/frontend/src/components/layout/nav/Layout.tsx +++ b/frontend/src/components/layout/nav/Layout.tsx @@ -1,4 +1,4 @@ -import { AuthenticatedTemplate, useIsAuthenticated } from "@azure/msal-react" + import AuthNav from "./AuthNav" import { FC, PropsWithChildren } from "react" import { Layout as AntLayout, Flex } from "antd" @@ -6,29 +6,24 @@ import Logo from "../../Logo" import Sidebar from "../sidebar/Sidebar" import LanguageDropdown from "../../LanguageDropdown" - +import useAuth from "../../../hooks/useAuth"; const Layout: FC> = ({ children }) => { - const isAuthenticated = useIsAuthenticated() + const auth = useAuth() - if(!isAuthenticated) return <>{children} + if(!auth.isAuthenticated) return <>{children} return (
    - {/* */} - - - - - + diff --git a/frontend/src/components/layout/nav/UnauthNav.tsx b/frontend/src/components/layout/nav/UnauthNav.tsx index 78529094..761731ce 100644 --- a/frontend/src/components/layout/nav/UnauthNav.tsx +++ b/frontend/src/components/layout/nav/UnauthNav.tsx @@ -1,15 +1,16 @@ import { useTranslation } from "react-i18next"; -import { msalInstance } from "../../../index" import { Button } from "antd"; +import useAuth from "../../../hooks/useAuth"; +import {useNavigate} from "react-router-dom"; const UnauthNav = () => { const { t } = useTranslation(); + const auth = useAuth(); + const navigate = useNavigate(); const handleLogin = async () => { try { - await msalInstance.loginPopup({ - scopes: ['openid', 'profile', 'User.Read'], - }); - + await auth.login() + window.location.replace("http://localhost:3000/web/auth/signin") } catch (error) { console.error(error) } diff --git a/frontend/src/hooks/useAuth.tsx b/frontend/src/hooks/useAuth.tsx new file mode 100644 index 00000000..202edb1a --- /dev/null +++ b/frontend/src/hooks/useAuth.tsx @@ -0,0 +1,8 @@ +import {useContext} from "react"; +import {AuthContext} from "../providers/AuthProvider" + +const useAuth = () => { + return useContext(AuthContext) +} + +export default useAuth \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 4175d34b..4e8321f3 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -3,31 +3,12 @@ import ReactDOM from "react-dom/client" import App from "./App" import { BrowserRouter } from "react-router-dom" import "./styles.css" -import { msalConfig } from "./auth/AuthConfig" -import { PublicClientApplication, EventType, EventMessage, AuthenticationResult } from "@azure/msal-browser" -export const msalInstance = new PublicClientApplication(msalConfig) -msalInstance.initialize().then(() => { - // Account selection logic is app dependent. Adjust as needed for different use cases. - const accounts = msalInstance.getAllAccounts() - if (accounts.length > 0) { - msalInstance.setActiveAccount(accounts[0]) - } +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) +root.render( + + + +) - - msalInstance.addEventCallback((event: EventMessage) => { - if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { - const payload = event.payload as AuthenticationResult - const account = payload.account - msalInstance.setActiveAccount(account) - } - }) - - const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) - root.render( - - - - ) -}) diff --git a/frontend/src/pages/apiTest/ApiTest.tsx b/frontend/src/pages/apiTest/ApiTest.tsx index 19a832e9..0f5ba1b3 100644 --- a/frontend/src/pages/apiTest/ApiTest.tsx +++ b/frontend/src/pages/apiTest/ApiTest.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react" -import apiCall, { accessToken, apiCallInit } from "../../util/apiFetch" +import apiCall from "../../util/apiFetch" import { Button, Input, InputRef, Result, Select, Space, Typography } from "antd" const { Option } = Select @@ -9,12 +9,8 @@ const ApiTest = () => { const [method, setMethod] = useState("get") const routeRef = useRef(null) const [error, setError] = useState<[string, number] | null>(null) - const [apiToken, setApiToken] = useState(null) const [requestBody, setRequestBody] = useState("{}") - useEffect(() => { - apiCallInit().then(setApiToken) - }, []) const makeApiCall = async () => { const route = routeRef.current?.input?.value @@ -64,16 +60,13 @@ const ApiTest = () => { code style={{ maxHeight: "100%" }} > - {apiToken - ? JSON.stringify( + {JSON.stringify( { - Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, null, 2 - ) - : "Loading token..."} + )} Test: diff --git a/frontend/src/pages/index/HomeAuthCheck.tsx b/frontend/src/pages/index/HomeAuthCheck.tsx index ed2c172e..5f1e9588 100644 --- a/frontend/src/pages/index/HomeAuthCheck.tsx +++ b/frontend/src/pages/index/HomeAuthCheck.tsx @@ -1,16 +1,14 @@ -import { useIsAuthenticated, useMsal } from "@azure/msal-react" import Home from "./Home" import LandingPage from "./landing/LandingPage" +import useAuth from "../../hooks/useAuth"; const HomeAuthCheck = () => { - const isAuthenticated = useIsAuthenticated() - const { inProgress } = useMsal() - -if(inProgress === "startup") return null - if (isAuthenticated) { - return + const auth = useAuth() + auth.updateAccount() + if (auth.isAuthenticated) { + return } - return + return } export default HomeAuthCheck diff --git a/frontend/src/pages/index/landing/LandingPage.tsx b/frontend/src/pages/index/landing/LandingPage.tsx index 0e26fe30..bb0e6e19 100644 --- a/frontend/src/pages/index/landing/LandingPage.tsx +++ b/frontend/src/pages/index/landing/LandingPage.tsx @@ -10,8 +10,9 @@ import jsLogo from "../../../assets/landingPageLogos/jsLogo.png" import dockerLogo from "../../../assets/landingPageLogos/dockerLogo.png" import codeLogo from "../../../assets/landingPageLogos/codeLogo.png" import { useTranslation } from "react-i18next" -import { msalInstance } from "../../.." import { motion } from "framer-motion" +import useAuth from "../../../hooks/useAuth"; +import {useNavigate} from "react-router-dom"; const defaultTransition = { duration: 0.5, ease: [0.44, 0, 0.56, 1], type: "tween" } @@ -21,12 +22,12 @@ const defaultInitial = { opacity: 0.001, y: 64 } const LandingPage: FC = () => { const { t } = useTranslation() - + const auth = useAuth() + const navigate = useNavigate() const handleLogin = async () => { try { - await msalInstance.loginPopup({ - scopes: ["openid", "profile", "User.Read"], - }) + await auth.login() + window.location.replace("http://localhost:3000/web/auth/signin") } catch (error) { console.error(error) } diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 3aad5c10..b15b20c2 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -1,8 +1,8 @@ -import {createContext, FC, PropsWithChildren, useEffect, useState} from "react" +import {createContext, Dispatch, FC, PropsWithChildren, useEffect, useState} from "react" import {LoginStatus} from "../@types/appTypes"; -import {apiFetch} from "../util/apiFetch"; -import {UserContext} from "./UserProvider"; - +import {useNavigate} from "react-router-dom"; +import axios from "axios"; +import { AxiosRequestConfig } from "axios" type Account = { name: string @@ -12,7 +12,9 @@ export type AuthContextProps = { isAuthenticated: Boolean, loginStatus: LoginStatus, account: Account | null, - updateAccount: () => void + updateAccount: () => void, + login: () => void, + logout: () => void, } const AuthContext = createContext({} as AuthContextProps) @@ -24,20 +26,39 @@ const AuthProvider : FC = ({children}) => { useEffect(() => { updateAccount() - }, [loginStatus]); + }, []); const updateAccount = async () => { try { - const res = await apiFetch("GET", "localhost:3000/users/account"); + const res = await axios.get( + 'http://localhost:3000/web/users/isAuthenticated', + {withCredentials:true } as AxiosRequestConfig + ) if (res.data.isAuthenticated) { setIsAuthenticated(true) setLoginStatus(LoginStatus.LOGGED_IN) setAccount(res.data.account) + } else { + setIsAuthenticated(false) + setLoginStatus(LoginStatus.LOGGED_OUT) + setAccount(null) } } catch (err) { console.log(err) } } - return {children} + + const login = async () => { + setLoginStatus(LoginStatus.LOGIN_IN_PROGRESS) + } + + const logout = async () => { + setIsAuthenticated(false) + setLoginStatus(LoginStatus.LOGOUT_IN_PROGRESS) + } + + return {children} } + +export {AuthContext, AuthProvider} \ No newline at end of file diff --git a/frontend/src/providers/UserProvider.tsx b/frontend/src/providers/UserProvider.tsx index d35fadc6..999976b9 100644 --- a/frontend/src/providers/UserProvider.tsx +++ b/frontend/src/providers/UserProvider.tsx @@ -1,9 +1,10 @@ -import { FC, PropsWithChildren, createContext, useEffect, useState } from "react" -import { ApiRoutes, GET_Responses } from "../@types/requests.d" +import {createContext, FC, PropsWithChildren, useEffect, useState} from "react" +import {ApiRoutes, GET_Responses} from "../@types/requests.d" import apiCall from "../util/apiFetch" -import { useIsAuthenticated, useMsal } from "@azure/msal-react" -import { Spin } from "antd" -import { InteractionStatus } from "@azure/msal-browser" +import {Spin} from "antd" +import useAuth from "../hooks/useAuth"; +import {LoginStatus} from "../@types/appTypes"; + type UserContextProps = { user: User | null @@ -18,16 +19,16 @@ const UserContext = createContext({} as UserContextProps) export type User = GET_Responses[ApiRoutes.USER] const UserProvider: FC = ({ children }) => { - const isAuthenticated = useIsAuthenticated() + const auth = useAuth() const [user, setUser] = useState(null) const [courses, setCourses] = useState(null) - const { inProgress } = useMsal() + useEffect(() => { - if (isAuthenticated) { + if (auth.isAuthenticated) { updateUser() } - }, [isAuthenticated]) + }, [auth]) const updateCourses = async (userId: number | undefined = user?.id) => { if (!userId) return console.error("No user id provided") @@ -51,7 +52,7 @@ const UserProvider: FC = ({ children }) => { } } - if (!user && (!(inProgress === InteractionStatus.Startup || inProgress === InteractionStatus.None || inProgress === InteractionStatus.Logout) || isAuthenticated)) + if (!user && (auth.loginStatus === LoginStatus.LOGIN_IN_PROGRESS || auth.loginStatus === LoginStatus.LOGOUT_IN_PROGRESS || auth.isAuthenticated)) return (
    diff --git a/frontend/src/router/AuthenticatedRoute.tsx b/frontend/src/router/AuthenticatedRoute.tsx index 42d4240a..efd2cc00 100644 --- a/frontend/src/router/AuthenticatedRoute.tsx +++ b/frontend/src/router/AuthenticatedRoute.tsx @@ -1,25 +1,24 @@ -import { FC, useEffect } from "react" -import { useIsAuthenticated, useMsal } from "@azure/msal-react" -import { Outlet, useNavigate } from "react-router-dom" -import { AppRoutes } from "../@types/routes" -import { InteractionStatus } from "@azure/msal-browser" +import {FC, useEffect} from "react" +import {Outlet, useNavigate} from "react-router-dom" +import {AppRoutes} from "../@types/routes" +import useAuth from "../hooks/useAuth"; +import {LoginStatus} from "../@types/appTypes"; const AuthenticatedRoute: FC = () => { - const isAuthenticated = useIsAuthenticated() - const { inProgress } = useMsal() + const auth = useAuth() const navigate = useNavigate() useEffect(() => { - if ((inProgress === InteractionStatus.None || inProgress === InteractionStatus.Logout ) && !isAuthenticated) { + if ((auth.loginStatus === LoginStatus.LOGGED_OUT || auth.loginStatus === LoginStatus.LOGOUT_IN_PROGRESS ) && !auth.isAuthenticated) { // instance.loginRedirect(loginRequest); console.log("NOT AUTHENTICATED"); navigate(AppRoutes.HOME) } - }, [isAuthenticated,inProgress]) + }, [auth]) - if (isAuthenticated) { + if (auth.isAuthenticated) { return } diff --git a/frontend/src/test/pages/home/Home.test.tsx b/frontend/src/test/pages/home/Home.test.tsx index 3fcd593f..537a6f27 100644 --- a/frontend/src/test/pages/home/Home.test.tsx +++ b/frontend/src/test/pages/home/Home.test.tsx @@ -6,12 +6,7 @@ import { UserCourseType } from "../../../providers/UserProvider" //TODO: Find better way to write all the mocks -jest.mock("@azure/msal-react",()=> ({ - useAccount: jest.fn(() => ({})), - MsalAuthenticationTemplate: () => null, - useMsal: jest.fn(() => ({})), - MsalAuthenticationResult: () => ({}), -})) + jest.mock("react-syntax-highlighter/dist/esm/styles/prism",()=> ({ oneDark: {}, @@ -22,9 +17,6 @@ jest.mock('react-markdown', () => ({ Markdown: () => null, })); -jest.mock("@azure/msal-react", () => ({ - useIsAuthenticated: () => true, -})) window.matchMedia = window.matchMedia || function() { diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index d542607d..dac5c2bb 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -1,10 +1,9 @@ import { ApiRoutes, DELETE_Requests, GET_Responses, POST_Requests, POST_Responses, PUT_Requests, PUT_Responses } from "../@types/requests" import axios, { AxiosError, AxiosResponse } from "axios" -import { msalInstance } from "../index" import { AxiosRequestConfig } from "axios" -import { msalConfig } from "../auth/AuthConfig" -const serverHost = window.location.origin.includes("localhost") ? "http://localhost:8080" : window.location.origin + +const serverHost = window.location.origin.includes("localhost") ? "http://localhost:3000" : window.location.origin let accessToken: string | null = null let tokenExpiry: Date | null = null @@ -22,11 +21,6 @@ export type ApiCallPathValues = {[param: string]: string | number} * */ export async function apiFetch(method: ApiMethods, route: string, body?: any, pathValues?:ApiCallPathValues): Promise> { - const account = msalInstance.getActiveAccount() - - if (!account) { - throw Error("No active account found") - } if(pathValues) { Object.entries(pathValues).forEach(([key, value]) => { @@ -34,21 +28,8 @@ export async function apiFetch(method: ApiMethods, route: string, body?: any, pa }) } - // check if we have access token - const now = new Date() - - if (!accessToken || !tokenExpiry || now >= tokenExpiry) { - const response = await msalInstance.acquireTokenSilent({ - scopes: [msalConfig.auth.clientId + "/.default"], - account: account, - }) - - accessToken = response.accessToken - tokenExpiry = response.expiresOn // convert expiry time to JavaScript Date - } const headers = { - Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", } @@ -58,6 +39,7 @@ export async function apiFetch(method: ApiMethods, route: string, body?: any, pa method: method, url: url.toString(), headers: headers, + withCredentials:true, data: body, } @@ -73,28 +55,6 @@ const apiCall = { patch: async (route: T, body: Partial, pathValues?:ApiCallPathValues) => apiFetch("PATCH", route, body,pathValues) as Promise>, } -const apiCallInit = async () => { - const account = msalInstance.getActiveAccount() - - if (!account) { - throw Error("No active account found") - } - - const now = new Date() - if (!accessToken || !tokenExpiry || now >= tokenExpiry) { - const response = await msalInstance.acquireTokenSilent({ - scopes: [msalConfig.auth.clientId + "/.default"], - account: account, - }) - - accessToken = response.accessToken - tokenExpiry = response.expiresOn // convert expiry time to JavaScript Date - return accessToken - } else{ - return accessToken - } -} -export { accessToken,apiCallInit } export default apiCall From 7164ff4bf969b1690bed4fb14d38b20475a66a09 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 19 May 2024 22:17:53 +0200 Subject: [PATCH 110/130] request spam fixed --- backend/web-bff/App/app.js | 2 +- frontend/src/components/layout/nav/AuthNav.tsx | 2 +- frontend/src/pages/index/HomeAuthCheck.tsx | 1 - frontend/src/providers/AuthProvider.tsx | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index afdf6b5f..7af07fbb 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -46,7 +46,7 @@ app.use(session({ const limiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 100, + max: 10000, }); app.use(limiter); diff --git a/frontend/src/components/layout/nav/AuthNav.tsx b/frontend/src/components/layout/nav/AuthNav.tsx index 8d9befbf..6b3d2b4b 100644 --- a/frontend/src/components/layout/nav/AuthNav.tsx +++ b/frontend/src/components/layout/nav/AuthNav.tsx @@ -50,7 +50,7 @@ const AuthNav = () => { break case "logout": auth.logout() - navigate('http://localhost:3000/web/auth/signout') + window.location.replace("http://localhost:3000/web/auth/signout") break case Themes.DARK: case Themes.LIGHT: diff --git a/frontend/src/pages/index/HomeAuthCheck.tsx b/frontend/src/pages/index/HomeAuthCheck.tsx index 5f1e9588..125d993a 100644 --- a/frontend/src/pages/index/HomeAuthCheck.tsx +++ b/frontend/src/pages/index/HomeAuthCheck.tsx @@ -4,7 +4,6 @@ import useAuth from "../../hooks/useAuth"; const HomeAuthCheck = () => { const auth = useAuth() - auth.updateAccount() if (auth.isAuthenticated) { return } diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index b15b20c2..d493d5f9 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -21,7 +21,7 @@ const AuthContext = createContext({} as AuthContextProps) const AuthProvider : FC = ({children}) => { const [isAuthenticated, setIsAuthenticated] = useState(false) - const [loginStatus, setLoginStatus] = useState(LoginStatus.LOGGED_OUT) + const [loginStatus, setLoginStatus] = useState(LoginStatus.LOGIN_IN_PROGRESS) const [account, setAccount] = useState(null) useEffect(() => { From fcbd844b2f608cda6057a94895fae199d524236a Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Sun, 19 May 2024 22:40:53 +0200 Subject: [PATCH 111/130] bugfixes --- backend/web-bff/App/app.js | 2 +- backend/web-bff/App/routes/api.js | 2 +- frontend/src/@types/requests.d.ts | 2 +- frontend/src/pages/apiTest/ApiTest.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index 7af07fbb..1ff71e02 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -46,7 +46,7 @@ app.use(session({ const limiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 10000, + max: 4000, }); app.use(limiter); diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js index 416daf58..e60da688 100644 --- a/backend/web-bff/App/routes/api.js +++ b/backend/web-bff/App/routes/api.js @@ -10,7 +10,7 @@ const { BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig // custom middleware to check auth state function isAuthenticated(req, res, next) { if (!req.session.isAuthenticated) { - return res.redirect('/auth/signin'); // redirect to sign-in route + return res.redirect('/web/auth/signin'); // redirect to sign-in route } next(); diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 1ab473b6..179b3755 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -12,7 +12,7 @@ export enum ApiRoutes { COURSE_MEMBER = "/web/api/courses/:courseId/members/:userId", COURSE_PROJECTS = "/web/api/courses/:id/projects", COURSE_CLUSTERS = "/web/api/courses/:id/clusters", - COURSE_GRADES = '/web/api/courses/:id/grades', + COURSE_GRADES = "/web/api/courses/:id/grades", COURSE_LEAVE = "/web/api/courses/:courseId/leave", COURSE_COPY = "/web/api/courses/:courseId/copy", diff --git a/frontend/src/pages/apiTest/ApiTest.tsx b/frontend/src/pages/apiTest/ApiTest.tsx index 0f5ba1b3..ba94f5fa 100644 --- a/frontend/src/pages/apiTest/ApiTest.tsx +++ b/frontend/src/pages/apiTest/ApiTest.tsx @@ -76,7 +76,7 @@ const ApiTest = () => { ref={routeRef} size="large" addonBefore={selectBefore} - defaultValue="/api/test" + defaultValue="/web/api/test" />
    - - {{#each profile}} - - - - - {{/each}} - -
    {{@key}}{{this}}
    -
    -Go back \ No newline at end of file diff --git a/backend/web-bff/App/views/token.hbs b/backend/web-bff/App/views/token.hbs new file mode 100644 index 00000000..ab04c4a5 --- /dev/null +++ b/backend/web-bff/App/views/token.hbs @@ -0,0 +1,7 @@ +

    Backend API

    +

    /token endpoint response

    + +
    +Go back \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/README.md b/backend/web-bff/temp-frontend/README.md index 0d6babed..b2224872 100644 --- a/backend/web-bff/temp-frontend/README.md +++ b/backend/web-bff/temp-frontend/README.md @@ -1,30 +1,3 @@ -# React + TypeScript + Vite +# Testing frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +Our actual frontend is a bit too complex to test basic features in. This is why this small react project exists. \ No newline at end of file diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index cfd5a2c1..82d39a60 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -1,7 +1,13 @@ import {createContext, FC, PropsWithChildren, useEffect, useState} from "react" import {LoginStatus} from "../@types/appTypes"; import apiCall from "../util/apiFetch"; -import {ApiRoutes} from "../@types/requests"; +import {ApiRoutes} from "../@types/requests.d"; + +/** + * Context provider that contains the authentication state and account name for in the nav bar. + */ + + export type Account = { name: string @@ -27,6 +33,10 @@ const AuthProvider : FC = ({children}) => { updateAccount() }, []); + /** + * Function that contacts the backend for information on the current authentication state. + * Stores the result in the state. + */ const updateAccount = async () => { try { const res = await apiCall.get(ApiRoutes.AUTH_INFO) @@ -44,10 +54,18 @@ const AuthProvider : FC = ({children}) => { } } + /** + * Function that updates the login state. + * Should be used when logging in. + */ const login = async () => { setLoginStatus(LoginStatus.LOGIN_IN_PROGRESS) } + /** + * Function that updates the login state. + * Should be used when logging out. + */ const logout = async () => { setIsAuthenticated(false) setLoginStatus(LoginStatus.LOGOUT_IN_PROGRESS) From 6fa01c4a0476ee9755f81843cd05d6b1c1c30888 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 04:19:33 +0200 Subject: [PATCH 115/130] post data bug fix --- backend/web-bff/App/fetch.js | 2 +- frontend/src/util/apiFetch.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index 3ee68b4e..b32fc34d 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -28,7 +28,7 @@ async function fetch(endpoint, accessToken, method, body, headers) { const config= { method: method, url: url.toString(), - body: body, + data: body, headers: finalHeaders, } diff --git a/frontend/src/util/apiFetch.ts b/frontend/src/util/apiFetch.ts index fd043dec..ff20260f 100644 --- a/frontend/src/util/apiFetch.ts +++ b/frontend/src/util/apiFetch.ts @@ -4,6 +4,7 @@ import { ApiRoutes, DELETE_Requests, GET_Responses, POST_Requests, POST_Response import axios, { AxiosError, AxiosResponse, RawAxiosRequestHeaders } from "axios" import { AxiosRequestConfig } from "axios" +import {file} from "jszip"; @@ -51,6 +52,7 @@ export async function apiFetch(method: ApiMethods, route: string, body?: any, pa ...config, // spread the config object to merge it with the existing configuration } + return axios(finalConfig) } From 6d1991031c6e5207501d7a3a4917721b6168c3db Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 04:21:23 +0200 Subject: [PATCH 116/130] comment mongostore --- backend/web-bff/App/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index 3b713261..acd489fd 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -42,8 +42,8 @@ if (DEVELOPMENT) { secure: false, // make sure this is true in production maxAge: 7 * 24 * 60 * 60 * 1000, }, - store: MongoStore.create( - {mongoUrl: connection_string}) + //store: MongoStore.create( + // {mongoUrl: connection_string}) })); } else { From 346c866272700f60c2c7b6c51bfa12606ed130ab Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 04:23:02 +0200 Subject: [PATCH 117/130] comment --- backend/web-bff/App/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index acd489fd..df98daee 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -47,6 +47,8 @@ if (DEVELOPMENT) { })); } else { + // When using production mode, please make sure a mongodb instance is running and accepting connections + // on port PORT. Also make sure the user exists. app.use(session({ name: 'pigeon session', secret: process.env.EXPRESS_SESSION_SECRET, From d79a4442e66bd0d6da4f13c9beb275c6bf2c4494 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 10:17:43 +0200 Subject: [PATCH 118/130] removed unsued route --- backend/web-bff/App/routes/auth.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index e58df547..d90f8041 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -18,18 +18,7 @@ router.get('/signin', authProvider.login({ successRedirect: FRONTEND_URI, })); -/** - * No longer used. TODO: remove - * - * Route that acquires a token for accessing the backend resource server. - * It stores this token in the session, it does not return the token. - * - * @route GET /web/auth/acquireToken - */ -router.get('/acquireToken', authProvider.acquireToken({ - scopes: [msalConfig.auth.clientId + "/.default"], - redirectUri: REDIRECT_URI -})); + /** * Route that starts the logout flow for msal. From 8090bf914d05ade0b4905c884d4b780e8c8c8f5c Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 10:23:32 +0200 Subject: [PATCH 119/130] fix route --- backend/web-bff/App/views/index.hbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/web-bff/App/views/index.hbs b/backend/web-bff/App/views/index.hbs index 9dc4562f..33fa9826 100644 --- a/backend/web-bff/App/views/index.hbs +++ b/backend/web-bff/App/views/index.hbs @@ -1,11 +1,11 @@

    {{title}}

    {{#if isAuthenticated }}

    Hi {{username}}!

    - View ID token claims + View ID token claims
    - Sign out + view token + Sign out {{else}}

    Welcome to {{title}}

    - Sign in + Sign in {{/if}} \ No newline at end of file From d21c93c829018135865abfee5ae4da70fd1fae30 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Mon, 20 May 2024 10:34:24 +0200 Subject: [PATCH 120/130] Fix tests --- .../java/com/ugent/pidgeon/util/TestUtil.java | 15 +++----- .../controllers/TestControllerTest.java | 2 +- .../com/ugent/pidgeon/util/TestUtilTest.java | 35 +++++++++++++++---- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java index 098cf99f..663ac04e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java @@ -3,6 +3,7 @@ import com.ugent.pidgeon.controllers.ApiRoutes; import com.ugent.pidgeon.model.json.TestJson; import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.TestEntity; import com.ugent.pidgeon.postgre.models.UserEntity; @@ -80,7 +81,6 @@ public CheckResult> checkForTestUpdate( return new CheckResult<>(HttpStatus.BAD_REQUEST, "A docker image is required if u add a script", null); } - // This returns false if the image isn't pullt yet! FIX PLS if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); } @@ -97,23 +97,16 @@ public CheckResult> checkForTestUpdate( return new CheckResult<>(HttpStatus.BAD_REQUEST, "No docker test script is configured for this test", null); } - try{ + try { // throws error if there are issues in the template if(dockerTemplate != null) DockerSubmissionTestModel.tryTemplate(dockerTemplate); - if(structureTemplate != null) DockerSubmissionTestModel.tryTemplate(structureTemplate); + if(structureTemplate != null) SubmissionTemplateModel.tryTemplate(structureTemplate); - }catch(IllegalArgumentException e){ + } catch(IllegalArgumentException e){ return new CheckResult<>(HttpStatus.BAD_REQUEST, e.getMessage(), null); } - if(dockerTemplate != null){ - try{ - DockerSubmissionTestModel.tryTemplate(dockerTemplate); - }catch (IllegalArgumentException e){ - return new CheckResult<>(HttpStatus.BAD_REQUEST, e.getMessage(), null); - } - } return new CheckResult<>(HttpStatus.OK, "", new Pair<>(testEntity, projectEntity)); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java index 388a2675..16664d59 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java @@ -596,7 +596,7 @@ public void testGetPatch() throws Exception { eq(null), eq(null), eq(null), - eq(null), + eq(structureTemplate), eq(HttpMethod.PATCH) )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java index a525f523..34359d70 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java @@ -102,9 +102,14 @@ public void testCheckForTestUpdate() { doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); - try (MockedStatic mockedTestModel = mockStatic(DockerSubmissionTestModel.class)) { + try (MockedStatic mockedTestModel = mockStatic(DockerSubmissionTestModel.class); + MockedStatic mockedTemplateModel = mockStatic(SubmissionTemplateModel.class) + ) { mockedTestModel.when(() -> DockerSubmissionTestModel.imageExists(dockerImage)).thenReturn(true); - mockedTestModel.when(() -> DockerSubmissionTestModel.isValidTemplate(any())).thenReturn(true); + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)).then( + invocation -> null); + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)).then( + invocation -> null); projectEntity.setTestId(null); CheckResult> result = testUtil.checkForTestUpdate( @@ -128,15 +133,32 @@ public void testCheckForTestUpdate() { dockerImage, dockerScript, dockerTemplate, - structureTemplate, + null, HttpMethod.POST ); assertEquals(HttpStatus.OK, result.getStatus()); doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); - /* Not a valid template */ - when(DockerSubmissionTestModel.isValidTemplate(any())).thenReturn(false); + /* Not a valid docker template */ + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)) + .thenThrow(new IllegalArgumentException("Invalid template")); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)).then( + invocation -> null); + + /* Invalid structure template */ + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)) + .thenThrow(new IllegalArgumentException("Invalid template")); result = testUtil.checkForTestUpdate( projectEntity.getId(), userEntity, @@ -147,7 +169,8 @@ public void testCheckForTestUpdate() { httpMethod ); assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - when(DockerSubmissionTestModel.isValidTemplate(any())).thenReturn(true); + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)). + then(invocation -> null); /* Method is patch and no template provided */ From 2e57e2e5a0e953a44bb756d3fab8edc1050606de Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 10:45:34 +0200 Subject: [PATCH 121/130] edit hard coded urls --- frontend/src/components/layout/nav/AuthNav.tsx | 3 ++- frontend/src/components/layout/nav/UnauthNav.tsx | 3 ++- frontend/src/pages/index/landing/LandingPage.tsx | 3 ++- frontend/src/util/backendServer.ts | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 frontend/src/util/backendServer.ts diff --git a/frontend/src/components/layout/nav/AuthNav.tsx b/frontend/src/components/layout/nav/AuthNav.tsx index feaae7bc..f44aba9e 100644 --- a/frontend/src/components/layout/nav/AuthNav.tsx +++ b/frontend/src/components/layout/nav/AuthNav.tsx @@ -11,6 +11,7 @@ import useAuth from "../../../hooks/useAuth" import createCourseModal from "../../../pages/index/components/CreateCourseModal" import useIsTeacher from "../../../hooks/useIsTeacher" +import {BACKEND_SERVER} from "../../../util/backendServer"; const AuthNav = () => { @@ -66,7 +67,7 @@ const AuthNav = () => { break case "logout": auth.logout() - window.location.replace("http://localhost:3000/web/auth/signout") + window.location.replace(BACKEND_SERVER + "/web/auth/signout") break case Themes.DARK: case Themes.LIGHT: diff --git a/frontend/src/components/layout/nav/UnauthNav.tsx b/frontend/src/components/layout/nav/UnauthNav.tsx index 761731ce..7066c747 100644 --- a/frontend/src/components/layout/nav/UnauthNav.tsx +++ b/frontend/src/components/layout/nav/UnauthNav.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { Button } from "antd"; import useAuth from "../../../hooks/useAuth"; import {useNavigate} from "react-router-dom"; +import {BACKEND_SERVER} from "../../../util/backendServer"; const UnauthNav = () => { const { t } = useTranslation(); @@ -10,7 +11,7 @@ const UnauthNav = () => { const handleLogin = async () => { try { await auth.login() - window.location.replace("http://localhost:3000/web/auth/signin") + window.location.replace(BACKEND_SERVER + "/web/auth/signin") } catch (error) { console.error(error) } diff --git a/frontend/src/pages/index/landing/LandingPage.tsx b/frontend/src/pages/index/landing/LandingPage.tsx index bb0e6e19..d210000a 100644 --- a/frontend/src/pages/index/landing/LandingPage.tsx +++ b/frontend/src/pages/index/landing/LandingPage.tsx @@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next" import { motion } from "framer-motion" import useAuth from "../../../hooks/useAuth"; import {useNavigate} from "react-router-dom"; +import {BACKEND_SERVER} from "../../../util/backendServer"; const defaultTransition = { duration: 0.5, ease: [0.44, 0, 0.56, 1], type: "tween" } @@ -27,7 +28,7 @@ const LandingPage: FC = () => { const handleLogin = async () => { try { await auth.login() - window.location.replace("http://localhost:3000/web/auth/signin") + window.location.replace(BACKEND_SERVER + "/web/auth/signin") } catch (error) { console.error(error) } diff --git a/frontend/src/util/backendServer.ts b/frontend/src/util/backendServer.ts new file mode 100644 index 00000000..ec14c445 --- /dev/null +++ b/frontend/src/util/backendServer.ts @@ -0,0 +1,3 @@ + +export const BACKEND_SERVER = window.location.origin.includes("localhost") ? "http://localhost:3000" : window.location.origin + From 1d2bcd2beff4c57acadab74b4b2a1ac41e9e7ad7 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Mon, 20 May 2024 10:59:17 +0200 Subject: [PATCH 122/130] small change to feedback --- .../model/submissionTesting/SubmissionTemplateModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java index cf24c9e1..6b26100e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java @@ -239,7 +239,7 @@ public static void tryTemplate(String template) throws IllegalArgumentException // first check if file contains valid file names if(line.substring(0,line.length() - 1).contains("/")){ - throw new IllegalArgumentException("File at line "+ line_index + " contains invalid characters"); + throw new IllegalArgumentException("File/folder at line "+ (line_index+1) + " contains invalid characters"); } // check if file is a folder if(line.charAt(line.length() - 1) == '/') { From d6c846d983b03b14b03bfcc2dbe79c30efd2a253 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 11:16:57 +0200 Subject: [PATCH 123/130] app.conf changes --- backend/web-bff/App/app.js | 1 + nginx/conf/app.conf | 87 +++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index df98daee..22211508 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -49,6 +49,7 @@ if (DEVELOPMENT) { } else { // When using production mode, please make sure a mongodb instance is running and accepting connections // on port PORT. Also make sure the user exists. + app.set('trust proxy', 1) app.use(session({ name: 'pigeon session', secret: process.env.EXPRESS_SESSION_SECRET, diff --git a/nginx/conf/app.conf b/nginx/conf/app.conf index e3960ff3..c801c2d3 100644 --- a/nginx/conf/app.conf +++ b/nginx/conf/app.conf @@ -1,38 +1,67 @@ -server { - listen 80; - listen [::]:80; +http { + map $request_method $index_req { + default "default_index"; # A default value to prevent errors. + POST "index_post"; + } + + upstream index_post { + server localhost:3000; + } + + + + server { + listen 80; + listen [::]:80; - server_name sel2-6.ugent.be www.sel2-6.ugent.be; - server_tokens off; + server_name sel2-6.ugent.be www.sel2-6.ugent.be; + server_tokens off; - location /.well-known/acme-challenge/ { + location /.well-known/acme-challenge/ { root /var/www/certbot; - } + } - location / { - return 301 https://sel2-6.ugent.be$request_uri; - } -} + location / { + return 301 https://sel2-6.ugent.be$request_uri; + } + } -server { - listen 443 default_server ssl http2; - listen [::]:443 ssl http2; + server { + listen 443 default_server ssl http2; + listen [::]:443 ssl http2; - server_name sel2-6.ugent.be; + server_name sel2-6.ugent.be; - ssl_certificate /etc/nginx/ssl/live/sel2-6.ugent.be/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/sel2-6.ugent.be/privkey.pem; + ssl_certificate /etc/nginx/ssl/live/sel2-6.ugent.be/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/sel2-6.ugent.be/privkey.pem; - location /api/ { - proxy_pass http://spring_container:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Authorization $http_authorization; - } - location / { - root /usr/share/nginx/html/build; - try_files $uri $uri/ /index.html; - } + location /api/ { + proxy_pass http://spring_container:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + break; + } + + location /web/ { + proxy_pass http://index_post + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + break; + + location / { + + if ($index_req = "index_post") { + proxy_pass http://index_post; + break; + } + + root /usr/share/nginx/html/build; + try_files $uri $uri/ /index.html; + } + } } From 2282ad7c9b24ae047ab941c760f887d4b43d54ac Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 11:18:35 +0200 Subject: [PATCH 124/130] small conf change --- nginx/conf/app.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/conf/app.conf b/nginx/conf/app.conf index c801c2d3..26acadcf 100644 --- a/nginx/conf/app.conf +++ b/nginx/conf/app.conf @@ -1,7 +1,7 @@ http { map $request_method $index_req { - default "default_index"; # A default value to prevent errors. POST "index_post"; + default "default_index"; # A default value to prevent errors. } upstream index_post { From 31b9290e14fa43928b69971dd32edd5d9b34a03d Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 11:21:59 +0200 Subject: [PATCH 125/130] small conf change --- nginx/conf/app.conf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nginx/conf/app.conf b/nginx/conf/app.conf index 26acadcf..0c83679b 100644 --- a/nginx/conf/app.conf +++ b/nginx/conf/app.conf @@ -4,7 +4,7 @@ http { default "default_index"; # A default value to prevent errors. } - upstream index_post { + upstream express_server { server localhost:3000; } @@ -46,17 +46,18 @@ http { } location /web/ { - proxy_pass http://index_post + proxy_pass http://express_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; break; + } location / { if ($index_req = "index_post") { - proxy_pass http://index_post; + proxy_pass http://express_server; break; } From 5ca9154e261f45e4d16336fd5c3c3b854795634c Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Mon, 20 May 2024 22:46:58 +0200 Subject: [PATCH 126/130] alle docstrings toegevoegd en correcte apidog link gemaakt --- .../controllers/ClusterController.java | 19 +++++-- .../pidgeon/controllers/CourseController.java | 27 +++++++++- .../pidgeon/controllers/GroupController.java | 16 +++--- .../controllers/GroupFeedbackController.java | 44 +++++++++++++-- .../controllers/GroupMemberController.java | 4 +- .../controllers/ProjectController.java | 6 +-- .../pidgeon/controllers/TestController.java | 53 ++++++++++++++++++- .../pidgeon/controllers/UserController.java | 23 +++++++- 8 files changed, 169 insertions(+), 23 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java index ebc911bd..36a42280 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java @@ -126,7 +126,7 @@ public ResponseEntity createClusterForCourse(@PathVariable("courseid") Long c } /** - * Returns all groups for a cluster + * Get cluster by ID * * @param clusterid identifier of a cluster * @param auth authentication object of the requesting user @@ -193,9 +193,9 @@ public ResponseEntity doGroupClusterUpdate(GroupClusterEntity clusterEntity, * * @param clusterid identifier of a cluster * @param auth authentication object of the requesting user - * @param clusterFillMap Map object containing a map of all groups and their - * members of that cluster + * @param clusterFillMap Map object containing a map of all groups and their members of that cluster * @return ResponseEntity + * @ApiDog apiDog documentation * @HttpMethod PUT * @ApiPath /api/clusters/{clusterid}/fill * @AllowedRoles student, teacher @@ -243,7 +243,18 @@ public ResponseEntity fillCluster(@PathVariable("clusterid") Long clusterid, } } - + /** + * Updates a cluster + * + * @param clusterid identifier of a cluster + * @param auth authentication object of the requesting user + * @param clusterJson ClusterJson object containing the cluster data + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod PATCH + * @ApiPath /api/clusters/{clusterid} + * @AllowedRoles student, teacher + */ @PatchMapping(ApiRoutes.CLUSTER_BASE_PATH + "/{clusterid}") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchCluster(@PathVariable("clusterid") Long clusterid, Auth auth, @RequestBody GroupClusterUpdateJson clusterJson) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java index 8dfc5072..4c61cd39 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java @@ -185,6 +185,18 @@ public ResponseEntity updateCourse(@RequestBody CourseJson courseJson, @PathV } } + /** + * Function to update a course + * + * @param courseJson JSON object containing the course name and description + * @param courseId ID of the course to update + * @param auth authentication object of the requesting user + * @return ResponseEntity with the updated course entity + * @ApiDog apiDog documentation + * @HttpMethod PATCH + * @AllowedRoles teacher, student + * @ApiPath /api/courses/{courseId} + */ @PatchMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchCourse(@RequestBody CourseJson courseJson, @PathVariable long courseId, Auth auth) { @@ -417,7 +429,7 @@ public ResponseEntity joinCourse(Auth auth, @PathVariable Long courseId) { * @param auth authentication object of the requesting user * @param courseId ID of the course to get the join key from * @return ResponseEntity with a statuscode and a JSON object containing the course information - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod GET * @AllowedRoles teacher, student * @ApiPath /api/courses/{courseId}/join @@ -496,7 +508,7 @@ private ResponseEntity doRemoveFromCourse( * @param courseId ID of the course to add the user to * @param request JSON object containing the user id and relation * @return ResponseEntity with a statuscode and no body - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod POST * @AllowedRoles teacher, admin, student * @ApiPath /api/courses/{courseId}/members @@ -680,6 +692,17 @@ public ResponseEntity deleteCourseKey(Auth auth, @PathVariable Long cour return ResponseEntity.ok(""); } + /** + * Function to copy a course + * + * @param courseId ID of the course to copy + * @param auth authentication object of the requesting user + * @return ResponseEntity with the copied course entity + * @ApiDog apiDog documentation + * @HttpMethod POST + * @AllowedRoles teacher + * @ApiPath /api/courses/{courseId}/copy + */ @PostMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/copy") @Roles({UserRole.teacher}) @Transactional diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java index bd579216..141fb9ff 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java @@ -38,9 +38,13 @@ public class GroupController { /** * Function to get a group by its identifier - * @param groupid - * @param auth - * @return + * @param groupid identifier of a group + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod GET + * @AllowedRoles student, teacher + * @ApiPath /api/groups/{groupid} */ @GetMapping(ApiRoutes.GROUP_BASE_PATH + "/{groupid}") @Roles({UserRole.student, UserRole.teacher}) @@ -75,7 +79,7 @@ public ResponseEntity getGroupById(@PathVariable("groupid") Long groupid, Aut * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Put + * @HttpMethod PUT * @AllowedRoles teacher * @ApiPath /api/groups/{groupid} */ @@ -93,7 +97,7 @@ public ResponseEntity updateGroupName(@PathVariable("groupid") Long groupid, * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher * @ApiPath /api/groups/{groupid} */ @@ -136,7 +140,7 @@ private ResponseEntity doGroupNameUpdate(Long groupid, NameRequest nameReques * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Delete + * @HttpMethod DELETE * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid} */ diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java index 49318700..54a52098 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java @@ -52,7 +52,7 @@ public class GroupFeedbackController { * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher, student * @ApiPath /api/projects/{projectid}/groups/{groupid}/score */ @@ -83,6 +83,18 @@ public ResponseEntity updateGroupScore(@PathVariable("groupid") long groupId, return doGroupFeedbackUpdate(groupFeedbackEntity, request); } + /** + * Function to delete the score of a group + * + * @param groupId identifier of a group + * @param projectId identifier of a project + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod Delete + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/groups/{groupid}/score + */ @DeleteMapping(ApiRoutes.GROUP_FEEDBACK_PATH) @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteGroupScore(@PathVariable("groupid") long groupId, @PathVariable("projectid") long projectId, Auth auth) { @@ -99,6 +111,19 @@ public ResponseEntity deleteGroupScore(@PathVariable("groupid") long groupId, } } + /** + * Function to update the score of a group + * + * @param groupId identifier of a group + * @param projectId identifier of a project + * @param request request object containing the new score + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod PUT + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/groups/{groupid}/score + */ @PutMapping(ApiRoutes.GROUP_FEEDBACK_PATH) @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity updateGroupScorePut(@PathVariable("groupid") long groupId, @PathVariable("projectid") long projectId, @RequestBody UpdateGroupScoreRequest request, Auth auth) { @@ -136,8 +161,8 @@ public ResponseEntity doGroupFeedbackUpdate(GroupFeedbackEntity groupFeedback * @param request request object containing the new score * @param auth authentication object of the requesting user * @return ResponseEntity - * @ApiDog apiDog documentation - * @HttpMethod Post + * @ApiDog apiDog documentation + * @HttpMethod POST * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/projects/{projectid}/feedback */ @@ -174,7 +199,7 @@ public ResponseEntity addGroupScore(@PathVariable("groupid") long groupId, @P * @param projectId identifier of a project * @param auth authentication object of the requesting user * @return ResponseEntity - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod Get * @AllowedRoles teacher, student * @ApiPath /api/projects/{projectid}/groups/{groupid}/score @@ -203,6 +228,17 @@ public ResponseEntity getGroupScore(@PathVariable("groupid") long groupI return ResponseEntity.ok(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)); } + /** + * Function to get the grades of a course + * + * @param courseId identifier of a course + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod Get + * @AllowedRoles teacher, student + * @ApiPath /api/courses/{courseId}/grades + */ @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/grades") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getCourseGrades(@PathVariable("courseId") long courseId, Auth auth) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java index e7e9397c..97d759ee 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java @@ -66,7 +66,7 @@ public ResponseEntity removeMemberFromGroup(@PathVariable("groupid") lon * @param groupId ID of the group to remove the member from * @param auth authentication object of the requesting user * @return ResponseEntity with a string message about the operation result - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod DELETE * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/members @@ -128,7 +128,7 @@ public ResponseEntity addMemberToGroup(@PathVariable("groupid") long gro * @param groupId ID of the group to add the member to * @param auth authentication object of the requesting user * @return ResponseEntity with a list of UserJson objects containing the members of the group - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod POST * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/members diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 3e68a627..0193a4fe 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -50,7 +50,7 @@ public class ProjectController { /** * Function to get all projects of a user * @param auth authentication object of the requesting user - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod GET * @AllowedRoles teacher, student * @ApiPath /api/projects @@ -206,7 +206,7 @@ private ResponseEntity doProjectUpdate(ProjectEntity project, ProjectJson pro * @param projectJson ProjectUpdateDTO object containing the new project's information * @param auth authentication object of the requesting user * @ApiDog apiDog documentation - * @HttpMethod Put + * @HttpMethod PUT * @AllowedRoles teacher * @ApiPath /api/projects/{projectId} * @return ResponseEntity with the created project @@ -243,7 +243,7 @@ public ResponseEntity putProjectById(@PathVariable Long projectId, @RequestBo * @param projectJson ProjectUpdateDTO object containing the new project's information * @param auth authentication object of the requesting user * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher * @ApiPath /api/projects/{projectId} * @return ResponseEntity with the created project diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index e614fc5b..e178d277 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -44,7 +44,7 @@ public class TestController { * @param projectId the id of the project to update the tests for * @param auth the authentication object of the requesting user * @HttpMethod POST - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @AllowedRoles teacher * @ApiPath /api/projects/{projectid}/tests * @return ResponseEntity with the updated tests @@ -60,6 +60,16 @@ public ResponseEntity updateTests( testJson.getDockerTemplate(), testJson.getStructureTest(), HttpMethod.POST); } + /** + * Function to update the tests of a project + * @param projectId the id of the project to update the tests for + * @param auth the authentication object of the requesting user + * @HttpMethod PATCH + * @ApiDog apiDog documentation + * @AllowedRoles teacher + * @ApiPath /api/projects/{projectid}/tests + * @return ResponseEntity with the updated tests + */ @PatchMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchTests( @@ -71,6 +81,16 @@ public ResponseEntity patchTests( testJson.getDockerTemplate(), testJson.getStructureTest(), HttpMethod.PATCH); } + /** + * Function to update the tests of a project + * @param projectId the id of the project to update the tests for + * @param auth the authentication object of the requesting user + * @HttpMethod PUT + * @ApiDog apiDog documentation + * @AllowedRoles teacher + * @ApiPath /api/projects/{projectid}/tests + * @return ResponseEntity with the updated tests + */ @PutMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity putTests( @@ -235,6 +255,17 @@ public ResponseEntity deleteTestById(@PathVariable("projectid") long projectI return ResponseEntity.ok().build(); } + /** + * Function to upload extra files for a test + * @param projectId the id of the project to upload the files for + * @param file the file to upload + * @param auth the authentication object of the requesting user + * @HttpMethod PUT + * @ApiDog apiDog documentation + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests + */ @PutMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity uploadExtraTestFiles( @@ -268,6 +299,16 @@ public ResponseEntity uploadExtraTestFiles( } } + /** + * Function to delete extra files for a test + * @param projectId the id of the project to delete the files for + * @param auth the authentication object of the requesting user + * @HttpMethod DELETE + * @ApiDog apiDog documentation + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests + */ @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteExtraTestFiles( @@ -303,6 +344,16 @@ public ResponseEntity deleteExtraTestFiles( } } + /** + * Function to get extra files for a test + * @param projectId the id of the project to get the files for + * @param auth the authentication object of the requesting user + * @HttpMethod GET + * @ApiDog apiDog documentation + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests + */ @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getExtraTestFiles( diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java index 7351275b..566c2407 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java @@ -56,6 +56,18 @@ public ResponseEntity getUserById(@PathVariable("userid") Long userid,Au return ResponseEntity.ok().body(res); } + /** + * Function to search users by email, name and surname + * + * @param email email of a user + * @param name name of a user + * @param surname surname of a user + * @HttpMethod GET + * @ApiPath /api/user + * @AllowedRoles admin + * @ApiDog apiDog documentation + * @return user object + */ @GetMapping(ApiRoutes.USERS_BASE_PATH) @Roles({UserRole.admin}) public ResponseEntity getUsersByNameOrSurname( @@ -91,7 +103,16 @@ public ResponseEntity getUsersByNameOrSurname( return ResponseEntity.ok().body(usersByName.stream().map(UserJson::new).toList()); } - + /** + * Function to get the logged in user + * + * @param auth authentication object + * @HttpMethod GET + * @ApiPath /api/user + * @AllowedRoles student + * @ApiDog apiDog documentation + * @return user object + */ @GetMapping(ApiRoutes.LOGGEDIN_USER_PATH) @Roles({UserRole.student, UserRole.teacher}) public ResponseEntity getLoggedInUser(Auth auth) { From 79e50778f77aa0036910628598f884458c294706 Mon Sep 17 00:00:00 2001 From: Matthias-VE Date: Mon, 20 May 2024 23:52:41 +0200 Subject: [PATCH 127/130] error handling --- backend/web-bff/App/fetch.js | 12 +++++++++--- backend/web-bff/App/routes/api.js | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index b32fc34d..5587ce1e 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -35,11 +35,17 @@ async function fetch(endpoint, accessToken, method, body, headers) { console.log(`${method} request made to ${BACKEND_API_ENDPOINT}/${endpoint} at: ` + new Date().toString()); try { - const response = await axios(config); - return await response.data; + const res = await axios(config) + return {code: res.status, data: res.data} } catch (error) { - throw new Error(error); + if (error.response) { + return {code: error.response.status, data: error.response.data} + } else { + throw Error(error); + } } + + } module.exports = fetch; diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js index 06b8c476..b15647ae 100644 --- a/backend/web-bff/App/routes/api.js +++ b/backend/web-bff/App/routes/api.js @@ -25,7 +25,7 @@ router.all('/*', try { const response = await fetch( "api" + req.url , req.session.accessToken, req.method, req.body, req.headers) - res.send(response) + res.status(response.code).send(response.data) } catch(error) { next(error); } From 8f81a8708d2ccb411e19acb0d6fb3f6c3e6700ef Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Tue, 21 May 2024 09:52:37 +0200 Subject: [PATCH 128/130] Added .env to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 795c9320..cdae00ec 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ docker.env startBackend.sh /.env +backend/web-bff/App/.env From 215182aaf92ce83c532d6b32d5be010b25d8afd0 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Tue, 21 May 2024 12:10:16 +0200 Subject: [PATCH 129/130] get(...) ipv getLast() --- .../model/submissionTesting/SubmissionTemplateModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java index 6b26100e..1e83f944 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java @@ -221,7 +221,7 @@ public static void tryTemplate(String template) throws IllegalArgumentException if(line.isEmpty()){ throw new IllegalArgumentException("Empty file name in template, remove blank lines"); } - if(newFolder && indentation > indentionAmounts.getLast()){ + if(newFolder && indentation > indentionAmounts.get(indentionAmounts.size() - 1)){ // since the indentation is larger than the previous, we are dealing with the first file in a new folder indentionAmounts.add(indentation); newFolder = false; From 96cad66ab44688df7ea2137d425b2f18ba9c5ab5 Mon Sep 17 00:00:00 2001 From: Arthur Werbrouck Date: Tue, 21 May 2024 15:39:17 +0200 Subject: [PATCH 130/130] better env build + cleanup --- .env-template | 21 ++++++++++------- dind-chart.png | Bin 453988 -> 0 bytes docker.env.template | 2 -- envBuilder.bat | 28 ----------------------- envBuilder.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ envBuilder.sh | 18 --------------- gha | 0 7 files changed, 67 insertions(+), 56 deletions(-) delete mode 100644 dind-chart.png delete mode 100644 docker.env.template delete mode 100644 envBuilder.bat create mode 100644 envBuilder.py delete mode 100644 envBuilder.sh delete mode 100644 gha diff --git a/.env-template b/.env-template index 6f466741..1b18f9ec 100644 --- a/.env-template +++ b/.env-template @@ -1,8 +1,13 @@ -backend/app/src/main/resources/application-secrets.properties,spring.datasource.username= -backend/app/src/main/resources/application-secrets.properties,spring.datasource.password= -backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.client-id= -backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.b2c.client-secret= -backend/app/src/main/resources/application-secrets.properties,azure.activedirectory.tenant-id= -docker.env,PGU= -docker.env,PGP= -docker.env,POSTGRES_USER=${PGU} +client-secret= +client-id= +tenant-id= +PGU= +PGP= +POSTGRES_USER=${PGU} +URI= +EXPRESS_SESSION_SECRET= +PORT= +ENVIRONMENT= +DB_HOST= +DB_PORT= +DB_NAME= \ No newline at end of file diff --git a/dind-chart.png b/dind-chart.png deleted file mode 100644 index 45000d2f0d4664b07b878f4692fd09cdbc7dc9bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453988 zcmeFZNzUxbk|vhWpdb`b8_@g#(jXJ;85{=5U=zVE8aBa+z`pN$?m$Zsv;l2HJCgZ- z&VBD@)>9&@N`h*T2XW#LI^CV_=GJ^>ZqEPBGUWgCAOENS_z(Z^5C2J3#MeLk!~gJq z!TO-8BD&6#qgHLEryVTm;z| zG}Aw|$qYqszv<#4Et2?G9Ree$f5fPNB=8ST{Dr*IB>JNN>B6%9ulhljw7)ylyR)e4 z;4g$k{}TpX{m&kb|Apk?msYgP4uAIx^S?6Gud;E4u5bdX{x5JxPNJ%VmIQ`l@WG)- zD#j%3pqcbj>(}FwImmtuP0K%hauXXx%`Zrz_O)?lj(QkUwzG8SIQYB*#v)7{#`KTLELS> z+W!>&d0BoD@(*&3T?hUBY1RIG`;SV^E+jRg_8)Zo{T$%9|MIqfrN;PIdH8SXu5>W* zAHQ_=Y0gX#-SNIl)IT!62(t$D@|)y;A($E_%BLL&A%RWv@tc*IVL=r5x$%$F4^top zCa4}b3qta^KYhPKzMjiNVU+_-7oPX(@T^@(ND#(tCBz62`@ASOo9fBWD6&C759s!HkC)$T^;aSLeZBpSV_DQ5jU@jQw=#b;41P;V z_Sdda)qe{oL7m@>_^%Hi>F|HSuYZ?Gertz6F^SkkRWkmg&^3KG{h{>!$|?T^i}^3s zF2Q3uVgKIRaSTfa%)fQ*{_Zq>c>Q0e$iKFr)IVR)KbGlF>&g64<OB0gm?DhAJrif#t_xUjaRUd>u zHUO#4zAM`KM>EmpJ-F%JC2T{?~H= z_s`|{5Apo79RIHIX9ghuyIX(a_xk^7p_3SJ(r-1~jdR{*T^rQ5zm@W1x3uvO6i18s zw>nza^}p?FnauO?+rFZ;g?fMDoMbO%4{BonM8geq_5Ixr{T7k&Lp}doAk&8~el0Z` z92CIC-!ZoT&kGF@q+7-xEC5aaZathsPx!Ckh%u>yxmf@Cu>PK&{-izdlM*_{T}NG4PRY%6ufC>#uiv|Mm74GM$y?%8*tSei>5R|TYQ1hxUeBrU1_+P;sA}+e zzBJ{WKBlk8@9L$mRpL16qP=K*n@kpc!_1U!N1K4f|zpw1*an1Y6lfw?tkqxy1`!A`Q(X z(Z`3vsU?Xx`>12TvQ?$cleP>V*|++pci})p#|`@C?Hn&#&ysT{dGr?f zDTmRV4%RA$^@V5fRDP@U0uX>$-bRbI*6?J)OQ9F$JkPQh4w2a;x7RCH_mqCT6?0no zV{rNv7Y6!mP?L9Ig1KsU0k89SKDEsBQuiu<9?%Xi)b{ea0uIPtFRZ&h;c=9%BfudR zl~tztQ4-Ps<1`Xq^34WIxSb(K4ZmWQ*0r%=KGJtpIgea#n1&bRklj#iwZ0sI+7;^B zn_FV{yJ5}TH!iZW9T%Te?i`s~^gP-k5L3yit8~2ziqn)^vNsQ1Yd!Y~UKoIG#Tap(4}VpkHr&2NSi1^shO<1k64t|Hz^jsrWMQQ!2(Rk**1Rz3mEU9X9l zA^B3{cLlYi%=dIeviWn$yxxEi@P;Mb9G$_x#=C*|o~|9=`!wWGE1NtB5^=p5;nOnQ zki-nEKYANoik(3_`Q#oOJY`dF+4p+8ZV#!VF#odHu4*ggBrGcQ@7tT6_qj?ir~~Gr ziDTuF)u<*(G83_X7`=HU)ie!rv6&LZ0i#r__MRPfmPj#`785umy8kBME%z}G7l%x@ z9(^c{04QzdHt?VOv2^cxETm?78Y&^aCLi~!FM!fbP3q^Z2tj$k25JD)F}MwI6aifXQl>Z=aQV+i_BQ6uZgh0d!yXDB zIi6UrHuO9)?F0qt+TkVJ|uQ(H<0k zJ*K1Ys&G(x^%h;a%v@XiRuBgH4vqTyoeNHlhjb&{@mLBX;8-+mNrDyIXM?{)^*AYa zedH+I*j{Tr*bF=UthT}sZEb^3a1E<2`7rmcG8WsWuQK*H3D@v9WwUFIhaHxg{m8eX zdrj3u`gi562Y3(ClS@1&loe2-AGl=jR^M)jcMb81G$AwIgj~%q zLKd&l*eoml5!9Rmg{g+~{(1Q>&|*5PhFjmEYz3jrtw%a;t^D3A2^>ACTGT4+?<;Gc zYy)tVWPP7SoUlP}I*pU&*d8)xQ2_v5p`1IsY# ziY<|y(27}*fD8(+F3#E9%UFXER8H|M48tIDlX4pH!C~Ea?M1RQGAbX|xYO@gOT0Ag zu-Zz;8E2LLr8h0yt>5Q}YJ+W?F7x^^XR9?rsU#VNTWGpDUVXgSMw_1g%8)+i&YvaJ z;TqZ!EaxSjn>0ANu;ywbB~_+A7py%da~Fn;2kvr+RbMwXcvnc%@<<>qr5UX7ccSg_ zOVCtT$OW3dgcZ>26b)3*-#|wv~$D3gv0NC+{v3AFL)GH7S zBop+(%x-HBz!WPn(5?20_uNC$z`2kPy1rcT0<_`*WMr2 zqrqRH_)q}O>syN8AevO~eRk*6Wco$0tOM^>)ujJrgY zN!t7DwWB-Gif8WHo9g4626AWerYc_%MRFE8K~Em{rWHaj!@(FDq7EwQNEeqa(%}oJt>3x@m(fWL#OeT;4X0byTBB9tU{P4F$rAr zb}CRJ?I@>+xpwu`8ud7oJOfLAjd)Q-T9XpR!+J=q*MCoK2(xTjJ^ob#L*4Y9q&jL# z-Eh9?O?jJ^o+9S=8Zz0TCFPQ0RSJlJc#PK`f#rjuc_4-<^1o~hUrO?a)i>CqYny7FpS?c?c z5IZ_~ebY_M7d8VyT4I-BO~ve{J)Yr5lq5xKZ<<*=Fq{3j)hS?UYS|x$TTD4E3GKB5 zyJ z%nH;8zbF{hyWXdlb@Gd8?2^4sa`0<(uQiy8Ftot6Uy<_?%F~iJ7MTF?o#|j=4RO1w z*X*YEhJKdcyWX3`ec1E_$;_yMLo$T;E~`mDFZK;HuDQq#uEUou{1s^SoUbQU3*QjRJ7Oe%{WD*+^BbvQJ-1C)mygEfqhfFi_`U8n%!K z1vO(fwV7Ve6|g4%Rc*gxFiQRL2iWZSo4VnG(O0`DO*!F5>-$2gV9SEM@Pg;Z2 z>)t4w!g}zvvTtAU;rimp4SaW5)?70uZfauQxChj*Xp%wRK1e(S>bB)b2sioO*6)v{ z58(1_!^mr}XRm1{qr04uce-rT6Txt4tBubOT9_}_58~s}zm0K9AF5Wo`clD7^VL~y z(c*Uu!7xhwE6O{#<3_37=}@GTr9PNa-aeRf)QnWL#M}tZ^O(jv3DQ|(i=lP_QX1rY+laQ zLQ~!wa9#MVnETB_AOy=4$$s7u&V)P|#kH}yt-?jRX@I*xyEZ7S+6y4_nYgs=^J9M} zlMA_dWGdqw>-83%A+XeYo(e0_@|9~J@vt*x_iixZCJ~oz>2j*Qr|naI7u!n9L-oM+ zEX@UkoAUF}U^*uMJQln-0+Ly}iEELL}w zOEyuSVg|QD^#exuTL^N5{2V3v?L9N~vH}!Z<~d_o!?vzaw_vpIVzeIoJP{SBp9 zSVK94762bjA-F3xF7p(7F4ChcA`ua8+S%ouK4pG?@1pNzgw8=MT9|W~| z@%=@wD6V=%_4)KgM&Fg$$8KQYE;6NyqRoB4Te_c8L{g;6q~5+p?YA(~P#H}f51!Zj zy%=9xrmmTAH7WDTgTI~Q*T+kJ$th2dShq}9{Bpjg*GA}bLw-kYd}0dDS5A1J+L7#2xeLh-e zd2D*1!+_cA)z!;`>25X6Y9S?srb|Dllmdcjh4CnzQ;@)MkC3|VO8NF<;~kp{VJQPz zwRW}s2B)RH!XN38g5i2-D#zqwwb^NC+SruKrk?!bh1)M7Wa_}?4T#tvAQ#rl2dzC) zBoERFF086+WFW+6$czj9?gj&=ijx()latrr8n4IWrWt1)MTgGKr&mrBkLJw(p7Xn{ z0-Lt(+nhOU9u~qvT$_DpzD_}(o_I=MV@P_FqYO$9UbpsMsP|XOIF-OM3Tp0AKPoS-!+%?2(Spae~b)_jM%C z(}qMOLNcEL*#M-5*qTh;epJ!pEVBD5*>~#$0HUU;);UwxX+K^dU5gR9+@%}u7!|7h zqc%Xc_Bbcq5_*$fwhrk?@#{RD#}}`s&uh5;AP)7nwyqg-Htr_JHwIRB^5^;$n}L30 zZb5oJ2Us@Pn#YCoSV1s+#dv<#HOkz{rN%7>-D7P2ua3$p>hueL3Qd?lmUu27aV=^V z;+b6Vib>I4r^34XoZ_mp^32IN_!OQ=(GZ$!g93m!t4aWJzKYdlJ`xeHjIbfETQE!l zg2%>NsbF+{pZ9kHu1WP-ikZCo6C!}7-)H6t`crZ;kN&(DrBoND-++MphM7J%Hh^!z zejJYJ)r-!#ynGq7xx@|A{LGbgrUMb9o~#nWG*fkx^8xo)@^g;ue!q-&tALP^q|9d4 zSUC22{)z)AzvBS8KEE;n0meQ&7~OKS4j(e>9i z`35_bAiIST=(mfYuaCHxACEzr%fU2$c_M~hk(p%U{?TiC z-nhuzeDX{A_)-d5w4E)(w>G~N>HI`J`3Oo`MUozGV0~bPA|dzNDg31TKz9D}#?^0W z3yj&uZ#*UmF07H^d(saKmP&eHQk9r}OoH0vi^=QfRAB+5q8PxfOcg`Jcb4tIbDrHs zA-?_v>Rk%^NQC#PeDLWB74Oie@xrml^x1(;_1W`Q%8ENSof9mO&1Ln?7BxI7_golg zF^gUhO59x|lLg9WUAWz;`g41UMM6uiag^f%=w7&H7zD||#%&jHx!BDYL=z)*;&OJK z+B^%EV{tJg&m&>hiPybFeus10)+&^-+|e1isUXxfuoCvt(UWNuh#7`LVU|NLZ2}SD za-_}(5sp>5&~&z>>wfO;^+QG1{Hp0xT6JDa;(4U8!&GjQQ+CpW2|e8l-(s$VjRfZj z{EzERM3I-)YW+5jApsfSWR1MYfZ`1Db6KPQ5=;wa6CD-TQ{wq|(^7V>Tm}~l_A}k6 zP9_~J!5sBb`#;{$XV^v3fN-k=A=&=*5eNEh6IkZd}|bPZ}wd@bS_1YTU=!ZrzAl*YWf=O>>X*sUe*+=0|f&Tc)~G?Q+mu zVjbrxBcK`u1HHb@!sFA@g1V=mZZTSfqdEhg9 z32uU7c63K9KZ@R3 z407!I>t#OAao=&M_D+&CX`GaCX1Jp96uz}vx^RYb?ZmL`V4p=0?&2ru8Bu!yVr=(% zO&`|xt6U2{8(QrxXFozv6kqX1*`6=GAGUM<#$aj}9GQZRWO*vA^DmmeatkZQ;jq00 zSNxeT_Z#zhyr?WnmInd;jJ&`7oUGi>>!_&_>jjF%J1d9O5qWLX7aIM=bN9RDuR| z`M{PPQvYDddsnpP`)3l?Grt&TySA2s)CUsu538p2GNsHi79&#GBmp4|U}cFx#tUiy z0VVRrt^;>X1WU^RQ>(&$laHq(I?RR4K7spI)+rW)<>tMih4B@M`uf^Ep64Cpv(O%V zX{ini>r2N~QSEc+NUc**Il{gBcx#UK5o%WsXSg9N9F@$1Q{VNh^zb}T9(1b1I>C>_Yq0iZlpi_|+YaZ6Q*CNV zRPQf~ zt?=QFN1fb`adzcif+UIKi{yc`t z>{A1_<+2OJJJk8j2@+#J{eb$sSL>R0!j*9Jd(P}-YgH1*BxTrLfxaV>OPjA%(@lCQ zufm`3os3<4ys~RXr7U87ln>dT=FynndEtDNtRoQXv$gdBW3QB4Rj8n#OXY2Ea11EK zxHXrOpHM=gDXJm(t}q-$abT$}ixcZlB*)C7OIQT3%cv3{s^nO|NLM z#TiuNBGQ@!U}pqITkNrV;o;h@t{Q(i5&~A_qj}@5*B~nZ&1T*EvDEkJ$(y*lJPxjX zXEor3FaD)un{-&_aU;R2P4dL*bvn<#&@vNl%$}O(x95t3f-x{_sKo&8L$14u9{+4x za`zqb6n3cnpBEa z{*2{sY%IP9dU-IB8s_cD6= zEFQ4SiI~6i`?s%X6`MJ&p9#>HsbO5~F?~Tg>ej#5&lYQV5r7+@Nf|_3D9M`X$9UH} zR$#s^D^28S(SGP1>Nq~U*5rjo)E;g4k!mzHP4^A)iL=vu$q!BIw$98`h%z!Aljhla zH{3i(3=1c&k4&;@)$y`UJ0@`DBCW2z!r^i|tR2xET;)aP(^r~R>*wO^{n;I&&(n%N z1q2v%WL9vUNUE1zLWGpfqdGQ^kmKLvCB<=7Ax9$jj4)=9a=$89_Q!b>-hJkD7ay_E z7%kV_FJ&9Sk^R#g6sLztWWSULch9+-q1sU$3Bf+RoRusgk70j4vsy9`4C|}lB1N() zLG*#);9wi{A*CV&J>8ocaA0;f&eihq&SJJ;`FLW6brm5y1xyH%AZg%hougb{G4ivK zoOe@S^|fNT8pDZ-3Y_evU*_K4Md5j)mNmok1C$yc2P={0q-+wemx3LWY2=!|UUS&7 zw%zR>URvF~HLYz?sp(x^r6Ptku(DtrS{2NZ3J_2>AUEV4H`xKUS&YAOW<`JV5OdE2jtI6+;=HTW76<8!kvzn(t0q;~;x z3c!pqclOVT3TpP5PMH;67I}R?u3yLl2+qhgSvh#*Evc6??mqO4348YpY`y(6Mz|@5v!Kh6>6qOsv6-XG7tJNJ2{Vo6`gq-3++P;F_8_K`kLY>|rfG{R+OSN1KDpT;0*X`XY#dL50bV%6e>mi2g{fWDLgRrw?&lz{C= z&x4pb5-w^F!*dVXA(kr$99+nCO`V`2v?SalPxg z8*2uEl>N9em)JVrX!Na)Xi*FwcBrIgeeQOxO5c;xjeR3({&G(F8LZ5Q1U)F#Nt&*V zqrp!`Yl++T$qO}L7VEz2Ul{ZFjr zG10NcnWIZM3fk0T}#o}JJ3aN38xiK2^j;ievpP#;OFe&UzDfCXY=Q+sR;=krjaGp&C4b`4re7c&8r2L57 z5t68sQ|ZM^Pk72bMN{+XUn|Sca4<=t^b?tV%8YJT6by0j3sc@Q@v6BHOHq{Ca zjl#~bhLuUUA}2Jy&4*b);@Li__l!&NtA=!vczsODtlWZUSA)Cp>b9`g!3CNb`?l#; zFtAG12vR~~ImSSAP$?|hi9B=U%#1MI=d;5_?>Wx3m5j8el~*FBaw3FHig#?hg@{{)t^7NPMnzVnJ3=vu)Y5R$zpYV zP1nTq#Y;wf<6~u3Bqg*~RO9XASlN;*e8{XjM7!Gy!TJa}bOWdvsULi+J+A$xU*~Ou zP$GZ5pLW~eRef3emob?EHjnam>yug_U)^;P-de?@{-vpul+wZ7aAkT14n*_H<2c`v z)wRtq5QBX`AFp%h5mjL{(FOK|YnEJW;{4RU>w;>9+^2`cdDRzS0Agq@AUrALPdu)$ z=in?z(?O2q$gSkaZcASbD*lB{zmO(7;6oh2-*{zf8FH)gi{ch(u%R7vO1Pu!56BdJ zTgBvD?8#D}+1VuN3Np3!@5&rS$iPI*3oOL5wbs%aX3#WuiT9I-AQ6hWVS;_T-i3&ySJ`=hJSioVE^uE<0gdtR zSzGXqnA%UX?7E-ohQ=}OXNIhn@f?B`4o>Wv0~Ie!#f4UkX5SGHq+ZZK=RwCY=0aQt zK%J1q6AmmBa`&WtTTgD#0papCZ46XeV{FJC5Kg6FL+)OD8D_j=^cWILD^S|);b`X* z`m70I;<71N3>@T!pUPvLHEi9pa7~IF3Q&8xHrflI=ju!c#flH+oJWV`mitdYoCCuFo-P z0=A*=e%({t=rB&VZtGxt9D-(cYNls{Aqa>vaAutV;mKm9pXlT)QmMx3BxFmoS<+kz z&;M4Oa<-f#pUKjr<9s|9y?V&zqm9w)x_QND$eE02G~bXjZM`jaXTR^Kc6BjGWX*(UFV}#rZpy$QLPI1mo$TxY zqzTpGdS96ZnfK#%R7hDy4l;gYm$bbvl zJmBF2dK8HwP8WazIpc{Mq}O>GS_H|0{|!Eq8ZvkYCP@Y-bI^hxIGm2(Tvg4Aw_Zdm z4XqTvE075;X>)>8WIEzBSs@~{h;KJ7x{R!GM5kPoqn6CDW#u|MDpZp z5WB4t30hjcvr{O30Uwrwq4OttE+}`1k3t!wotNacHpAAyK%a$Hns)c@8q47ac>AN08|~BFJ8MmlzZvWB}5LGW5bM zXxZRra%(xGM{s6vF!jgZq>vSS7KpM3lAD%Wtx&y@<4h`CYE3?J5utqis*eA|+d+Os zB)Rk}Ln96W`UOi`FutMHHe9Cz4w}(^fsLV}iiz`aWh@1z4-u8wBOy2L^RB~n!G5hK zjEhHO^LB4(b?AU6ezF&yKa?Tk^9?uS)&Wq75uC{ZbMTN=k(vYiV8AP3lYv8v5tFl` z%)!#rm*oQ2@Nr|iR0F zsUeFjgG@>y1WBQoclAwHdEAqP6o&!M6ZiAWL4YuDptT4nAi(pUp?TnwOXsCBaQ_`^04;L!uDcLAbEl|M$?ztT0FhlkWmz0P7fa@m8*a&Cd+~j=ICQF z@iUTveE5~L7P^NDXt^Kf`+0fsD=R!%*Jy|m-(LgbVHQ|a>gJyq50WZqRh)+3v{9YD z`*KYly5JjfO)FKua41sRBFs-%ZMu+JBbv_bnp~%!Bm-Cj??)CnPkhD%lP-aKNLx1$ zaK02+1al_Revk}OtYf~8BQp<&w2%M;Ya53RtnC)gIB_s@4m%)r@vQtHtj>5sOLlQn zGTN@Gjk_!&fY`c5xn9>bzZWma>M5=oB~p)nrG@x{Jo3twV|1j<+3!J` zetPM3|M2R`wT4L!D9t7{jXm1{^e2ACs?qBN4Qsc_7ITzH3`+21E}dkD?Xt>Bcvi=U z+;Nf_kkYtZAh#Kw(gw+W6E4nAwy}U%?|xn2zJ51CuNhlzl>C&OZk;Y1cpBID6rXDn zyngY%V%vV1K9G)d3tv5j`=-l7GLARN18@ev=5Pq(8;`=mS|k%nu`wy<9dZ78Xi=ZS zX^+mSU#rteOW-qAFS7l_fFCR}tQb6EER2V#9)?p;t{NLx{*f}pL8gI|DklwZEi&!h zQ`U|7$>qM@nPd->JK@hA7lG13>Nd4P7J^yqin$PHA|oc)?%EVxV|CcesC09%%Z zUTt6oq=-;R?0FFijcDS>^S4?MNEcfik z^=7tBlZAu*j1+A+pNomOskX9AuF*&d?5)6oj`R_8#z9ZT@x-=uBj*pdJnq-M9ul$P zK@|A$ypO~R>B5Y4O^7qH2m+aGO>&PH(g>GKGeI@;{D&C! z=scANPWE4dtn0%w@5TM@Bj8OjVD-}E;nRqpURow@`9_iVjK5NgYth{6iG=ZmX~;P5 z1vmTCA(AWPFO4fZ>98sBu0QeZ-YdCF8&~$;SJwExz6)>VJ7YJ?eP$`=dqSp#_MmWV zEsRt;;K`VD?T+!&z%D?GM4C<+-sjWThs;q74F`e8vV zc!-81q1#qRb}?7M4!+50l{<^D)5D*N^b>GoKk^!Iot37-@-cx~q-MiE-ZO&jFvg+t z%RJmhk>%2Q=a^?d){u?qa70ex9P@&>95mq2vj!ZY9*lMRN%0+8R5(gWo-@1KKxe73 znC$UPs{%jBUA;p6n+eHn_Eo)A)@9beQVt8opKM=zDEQGwFDgxwNY5oT)gQScI3>Ls za7>HTe7|{5p$N4I`E}x#Z_4vtjf+y0>@quWBadOK@yxH7i+9VAs?KZgUSaB&d?{SV zZxQ0g7hmhe1K=0%vtmYoK_D{Lb}0dE;&v5?G% z8XS9?HG7UfP<+QfeHEWiEtBd}wT`uqq8XhKVdBSS1BCE>I3%?U@M;~X{iar{yeyDq zuIcCU<;Ktqz9hM(NS80Y;&Gn@AH!%HvF&3(y7|!VOa-rX0PsM+fFUI|h!A(cNAJs4 zeu|5NjzFn`nWI{oUk1hp=^r=Q;XGDTS198~yy`ONSUA)eYO=Mp7r(zBHI9>&Cx2#< zzK~5CbB_Ld+@MdB)rlSfSH zVVn=Wr4N1VD4| z0)1V|iH632#fJ76r@B=A!r(0!1f3UR`i(?*O_s!E$N5`Iv3OxqXHUQ4 zu!n6fsTQIbVjG;%xFxAYcM8u*Rdn)x6d(tQ+HL-ZY>BaZc?Ac8ToTT?&?%f=q`<6R zzl8k?I}7JN-b~K619m~XQluR0Zt@1V3BUrsgYk&gaB?FYdXlQ@Ol|RS6WT;~;WJ}k z^`zY!WiqwT78qJ8XKeE2D7p-6Z70u-#Ua4GFF^%++_gEL>^Zf8MLNWE@!o5r4HRsv z-3=d(0W`q*QU!b{5lWe}93p)@H;@5o%@WE?$MhDCLq2w-VBPj&*jG3{?&f=blYbqM?WBA#0v)+YN?D3` zhj4vX8E&|kBiRMBl4?mCaFx_2W8eVb=2u03L5My>({+>5D&W^nMC`qt|z3XR9J>=>U=&x(^ zWY#*=%f{ZIHCoQ~O(BVVG3XnvXDcw<0Qi9?ar6Cp!LCfi`7f6aqY(q5@Bo8&HeS#7 zC7$7@{p4W>=0~=9QWg!1dYu;nuVvD+H;Ih>X442gh2Vn)F0fm2;G**zgeBlySl=gF z>f8nst8B@@YeE^|RgUfLQTlBa*SQlx+Vg|EC>l};q*LS-p8Di)#<)U#ug)EB>c)n| z{(fv2%VEtYwh+jSzWav{2Q?pZLV`0eh_4ougH*%Lq5Tq1IO|AGACI_ZAJ|fblmV-_ z_g?1s7#YnPzD|l^N!+=e8XOagt7YH0*Q^C641yOc0<0JQZ4K>b+l-qfWXPG)I?ePCBbo@ru&H6J@RFN0SH5UlOF(%}T!>6Do#OTGh?HD-RU7il9 zTx1^JtFqAM2Qt|^?Z?3|?-@!T=SO+940E)a)g2btfsJjMerx!EDxLD9ml`r;XM^wr zy^N%9o0fWWZrUn3?MV~i+Z)neNXx1GuqErJ&)9Twj%h(|V@OT$GzE{3#@-FaGW2dH z128L6gm(^UF(yQUv-2{ya@Ee#DsaJ4Ctpq#D%c@ide73q+-GHRhqE=;uYe-Ke!h|3 zQmn!tE3sh_Vf?jZ5Jf&ms~wCUeB%V^t5BIYMNzHfGCbY6De3}`^PLHxo`j7D;(AsL zRq@uWbAe937HEpiwarr!8VVtrWcgArt= z!O;>>myZj$GW;hNHCd?AR0qerk>Ob*H7;=D`CaBVd|pJB_rcAT&?)OqlpQ+@N2WG7 z7RpX<k;bWA&c_Wp2yVkNAiT1Q2tm0Q=nMWP>R7U<4eVnjr(B7fG{g3ddaaa_ z9m$CkVTTe!9t#=f%QgH8Wnzr$CpA|FP9&X=-k0tm<&q;Mr{}!z!-O2$uLGwQIa3zx zXoP*wK=a`|ZxTUB-j3Z;ar>NrBO?R9jN1mMT*DLyJ2aJL(b>vC?qryidMsAR!aiVw z8D1qYszX^C5l)A%bT1fy3m(BeA{$2s6Vc-GtxyJ$WO8?pG5ga^*rHl$fi zm!xD=I1Vqb-+IPWqN%m!AlSLNK_9PWe_?XGNw+odvUr|1-G~lm=u^o#MANTp_kJba z5y!sHxCOK#+q0@sfL?xu*(B<5pU>`k>yr0sQv?G3ElNhD%N2*+v7OzFQkZQBn@ zqf>Ht9W!xieebuI1no{FZi!a;C?F#e{{CbI3!i>)W(JNb_Lv_A(F3w3Z<|4kuma$W zrc1~VehOR)2aP&_06gx+Pqgb%W)E`d)sgqp^tTuk&C@G(Rfw01R3@C(aob+Qw z4R;>_CrYP3$Hjb5JwExS%0%HEQT)T?s6h!XuM}}|vE(=}BYYx_HbC#EM-4Na+eH|XDSI386 z-yTu8DKD&syoXl~=guKxG_<}m22fsbUG7}v3v1pdee5xXUw4K@b zpF8JX9OvA>oX56oRkg6iH_Jdt?j;m=mH8SA+ca{;7wUJ8eXl7$OWA?T$G?m ztmzn;2|g>d5t3~ZXrT)g95`J}BtM{N)C_pxO1R`1yo2G_DZgsoPNGLbu1o7e6Ob7U zzt77@>mz67Zg6s&jAhrZoiec`L}i&g=i7HW>ggo0&*w%EUJ}j|h~Z^4V5_oqKP-g&=O=k*7ad-cAmO5>+a?@$`M|7r z#LGY%DKJUj6$L@jjtIeTGt8enyb)u>?s^E<>Q~a97zYDf+HeKcBmD~4(Rc03B%)d$ zlg;9*dk^Sd#gM{mKK}X@6>{zCfDGdRyLPvq?Dc1_`aV>MOp4LB2xlGE35P{=h%t&q zql4E*Xz==%eW#ERhuQ5+B+q)8d!e*81X;Pz*u0dSFPC{-T#A&wH?nw<)6VRb(vLpcmf{03ZWnJ3l*Vkod$k7V z^cb+=%W7FpOBFMIhi8eomT`A^e-aW?F+|JPmU_6t*>U_#*xlv%2sn;c$xkmpIuRMS zA3Tdv2(n9&#yvWe-`PHx2+potlcqEa`6S;E9XS;tiY$(~j3ELkmIn@obe#zlQs)Vz zR<(^?rKHZP_0viNZM7l}c~*WTKlyKg-ti&}{iUlT*q=`Ev=*@DS_r{HFh?rBA$V&; zp!WHhk?w~ilNY@IgaBaBdEG$L5ZZuLYa;NCQevGLa-nB8z6g0&Top`Q^hysRuJp{| zjbzE1N-YDzm0(c$A_TRZ1`kTd!^G@Xn?cHWBp0g`VH*o+u5e0B#`?dRy0R@rfo1zk z%;=+th%(BIZy+Ejpv=P8pPkirIBQjRS63+_CJ~Vld+2JicUJ1%bG@O7j&B((DoOjY zwbqJ{>TwS9Gk-S{?!$^VQvCU{4p8L_OfB9W?dQ2U*E7$fRZ(P@t$S%2ZDP;;Z24<_ z4lerwG?huD(Fk+ZB6zR*7SdT^FKJdZj!Z#V`mU6pLJHb*a!pY7T2;%UpG;DZOe+A0 z9MW?4*D?HBzZH@+z<@b53C|*c(np`3mQNGR+3q0L{0)qjrl(FZ3FSrjyWx}37Vc{& z^kX-Y`dSGuiMg{q&*qp1`*U1-XlJR#(56-1=lZx!NA9~5uG4IKFZ;HxholQ{bRCHZ zFH&jsVr`*OQ9(wfK6q8)Rjz$#0rs%oIZ1Hh{+cupI3X*A5!sZ9mxt$%-&Z2Xbod02 zD*oDEevBhk`ts(FpYr59#O48{vzB}mds1~yxKA;YB2ln3e$l8*V<6z@`pOIY&tBY1 zWbx6j%h!0`?H^g(R{{7hJidSDn~DFI8Od=ZipbY}`+CH$J{ZVhrvXAc_;;twBx3qo zN;W>4mUDs96!*c~g6`hhcRMj$^7IV_AO4wH<52&=k@t&#b6@)o>?wXx?E3vz=!f9I zSdEA=pSX`{on?LeF41`_71nCk`iCQ>rBW|Fg<|*N05Fd*WqRxq$`Q6a7K`ia7aQF5 zlviE?c?tv(i3?k4cyNcEl|FGaNi?dBYMQ-=5&JpM-dU$^wuVjWPAtLR69e)XFH5nw zsvrhH=$!A{m^U(t>2}>5ts*aK_XDH4B!VmA>2|okLQ6Ko@pXO!MXx;tzsYw2wXvu8 zlSECzL7nTi`#FCSU4Q%JZT;=a}l{<93feAG!#{ZaN-v+x7##NgCth`LeCRoGdkY>dMIV?mB`3NAo+A{aV|@{FU#my>vgF|K4p=Z^vJK37RqY ziihFuNcO~&ZFC?CL2h}^=F#fditGOJ6N}qdK5bXwwm(0hO(Xl&`uA|D%7-I*K7VQi zZ`H}H$JLvyM(vZl&6@FbtiT{It6EGmz0iNx)XG`*R)zqmrlWorHo!x=ymINbPA0PI z&pA!6)80bYyx!PpvPQC^`{CtS!Qs{ii2KfFP4wYRDi>_8zsJDj&T?X~0l|!A819N4-opu5Vw1Kg`MOvk5<+|mh+)*ZUQPX{z?=lOmkrDy$-`le; z_$B>uxH)C?pDMjXVJy--vZqK?8m-%Y{HmyrR7 zFD=K(0Q~P$ln-wgcmlL~_*ZE1QQWPKSZHH#fI^80mHywvoNlTzlY>kc$}s>rbV{`jx=q8;VdVqU_bb>slI3}$fV`G!=F3*>-yKU2Z_5z5mc?-`{8Z$9-U7g5|^VDPfv{UVl{{R82ka7kl;sVU%iH*HDV zm5cmmyf1IorFlXzXAQjYLyF#D_^fe}GBzHM|CU1?T3Ze!xq)SN`yJ@_C`kI3m6NSX zCOU<^pChqRD6hRQ=Hjjo(l^Mm02 z+`$_V98$& z($zUBKC;`*y-XyV%$F^Ng5AlwDvL%6Ca+A{@HR=ja&DSW$hXI>(AM8>%~tJQYX`l( zU5BE_`-#sfsU_~|-|!#9c(@zjf#!}MZJU|Td1Sl zM7&vjS2|bymacg()PC!)?_KdSPlIf(&yT-~fbT+~N<0`Y0WT@N`5E64uwxmUJ!Vk@ z;y1+HS`F!@00$v2g!J<8tKULG6-i)`b#F0lP*C9dTr@GrhSKU6I(M&!8|ken(N48% z{1olinM=c{*pWc--CyObei?l5x@PO_{4oWCsz(j7tHCoJ!eP`QKKbOHle%3hB~PL-IvSk2BXW2sOgp;&l&5=4am*9v z0ewI&|6oYA{6gA$=gGY-$>u?Zy6lHUDy3)9er9W1*kQ^$sr|l)S}_+kk%ZSLgqGf) zHO*>ff8TCy+5X8HO{mop4?9?UGnC>@4(QA;D+Y)@oo+{`V2@Arq{^=K<0|PlUCv2G zMuHH6#Jwz6vODhBfu=Z+;Io`1PsblKcibP9A;TfdQZti8S*t^JVc~y$Of-URQ`+lyH---b&@cS=r6*K4-Lzq@7PjIlErjs`<4( zPuKJ%#`l<^m&A&m-Nkb0WfK_3=kd_$N3iG0=MYvaG^C(lM&5>P{+^%7oGfHqA!j^o zg13^{7tjwKz6>sqvI%nH0)7AiTO}X8gLaNzkE-kASy0X)vlrusxmzHgc(ypmfksig zu;~|A0s6~ezI}X|BDy5ft(ZB;pxzGubUkF}c-iOr;tnLL_{F$j#DnjG2W>R$4J8CK z-QQ;ptyYu^37taXcKi+9kgvmy?MPmWeQ}MTDR42W>LSAq@YLlKxBC!mu*CM@R`*w- z!KE!5(`e$IB=r6~LKPIh#w-2y#|o)RI;+i(`i{P47WH`l?nBT%Hs<}NzixAN^G5}d z-lrQNWZ6SvBy7BV zAV)&aQBp=U~+ zs~_bb&g>q&-vE>Y^)XH$p^}_71>VL=);+#Mm?NpR?)p%MtgjBIKFe}uGrLa(HEX0M zu=B6@JKm&RSRV$xu(Zl}N<_+g1V1Pg*%~5>5+3ZiCO0Hmgu@#gFbrLxnZYw==cEj8 z_Zv0qSc{4skR_cx`UqlCmSP@%a}=V6&#l!n@^09owHBk@?~>7H*31ekSHYy4Mr>(s zXOrp|P^W);kikL((&@WGsm%TTS$Mqng|ROqsLg`GV2@>Ha!n&=rR|t+YBQNMy;wr$ z@rHm{F1=`+%;c6fj}I{g$Hxo4)toSfU%hwXS4WnP4h2Z)3?BF3*`A|#sXQq+D7DpL zs9nm`>(fEgg&|tKOM#-=JG)VPU6qoK#wi1X<-WMw>zM>KgPYI{H2I1lYLr{xBc|Wd(b8II7(n0E#`+TAkOG zbKw=v%N1k)5kl5>y&Dy?o0Iq(o(~$uN9TQpYLSl2iRyuyJ?D+@1UF3;ahX8;1-W#z z>*%vgn9jzmUxegh7|_LjF1#<_KZ^5gk}NM2b;9Or?TZ&U5K?vlQx)rz|F*xE&#LE_ z^K1>pir+oL-X*R3*Np>@f4?i8Vav~IE6=5MzjqXxk;2sN?&;6=%9?oRDA&Jb-eJfN z02hwCU^e^y3ZTY8P5&E3%0n(4(lRlR7wF%tcn$!p-d#w{PGS4Bxt##h1X)NIIQb|5 z4IW=KCGVIEvbf@!EAFK5Nm4gF<3_m7-$|GaLOr`bL;yRm3w*I-_S zmwVA(ZU!MnByAY^r{p?apxd4)UTfz+d)W1zu-{seQyr-eLOFqefMHDb*nZ2f@#;rz z;v@zkaeiwhu>58)7R1PEHCf2JF|}6kf9-k4oUMm@v-y^v=(dmqMhKunRK3>Fm*|78 zKf32{hFb3J@N^9=F08gz5J7E}-FKsvSVL5mjfrhu3@=qJKtKP>ezwO!>M z?(>PFt?n%DJ+@%lq6`)8Gt$o_v(rDM775UMqbTTdIsGb!r$tf`Wh(P#RB$3uh=$A| z9_Rry2pL5f-P_s1d#Ah5vRnQm4|WIUWt@2xgJLh*kvJ6f>Aa7q<)bm|t?KvFL3{~_ zH*~Y3j{TP-d!FGb-O`w3l6>DJHwgCq@dx{jf7;*S3RKVTYK3TogtwC7D>G3&Yo3D} zsrf?Pq6(w}x^IsL?x6Vzp^n!ET0J>?Y4>t-E>xCbq0D`*-aiAdmScWB0L(!23ZW;n zx2#RL2qixiE3I99GYx7I{iUg(`-lXnquq$ETv+A}_bhadHr=yc+i~gx7K02DL#zp6 z|2-t^ykqO20{ck5PkCpMXUQrG;_U8B$!Izwhi*n@VJF4h-x&S?IGoyJV%hwe>1w>x zJXc~*8Wd}9v}pxl3`|r-^DZ=ec}(l)4du?!WUNPh7hCpl55NZ&4ir+TVY*}MUHj=! z)VUE+jF*BGVd?fRxp+>g91?M_8YBpRr)mm(1phehclZR4{FzN`ExUEoLo9+OOqs2t zUif5`(YofRl-%XlQ)9cc$lZaCYzi9h;Sm47Dk>h@%K*UJr&i>D?dj^Nmjk(PpB<^h zj^m@+H|MO+p);~d^MeA+)*$8od7apV$@a#`p@S_p&)&ap_Lw^KbwPorWPM0~l~%l9 zO`=j*GB+D${yV|)&7Ohm9#ez@#kq72Dd`v>S4lWIo&uYG{n&d9+ck^4= zsiY}no4hon;jpmZ#ZR5+i_VB@3%KjNNjcc$BZb6evEdXIfhw5tvdQT?Ca)3sC}p9) z)30lBLaJ0sh$Wc488LzOSm z&ezh!)lR!)za{K0B1>pqq2AnXE5GjAm6+ivM@-7^c0FE(x`9!qV+Zp(e52FIwfVvv zQw$9p;~s86+SaHTapWj~CoZ)j;?N7$-R?O_7-OW>6?#PnHSUXVj55B2q}9z zU*=W@C%VHak76^<5B3IbO4gH)j%zd_w~utHAD!z2phjldpW`EXyyCG{M-oNwHNRR4 zx=|Yh{A~8??gGNjHCkx@lc-Q-Yvfxmvk))rNbw(((+cJNwJPviR~x}SAMRRT(08-l zO9KgQ>5Tyc7R}&9MABVbkg}<(5c0PZo-&79tl=mU!)`+5lFi;;u8IAgBHCYQ9A;UP z>>9ma29k3cCa?Bi--5}zTxgKmBP8AdvEc*moZ7-_?+B>{WRILT>PE529Vde z7|HrHGdudpObcg?*~Kx3&>=U>3G#9pn)x$yij`UI$f*pGF{sAWQ4}Y4zZB_8dJo&2 zmJ~S`R=|EIr$KmY38cWr1%xDgQv)jkrH_rAJkM^O^SxK#pvcs& z7>VGT9mO46)7n$ z@$p1SJA~K#CWj}U60Q&Nm(HtP=M86s7ozu(}k#fu4^nY0TFp4 zyxJ`VUI6<77%_jCOIj9FPLquroQR%kR`ll)I%;E!(CKibyx-;rMZMtzKK zJPO^h{UQ7_8?(3VmT!R~p6=pF7+9c)(Ns?P=&{O8p3XyDu-AwgJ9dQC4d=7_+MkoI z4Ce)kBv6Vc`)5cqr9bhUx(hHSNEyoOtNzR>m<7W<^#$1wL$@f^m*B>-!Fh)hmXPV@ zilT16Js{GnBkgBO`wU;ni-+{P*K|&(0qpufl@ z%R~BUA@}SppudpM5cxh*es9x@>fZ>BzsLBUdjimvL{TmYrCqv9Y%-5uQk+jB?@*5# z^5;0F6c#Tx(u({`*)Lz;{7bcy&P~V{-@_6qs|u>2zgECe2FZ5c;`vqYr{K)gkD$&Y zr(K0}n)E-aTMC)!xYOH_PcO9^&Nykl21?d1iTRLpkB{Yif#LyloS`abK=OXuJ>2AS zy@&I222~Kih0i@vf6m<>_Fb6P@-BEM(!EF^rhUB8>?O|83KDYYiU7N}-&^F8V9HFQ z%x8u;sz{$_WO1>+#{u^KlDmMNV{1m5cGfQV6{1-rYM#w>B#d#1C2RBgc=Pw$_h3F-GD$FW zd0_(O$}$Y87zEzXUPss>N!MxF;Yu@VaV^rockyO+2-QBxAE~CwVyY}0w z_yejCV6ciwV|GnP}!c+7{8*Z>htj>$B7c#dC%wH0~QnH zInQwa8A|6SZqO%ambkB}A{lX^mAMqM3%hn5fg(bq;2ilqs-4T(`g~Rn+Y2Rwwaa!I zb+TyF0XuE{+X>M~db!dFfyZcW@4BAT#X%Gy`qn4C{8lfW(93JKALx75dzB^h@%6Bp zW?`wUP2wJ}75)_U^nSzHX24yGf-hHidX>#;93-#~pYX~2P(w-&BMG_V z{Yc6AcUNcu6Z04Wpxid%r1D)ReU(%i?7mW+R%UriDS=c^tI9?Lo$dD(UGOsh_*2H@ z#y`Wgu0)r+$wl`!gtIVIByH$FUq0*n7)lI(zy2+{*8n#*Dj~o$;jb9N{5H4gWEI3G zihihut~Sn3sJc)D)%q0uj5Fv8yMy&>-l=-bv#)G^C&qrb&ak@SRH3DvBq0DtnR2m1 z$US`a=j0uZRPqXvci`~*bGG_RU#0W!$eAyQor@yW6lGwwQsdYdk~e=knCbz(Qp}my z%H+Onkpur$2s#6Ke=sLfdHXgAtPi-wf!T5w@I59y^L*LSyrMBD+Qdg9=~s>i_C!3T zjF{hbIQ#kJk--}fo9B&pZYD%doa^0@K0(-Bj}G}9>HDj6C51kGgrD+;7{<26RFZ?Y zy_VBU4iD;^q{NdxKBs9qrZ-Iw^UI!Y&Zh%)Ss;4L`d?-w$8mT3r>|R0VVt#6wcNr~u;}02kN)FY)JH8?fiz@bWfPvSN>QUh#7f}5=aQafPx1jxluYZA*lFIaXUNqZUF^dzgV8~y`LK81 zH?9=aoBGlq=KSk!PqaI!QptW#-Bs%cV<}LlCX4YCC;7VV@}o9S;V*rY3M|$?IOE59 zjv3Ru$h&;|cIpvRsl)91n^V0y7ZPMUt86%BNPBBvrI@ORC;H%$P&Uj@{C!Cih_U4~ zfSlCx@Z5!l!$E?P1SAYR4D%Nly4=@%h~RdwC!s ziG=-H`GS`%HK*bcpxh(tHFeznB5QRvJzMz+0fVJ$2(|#fJy(ZHzslYIwwgiuH*kOC zf_*D>l5B8m!-^UY+YF6+8kkiGj~e3f;`K3Wr}M8rCcxAgvGFur)2d8LjD=4gJVP0x z+_@DC0GIt5>dJrL1)n8xvO+H~^4-Tm(t~KYeN01)<&IVx>&ZMcWSNyYZLZ~Z+fc|) z%h?y8`RtB8HXf2b>+k9~BDcG~O79wfs-MICQjFVt8mG33Q7c6}aOHa|^~~dI@GknQ zjXGkk&$1`ZleG@Y9G%`!+dS+}7!e2M<6;)RwxyWv-K<=8q}6?x(g-uNxgwDlMkz_5 zuM9XCkSgMXn!z^NCRNd@ejH;BUS<3{w5Q5>l}UB>&3KK1Q$uxd3H{>czym4m@i^_! zjDuxEys!=UkoI9Q>6FX6sX@xY)`>LR!x(Uo@o<0yJTZP(_TL7~273)0Aa>qdX}zA4 zdCV@Oa#qd#Bm=^Hb1(+^9qr(EG95)7Ngzt_o|XN0>T;X)8CeE z)J|*cQe|&>^%}DEb5<5uI$UpCTy@chu6>z0C)Or{v>wQ&cWMq-jDwD{fk=5AG{3{7 zzIeR;Ji)l3#6Ar6q1?wggjk7+@4gr&%o_9(_gj7 zIWbd{2z`>=n_U+C1e8P9zpUE`M9jOFq1Mm$DA)8R4EQh&^v`K^?(UoSU&3(w_)EoxoCB>o{uC&lc4^e- zx+d-;yd{yaIN1ko2BMue)#YPxDt8<@mXF6jWQ!%XSW$9!^8g;#{ZV zRN1@w{dtpEa6XPl!+*FJ8g;A@Yf-pU6i%Cq97)rLv-y)^G#yt*OuW>(&70$jWC2M5s25lr#$hP z-~hwk@a$~^dfDZVB2A+{WJwixqpI+kFH&=qs}qM`NOE=fEK>d<%G1)^Os9{S?L^6F zm!Om}BYavtvp5>bG1-)7yt_im_fnj0-_j4TcFn{2L*1mC@*5SP>3xHb$tgdyD(Xhv zi{3OybMO15=ds~*-{KcLNZhDWKKeKzL}k*yl$_)H<9+XfA-#1)4G1u$iP0>6gYH*ixH zFW#8@j^B&d>+-Hm4#ry*_CIaofpYowNsB=G9?NBadg_IHw3_V=g{J!^%ts@9>#-F_ zDBjJz;nr(UzNE@sMc`Z-%7TQ1WN0 zD*UW_!BS+}*ISx;@oM~t>zE|3(R_a%N$Yz1rHt7vfJhI016wE$v z)dSClw!q+OH}GyMH@ROGBRf^Pw?CuhuPiXJgc{W>LIf&O|9X5Gb-!)Z=$?w@b@9NN7lDhIwF{ zlR>Gcn@R=pU0*3PzqY9;(j$oc3gY$~?FG99O~1?WnH%T`5KTci6?5-qmCYi$g`NC+2&hF9rLh zbJq6MYGE1|Nlt8zg^E!$^=W=XMPIb%@boeTYKH5y`#?x&)^43@PSGF-OW@LU+Cbm; zgg7c`VfO7x?kf4tx*6~5CD;Q|E`{tc{e-q@ zSR~=}Ov}&j#xPH4TtYWGN4NjWA6stRvwgA8kDMZHtvi0+H;;YJzvc7C;ah=%*I#dE zk>4+V;?_48!PieR^kfDh`&v&=!6{4q;2c9vVEj!REZYh8251s&^Mao&4fY|(?Q$J5_%XL%wAsEtviF!EPFyHE+kw2!Qce-9dN*@_V-I5=D~rTo4E$uC zgI}g~g&32Ewt9rG+G@CJW!1OLc=!M;h1_z;;PfXJxf}jr#i2H%vH`u|Ebui4qTNI8 zSOh>MNggap@EP_?lL8n`X^u;A#S$*N!b%ivK}U8Xlbx2fT%W2O&#xV?A#b zLX-jq;IR`Y2Ntnq)JKlDm)hfTHop;GO=r|Y@=+|dXp{t-W$pd8;~ATCU%fQvGw;LD zjAGXQ4}tbJzW1{m0_OJ#UBC9nt9bRCw+(g&UWY%z9-t}8NVk*VMSk*GwF42UQ-*+o z!#ocz^~Jgh^e1kq^y{(IYhRr)1Qmo;o3S$~#Rr8)vu~=)Sq_!__xC3G4?we%SE$&c zQkPFb!+9g?^6fC!j!}d63c$)!S8BLu;K`L4FlLDcdF2>*>M6up)4r#dRK}Jh-(;2N zC`iiQbQ;N0o3h2MH0!HvgQs0b_^4h6Kru(mQ@SvZxNGTkib&$@#^a zW6L6)E?#0K^5aPxWp{o~O0UrN6lib5;Al*7jVsFiI4Fz*2Ev+IJ-=-zpCamW-&Imn zHD?dLwMcBQ^DM!f^!$#lUh2MX9v8E#J4rU-MCj*sFp6Eu-v*=c0ewpwcP+Uw9A-|FuW$2&{Jphk1xK>tb|R| z>MK-Irn^+@r8kfiySz%E1ngV%r_LFFT;&=|RQr33YvGN-%7v=XnQ_j*z_YzjTG%56$@$#}Q)*Drzg+|4r4Q&Q^k2Wt`*n&ywroz)lF-4+9S zR=fQ6)Ls%+?d`Z!r-fZAvL#vChqd$zP0KI+oz7e^PIJzZ^6IMX1rn0bwNl8Y{Podf zm@}U)6{lhOzyeGu?~mS8M!$pvZExn*qciJ*c_#2KP0vL|lcC;fGkUfsn2^kRIJiiGCXb;NpFewqRAC%nS9Tilx~W9zD_m zYM`$Mr;JZTdcO^GUu0+cULTIyK$hg;)xc00-<%$A&Vk2?PkwrS{rytbE>|StioA<$ zfIfywqs4SGl z>Tq!s_&*Ev2jp$C-BT%YPvT2bNq{DWVqQF{L$Ha<6%qnsJChUV~d-E|~!<#ss4Pf#QT7rvc zGVyuLlq1P8_hIP$8&L7rqp3kBt3SK9pt_XO>CA^PaEZ@RSzTH9p?}Zc{sAMa`ow1C zR&_AIe*IDhz?ShaNJqsb@8y`^AFg$IJ+EAO#`Td_o?NBHCGQn~$SD^^@x4s^fpmmm z7O7R_Gre2@L7l>8=WO+vnbK4mVBM@^Q+UsNBdR2Bx`pcKSw*k^R>Ilbg_LWk@ zBu=Sz>879(72N_Ln+V!+*Zle>*S=15vOb^D*hcSwNp1Y6wbwmYdH9Zpc~o_#MjtRX zJa~9sVZ4cN`YIw3|HV6zp_FM#iT4`bt{#MYkO`GD9X(R=eEq$Fhc50qmibdp|&lbQ7$kK;uFW&Y{mRt>by zjtqyeBu$|BX+V+J3J`7$Yo363GTf*S(VxH)$f!}L0#i7(kW;t<-T+b1_%{X0X?m|;Jmc*o+pY}d^`hs$ z*2AvasgsL)-+T4-h6CJaA{C~1Xav8l=4u_{pi2SxKiy1@pu1`cz?o+G4gkor=dWZ{ z^$bta5BMrLdXwjSahmw0*deA!BKK~BYe+!@OuL|6_ZIz}HGYli3bO-l?{S?%?*lSD zhI*&$oBEy$tkti(&Z^!l%H?}vq7^4`@3RGfXl;Gx`yr3eoT2-~Ptc(>ZpnAp$_{v$ z>QlgBdwAw3s;-OTkky8TEUC{QYaNAhW`UEJV6Qeo0$GD^^Aryz@R8RCHDBvC#X1UZ z-{U0^AI59gJs1vi>j*5^+fhs?>V9kZiGh>%l$`Isr`1AZ0Y?|$_(s1uuZ(HcE$ccN zKU3oY(y6=fI9ly*xjqoa!M%UDn4De;>(9Z_y$JZYBn@I(+A6`j6KAX3E-e1GpvT3- zA#X=IBs=`z@df)^M0F)_hXqAnPJNUzCNV>aLWwVc_W1+KfqoNDhuaCZ_m8X;*`IfD z1$&WkP-#ADX}DVzc)&da63!XD5-YWTos!Br@!~x`R8u}5r2K0-@?rnv;jriLl+JiV z`D94}^4_bgGo1Xl#-C^@Dze)c()}?=^x<_$uj+02tX*}zA3n*8f+N?oj3?)HNDp1| zl1|N|_H8%v6J%SJe7@af%2{UbzJ&Di!d;-BJGX8{O7|4|vBkJVzALT?v*Y@hpLZZ{U(QWm zW$A6SK9SNg%*d$6MW&lq#YrEE`5aUFNR4JLX9t09H5h` zcKC(E(Fpj8lb^D*GizN?>?)_?_evRCziPXJLu555Npu9|zQp2@Y9Z_y6o+bw$nJN{V1I@pA7Rl9lxb{sO3o?lB$l0*9<#VSMFOo4xSk9&g6qNq=Q z=`^yJdOkPVMsF8-(c^^+Qe^<;9iq7T3uKc|_b7h3p18LI^z`Ivou;K2)f%R`d&-UB z$9gSw4T|U^p$_BK{=O79TT)U8lqd64lh>DQ?t$H&-NzOqk|6(4^=0yY%N4U=riEoL zIcsQm(w1ufFyML*S1^NF(~iwYzbz3Lq&1vDB7sN*iD3(XEX4)&+f0Vy>jYC%B+YOvY%pAZg;xN6vsO} z&+S?!51x_DOj>^3Q!7$)U=A1&4g1BDDJ+mad(5 z$Guw1Q7E^JYR6gbtsHsBV*cXud0t^3DQ>H7|M|mG4i)Vgy9U1RLF7`lk7FNxKJm3-fJU?OB2}IS7l={scyDxch>CyKlLe}T+5l< zlR3DjpS&g;Lj3QZ-QXVK*=S9OH}T2-Ra?)o?Vd@}jjx21(SPC*H?mEjj6a#`*A9ir zPJL5mWRg@K3x6V1oHS#st5b&Zk_1e3nW=g+R@v_1~cD2#`zLvDQQpQc=Y9g z!%9hWJD}Pq_-ikbHa8TjV{=}1ZwNLp@1N1b7fVJwfLa47B=AI%7H%8q!GL^ylNXS% zw$Yj|i|lO_Y*g`(WO9aio(lOO2hgC>lScqmmV+%7-uJPnHYvDC3Hv<8Mg2F;f0BDw zPCSk7_&`->6^GxXJM(?FDyO@`&q+J_*3+qdNLs#C1z_;B^=#LPINlR5J5PX65Xzx422)DG{d#J-EU*=rXj z&+L6dRD8k(!i4_7WlktFuyE2}!|8^1n0F?`~@4Pq^uS-GJ8kg!k8kv3msj3qJ91xtrqr80f zdq}K6cyl*~yT_%+Eppp$WPP32{(L@NpMy9Y`S}LLxhLE&8`rQOBoM3OWX;R*U|56w zC{mDDo$`<`;_m5&TaTVCD=^t4uO$}Gz%P6Kx1HB&RBuz4c0Uw54>>)Ld-@eHvwfL# zncelP{7iLAe%Cj;ycNDNaoqmJ4K^w0x6`#f&gBne$tO(J7M8Dm<}JX`2zs%h%~A%8 z8u2*RtzR$uR0oW`*$%(~rwC`u8)IBdq&#zmcfE{o31HM-m>)-rjUm_#$-GA9hhn_F zyz~&C7CXAACiX)L^&a%Cs?D3J|6N*og$CD4Cb7Q))M|O^_fdLx>*?KIbv=l&1IDxV z;`3+>U2FECOsb_OUQwd0VPdO<@v2`OL`nG?nhdaT~)9y3EQM~NULfyzPq-I zb<#vRb^ctO<1Zxl{-PP&eWZ0fBZB{xh1-0@6G?mv+JQX&a^l$F;G7zF8v%WlgE$RV z8eq3l?^gw3i*4iVDG8SZkbHYeAD}Bis;&&KDAO2KU6aFNbH-9*;YezYzHa%6KvpPw zc{{h9I!1S&E(PwbQmCV;d(=9Y@R#KgEo%oEvie9S?jyh9PF2S1Nj_xOUP=`f7G7Cw zX#MYanrt>Ma_srPT)-9QKa-s3WX9HEy7G@=I&c(xOwnW{>$tqO+&R%Tbd%7Tkf4RD zL+DE2&d1A|oLOn7fpVTK_>~O>I$Iu294*o2#exK6hU~m1qK*lebd8q-Wc3|LM#KQv zKWW^`O6x8H{~|dE`s5*bH5eTl&*RuJZ*BIyLwcL8=|}&ZKY#Jjidtp*%}`|#exn+@ z(t=p4D0Yv}6Ebd1b~>Y!uEM==F0R% zoj#S1ZVm8&yZE>%Lk$T*2^BIr|HhmBX+Wh=p~D)>w|!Z=Ykf<7=kDRGNRobMm&&n3KB>db&zb`homs82mz%BBGFvusB)(66#wX67nBBv9 zcqPN!FVgvbvwiGE;^V*0^0^Tz#vIYg`*Y;smgNi* z>mK~}5G6&OyE{;_j~hd(D!CvTZ%>xi46RB584W+zA$Bf3edGUnY%s~)8V6!VN6+wc z>qI@XOMU-AGyP@F3hHr;({evbbR|C}kn9ag+x}kTNTav?eKv$ETx|=qI20uz7YAo< z_@mDXce}K7z82n57(7U_{`u~g8e2|0@_b_JDzE2#%8ynWGdm3DE>Kh+1vNkKLZ#DL z=bph#mp6W!_lFPmb5X z6*uK-2ElMTR$P4Nb8sZ|BhX&?VRJ!YyqCIKyts$fZ=BkN8R%o|Z~o~}C8 zBAN=&B-z~i?%3_1!DwY3HG;!N%qf|?tP-h6DfasF*D z)`8&CYpgyaK~Te)m|dy6D(T0{v%9-|OzN5}`8gF`9})seJs;19yN@4jzoQpL`Fq!P z+WocHQ!CElT1%I^eefRCf|7u!ZoPZz+1i7BH{?37U2oc?GCvB^<5HN^jbN$&fZVo< zn@yRcn(W9uu8rU-AKzw8g^H!~KZqHJ{&=eXYHH~ve!Ich`u88!H>neX6k6mkVCYmNj_1@xV4=(PPfW{OKUON1A z-nphrTRw8?MVewQY(u8U4c@V*OVeC_Q26c+S{^=|v-<${lbk%Xj&KV5Y=#MXkcZuG z_t%j|KcI2N^2{9j6Dw&CQfT)l1!aOiDr#~5oXhn64uN>66QeHVg_QRs6@dE7TEb!( zqH=H=vut`0R^>`C;6ioF?ai`Sb^C4N?{t!S2Np$2I2?A$&~lO&CjBavA;$4)dkJYA z%{ljd)m+IgST|iu>}zH_U(kNe;I@4zd%q+*f@3BnZF9eu*nKh(Y~D_m12&{I(98?g zHZS`QdX=R@Zt#Nx;R)e~gvKsMz z9g8MHqbXFyrDeaj3#kJjx4}t+E3Vn=GqI?#2u$9k!vkJz%Q(4Yoy*!hQ3< z-QUk)cwaekxT?HgE9l}DUQeegNNm~f)IHw2-v^|4Cyl3b9v|BGYcAGaF@@7xmbK_~ z{hYOCYfmIoM0@PFW4zJZ_;%s-(ukvqW89G z*Ve9Z;@cuqZVW#lpc+YW3u9+petgpGlYVX)=y$F;6V@)cm!|YmT6hSdOxij@`@xZ? zXhiG1I86v=Ex4k)Mm;+ksKrn!kZw7BuPs6 z`ktJs?jFOsbxeS|!wNItsBXk5gUNmZ=oksgTY}6Q0;BzW!Xxui@y0w9Nt#dPQ!IWV zHT0koT?=hYtJ5@+t056r6?&?Xt3*GJ|GkcF`-1Y$`=Xu3$y*-<)5y%E8Vg|Y=BYl& zn{&NK$-)qtc#GBBp1=D0x19tBlQyG|D(%Zme>Y2~o^KQ(Ze1B5#$AlOw>cFSY}{pE z%CEXcMFwq<=CwL`yan$^^PkxwF(N$Ie;=X#?3wU+9;^F!7aCdV69yA5=KsjvdwLWZ zJ~;(NdA^tXr`DZ4UfWnZ{;@}TuTx0WVT>twrOm#Jd#AOrc8ez?)&|t#PjPyFWTE*qb*EqSi8(XMd29qU_B?prbH|B$6EZ^3%vYp z!tZ%>B4&-0)Ge~pw_Bn%#K-~fP}R<8=Sk!OOL6=lFB--?i*{Mn7U|P!!is%3QpM_g zRwSzzYL8zcbl5%+q8GBGZ`7Gmo$N?39{KS%9-5|uV8mN3iDS>*-&P>I*F9`P#r!he zmel~Nmi?&0yM|IAEgX5Uw-mPfzOd^(m$tpa_?+TXQS≈x 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

    `_YFxkK;T!_`8s}vpOMnAoB|UqsTLm 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­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 diff --git a/docker.env.template b/docker.env.template deleted file mode 100644 index b81bbf3e..00000000 --- a/docker.env.template +++ /dev/null @@ -1,2 +0,0 @@ -PGU= -PGP= diff --git a/envBuilder.bat b/envBuilder.bat deleted file mode 100644 index fa93db80..00000000 --- a/envBuilder.bat +++ /dev/null @@ -1,28 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -set "prevFile=" - -for /F "tokens=1,2 delims=," %%a in (.env) do ( - echo Processing line: %%a,%%b - for /F "tokens=1,2 delims==" %%c in ("%%b") do ( - echo File: %%a - echo Variable: %%c - echo Value: %%d - - if not "%%a"=="!prevFile!" ( - if exist %%a ( - del %%a - echo Deleted file: %%a - ) - type nul > %%a - echo Created file: %%a - ) - - echo. >> %%a - echo %%c=%%d >> %%a - echo Added variable to file - - set "prevFile=%%a" - ) -) diff --git a/envBuilder.py b/envBuilder.py new file mode 100644 index 00000000..9b05ba87 --- /dev/null +++ b/envBuilder.py @@ -0,0 +1,54 @@ + + +class envBuilder(): + def __init__(self): + self.env = {} + self.javaEnv = {'client-secret': ['azure.activedirectory.b2c.client-secret'],'client-id':['azure.activedirectory.client-id'],'tenant-id':['azure.activedirectory.tenant-id'],'PGP':['spring.datasource.password'],'PGU':['spring.datasource.username']} + self.expressEnv = {'URI':['REDIRECT_URI','FRONTEND_URI','BACKEND_API_ENDPOINT'], + 'client-id':['CLIENT_ID'],'client-secret':['CLIENT_SECRET'], + 'tenant-id':['TENANT_ID'],'PGP':['DB_PASSWORD'],'PGU':['DB_USER'],'DB_HOST':['DB_HOST'], + 'DB_PORT':['DB_PORT'],'DB_NAME':['DB_NAME'],'EXPRESS_SESSION_SECRET':['EXPRESS_SESSION_SECRET']} + self.javaEnvLocation = 'backend/app/src/main/resources/application-secrets.properties' + self.expressEnvLocation = 'backend/web-bff/App/.env' + def readEnv(self): + with open('.env', 'r') as file: + for line in file: + [key, value] = line.split('=') + self.env[key] = value + + def javaBuilder(self): + with open(self.javaEnvLocation, 'a+') as file: + for key in self.javaEnv: + if key in self.env: + value = self.env[key] + if value == '': + print(f'{key} is empty') + else: + for envName in self.javaEnv[key]: + file.seek(0) + if sum(line.count(f'{envName}') for line in file) == 0: + file.write(f'{envName}={value}\n') + else : + print(f'{key} not found in .env file') + + def expressBuilder(self): + with open(self.expressEnvLocation, 'a+') as file: + for key in self.expressEnv: + if key in self.env: + value = self.env[key] + if value == '': + print(f'{key} is empty') + else: + for envName in self.expressEnv[key]: + file.seek(0) + if sum(line.count(f'{envName}') for line in file) == 0: + file.write(f'{envName}={value}\n') + else : + print(f'{key} not found in .env file') + + +if __name__ == '__main__': + env = envBuilder() + env.readEnv() + env.javaBuilder() + env.expressBuilder() diff --git a/envBuilder.sh b/envBuilder.sh deleted file mode 100644 index 99413a31..00000000 --- a/envBuilder.sh +++ /dev/null @@ -1,18 +0,0 @@ -ENV_FILE=".env" - -while IFS= read -r line -do - echo "Processing line: $line" - IFS=',' read -r full_addr var <<< "$line" - IFS='=' read -r file env <<< "$full_addr" - echo "File: $file" - echo "Variable: $var" - echo "Value: $env" - touch "$file" - if ! grep -q "${var}=" "$file"; then - echo "Variable not set, appending to file..." - echo "${var}=${env}" >> "$file" - else - echo "Variable already set in file." - fi -done < "$ENV_FILE" \ No newline at end of file diff --git a/gha b/gha deleted file mode 100644 index e69de29b..00000000

    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