From 0c7fc6fdf7072ffe61655a3552e8b7233cc4fdbb Mon Sep 17 00:00:00 2001 From: Cyril Rouillon Date: Mon, 24 Jun 2024 18:37:04 +0200 Subject: [PATCH] user managment --- .gitignore | 1 + backend/config.js | 23 + backend/controllers/lot.js | 65 +- backend/controllers/sale.js | 170 ++- backend/controllers/user.js | 199 +++ backend/index.js | 48 +- backend/middleware/authMiddleware.js | 33 + backend/package-lock.json | 1141 ++++++++++++++++- backend/package.json | 11 +- backend/routes/auth.js | 113 ++ backend/routes/favorite.js | 6 +- backend/routes/lot.js | 21 +- backend/routes/sale.js | 5 +- backend/routes/user.js | 15 + backend/services/db.js | 5 +- backend/services/userDb.js | 91 ++ .../app/core/services/api/api.auth.service.ts | 19 + .../core/services/api/api.favorite.service.ts | 26 + .../app/core/services/api/api.lot.service.ts | 49 + .../api.sale.service.ts} | 50 +- .../app/core/services/api/api.user.service.ts | 43 + client/src/app/core/services/auth.service.ts | 79 +- .../app/core/services/model/lot.interface.ts | 15 +- .../app/core/services/model/sale.interface.ts | 5 + .../app/core/services/model/user.interface.ts | 10 + .../features/auth/login/login.component.ts | 2 +- .../favorites-page.component.ts | 6 +- .../new-favorite-page.component.ts | 16 +- .../pictures-page/pictures-page.component.ts | 7 +- .../loading-sale-dialog.component.ts | 8 +- .../lot-detail-dialog.component.css | 4 + .../lot-detail-dialog.component.html | 71 + .../lot-detail-dialog.component.ts | 76 ++ .../sale-detail-page.component.css | 9 + .../sale-detail-page.component.html | 55 +- .../sale-detail-page.component.ts | 87 +- .../sales-page/sales-page.component.html | 27 +- .../sales/sales-page/sales-page.component.ts | 30 +- client/src/app/features/sales/sales.module.ts | 4 +- .../user-edit-page.component.css | 9 + .../user-edit-page.component.html | 60 + .../user-edit-page.component.ts | 123 ++ .../users/user-list/user-list.component.html | 61 +- .../users/user-list/user-list.component.ts | 36 +- .../features/users/users-routing.module.ts | 2 + client/src/app/features/users/users.module.ts | 7 +- .../app/shared/layout/layout.component.html | 17 +- client/src/environments/environment.prod.ts | 3 +- client/src/environments/environment.ts | 3 +- docker-compose-dev.yml | 1 + 50 files changed, 2748 insertions(+), 219 deletions(-) create mode 100644 backend/config.js create mode 100644 backend/controllers/user.js create mode 100644 backend/middleware/authMiddleware.js create mode 100644 backend/routes/auth.js create mode 100644 backend/routes/user.js create mode 100644 backend/services/userDb.js create mode 100644 client/src/app/core/services/api/api.auth.service.ts create mode 100644 client/src/app/core/services/api/api.favorite.service.ts create mode 100644 client/src/app/core/services/api/api.lot.service.ts rename client/src/app/core/services/{api.service.ts => api/api.sale.service.ts} (52%) create mode 100644 client/src/app/core/services/api/api.user.service.ts create mode 100644 client/src/app/core/services/model/user.interface.ts create mode 100644 client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.css create mode 100644 client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.html create mode 100644 client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.ts create mode 100644 client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.css create mode 100644 client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.html create mode 100644 client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.ts diff --git a/.gitignore b/.gitignore index 5e8edc8..b39834b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ backend/node_modules +backend/.Keys.js client/.angular client/node_modules diff --git a/backend/config.js b/backend/config.js new file mode 100644 index 0000000..561d12a --- /dev/null +++ b/backend/config.js @@ -0,0 +1,23 @@ +module.exports = { + db: { + connectionString: "mongodb://db:27017", + dbName: "jucundus", + }, + session: { + sessionCollection: "Session", + sessionConfig: { + name: "jucundus.sid", + cookie: { + maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days + httpOnly: true, // only accessible by the server + secure: true, + }, + resave: false, // don't save session if unmodified + saveUninitialized: true, + } + }, + jwtOptions: { + issuer: "jucundus.com", + audience: "yoursite", + } +}; \ No newline at end of file diff --git a/backend/controllers/lot.js b/backend/controllers/lot.js index 4193991..7f1e659 100644 --- a/backend/controllers/lot.js +++ b/backend/controllers/lot.js @@ -38,7 +38,6 @@ exports.getPictures = asyncHandler(async (req, res, next) => { }); }); - exports.getLotsBySale = asyncHandler(async (req, res, next) => { let id = req.params.id @@ -165,3 +164,67 @@ exports.AuctionedItem = asyncHandler(async (req, res, next) => { } }); +// DB +exports.get = asyncHandler(async (req, res, next) => { + + try{ + const id = req.params.id; + let result = await lotDb.get(id); + res.status(200).send(result); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.post = asyncHandler(async (req, res, next) => { + + try{ + // check if double + let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform); + if(Sale){ + return res.status(500).send("Lot already exists"); + } + + let createLot = await lotDb.post(req.body); + + res.status(204).send(); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.put = asyncHandler(async (req, res, next) => { + + try{ + const id = req.params.id; + let updatedDocument = { ...req.body }; + delete updatedDocument._id; + console.log(updatedDocument); + let result = await lotDb.put(id, updatedDocument); + console.log(result); + res.status(200).send(result); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.delete = asyncHandler(async (req, res, next) => { + try{ + const id = req.params.id; + + // Remove the lot + await lotDb.remove(id); + + res.status(200).send({"message": "Lots deleted"}); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); \ No newline at end of file diff --git a/backend/controllers/sale.js b/backend/controllers/sale.js index 4e3a135..2a6fa28 100644 --- a/backend/controllers/sale.js +++ b/backend/controllers/sale.js @@ -8,7 +8,7 @@ const lotDb = new LotDb(); const agenda = require('../services/agenda'); const {Agent} = require('../services/agent'); const agent = new Agent(); - +const ExcelJS = require('exceljs'); exports.getSaleInfos = asyncHandler(async (req, res, next) => { let url = req.params.url @@ -218,31 +218,102 @@ exports.postProcessing = asyncHandler(async (req, res, next) => { } Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform); + + TimestampInSecond = (timestamp) => { + const stringTimestamp = String(timestamp); + if (stringTimestamp.length === 13) { + return timestamp / 1000; + } else if (stringTimestamp.length === 10) { + return timestamp; + } else { + return 0; + } + } + + // Create an array to hold the updated lots + let updatedLots = []; + let bidsDuration = 0; + + // process each lot + for (let lot of Lots) { + let highestBid, duration, percentageAboveUnderLow, percentageAboveUnderHigh = 0; + + // if bid + let nbrBids = 0; + if (Array.isArray(lot.Bids)) { + + nbrBids = lot.Bids.length; + + highestBid = lot.Bids.reduce((prev, current) => (prev.amount > current.amount) ? prev : current).amount; + let startTime = TimestampInSecond(lot.Bids[0].timestamp); + let endTime = TimestampInSecond(lot.Bids[lot.Bids.length-1].timestamp); + duration = endTime - startTime; + + // total time of bids + bidsDuration += duration; + + duration = duration.toFixed(0); + } + + // if auctioned + percentageAboveUnderLow = 0; + percentageAboveUnderHigh = 0; + if (lot.auctioned) { + + if(lot.EstimateLow){ + percentageAboveUnderLow = ((lot.auctioned.amount - lot.EstimateLow) / lot.EstimateLow) * 100; + } + + if(lot.EstimateHigh){ + percentageAboveUnderHigh = ((lot.auctioned.amount - lot.EstimateHigh) / lot.EstimateHigh) * 100; + } + } + + let lotPostProcessing = { + nbrBids: nbrBids, + highestBid: highestBid, + duration: duration, + percentageAboveUnderLow: percentageAboveUnderLow.toFixed(0), + percentageAboveUnderHigh: percentageAboveUnderHigh.toFixed(0) + } + lot.postProcessing = lotPostProcessing; + await lotDb.put(lot._id, lot); + + // Add the updated lot to the array + updatedLots.push(lot); + } + + // refresh with postprocess datas + Lots = updatedLots; + let startTime = 0; if (Array.isArray(Lots[0].Bids)) { - startTime = Lots[0].Bids[0].timestamp; + startTime = TimestampInSecond(Lots[0].Bids[0].timestamp); }else{ - startTime = Lots[0].timestamp; + startTime = TimestampInSecond(Lots[0].timestamp); } let LastBid = [...Lots].reverse().find(lot => lot.auctioned !== undefined); let endTime = 0; if (Array.isArray(LastBid.Bids)) { - endTime = LastBid.Bids[LastBid.Bids.length-1].timestamp; + endTime = TimestampInSecond(LastBid.Bids[LastBid.Bids.length-1].timestamp); }else{ - endTime = LastBid.timestamp; + endTime = TimestampInSecond(LastBid.timestamp); } console.log("Start Time: "+startTime); console.log("End Time: "+endTime); - let duration = endTime-startTime; + let duration = (endTime-startTime).toFixed(0); let totalAmount = 0; + let unsoldLots = 0; for (let lot of Lots) { if (lot.auctioned) { totalAmount += lot.auctioned.amount; + } else { + unsoldLots++; } } @@ -262,10 +333,15 @@ exports.postProcessing = asyncHandler(async (req, res, next) => { let postProcessing = { nbrLots: Lots.length, duration: duration, + bidsDuration: bidsDuration.toFixed(0), durationPerLots: (duration/Lots.length).toFixed(0), totalAmount: totalAmount, averageAmount: (totalAmount/Lots.length).toFixed(2), medianAmount: calculateMedian(amounts).toFixed(2), + minAmount: Math.min(...amounts).toFixed(2), + maxAmount: Math.max(...amounts).toFixed(2), + unsoldLots: unsoldLots, + unsoldPercentage: ((unsoldLots/Lots.length)*100).toFixed(2) } console.log(postProcessing); @@ -279,4 +355,86 @@ exports.postProcessing = asyncHandler(async (req, res, next) => { return res.status(500).send(err); } +}); + +exports.SaleStatXsl = asyncHandler(async (req, res, next) => { + try{ + const id = req.params.id; + + Sale = await saleDb.get(id); + if(!Sale){ + console.error("Sale not found"); + return res.status(404).send("Sale not found"); + } + + Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform); + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sale Stats'); + + worksheet.columns = [ + { header: 'Lot #', key: 'lotNumber', width: 10 }, + { header: 'Description', key: 'description', width: 100 }, + { header: 'Auctioned Amount', key: 'auctionedAmount', width: 20 }, + { header: 'Estimate Low', key: 'estimateLow', width: 20 }, + { header: 'Estimate High', key: 'estimateHigh', width: 20 }, + { header: 'Bids', key: 'nbrBids', width: 10 }, + { header: 'Highest Bid', key: 'highestBid', width: 20 }, + { header: 'Duration (in s)', key: 'duration', width: 10 }, + { header: '% Above Low', key: 'percentageAboveUnderLow', width: 20 }, + { header: '% Above High', key: 'percentageAboveUnderHigh', width: 20 } + ]; + + let row = 2; + for (let lot of Lots) { + let Row = worksheet.addRow({ + lotNumber: lot.lotNumber, + description: lot.description, + auctionedAmount: lot.auctioned?.amount, + estimateLow: lot.EstimateLow, + estimateHigh: lot.EstimateHigh, + nbrBids: lot.postProcessing?.nbrBids, + highestBid: lot.postProcessing?.highestBid, + duration: lot.postProcessing?.duration, + percentageAboveUnderLow: lot.postProcessing?.percentageAboveUnderLow/100, + percentageAboveUnderHigh: lot.postProcessing?.percentageAboveUnderHigh/100 + }); + + Row.getCell('C').numFmt = '€0.00'; + Row.getCell('D').numFmt = '€0.00'; + Row.getCell('E').numFmt = '€0.00'; + Row.getCell('B').numFmt = '€0.00'; + Row.getCell('G').numFmt = '€0.00'; + + Row.getCell('I').numFmt = '0%'; + Row.getCell('J').numFmt = '0%'; + + row++; + } + + worksheet.addRow({ + lotNumber: 'Total', + auctionedAmount: Sale.postProcessing?.totalAmount, + estimateLow: '', + estimateHigh: '', + nbrBids: '', + highestBid: '', + duration: Sale.postProcessing?.duration, + percentageAboveUnderLow: '', + percentageAboveUnderHigh: '' + }); + + // send the Xls File + res.setHeader( + "Content-Disposition", + "attachment; filename=" + "SaleStats.xlsx" + ); + + await workbook.xlsx.write(res); + return res.status(200).end(); + + }catch(err){ + console.log(err); + return res.status(500).send + } }); \ No newline at end of file diff --git a/backend/controllers/user.js b/backend/controllers/user.js new file mode 100644 index 0000000..6d63242 --- /dev/null +++ b/backend/controllers/user.js @@ -0,0 +1,199 @@ +const asyncHandler = require("express-async-handler"); +const moment = require('moment-timezone'); +const { ObjectId } = require('mongodb'); +const { UserDb } = require("../services/userDb"); +const crypto = require('crypto'); + +function ClearUserData(user){ + delete user.salt; + delete user.hashed_password; + delete user.salt; + delete user.isAgent; + return user; +} + +function ClearUserDataForAdmin(user){ + delete user.salt; + delete user.hashed_password; + delete user.salt; + return user; +} + +// DB +exports.get = asyncHandler(async (req, res, next) => { + + try{ + const userDb = await UserDb.init(); + const id = req.params.id; + let result = await userDb.get(id); + if (req.user.isAdmin){ + res.status(200).send(ClearUserDataForAdmin(result)); + }else{ + res.status(200).send(ClearUserData(result)); + } + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.post = asyncHandler(async (req, res, next) => { + + try{ + const userDb = await UserDb.init(); + // check if double + let User = await userDb.getByEmail(req.body.email); + if(User){ + return res.status(500).send("User already exists"); + } + + // check password + if(!req.body.password){ + return res.status(500).send("Password not set"); + } + if(req.body.password != req.body.confirmPassword){ + return res.status(500).send("Passwords do not match"); + } + if(req.body.password.length < 8){ + return res.status(500).send("Password too short"); + } + + if(req.body.isAdmin){ + if(req.user){ + if(!req.user.isAdmin){ + return res.status(500).send("You are not allowed to create an admin user"); + } + }else{ + req.body.isAdmin = false + } + } + + if(req.body.isAgent){ + if(req.user){ + if(!req.user.isAgent){ + return res.status(500).send("You are not allowed to create an agent user"); + } + }else{ + req.body.isAgent = false + } + } + + let salt = crypto.randomBytes(16).toString('hex'); + let user = { + username: req.body.username, + hashed_password: crypto.pbkdf2Sync(req.body.password, salt, 310000, 32, 'sha256').toString('hex'), + salt: salt, + email: req.body.email, + isAdmin: req.body.isAdmin, + isAgent: req.body.isAgent, + } + + let createData = await userDb.post(user); + + res.status(204).send(); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.put = asyncHandler(async (req, res, next) => { + + try{ + const userDb = await UserDb.init(); + const id = req.params.id; + + const User = await userDb.get(id); + if(!User){ + return res.status(500).send("User not found"); + } + + // check password + let hashed_password = ""; + let salt = ""; + if(req.body.password){ + if(req.body.password != req.body.confirmPassword){ + return res.status(500).send("Passwords do not match"); + } + if(req.body.password.length < 8){ + return res.status(500).send("Password too short"); + } + salt = crypto.randomBytes(16).toString('hex'); + hashed_password = crypto.pbkdf2Sync(req.body.password, salt, 310000, 32, 'sha256').toString('hex'); + }else{ + salt = User.salt; + hashed_password = User.hashed_password; + } + + if(req.body.isAdmin){ + if(!req.user.isAdmin){ + return res.status(500).send("You are not allowed to create an admin user"); + } + } + if(req.body.isAgent){ + if(!req.user.isAdmin){ + return res.status(500).send("You are not allowed to create an agent user"); + } + } + + let user = { + username: req.body.username, + hashed_password: hashed_password, + salt: salt, + email: req.body.email, + isAdmin: req.body.isAdmin, + isAgent: req.body.isAgent, + } + + let result = await userDb.put(id, user); + console.log(result); + res.status(200).send(result); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.delete = asyncHandler(async (req, res, next) => { + try{ + const userDb = await UserDb.init(); + const id = req.params.id; + + // Remove the sale + await userDb.remove(id); + + res.status(200).send({"message": "User deleted"}); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +// Functions +exports.current = asyncHandler(async (req, res, next) => { + try{ + const user = ClearUserData(req.user); + res.status(200).send(user); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); + +exports.getAllUsers = asyncHandler(async (req, res, next) => { + try{ + const userDb = await UserDb.init(); + let result = await userDb.getAll(); + result = result.map(user => ClearUserDataForAdmin(user)); + res.status(200).send(result); + }catch(err){ + console.log(err); + return res.status(500).send(err); + } + +}); \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index eb82e7e..1a08b17 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,3 +1,4 @@ +const config = require("./config.js"); const express = require('express') const app = express() @@ -9,17 +10,60 @@ app.use(cors()); // Enable preflight requests for all routes app.options('*', cors()); + +// Session support +const session = require('express-session'); +app.use(session({ + secret: 'jucundus.ses', + resave: false, + saveUninitialized: true, + cookie: { secure: true } +})) +// const MongoStore = require('connect-mongo'); + +// const keys = require("./.Keys.js"); + +// const sassionConfig = { +// ...config.session.sessionConfig, +// secret: keys.session, +// store: MongoStore.create({ +// mongoUrl: `${config.db.connectionString}/${config.db.dbName}`, +// collection: config.session.sessionCollection, +// stringify: false, +// autoReconnect: true, +// autoRemove: 'native' +// })}; + +// app.use(session(sassionConfig)); + +// Authentication +const passport = require('passport'); +app.use(passport.initialize()); +app.use(passport.session()); + +app.use('/', require('./routes/auth')); + + // Agenda Scheduller const agenda = require('./services/agenda'); (async function() { + + //lunch sheduller await agenda.start(); - })(); - + + //create first user + const { UserDb } = require('./services/userDb'); + const userDb = await UserDb.init(); + userDb.creatFirstUserifEmpty(); + +})(); + // Agenda UI var Agendash = require("agendash"); app.use("/dash", Agendash(agenda)); // routes +app.use('/api/user', require('./routes/user')); app.use('/api/lot', require('./routes/lot')); app.use('/api/sale', require('./routes/sale')); app.use('/api/favorite', require('./routes/favorite')); diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..7d16aa0 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,33 @@ + + +function checkIsConcernedUserOrAdmin(req, res, next) { + const user = req.user; // User is set by Passport + const userIdParam = req.params.id; + + if (user.isAdmin === true || user._id === userIdParam) { + next(); + } else { + res.status(403).json({ error: 'Forbidden' }); + } + } + + function checkIsAdmin(req, res, next) { + const user = req.user; // User is set by Passport + + if (user.isAdmin === true) { + next(); + } else { + res.status(403).json({ error: 'Forbidden' }); + } + } + + function checkIsAgent(req, res, next) { + const user = req.user; // User is set by Passport + + if (user.isAgent === true) { + next(); + } else { + res.status(403).json({ error: 'Forbidden' }); + } + } + module.exports = { checkIsConcernedUserOrAdmin, checkIsAgent, checkIsAdmin }; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 6efd84d..ccc2c6b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,11 +12,18 @@ "@angular/cli": "^17.1.3", "@hokify/agenda": "^6.3.0", "agendash": "^4.0.0", + "connect-mongo": "^5.1.0", "cors": "^2.8.5", + "exceljs": "^4.4.0", "express": "^4.18.2", "express-async-handler": "^1.2.0", + "express-session": "^1.18.0", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.5.0", - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0" }, "devDependencies": { "nodemon": "^3.0.3" @@ -772,6 +779,43 @@ "tslib": "^2.3.1" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, "node_modules/@hokify/agenda": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@hokify/agenda/-/agenda-6.3.0.tgz", @@ -2111,11 +2155,131 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/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/archiver-utils/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/archiver-utils/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/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/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2140,6 +2304,26 @@ } ] }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2159,13 +2343,23 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "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.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "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.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -2173,7 +2367,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -2197,12 +2391,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "devOptional": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2239,6 +2433,35 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "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/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -2295,6 +2518,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -2439,11 +2673,61 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "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 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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/connect-mongo/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/connect-mongo/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/content-disposition": { "version": "0.5.4", @@ -2465,9 +2749,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2477,6 +2761,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "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/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2489,6 +2778,29 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cron-parser": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", @@ -2549,6 +2861,11 @@ "ms": "2.0.0" } }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2625,11 +2942,54 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/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/duplexer2/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/duplexer2/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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "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", @@ -2669,6 +3029,14 @@ "node": ">=0.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -2733,22 +3101,57 @@ "node": ">= 0.6" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/exceljs/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/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "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.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2784,6 +3187,29 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -2797,6 +3223,18 @@ "node": ">=4" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2840,9 +3278,9 @@ } }, "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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2899,6 +3337,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -2910,6 +3353,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "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==" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2924,6 +3372,32 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3211,6 +3685,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3227,6 +3706,16 @@ "node": ">=8" } }, + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3410,6 +3899,11 @@ "node": ">=8" } }, + "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/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -3466,11 +3960,246 @@ "node >= 0.2.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/jsonwebtoken/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/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/jszip/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/jszip/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/jszip/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/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/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/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/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/lazystream/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/lazystream/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/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/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "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.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "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.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "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.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, + "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.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, + "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/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3624,6 +4353,11 @@ "node": ">=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": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -3638,6 +4372,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -4086,7 +4828,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -4217,6 +4958,22 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -4348,6 +5105,11 @@ "node": "^16.14.0 || >=18.0.0" } }, + "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/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4356,6 +5118,59 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/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==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4389,6 +5204,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picomatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", @@ -4408,6 +5228,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "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/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4465,6 +5290,14 @@ "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", @@ -4474,9 +5307,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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", @@ -4526,6 +5359,25 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4607,6 +5459,58 @@ "node": ">= 4" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -4647,6 +5551,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4730,6 +5645,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/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5029,9 +5949,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -5044,6 +5964,21 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -5143,6 +6078,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -5205,6 +6148,17 @@ "node": ">= 0.6" } }, + "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", @@ -5246,6 +6200,50 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/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/unzipper/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/unzipper/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/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5374,6 +6372,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "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==" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5411,6 +6419,79 @@ "engines": { "node": ">=12" } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } } } } diff --git a/backend/package.json b/backend/package.json index d19352e..848a971 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", - "dev": "nodemon --watch ./ server.js --ignore node_modules/" + "dev": "nodemon --inspect=0.0.0.0 --watch ./ server.js --ignore node_modules/" }, "keywords": [], "author": "", @@ -15,11 +15,18 @@ "@angular/cli": "^17.1.3", "@hokify/agenda": "^6.3.0", "agendash": "^4.0.0", + "connect-mongo": "^5.1.0", "cors": "^2.8.5", + "exceljs": "^4.4.0", "express": "^4.18.2", "express-async-handler": "^1.2.0", + "express-session": "^1.18.0", + "jsonwebtoken": "^9.0.2", "mongodb": "^6.5.0", - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0" }, "devDependencies": { "nodemon": "^3.0.3" diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..a446fe3 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,113 @@ +var passport = require('passport'); +var LocalStrategy = require('passport-local'); +var crypto = require('crypto'); +const router = require('express').Router() +const config = require("../config.js"); +const keys = require("../.Keys.js"); + +/* Configure password authentication strategy. + * + * The `LocalStrategy` authenticates users by verifying a username and password. + * The strategy parses the username and password from the request and calls the + * `verify` function. + * + * The `verify` function queries the database for the user record and verifies + * the password by hashing the password supplied by the user and comparing it to + * the hashed password stored in the database. If the comparison succeeds, the + * user is authenticated; otherwise, not. + */ +const { UserDb }= require('../services/userDb'); + +passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' + },async function verify(email, password, cb) { + try { + const userDb = await UserDb.init(); + const user = await userDb.getByEmail(email); + if (!user) { + return cb(null, false, { message: 'Incorrect username or password.' }); + } + + crypto.pbkdf2(password, user.salt, 310000, 32, 'sha256', async function(err, hashedPassword) { + if (err) { return cb(err); } + if (!crypto.timingSafeEqual(Buffer.from(user.hashed_password, 'hex'), Buffer.from(hashedPassword, 'hex'))) { + return cb(null, false, { message: 'Incorrect username or password.' }); + } + return cb(null, user); + }); + } catch (err) { + return cb(err); + } +})); + + /* Configure session management. + * + * When a login session is established, information about the user will be + * stored in the session. This information is supplied by the `serializeUser` + * function, which is yielding the user ID and username. + * + * As the user interacts with the app, subsequent requests will be authenticated + * by verifying the session. The same user information that was serialized at + * session establishment will be restored when the session is authenticated by + * the `deserializeUser` function. + * + * Since every request to the app needs the user ID and username, in order to + * fetch todo records and render the user element in the navigation bar, that + * information is stored in the session. + */ +passport.serializeUser(function(user, cb) { + process.nextTick(function() { + cb(null, { id: user._id, username: user.email }); + }); + }); + +passport.deserializeUser(function(user, cb) { + process.nextTick(function() { + return cb(null, user); + }); +}); + + +// JWT +var jwt = require('jsonwebtoken'); +var JwtStrategy = require('passport-jwt').Strategy, + ExtractJwt = require('passport-jwt').ExtractJwt; +var opts = {} +opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); +opts.secretOrKey = keys.jwt; +opts.issuer = config.jwtOptions.issuer; +opts.audience = config.jwtOptions.audience; +passport.use(new JwtStrategy(opts, async function(jwt_payload, done) { + const userDb = await UserDb.init(); + try { + const user = await userDb.get(jwt_payload.sub); + if (user) { + return done(null, user); + } else { + return done(null, false); + // or you could create a new account + } + } catch (err) { + return done(err, false); + } +})); + +router.post('/authenticate', async function(req, res) { + passport.authenticate('local', async function(err, user, info) { + if (err) { return res.status(500).json({message: err.message}); } + if (!user) { return res.status(401).json({message: 'Incorrect email or password.'}); } + + // User found, generate a JWT for the user + var token = jwt.sign({ sub: user._id, email: user.email }, opts.secretOrKey, { + issuer: opts.issuer, + audience: opts.audience, + expiresIn: 86400 * 30 // 30 days + }); + res.json({ token: token }); + //return res.status(500).json({message: 'Incorrect email or password.'}) + + })(req, res); + }); + + module.exports = router; \ No newline at end of file diff --git a/backend/routes/favorite.js b/backend/routes/favorite.js index d542c66..55c8b60 100644 --- a/backend/routes/favorite.js +++ b/backend/routes/favorite.js @@ -1,7 +1,9 @@ const controllers = require('../controllers/favorite') const router = require('express').Router() +const passport = require('passport'); +const { checkIsConcernedUserOrAdmin } = require('../middleware/authMiddleware') -router.post('/save/', controllers.save) -router.get('/getAll/', controllers.getAll) +router.post('/save/', passport.authenticate('jwt', { session: false }), controllers.save) +router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll) module.exports = router \ No newline at end of file diff --git a/backend/routes/lot.js b/backend/routes/lot.js index 823fa85..328d9d0 100644 --- a/backend/routes/lot.js +++ b/backend/routes/lot.js @@ -1,12 +1,21 @@ const controllers = require('../controllers/lot') const router = require('express').Router() +const passport = require('passport'); +const { checkIsAgent, checkIsAdmin } = require('../middleware/authMiddleware') -router.get('/getInfos/:url', controllers.getInfos) -router.get('/getPictures/:url', controllers.getPictures) -router.get('/getLotsBySale/:id', controllers.getLotsBySale) +router.get('/getInfos/:url', passport.authenticate('jwt', { session: false }), controllers.getInfos) +router.get('/getPictures/:url', passport.authenticate('jwt', { session: false }), controllers.getPictures) +router.get('/getLotsBySale/:id', passport.authenticate('jwt', { session: false }), controllers.getLotsBySale) -router.post('/NextItem/', controllers.NextItem) -router.post('/AuctionedItem/', controllers.AuctionedItem) -router.post('/Bid/', controllers.Bid) +// DB +router.get('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.get) +router.post('/lot/', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.post) +router.put('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.put) +router.delete('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.delete) + +// Live Data +router.post('/NextItem/', checkIsAgent, controllers.NextItem) +router.post('/AuctionedItem/', checkIsAgent, controllers.AuctionedItem) +router.post('/Bid/', checkIsAgent, controllers.Bid) module.exports = router \ No newline at end of file diff --git a/backend/routes/sale.js b/backend/routes/sale.js index 1c4d234..e165faf 100644 --- a/backend/routes/sale.js +++ b/backend/routes/sale.js @@ -1,4 +1,5 @@ const controllers = require('../controllers/sale') +const passport = require('passport'); const router = require('express').Router() // AuctionAgent @@ -13,9 +14,11 @@ router.post('/sale/', controllers.post) router.put('/sale/:id', controllers.put) router.delete('/sale/:id', controllers.delete) -router.get('/getAll/', controllers.getAll) +//router.get('/getAll/', controllers.getAll) +router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll); router.get('/getByUrl/:url', controllers.getByUrl) router.get('/postProcessing/:id', controllers.postProcessing) +router.get('/SaleStatXsl/:id', controllers.SaleStatXsl) module.exports = router \ No newline at end of file diff --git a/backend/routes/user.js b/backend/routes/user.js new file mode 100644 index 0000000..a6324db --- /dev/null +++ b/backend/routes/user.js @@ -0,0 +1,15 @@ +const controllers = require('../controllers/user') +const passport = require('passport'); +const router = require('express').Router() +const { checkIsConcernedUserOrAdmin, checkIsAdmin } = require('../middleware/authMiddleware') + +// DB +router.get('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.get); +router.post('/user/', controllers.post) +router.put('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.put) +router.delete('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.delete) + +router.get('/current', passport.authenticate('jwt', { session: false }), controllers.current) +router.get('/getAllUsers', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.getAllUsers) + +module.exports = router \ No newline at end of file diff --git a/backend/services/db.js b/backend/services/db.js index 4acc778..08b293b 100644 --- a/backend/services/db.js +++ b/backend/services/db.js @@ -1,5 +1,6 @@ const MongoClient = require("mongodb").MongoClient; -const connectionString = "mongodb://db:27017"; +const config = require("../config.js"); +const connectionString = config.db.connectionString; const client = new MongoClient(connectionString); let db; @@ -8,7 +9,7 @@ const connectDb = async () => { if (db) return db; try { const conn = await client.connect(); - db = conn.db("jucundus"); + db = conn.db(config.db.dbName); return db; } catch(e) { console.error(e); diff --git a/backend/services/userDb.js b/backend/services/userDb.js new file mode 100644 index 0000000..b5a1da7 --- /dev/null +++ b/backend/services/userDb.js @@ -0,0 +1,91 @@ +const { ObjectId } = require('mongodb'); +const connectDb = require("./db"); +const crypto = require('crypto'); + +const UserDb = class +{ + constructor() + { + } + + static async init() { + const userDb = new UserDb(); + await userDb.getCollection(); + return userDb; + } + + async getCollection() + { + const db = await connectDb(); + if (!db) { + throw new Error('Database not connected'); + } + this.collection = db.collection("Users"); + } + + // CRUD + async get(id) + { + let result = await this.collection.findOne({_id: new ObjectId(id)}); + return result; + } + + async post(newDocument) + { + delete newDocument._id; + let result = await this.collection.insertOne(newDocument); + return result; + } + + async put(id, data) + { + delete data._id; + let result = await this.collection.updateOne({_id: new ObjectId(id)}, {$set: data}); + return result; + } + + async remove(id) + { + let result = await this.collection.deleteOne({_id: new ObjectId(id)}); + return result; + } + + // Functions + + async getAll() + { + let result = await this.collection.find({}).toArray(); + return result; + } + + async getByUsername(username) + { + let result = await this.collection.findOne({username: username}); + return result; + } + async getByEmail(email) + { + let result = await this.collection.findOne({email: email}); + return result; + } + + async creatFirstUserifEmpty( ){ + const allUsers = await this.getAll(); + if(allUsers.length == 0){ + console.log("Creating first user"); + let salt = crypto.randomBytes(16).toString('hex'); + let user = { + username: "admin", + hashed_password: crypto.pbkdf2Sync('admin', salt, 310000, 32, 'sha256').toString('hex'), + salt: salt, + email: "admin@admin.com", + isAdmin: true, + isAgent: false, + + } + this.post(user); + } + } +} + +module.exports = { UserDb }; \ No newline at end of file diff --git a/client/src/app/core/services/api/api.auth.service.ts b/client/src/app/core/services/api/api.auth.service.ts new file mode 100644 index 0000000..3a4b5aa --- /dev/null +++ b/client/src/app/core/services/api/api.auth.service.ts @@ -0,0 +1,19 @@ +import { Injectable, Inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class apiAuthService { + ServeurURL = environment.ServeurURL; + ApiURL = this.ServeurURL+"/api"; + + + constructor(private http: HttpClient){} + + // authenticate + authenticate(email: string, password: string) { + return this.http.post<{token: string}>(this.ServeurURL+'/authenticate', {email, password}); + } +} \ No newline at end of file diff --git a/client/src/app/core/services/api/api.favorite.service.ts b/client/src/app/core/services/api/api.favorite.service.ts new file mode 100644 index 0000000..2e5d3af --- /dev/null +++ b/client/src/app/core/services/api/api.favorite.service.ts @@ -0,0 +1,26 @@ +import { Injectable, Inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../../../environments/environment'; + +import { LotInfo } from '../model/lotInfo.interface'; +import { SaleInfo } from '../model/saleInfo.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class apiFavoriteService { + ServeurURL = environment.ServeurURL; + ApiURL = this.ServeurURL+"/api"; + + constructor(private http: HttpClient){} + + //Favorites + saveFavorite(lotInfo: LotInfo, saleInfo: SaleInfo, picture: String, dateTime: string, buyProject: boolean, maxPrice: number, Note: string): Observable { + return this.http.post(this.ApiURL+'/favorite/save', {lotInfo, saleInfo, picture, dateTime, buyProject, maxPrice, Note}); + } + + getAllFavorite(): Observable { + return this.http.get(this.ApiURL+'/favorite/getAll'); + } +} diff --git a/client/src/app/core/services/api/api.lot.service.ts b/client/src/app/core/services/api/api.lot.service.ts new file mode 100644 index 0000000..3feb6f4 --- /dev/null +++ b/client/src/app/core/services/api/api.lot.service.ts @@ -0,0 +1,49 @@ +import { Injectable, Inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../../../environments/environment'; + +import { LotInfo } from '../model/lotInfo.interface'; +import { Lot } from '../model/lot.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class apiLotService { + ServeurURL = environment.ServeurURL; + ApiURL = this.ServeurURL+"/api"; + + constructor(private http: HttpClient){} + + // Lot + getLotInfo(url: string): Observable { + let encodeUrl = encodeURIComponent(url); + return this.http.get(this.ApiURL+'/lot/getInfos/'+encodeUrl); + } + + getPictures(url: string): Observable { + let encodeUrl = encodeURIComponent(url); + return this.http.get(this.ApiURL+'/lot/getPictures/'+encodeUrl); + } + + getLotsBySale(_id: String): Observable { + return this.http.get(this.ApiURL+'/lot/getLotsBySale/'+_id); + } + + // Lot CRUD + getLot(_id: String): Observable { + return this.http.get(this.ApiURL+'/lot/lot/'+_id); + } + + saveLot(Lot: Lot): Observable { + return this.http.post(this.ApiURL+'/lot/lot', Lot); + } + + updateLot(Lot: Lot): Observable { + return this.http.put(this.ApiURL+'/lot/lot/'+Lot._id, Lot); + } + + deleteLot(_id: String): Observable { + return this.http.delete(this.ApiURL+'/lot/lot/'+_id); + } +} \ No newline at end of file diff --git a/client/src/app/core/services/api.service.ts b/client/src/app/core/services/api/api.sale.service.ts similarity index 52% rename from client/src/app/core/services/api.service.ts rename to client/src/app/core/services/api/api.sale.service.ts index bdcd49a..18ddea8 100644 --- a/client/src/app/core/services/api.service.ts +++ b/client/src/app/core/services/api/api.sale.service.ts @@ -1,34 +1,20 @@ -import { Injectable, Inject } from '@angular/core'; +import { Injectable} from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; +import { environment } from '../../../../environments/environment'; -import { LotInfo } from './model/lotInfo.interface'; -import { SaleInfo } from './model/saleInfo.interface'; -import { Sale } from './model/sale.interface'; -import { Lot } from './model/lot.interface'; +import { SaleInfo } from '../model/saleInfo.interface'; +import { Sale } from '../model/sale.interface'; @Injectable({ providedIn: 'root' }) -export class apiService { - ApiURL = "http://localhost:3000/api"; +export class apiSaleService { + ServeurURL = environment.ServeurURL; + ApiURL = this.ServeurURL+"/api"; - constructor(private http: HttpClient){} - - // Lot - getLotInfo(url: string): Observable { - let encodeUrl = encodeURIComponent(url); - return this.http.get(this.ApiURL+'/lot/getInfos/'+encodeUrl); - } - - getPictures(url: string): Observable { - let encodeUrl = encodeURIComponent(url); - return this.http.get(this.ApiURL+'/lot/getPictures/'+encodeUrl); - } - - getLotsBySale(_id: String): Observable { - return this.http.get(this.ApiURL+'/lot/getLotsBySale/'+_id); - } + constructor( + private http: HttpClient){} // Sale getSaleInfos(url: string): Observable { @@ -74,24 +60,18 @@ export class apiService { return this.http.delete(this.ApiURL+'/sale/sale/'+_id); } - // Function DB Sale - + // Function DB Sale getAllSale(): Observable { - return this.http.get(this.ApiURL+'/sale/getAll'); + return this.http.get(`${this.ApiURL}/sale/getAll`); } postProcessing(_id: String): Observable { return this.http.get(this.ApiURL+'/sale/postProcessing/'+_id); } + + getSaleStatXsl(_id: String): Observable { + return this.http.get(this.ApiURL+'/sale/SaleStatXsl/'+_id, { responseType: 'blob' as 'json' }); + } - - //Favorites - saveFavorite(lotInfo: LotInfo, saleInfo: SaleInfo, picture: String, dateTime: string, buyProject: boolean, maxPrice: number, Note: string): Observable { - return this.http.post(this.ApiURL+'/favorite/save', {lotInfo, saleInfo, picture, dateTime, buyProject, maxPrice, Note}); - } - - getAllFavorite(): Observable { - return this.http.get(this.ApiURL+'/favorite/getAll'); - } } diff --git a/client/src/app/core/services/api/api.user.service.ts b/client/src/app/core/services/api/api.user.service.ts new file mode 100644 index 0000000..a36436b --- /dev/null +++ b/client/src/app/core/services/api/api.user.service.ts @@ -0,0 +1,43 @@ +import { Injectable} from '@angular/core'; +import { HttpClient} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../../../environments/environment'; + +import { User } from '../model/user.interface'; +@Injectable({ + providedIn: 'root' +}) +export class apiUserService { + ServeurURL = environment.ServeurURL; + ApiURL = this.ServeurURL+"/api"; + + constructor( + private http: HttpClient){} + + // User CRUD + + getUser(_id: String): Observable { + return this.http.get(this.ApiURL+'/user/user/'+_id); + } + + saveUser(User: User): Observable { + return this.http.post(this.ApiURL+'/user/user/', User); + } + + updateUser(User: User): Observable { + return this.http.put(this.ApiURL+'/user/user/'+User._id, User); + } + + deleteUser(_id: String): Observable { + return this.http.delete(this.ApiURL+'/user/user/'+_id); + } + + // User function + getCurrentUser(): Observable { + return this.http.get(this.ApiURL+'/user/current'); + } + + getAllUsers(): Observable { + return this.http.get(this.ApiURL+'/user/getAllUsers'); + } +} diff --git a/client/src/app/core/services/auth.service.ts b/client/src/app/core/services/auth.service.ts index a26e264..2b55a0f 100644 --- a/client/src/app/core/services/auth.service.ts +++ b/client/src/app/core/services/auth.service.ts @@ -1,10 +1,11 @@ import { Injectable, Inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; import { delay, map } from 'rxjs/operators'; -import * as jwt_decode from 'jwt-decode'; +import { apiAuthService } from './api/api.auth.service'; +import { apiUserService } from './api/api.user.service'; +import { User } from './model/user.interface'; + import * as moment from 'moment'; -import { environment } from '../../../environments/environment'; import { of, EMPTY } from 'rxjs'; @Injectable({ @@ -12,30 +13,40 @@ import { of, EMPTY } from 'rxjs'; }) export class AuthenticationService { - constructor(private http: HttpClient, + constructor( + private apiAuthService: apiAuthService, + private apiUserService: apiUserService, @Inject('LOCALSTORAGE') private localStorage: Storage) { } login(email: string, password: string) { - return of(true) - .pipe(delay(1000), - map((/*response*/) => { - // set token property - // const decodedToken = jwt_decode(response['token']); - - // store email and jwt token in local storage to keep user logged in between page refreshes - this.localStorage.setItem('currentUser', JSON.stringify({ - token: 'aisdnaksjdn,axmnczm', - isAdmin: true, - email: 'john.doe@gmail.com', - id: '12312323232', - alias: 'john.doe@gmail.com'.split('@')[0], - expiration: moment().add(1, 'days').toDate(), - fullName: 'John Doe' - })); - - return true; + return this.apiAuthService.authenticate(email, password). + pipe(map(response => { + // If the server returns a token, the login is successful + if (response.token) { + // Store user details and jwt token in local storage to keep user logged in between page refreshes + this.localStorage.setItem('currentUser', JSON.stringify({ + token: response.token, })); + + this.apiUserService.getCurrentUser().subscribe((user: User) => { + console.log(user); + const Storeduser = this.localStorage.getItem('currentUser'); + if(Storeduser) { + const currentUser = JSON.parse(Storeduser); + currentUser.isAdmin = user.isAdmin; + currentUser.email = user.email; + currentUser.id = user._id; + currentUser.alias = user.email.split('@')[0]; + currentUser.fullName = user.username; + currentUser.expiration = moment().add(30, 'days').toDate(); + this.localStorage.setItem('currentUser', JSON.stringify(currentUser)); + } + }); + return true; + } + return false; + })); } logout(): void { @@ -45,16 +56,20 @@ export class AuthenticationService { getCurrentUser(): any { // TODO: Enable after implementation - // return JSON.parse(this.localStorage.getItem('currentUser')); - return { - token: 'aisdnaksjdn,axmnczm', - isAdmin: true, - email: 'john.doe@gmail.com', - id: '12312323232', - alias: 'john.doe@gmail.com'.split('@')[0], - expiration: moment().add(1, 'days').toDate(), - fullName: 'John Doe' - }; + const user = this.localStorage.getItem('currentUser'); + if (!user) { + return null; + } + return JSON.parse(user); + // return { + // token: JSON.parse(user).token, + // isAdmin: true, + // email: 'john.doe@gmail.com', + // id: '12312323232', + // alias: 'john.doe@gmail.com'.split('@')[0], + // expiration: moment().add(1, 'days').toDate(), + // fullName: 'John Doe' + // }; } passwordResetRequest(email: string) { diff --git a/client/src/app/core/services/model/lot.interface.ts b/client/src/app/core/services/model/lot.interface.ts index 2436e61..ef4c384 100644 --- a/client/src/app/core/services/model/lot.interface.ts +++ b/client/src/app/core/services/model/lot.interface.ts @@ -2,7 +2,7 @@ export interface Bid { timestamp: string; amount: number; - auctioned_type: string; + auctioned_type?: string; } export interface Auctioned { @@ -12,6 +12,14 @@ export interface Auctioned { sold: boolean; } +export interface PostProcessing { + nbrBids: number; + highestBid: number; + duration: number; + percentageAboveUnderLow: number; + percentageAboveUnderHigh: number; +} + export interface Lot { _id: { $oid: string; @@ -20,10 +28,15 @@ export interface Lot { platform: string; timestamp: string; lotNumber: string; + title?: string; + description?: string; + EstimateLow?: number; + EstimateHigh?: number; RawData?: Object; sale_id: { $oid: string; }; Bids?: Bid[]; auctioned?: Auctioned; + postProcessing?: PostProcessing; } \ No newline at end of file diff --git a/client/src/app/core/services/model/sale.interface.ts b/client/src/app/core/services/model/sale.interface.ts index 8ad7cd1..292d07d 100644 --- a/client/src/app/core/services/model/sale.interface.ts +++ b/client/src/app/core/services/model/sale.interface.ts @@ -2,10 +2,15 @@ export interface PostProcessing { nbrLots: number; duration: number; + bidsDuration: number; durationPerLots: string; totalAmount: number; averageAmount: string; medianAmount: string; + minAmount: string; + maxAmount: string; + unsoldLots: number; + unsoldPercentage: string; } export interface Sale { diff --git a/client/src/app/core/services/model/user.interface.ts b/client/src/app/core/services/model/user.interface.ts new file mode 100644 index 0000000..50bfd2d --- /dev/null +++ b/client/src/app/core/services/model/user.interface.ts @@ -0,0 +1,10 @@ +export interface User { + _id: string; + email: string; + isAdmin: boolean; + isAgent?: boolean; + username: string; + expiration?: string; + password?: string; + confirmPassword?: string; +} \ No newline at end of file diff --git a/client/src/app/features/auth/login/login.component.ts b/client/src/app/features/auth/login/login.component.ts index 16dda0d..59575ce 100644 --- a/client/src/app/features/auth/login/login.component.ts +++ b/client/src/app/features/auth/login/login.component.ts @@ -55,7 +55,7 @@ export class LoginComponent implements OnInit { this.router.navigate(['/']); }, error => { - this.notificationService.openSnackBar(error.error); + this.notificationService.openSnackBar(error.error.message); this.loading = false; } ); diff --git a/client/src/app/features/favorites/favorites-page/favorites-page.component.ts b/client/src/app/features/favorites/favorites-page/favorites-page.component.ts index 264b1c4..7016890 100644 --- a/client/src/app/features/favorites/favorites-page/favorites-page.component.ts +++ b/client/src/app/features/favorites/favorites-page/favorites-page.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { apiService } from 'src/app/core/services/api.service'; +import { apiFavoriteService } from 'src/app/core/services/api/api.favorite.service'; @Component({ selector: 'app-favorites-page', @@ -16,7 +16,7 @@ export class FavoritesPageComponent implements OnInit { constructor( private router: Router, - private apiService: apiService) {} + private apiFavoriteService: apiFavoriteService) {} openDialog(): void { @@ -25,7 +25,7 @@ export class FavoritesPageComponent implements OnInit { } ngOnInit(): void { - this.apiService.getAllFavorite().subscribe((data: any) => { + this.apiFavoriteService.getAllFavorite().subscribe((data: any) => { this.dataSource = data; }); } diff --git a/client/src/app/features/favorites/favorites-page/new-favorite-page/new-favorite-page.component.ts b/client/src/app/features/favorites/favorites-page/new-favorite-page/new-favorite-page.component.ts index 99dcfef..3c2d64c 100644 --- a/client/src/app/features/favorites/favorites-page/new-favorite-page/new-favorite-page.component.ts +++ b/client/src/app/features/favorites/favorites-page/new-favorite-page/new-favorite-page.component.ts @@ -7,7 +7,9 @@ import * as moment from 'moment-timezone'; //Services import { NotificationService } from 'src/app/core/services/notification.service'; -import { apiService } from 'src/app/core/services/api.service'; +import { apiLotService } from 'src/app/core/services/api/api.lot.service'; +import { apiFavoriteService } from 'src/app/core/services/api/api.favorite.service'; +import { apiSaleService } from 'src/app/core/services/api/api.sale.service'; // Models import { LotInfo } from 'src/app/core/services/model/lotInfo.interface'; @@ -36,7 +38,9 @@ export class NewFavoritesPageComponent implements OnInit { private ActivatedRoute: ActivatedRoute, private router: Router, private notificationService: NotificationService, - private apiService: apiService,) { + private apiLotService: apiLotService, + private apiSaleService: apiSaleService, + private apiFavoriteService: apiFavoriteService,) { this.ActivatedRoute.params.subscribe(params => { this.url = params['url']; @@ -80,11 +84,11 @@ export class NewFavoritesPageComponent implements OnInit { // this.date = moment(this.SaleInfo.date).tz('Europe/Paris').toDate(); // this.hour = moment(this.SaleInfo.date).tz('Europe/Paris').format('HH:mm'); - this.apiService.getLotInfo(this.url).subscribe( lotInfo => { + this.apiLotService.getLotInfo(this.url).subscribe( lotInfo => { console.log(lotInfo); this.lotInfo = lotInfo; - this.apiService.getSaleInfos(this.lotInfo.saleInfo.url).subscribe( SaleInfo => { + this.apiSaleService.getSaleInfos(this.lotInfo.saleInfo.url).subscribe( SaleInfo => { console.log(SaleInfo); this.SaleInfo = SaleInfo; @@ -99,7 +103,7 @@ export class NewFavoritesPageComponent implements OnInit { } ); - this.apiService.getPictures(this.url).subscribe( pictures => { + this.apiLotService.getPictures(this.url).subscribe( pictures => { this.images = pictures; this.picture = pictures[0]; }); @@ -127,7 +131,7 @@ export class NewFavoritesPageComponent implements OnInit { // Europe/Paris is the timezone of the user let dateTime = moment.tz(`${this.date.toISOString().split('T')[0]}T${this.hour}`, 'Europe/Paris').format(); - this.apiService.saveFavorite(this.lotInfo, this.SaleInfo, this.picture, dateTime, this.buyProject, this.maxPrice, this.Note).subscribe( res => { + this.apiFavoriteService.saveFavorite(this.lotInfo, this.SaleInfo, this.picture, dateTime, this.buyProject, this.maxPrice, this.Note).subscribe( res => { this.notificationService.openSnackBar("Favorite saved"); this.router.navigate(['favorites']); }); diff --git a/client/src/app/features/pictures/pictures-page/pictures-page.component.ts b/client/src/app/features/pictures/pictures-page/pictures-page.component.ts index 57a3242..908ac68 100644 --- a/client/src/app/features/pictures/pictures-page/pictures-page.component.ts +++ b/client/src/app/features/pictures/pictures-page/pictures-page.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Title } from '@angular/platform-browser'; -import { apiService } from 'src/app/core/services/api.service'; +import { apiLotService } from 'src/app/core/services/api/api.lot.service'; + @Component({ selector: 'app-pictures-page', @@ -20,7 +21,7 @@ export class PicturesPageComponent implements OnInit { constructor( private titleService: Title, - private apiService: apiService, + private apiLotService: apiLotService, ) { } @@ -43,7 +44,7 @@ export class PicturesPageComponent implements OnInit { } getPictures(): void { - this.apiService.getPictures(this.url).subscribe( Pictures => { + this.apiLotService.getPictures(this.url).subscribe( Pictures => { this.images = []; const newImages = [...this.images]; diff --git a/client/src/app/features/sales/sales-page/loading-sale-dialog/loading-sale-dialog.component.ts b/client/src/app/features/sales/sales-page/loading-sale-dialog/loading-sale-dialog.component.ts index b69ff8f..3d16c50 100644 --- a/client/src/app/features/sales/sales-page/loading-sale-dialog/loading-sale-dialog.component.ts +++ b/client/src/app/features/sales/sales-page/loading-sale-dialog/loading-sale-dialog.component.ts @@ -3,7 +3,7 @@ import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; import * as moment from 'moment-timezone'; //Services -import { apiService } from 'src/app/core/services/api.service'; +import { apiSaleService } from 'src/app/core/services/api/api.sale.service'; // Models import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface'; @@ -23,7 +23,7 @@ export class LoadingSaleDialogComponent implements OnInit { hour: string = ""; constructor( - private apiService: apiService, + private apiSaleService: apiSaleService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { this.SaleInfo = { @@ -44,7 +44,7 @@ export class LoadingSaleDialogComponent implements OnInit { this.url = this.data.url; console.log(this.url); - this.apiService.getSaleInfos(this.url).subscribe( SaleInfo => { + this.apiSaleService.getSaleInfos(this.url).subscribe( SaleInfo => { console.log(SaleInfo); this.SaleInfo = SaleInfo; @@ -70,7 +70,7 @@ export class LoadingSaleDialogComponent implements OnInit { status: "ready" } - this.apiService.saveSale(Sale).subscribe( Sale => { + this.apiSaleService.saveSale(Sale).subscribe( Sale => { this.dialogRef.close(true); }); } diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.css b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.css new file mode 100644 index 0000000..a203e17 --- /dev/null +++ b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.css @@ -0,0 +1,4 @@ +.example-card { + + margin-bottom: 8px; + } \ No newline at end of file diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.html b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.html new file mode 100644 index 0000000..a77f63c --- /dev/null +++ b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.html @@ -0,0 +1,71 @@ + + + Lot + Lot informations + + + +
+ + + # + {{Lot.lotNumber}} + + + Title + {{Lot.title}} + + + Description + + + + Estimate + Low: {{Lot.EstimateLow}} | High: {{Lot.EstimateHigh}} + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Amount + {{element.amount}} + Type + {{element.auctioned_type}} + Time + {{element.timestamp | date:'HH:mm:ss':'Europe/Paris'}} +
+
+ +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.ts b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.ts new file mode 100644 index 0000000..c3377ec --- /dev/null +++ b/client/src/app/features/sales/sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit, Inject, ViewChild } from '@angular/core'; +import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTableDataSource } from '@angular/material/table';import * as moment from 'moment-timezone'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +//Services +import { apiLotService } from 'src/app/core/services/api/api.lot.service'; + +// Models +import { Lot, Bid } from 'src/app/core/services/model/lot.interface'; + + +@Component({ + selector: 'lot-detail-dialog-dialog', + templateUrl: './lot-detail-dialog.component.html', + styleUrls: ['./lot-detail-dialog.component.css'] +}) +export class LotDetailDialogComponent implements OnInit { + + id: string; + Lot: Lot; + + displayedColumns: string[] = ['amount', 'type', 'time']; + bids: MatTableDataSource = new MatTableDataSource(); + @ViewChild(MatPaginator) paginator?: MatPaginator; + + constructor( + private apiLotService: apiLotService, + public dialogRef: MatDialogRef, + private sanitizer: DomSanitizer, + @Inject(MAT_DIALOG_DATA) public data: any) { + + this.id = data.id; + this.Lot = { + _id: { $oid: '' }, + idPlatform: '', + platform: '', + timestamp: '', + lotNumber: '', + RawData: {}, + sale_id: { $oid: '' }, + Bids: [], + title: '', + description: '', + auctioned: { + timestamp: '', + amount: 0, + auctioned_type: '', + sold: false + } + }; + } + + ngOnInit(): void { + + this.apiLotService.getLot(this.id).subscribe( Lot => { + this.Lot = Lot; + + this.bids = new MatTableDataSource(Lot.Bids ?? []); + this.bids.paginator = this.paginator ?? null; + }); + } + + getSafeDescription(): SafeHtml { + if (this.Lot && this.Lot.description) { + return this.sanitizer.bypassSecurityTrustHtml(this.Lot.description.replace(/\n/g, '
')); + } + return ''; + } + + cancel(): void { + this.dialogRef.close(false); + } + +} diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.css b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.css index e69de29..0f2e243 100644 --- a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.css +++ b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.css @@ -0,0 +1,9 @@ +.mat-mdc-row .mat-mdc-cell { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + cursor: pointer; + } + + .mat-mdc-row:hover .mat-mdc-cell { + border-color: currentColor; + } \ No newline at end of file diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.html b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.html index df3750b..359e064 100644 --- a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.html +++ b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.html @@ -17,16 +17,23 @@

tag{{Sale.postProcessing?.nbrLots}} Lots

-

hourglass_bottom{{getDurationInMinutes(Sale.postProcessing?.duration ?? 0)}} min

+

hourglass_bottom{{convertSecondsToHHMM(Sale.postProcessing?.bidsDuration ?? 0)}}

hourglass_bottom/Lot {{getTimePerLot(parseToNumber(Sale.postProcessing?.durationPerLots ?? '0'))}}

Total amount: {{Sale.postProcessing?.totalAmount | currency:'EUR':'symbol':'1.2-2'}}

+

Max amount: {{Sale.postProcessing?.maxAmount | currency:'EUR':'symbol':'1.2-2'}}

+

Min amount: {{Sale.postProcessing?.minAmount | currency:'EUR':'symbol':'1.2-2'}}

+
+

Average amount: {{Sale.postProcessing?.averageAmount | currency:'EUR':'symbol':'1.2-2'}}

Median amount: {{Sale.postProcessing?.medianAmount | currency:'EUR':'symbol':'1.2-2'}}

+

Unsold: {{Sale.postProcessing?.unsoldLots}} ({{Sale.postProcessing?.unsoldPercentage}}%)

- - + +
+ +
@@ -48,12 +55,18 @@ - # + # {{element.lotNumber}} + + + Picture + Picture + + Title @@ -82,24 +95,48 @@ Price - {{element.auctionedAmount}} + {{element.auctionedAmount > 0 ? element.auctionedAmount : "-"}} - + nbrBids - {{element.bidsLength}} + {{element.postProcessing?.nbrBids}} + + + + + + Duration + + {{element.postProcessing?.duration}} s + + + + + + Above/Under Low + + {{element.postProcessing?.percentageAboveUnderLow}} % + + + + + + Above/Under High + + {{element.postProcessing?.percentageAboveUnderHigh}} % - + - + diff --git a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.ts b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.ts index 195eedd..b2061c7 100644 --- a/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.ts +++ b/client/src/app/features/sales/sales-page/sale-detail-page/sale-detail-page.component.ts @@ -4,17 +4,18 @@ import { Router, ActivatedRoute } from '@angular/router'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; +import { LotDetailDialogComponent } from './lot-detail-dialog/lot-detail-dialog.component'; import * as moment from 'moment'; // Services -import { apiService } from 'src/app/core/services/api.service'; +import { apiSaleService } from 'src/app/core/services/api/api.sale.service'; +import { apiLotService } from 'src/app/core/services/api/api.lot.service'; import { NotificationService } from 'src/app/core/services/notification.service'; //Models import { Sale } from 'src/app/core/services/model/sale.interface'; import { Lot } from 'src/app/core/services/model/lot.interface'; -import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface'; @Component({ selector: 'app-favorites-page', @@ -27,8 +28,8 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { id: any = ''; Sale: Sale; - displayedColumns: string[] = ['lotNum', 'title', 'estimateLow', 'estimateHigh', 'price', 'nbrBids']; - + + displayedColumns: string[] = ['lotNum', 'picture', 'title', 'estimateLow', 'estimateHigh', 'price', 'nbrBids', 'duration', 'percentageAboveUnderLow', 'percentageAboveUnderHigh']; lotList: MatTableDataSource = new MatTableDataSource(); @ViewChild(MatPaginator) paginator?: MatPaginator; @ViewChild(MatSort) sort?: MatSort; @@ -38,7 +39,8 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { private notificationService: NotificationService, private router: Router, public dialog: MatDialog, - private apiService: apiService) { + private apiSaleService: apiSaleService, + private apiLotService: apiLotService) { this.Sale = { _id: { $oid: '' }, @@ -53,10 +55,15 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { postProcessing: { nbrLots: 0, duration: 0, + bidsDuration: 0, durationPerLots: '', totalAmount: 0, averageAmount: '', - medianAmount: '' + medianAmount: '', + minAmount: '', + maxAmount: '', + unsoldLots: 0, + unsoldPercentage: '' } } @@ -76,17 +83,49 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { } getSale(){ - this.apiService.getSale(this.id).subscribe((sale: Sale) => { + this.apiSaleService.getSale(this.id).subscribe((sale: Sale) => { this.Sale = sale; }); } getLotList(){ - this.apiService.getLotsBySale(this.id).subscribe((lotList: Lot[]) => { + this.apiLotService.getLotsBySale(this.id).subscribe((lotList: Lot[]) => { // adding the Bids length info lotList = lotList.map((lot) => { - return {...lot, bidsLength: lot.Bids ? lot.Bids.length : 0}; + return {...lot, bidsLength: lot.postProcessing ? lot.postProcessing.nbrBids : 0}; + }); + + // adding the picture info + let path = ''; + lotList = lotList.map((lot) => { + switch (lot.platform) { + case 'drouot': + path = 'https://cdn.drouot.com/d/image/lot?size=phare&path='+(lot.RawData as { photos?: { path: string }[] })?.photos?.[0]?.path; + return {...lot, picture: path ?? ''}; + + case 'interencheres': + path = (lot.RawData as { medias?: { lg: string }[] })?.medias?.[0]?.lg ?? ''; + return {...lot, picture: path}; + + default: + return {...lot, picture: ''}; + } + }); + + // adding the duration info + lotList = lotList.map((lot) => { + return {...lot, duration: lot.postProcessing ? lot.postProcessing.duration : 0}; + }); + + // adding the percentageAboveUnderLow info + lotList = lotList.map((lot) => { + return {...lot, percentageAboveUnderLow: lot.postProcessing ? lot.postProcessing.percentageAboveUnderLow : 0}; + }); + + // adding the percentageAboveUnderHigh info + lotList = lotList.map((lot) => { + return {...lot, percentageAboveUnderHigh: lot.postProcessing ? lot.postProcessing.percentageAboveUnderHigh : 0}; }); // adding the Auctionned ammount info @@ -101,9 +140,9 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { }); } - getDurationInMinutes(duration: number): number { - return Math.floor(duration / 60); - } + // getDurationInMinutes(duration: number): number { + // return Math.floor(duration / 60); + // } getTimePerLot(duration: number): string { @@ -124,6 +163,30 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit { return parseFloat(value || '0'); } + convertSecondsToHHMM(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`; + } + + openDetailLot(idLot: string): void { + this.dialog.open(LotDetailDialogComponent, { + width: '80%', + data: {id: idLot} + }); + } + + downloadExcelStatsFile(_id: String): void { + this.apiSaleService.getSaleStatXsl(_id).subscribe((data: Blob) => { + const downloadURL = window.URL.createObjectURL(data); + const link = document.createElement('a'); + link.href = downloadURL; + link.download = 'SaleStats.xlsx'; + link.click(); + }); +} + + } diff --git a/client/src/app/features/sales/sales-page/sales-page.component.html b/client/src/app/features/sales/sales-page/sales-page.component.html index 9d9ab91..c01af30 100644 --- a/client/src/app/features/sales/sales-page/sales-page.component.html +++ b/client/src/app/features/sales/sales-page/sales-page.component.html @@ -63,31 +63,6 @@ {{element.platform}} - - - - - - - - - Action @@ -97,6 +72,8 @@ play_arrow Follow stop Stop Following Reset to Ready + info Details + query_stats Post-processing delete Delete diff --git a/client/src/app/features/sales/sales-page/sales-page.component.ts b/client/src/app/features/sales/sales-page/sales-page.component.ts index aa57f6e..0138b00 100644 --- a/client/src/app/features/sales/sales-page/sales-page.component.ts +++ b/client/src/app/features/sales/sales-page/sales-page.component.ts @@ -1,12 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Title } from '@angular/platform-browser'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { LoadingSaleDialogComponent } from './loading-sale-dialog/loading-sale-dialog.component'; import * as moment from 'moment'; // Services -import { apiService } from 'src/app/core/services/api.service'; import { NotificationService } from 'src/app/core/services/notification.service'; +import { apiSaleService } from 'src/app/core/services/api/api.sale.service'; import { Sale } from 'src/app/core/services/model/sale.interface'; @@ -15,7 +16,7 @@ import { Sale } from 'src/app/core/services/model/sale.interface'; templateUrl: './sales-page.component.html', styleUrls: ['./sales-page.component.css'] }) -export class SalesPageComponent implements OnInit { +export class SalesPageComponent implements OnInit,OnDestroy { url: string = ''; refreshSalesId: any; @@ -29,7 +30,8 @@ export class SalesPageComponent implements OnInit { private notificationService: NotificationService, private router: Router, public dialog: MatDialog, - private apiService: apiService) {} + private apiSaleService: apiSaleService, + private titleService: Title) {} openLoadingSale(): void { const dialogRef = this.dialog.open(LoadingSaleDialogComponent, { @@ -47,7 +49,7 @@ export class SalesPageComponent implements OnInit { } refreshSales(): void { - this.apiService.getAllSale().subscribe((data: any) => { + this.apiSaleService.getAllSale().subscribe((data: any) => { console.log(data); const today = moment().startOf('day'); @@ -62,15 +64,21 @@ export class SalesPageComponent implements OnInit { } ngOnInit(): void { + this.titleService.setTitle('Jucundus - Sales'); this.refreshSales(); this.refreshSalesId = setInterval(() => { this.refreshSales(); }, 5000); } + ngOnDestroy() { + if (this.refreshSalesId) { + clearInterval(this.refreshSalesId); + } + } prepareSale(Sale: Sale): void { - this.apiService.prepareSale(Sale).subscribe( data => { + this.apiSaleService.prepareSale(Sale).subscribe( data => { console.log(data); this.refreshSales(); this.notificationService.openSnackBar("Prepare Sale"); @@ -80,7 +88,7 @@ export class SalesPageComponent implements OnInit { followSale(Sale: Sale): void { - this.apiService.followSale(Sale).subscribe( data => { + this.apiSaleService.followSale(Sale).subscribe( data => { console.log(data); this.refreshSales(); this.notificationService.openSnackBar("Sale followed"); @@ -90,21 +98,21 @@ export class SalesPageComponent implements OnInit { stopFollowSale(Sale: Sale): void { Sale.status = "askStop"; - this.apiService.updateSale(Sale).subscribe( data => { + this.apiSaleService.updateSale(Sale).subscribe( data => { this.refreshSales(); this.notificationService.openSnackBar("Sale Stopping..."); }) } deleteSale(_id: string): void { - this.apiService.deleteSale(_id).subscribe( data => { + this.apiSaleService.deleteSale(_id).subscribe( data => { this.refreshSales(); this.notificationService.openSnackBar("Sale deleted"); }) } resetToReady(_id: string): void { - this.apiService.resetSaleToReady(_id) + this.apiSaleService.resetSaleToReady(_id) } navigateToSaleDetail(id: string) { @@ -114,7 +122,7 @@ export class SalesPageComponent implements OnInit { } postProcessing(id: string) { - this.apiService.postProcessing(id).subscribe( data => { + this.apiSaleService.postProcessing(id).subscribe( data => { this.notificationService.openSnackBar("Sale processing"); }) } diff --git a/client/src/app/features/sales/sales.module.ts b/client/src/app/features/sales/sales.module.ts index d053a45..3d35b56 100644 --- a/client/src/app/features/sales/sales.module.ts +++ b/client/src/app/features/sales/sales.module.ts @@ -5,13 +5,15 @@ import { SalesRoutingModule } from './sales-routing.module'; import { SalesPageComponent } from './sales-page/sales-page.component'; import { LoadingSaleDialogComponent } from './sales-page/loading-sale-dialog/loading-sale-dialog.component'; import { SaleDetailPageComponent } from './sales-page/sale-detail-page/sale-detail-page.component'; +import { LotDetailDialogComponent } from './sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component'; import { SharedModule } from '../../shared/shared.module'; @NgModule({ declarations: [ SalesPageComponent, LoadingSaleDialogComponent, - SaleDetailPageComponent + SaleDetailPageComponent, + LotDetailDialogComponent ], imports: [ CommonModule, diff --git a/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.css b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.css new file mode 100644 index 0000000..0f2e243 --- /dev/null +++ b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.css @@ -0,0 +1,9 @@ +.mat-mdc-row .mat-mdc-cell { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + cursor: pointer; + } + + .mat-mdc-row:hover .mat-mdc-cell { + border-color: currentColor; + } \ No newline at end of file diff --git a/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.html b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.html new file mode 100644 index 0000000..25de130 --- /dev/null +++ b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.html @@ -0,0 +1,60 @@ +
+
+ +

Users/{{id !== '' ? 'Edit' : 'New'}}

+
+ + + Sale Detail + + +
+
+ + + + Username is required + + + + + + Please enter a valid email address + + + Email is required + + +
+
+ + + + Password must be at least 8 characters long + + + Passwords do not match + + + + + + Passwords do not match + + +
+
+ Admin + Agent +
+
+ + +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.ts b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.ts new file mode 100644 index 0000000..a16b5de --- /dev/null +++ b/client/src/app/features/users/user-list/user-edit-page/user-edit-page.component.ts @@ -0,0 +1,123 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { ConfirmDialogComponent, ConfirmDialogModel } from 'src/app/shared/confirm-dialog/confirm-dialog.component'; + +// Services +import { NotificationService } from 'src/app/core/services/notification.service'; +import { apiUserService } from 'src/app/core/services/api/api.user.service'; + +function passwordMatchValidator(g: FormGroup) { + const password = g.get('password')?.value; + const confirmPassword = g.get('confirmPassword')?.value; + if (password){ + if (password.length < 8) { + g.get('password')?.setErrors({ passwordLength: true }); + return { passwordLength: true }; + } + if (password !== confirmPassword) { + g.get('confirmPassword')?.setErrors({ mismatch: true }); + return { mismatch: true }; + } + + } + return null; +} + +@Component({ + selector: 'app-favorites-page', + templateUrl: './user-edit-page.component.html', + styleUrls: ['./user-edit-page.component.css'] +}) + +export class UserEditPageComponent implements OnInit { + + id: any = ''; + + userForm: FormGroup; + + constructor( + private route: ActivatedRoute, + private notificationService: NotificationService, + private router: Router, + public dialog: MatDialog, + private apiUserService: apiUserService, + private fb: FormBuilder + ) { + + this.userForm = this.fb.group({ + username: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + password: [''], + confirmPassword: [''], + isAdmin: [false], + isAgent: [false] + }, { validator: passwordMatchValidator }); + + } + + ngOnInit(): void { + this.route.paramMap.subscribe(params => { + this.id = params.get('id'); + + if(this.id) { + this.apiUserService.getUser(this.id).subscribe((data: any) => { + console.log(data); + + // Update form values + this.userForm.patchValue({ + username: data.username, + email: data.email, + isAdmin: data.isAdmin, + isAgent: data.isAgent + }); + }); + } + }); + } + + save(): void { + if (this.userForm.valid) { + let userData = this.userForm.value + console.log(userData); + if(this.id) { + userData._id = this.id; + // Edit User + this.apiUserService.updateUser(userData).subscribe((data: any) => { + this.notificationService.openSnackBar("User updated successfully"); + this.router.navigate(['/users']); + }); + } else { + // New User + this.apiUserService.saveUser(userData).subscribe((data: any) => { + this.notificationService.openSnackBar("User created successfully"); + this.router.navigate(['/users']); + }); + } + } + } + + deleteUser(): void { + if (this.id) { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '250px', + data: {title: 'Delete User ?', message: 'Are you sure you want to delete this user?'} + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + // User clicked Yes + + this.apiUserService.deleteUser(this.id).subscribe((data: any) => { + this.notificationService.openSnackBar("User deleted successfully"); + this.router.navigate(['/users']); + }); + } + }); + } + } + +} + diff --git a/client/src/app/features/users/user-list/user-list.component.html b/client/src/app/features/users/user-list/user-list.component.html index 88cb815..c78cc1b 100644 --- a/client/src/app/features/users/user-list/user-list.component.html +++ b/client/src/app/features/users/user-list/user-list.component.html @@ -1,18 +1,61 @@
- + + +

Users

+
+ + + +
-

Users

+ +
+ -
-
- people_outline -

No users exist.

-
- -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Username + {{element.username}} + Email + {{element.email}} + Admin + check + Agent + check +
+
+
diff --git a/client/src/app/features/users/user-list/user-list.component.ts b/client/src/app/features/users/user-list/user-list.component.ts index a4f3063..2dbb5a0 100644 --- a/client/src/app/features/users/user-list/user-list.component.ts +++ b/client/src/app/features/users/user-list/user-list.component.ts @@ -1,8 +1,14 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; import { Title } from '@angular/platform-browser'; - import { NGXLogger } from 'ngx-logger'; import { NotificationService } from 'src/app/core/services/notification.service'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; + +import { apiUserService } from 'src/app/core/services/api/api.user.service'; +import { User } from 'src/app/core/services/model/user.interface'; @Component({ selector: 'app-user-list', @@ -11,14 +17,38 @@ import { NotificationService } from 'src/app/core/services/notification.service' }) export class UserListComponent implements OnInit { + displayedColumns: string[] = ['username', 'email', 'admin', 'agent']; + users : MatTableDataSource = new MatTableDataSource(); + @ViewChild(MatPaginator) paginator?: MatPaginator; + @ViewChild(MatSort) sort?: MatSort; + constructor( private logger: NGXLogger, private notificationService: NotificationService, - private titleService: Title + private titleService: Title, + private router: Router, + private apiUserService: apiUserService, ) { } ngOnInit() { this.titleService.setTitle('Jucundus - Users'); this.logger.log('Users loaded'); + this.refreshUsers() + } + + refreshUsers(): void { + this.apiUserService.getAllUsers().subscribe((data: any) => { + console.log(data); + + this.users = new MatTableDataSource(data); + this.users.paginator = this.paginator ?? null; + this.users.sort = this.sort ?? null; + + this.notificationService.openSnackBar("Users loaded"); + }); + } + + openUser(id: string): void { + this.router.navigate(['/users/edit', id]); } } diff --git a/client/src/app/features/users/users-routing.module.ts b/client/src/app/features/users/users-routing.module.ts index f5944f1..332c2dd 100644 --- a/client/src/app/features/users/users-routing.module.ts +++ b/client/src/app/features/users/users-routing.module.ts @@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router'; import { LayoutComponent } from 'src/app/shared/layout/layout.component'; import { UserListComponent } from './user-list/user-list.component'; +import { UserEditPageComponent } from './user-list/user-edit-page/user-edit-page.component'; const routes: Routes = [ { @@ -10,6 +11,7 @@ const routes: Routes = [ component: LayoutComponent, children: [ { path: '', component: UserListComponent }, + { path: 'edit/:id', component: UserEditPageComponent }, ] } ]; diff --git a/client/src/app/features/users/users.module.ts b/client/src/app/features/users/users.module.ts index b127a94..d43648a 100644 --- a/client/src/app/features/users/users.module.ts +++ b/client/src/app/features/users/users.module.ts @@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { UserListComponent } from './user-list/user-list.component'; +import { UserEditPageComponent } from './user-list/user-edit-page/user-edit-page.component'; + import { SharedModule } from 'src/app/shared/shared.module'; @NgModule({ @@ -11,6 +13,9 @@ import { SharedModule } from 'src/app/shared/shared.module'; SharedModule, UsersRoutingModule ], - declarations: [UserListComponent] + declarations: [ + UserListComponent, + UserEditPageComponent + ] }) export class UsersModule { } diff --git a/client/src/app/shared/layout/layout.component.html b/client/src/app/shared/layout/layout.component.html index 7ccd6f6..ead191c 100644 --- a/client/src/app/shared/layout/layout.component.html +++ b/client/src/app/shared/layout/layout.component.html @@ -83,7 +83,12 @@

Pictures

- + + + people + +

Users

+
- - + --> +

User

person diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts index b7b67b4..d47dce7 100644 --- a/client/src/environments/environment.prod.ts +++ b/client/src/environments/environment.prod.ts @@ -3,5 +3,6 @@ import { NgxLoggerLevel } from 'ngx-logger'; export const environment = { production: true, logLevel: NgxLoggerLevel.OFF, - serverLogLevel: NgxLoggerLevel.ERROR + serverLogLevel: NgxLoggerLevel.ERROR, + ServeurURL: "http://localhost:3000" }; diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index e01b397..ea70462 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts @@ -8,5 +8,6 @@ import { NgxLoggerLevel } from 'ngx-logger'; export const environment = { production: false, logLevel: NgxLoggerLevel.TRACE, - serverLogLevel: NgxLoggerLevel.OFF + serverLogLevel: NgxLoggerLevel.OFF, + ServeurURL: "http://localhost:3000" }; diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 13dd6e8..8b63bd1 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -16,6 +16,7 @@ services: - ./backend:/backend ports: - "3000:3000" + - "9229:9229" command: ["npm", "run", "dev"] depends_on: - db \ No newline at end of file