From c43c3b33e646e6299b49bac8d5d7a8032ff0489f Mon Sep 17 00:00:00 2001 From: JackyFTW Date: Thu, 26 Mar 2026 16:14:35 -0400 Subject: [PATCH 1/4] Began tedious process of nuking passport in auth layer --- server/app.js | 33 +- server/auth.js | 39 +- server/controllers/AuthController.js | 89 ++ server/controllers/UserController.js | 92 +- server/database/models/Session.js | 20 - server/database/models/UserSessions.js | 28 + server/database/models/Users.js | 58 +- server/database/schema.sql | 30 +- server/package-lock.json | 1090 +++++++++++++----------- server/package.json | 18 +- server/routes/index.js | 15 +- server/services/UserService.js | 110 --- 12 files changed, 803 insertions(+), 819 deletions(-) create mode 100644 server/controllers/AuthController.js delete mode 100644 server/database/models/Session.js create mode 100644 server/database/models/UserSessions.js diff --git a/server/app.js b/server/app.js index 743411cfc..a34b6b991 100644 --- a/server/app.js +++ b/server/app.js @@ -2,10 +2,7 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); -const cookieParser = require('cookie-parser'); -const session = require('express-session'); -const passport = require('passport'); -const auth = require('./auth'); +const session = require('express-session') const fs = require('fs'); const https = require('https'); const http = require('http'); @@ -21,19 +18,6 @@ app.use(express.urlencoded({ app.use(express.json()); -app.use(passport.initialize()); -app.use(session({ - name: 'session', - secret: process.env.KEY, - resave: false, - saveUninitialized: true, -})); -app.use(passport.authenticate('session')); -auth(passport); - - -app.use(cookieParser()); - app.use(cors({ origin: function (origin, callback) { if (allowedOrigins.indexOf(origin) !== -1) { @@ -44,6 +28,20 @@ app.use(cors({ }, credentials: true, })); +app.use(session({ + secret: process.env.KEY, + resave: false, + saveUninitialized: true, + cookie: function(req) { + var match = req.url.match(/^\/([^/]+)/); + return { + path: match ? '/' + match[1] : '/', + httpOnly: true, + secure: req.secure || false, + maxAge: 60000 + } + } +})); app.use(require('./routes')); @@ -67,5 +65,4 @@ if (fs.existsSync(private_key, fs.R_OK) && fs.existsSync(certificate, fs.R_OK)) server = http.createServer(app); } - server.listen(port, () => console.log(`Listening on port ${port}!`)); diff --git a/server/auth.js b/server/auth.js index 9299bf76c..8dc2459fa 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,29 +1,18 @@ -// let GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; -const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { google } = require('googleapis'); -module.exports = (passport) => { - passport.serializeUser((user, done) => { - done(null, user); - }); +const oauth2Client = new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_SECRET, + process.env.GOOGLE_CALLBACK_URL, +); - passport.deserializeUser((user, done) => { - done(null, user); - }); +const scopes = [ + 'profile', + 'email' +]; - passport.use(new GoogleStrategy({ - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_URL, - passReqToCallback: true, - }, - async (request, accessToken, refreshToken, profile, done) => { - try { - return done(null, { - profile: profile, - token: accessToken, - }); - } catch (error) { - return done(error, null); - } - })); +module.exports = { + oauth2Client, + scopes }; + diff --git a/server/controllers/AuthController.js b/server/controllers/AuthController.js new file mode 100644 index 000000000..e3d17013f --- /dev/null +++ b/server/controllers/AuthController.js @@ -0,0 +1,89 @@ +const db = require('../database'); +const crypto = require('crypto'); +const { oauth2Client, scopes } = require('../auth'); +const jwt = require('jsonwebtoken'); + +const google = (req, res) => { + const state = crypto.randomBytes(32).toString('hex'); + req.session.state = state; + + const authorizationUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: scopes, + include_granted_scopes: true, + state: state, + }); + + res.redirect(authorizationUrl) +}; + +const googleCallback = async (req, res) => { + if(req.query.error) { + res.status(400).json({ error: 'Error during Google consent screen authorization.' }); + return; + } + + if(req.query.state !== req.session.state) { + res.status(400).json({ error: 'Invalid state parameter. Aborting due to possible CSRF attack.' }); + return; + } + + let ticket; + try { + let { tokens } = await oauth2Client.getToken(req.query.code); + + ticket = await oauth2Client.verifyIdToken({ + idToken: tokens.id_token, + audience: oauth2Client._clientId, + }); + } catch { + res.status(500).json({ error: 'Failed to exchange provided code for Google token.' }); + } + + let user = await db.Users.findOne({ + where: { + googleAccountId: ticket.getUserId() + } + }); + + if(!user) { + user = await db.Users.create({ + googleAccountId: ticket.getUserId(), + email: ticket.getPayload().email, + firstName: ticket.getPayload().given_name, + lastInitial: ticket.getPayload().family_name ? ticket.getPayload().family_name.charAt(0) : null, + pfp: ticket.getPayload().picture, + }); + } + + const iat = Math.floor(Date.now() / 1000); + const token = jwt.sign({ + iss: "all.rit.edu", + sub: user.id, + aud: user.id, + nbf: iat, + iat: iat, + googleAccountId: user.googleAccountId, + email: user.email, + firstName: user.firstName, + lastInitial: user.lastInitial, + pfp: user.pfp + }, process.env.KEY, { + algorithm: 'HS256' + }); + + const userSession = await db.UserSessions.create({ + userId: user.id, + jwt: token, + issuedAt: new Date(), + }); + + return res.status(200).json({ + jwt: userSession.jwt + }); +}; + +module.exports = { + google, + googleCallback +} \ No newline at end of file diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 62d7fa61c..01506eda9 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,27 +1,14 @@ -const passport = require('passport'); const UserService = require('../services/UserService'); -// Checks if it's a guest or user entering webpage -const main = (req, res) => { +const index = (req, res) => { UserService.getSession(req.session.token).then((data) => { req.session.token = data.token; res.json(data.user); }); }; -const getUser = (req, res) => { - UserService.getUser(req.params.userID).then((records) => { - res.json(records); - }); -}; - -const getUserToDoLabs = (req, res) => { - UserService.getUserToDoLabs(req.params.userID).then((records) => { - res.json(records); - }); -}; -const getUserAssignedLabs = (req, res) => { - UserService.getUserAssignedLabs(req.params.userID).then((records) => { +const getUserInstructingGroups = (req, res) => { + UserService.getUserInstructingGroups(req.params.userID).then((records) => { res.json(records); }); }; @@ -32,83 +19,22 @@ const getUserEnrolledGroups = (req, res) => { }); }; -const getUserInstructingGroups = (req, res) => { - UserService.getUserInstructingGroups(req.params.userID).then((records) => { +const getUserAssignedLabs = (req, res) => { + UserService.getUserAssignedLabs(req.params.userID).then((records) => { res.json(records); }); }; -// Authenticates User through Google OAuth -const authenticate = passport.authenticate('google', { - scope: ['email', 'profile'], -}); - -// Callback used for Google OAuth -const authenticateRedirect = passport.authenticate('google', { - keepSessionInfo: true, - failureRedirect: '/', -}); - -const authenticateCallback = async (req, res) => { - try { - const data = await UserService.authenticate(req.user.profile); - - if (data) { - await UserService.updateGuestUserId(data.userid, req.session.token); - req.session.token = data.usersessionid; - res.redirect(req.session.url || '/'); - } else { - // Handle case where authentication failed - res.redirect('/login?error=auth_failed'); - } - } catch (error) { - console.error('Error while executing authenticateCallback', error); - // Send user to error page with more specific error message - res.redirect(`/login?error=${encodeURIComponent(error.message)}`); - } -}; - -const developmentLogin = async (req, res) => { - try { - await UserService.updateGuestUserId(req.params.userID, 1); - const user = await UserService.getUser(req.params.userID); - req.session.token = 1; - req.session.userID = user.userid; - req.session.save(); - res.json(user); - } catch (e) { - console.error('Development Login failed! ', e); - } -}; - -const storeURL = (req, res) => { - req.session.url = req.body.url.href; - res.sendStatus(200); -}; - -// Logging out will clear sessions -const logout = (req, res, next) => { - - const redirect = process.env.ENVIRONMENT === 'dev' ? process.env.CLIENT_URL + '/' : req.session.url; - req.logout({keepSessionInfo: true}, (error) => { - if (error) next(error); - req.session.token = null; - req.session.userID = null; - res.redirect(redirect); +const getUserToDoLabs = (req, res) => { + UserService.getUserToDoLabs(req.params.userID).then((records) => { + res.json(records); }); }; module.exports = { - main, - storeURL, - getUser, - logout, - authenticate, - authenticateRedirect, - authenticateCallback, + index, getUserEnrolledGroups, getUserInstructingGroups, getUserAssignedLabs, getUserToDoLabs, - developmentLogin, }; diff --git a/server/database/models/Session.js b/server/database/models/Session.js deleted file mode 100644 index 6496e3060..000000000 --- a/server/database/models/Session.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = (sequelize, DataTypes) => { - const Session = sequelize.define( - 'Session', - { - usersessionid: { - type: DataTypes.BIGINT, - unique: true, - primaryKey: true, - autoIncrement: true, - }, - userid: { - type: DataTypes.INTEGER, - }, - }, - {tableName: 'session'}, - ); - - Session.sync(); - return Session; -}; diff --git a/server/database/models/UserSessions.js b/server/database/models/UserSessions.js new file mode 100644 index 000000000..71e9cc7f6 --- /dev/null +++ b/server/database/models/UserSessions.js @@ -0,0 +1,28 @@ +module.exports = (sequelize, DataTypes) => { + const UserSessions = sequelize.define( + 'UserSessions', + { + id: { + type: DataTypes.BIGINT, + unique: true, + primaryKey: true, + autoIncrement: true, + }, + userId: { + type: DataTypes.BIGINT, + }, + jwt: { + type: DataTypes.TEXT + }, + issuedAt: { + type: DataTypes.DATE + } + }, + { + tableName: 'user_sessions' + }, + ); + + UserSessions.sync(); + return UserSessions; +}; diff --git a/server/database/models/Users.js b/server/database/models/Users.js index 029210795..bb92710a1 100644 --- a/server/database/models/Users.js +++ b/server/database/models/Users.js @@ -2,57 +2,33 @@ module.exports = (sequelize, DataTypes) => { const Users = sequelize.define( 'Users', { - userid: { - type: DataTypes.INTEGER, + id: { + type: DataTypes.BIGINT, unique: true, primaryKey: true, autoIncrement: true, }, - firstname: {type: DataTypes.TEXT}, - lastinitial: {type: DataTypes.CHAR(1)}, - email1: { + googleAccountId: { type: DataTypes.TEXT, - unique: { - args: true, - msg: 'Email is not unique!', - }, + unique: true, + }, + email: { + type: DataTypes.TEXT }, - email2: { + firstName: { type: DataTypes.TEXT, - unique: { - args: true, - msg: 'Email is not unique!', - }, }, - userpfp: {type: DataTypes.TEXT}, + lastInitial: { + type: DataTypes.CHAR(1) + }, + pfp: { + type: DataTypes.TEXT + } + }, + { + tableName: 'users' }, - {tableName: 'users'}, ); Users.sync(); - // Users.sync({ - // force: false - // }).then(function() { - // Users.create({ - // firstname: 'Samuel', - // lastinitial: 'M', - // email1: 'sam@test.com', - // }); - // Users.create({ - // firstname: 'Su Thit', - // lastinitial: 'T', - // email1: 'stthazi@mock.com', - // email2: 'stthazi2@test.com' - // }) - // Users.create({ - // firstname: 'John', - // lastinitial: 'D', - // email1: 'johndoe@test.com', - // }); - // Users.create({ - // firstname: 'Jane', - // lastinitial: 'D', - // email1: 'janedoe@test.com', - // }); - // }) return Users; }; diff --git a/server/database/schema.sql b/server/database/schema.sql index 69d6e3521..5c97c7a74 100644 --- a/server/database/schema.sql +++ b/server/database/schema.sql @@ -303,13 +303,6 @@ create table professors primary key (id) ); -create table session -( - usersessionid serial, - userid integer, - primary key (usersessionid) -); - create table team_members ( id serial, @@ -376,20 +369,19 @@ create table userlabcompletion primary key (id) ); -create table users +create table users ( - userid serial, - firstname text, - lastinitial char, - email1 text, - email2 text, - userpfp text, - PRIMARY KEY (userid), - UNIQUE (email1), - UNIQUE (email2) + id serial unique, + "googleAccountId" text unique, + email text, + "firstName" text, + "lastInitial" char(1), + pfp text, + primary key (id) ); -insert into users (userid, firstname, lastinitial, email1, email2, userpfp) -VALUES (98, 'Ally', 'A', 'allyaccessibility@all.edu', null, null), (99, 'Lily', 'L', 'lilylabs@all.edu', null, null), (100, 'Edna', 'E', 'ednaeducation@all.edu', null, null); +insert into users (id, "googleAccountId", email, "firstName", "lastInitial", pfp) +values (1, null, 'allyaccessibility@all.edu', 'Ally', 'A', null), (2, null, 'lilylabs@all.edu', 'Lily', 'L', null), (3, null, 'ednaeducation@all.edu', 'Edna', 'E', null); +ALTER SEQUENCE users_id_seq RESTART WITH 4; create table lab8_exercise ( diff --git a/server/package-lock.json b/server/package-lock.json index f055af876..26d9a731d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,15 +9,15 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "cookie-parser": "^1.4.6", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.18.2", - "express-session": "^1.17.3", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "pg": "^8.16.3", - "sequelize": "^6.37.1" + "cors": "^2.8.6", + "crypto": "^1.0.1", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "express-session": "^1.19.0", + "googleapis": "^171.4.0", + "jsonwebtoken": "^9.0.3", + "pg": "^8.20.0", + "sequelize": "^6.37.8" }, "devDependencies": { "eslint": "^9.39.2", @@ -1565,13 +1565,13 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -1600,6 +1600,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1693,12 +1702,6 @@ "dev": true, "license": "Python-2.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==", - "license": "MIT" - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1822,14 +1825,25 @@ "dev": true, "license": "MIT" }, - "node_modules/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.9.4", @@ -1841,45 +1855,39 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "*" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1948,6 +1956,12 @@ "node-int64": "^0.4.0" } }, + "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==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2241,15 +2255,16 @@ "license": "MIT" }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2277,29 +2292,19 @@ "node": ">= 0.6" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=6.6.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -2307,6 +2312,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/create-jest": { @@ -2346,6 +2355,22 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2404,16 +2429,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2435,9 +2450,9 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2466,6 +2481,15 @@ "node": ">= 0.4" } }, + "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==", + "license": "Apache-2.0", + "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", @@ -2898,45 +2922,42 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -2944,22 +2965,26 @@ } }, "node_modules/express-session": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", - "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", "license": "MIT", "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.7", - "debug": "2.6.9", + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", "depd": "~2.0.0", "on-headers": "~1.1.0", "parseurl": "~1.3.3", - "safe-buffer": "5.2.1", + "safe-buffer": "~5.2.1", "uid-safe": "~2.1.5" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-session/node_modules/cookie-signature": { @@ -2983,19 +3008,10 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -3036,6 +3052,29 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3063,38 +3102,26 @@ } }, "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3133,6 +3160,18 @@ "dev": true, "license": "ISC" }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3143,12 +3182,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs.realpath": { @@ -3182,6 +3221,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3323,6 +3390,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "171.4.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz", + "integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3403,6 +3525,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3414,15 +3549,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -3594,6 +3733,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4327,6 +4472,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4368,6 +4522,61 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4569,6 +4778,42 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4576,6 +4821,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -4756,19 +5007,22 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -4780,15 +5034,6 @@ "dev": true, "license": "MIT" }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4803,37 +5048,29 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -4920,14 +5157,52 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4965,12 +5240,6 @@ "node": ">=8" } }, - "node_modules/oauth": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", - "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5017,7 +5286,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5140,64 +5408,6 @@ "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==", - "license": "MIT", - "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-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", - "license": "MIT", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-oauth2": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", - "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", - "license": "MIT", - "dependencies": { - "base64url": "3.x.x", - "oauth": "0.10.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-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-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5236,25 +5446,24 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -5262,7 +5471,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -5274,16 +5483,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", "license": "MIT" }, "node_modules/pg-int8": { @@ -5296,18 +5505,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", "license": "MIT" }, "node_modules/pg-types": { @@ -5608,9 +5817,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5641,18 +5850,18 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/react-is": { @@ -5795,6 +6004,22 @@ "dev": true, "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5832,67 +6057,29 @@ } }, "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "node": ">= 18" }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/sequelize": { @@ -5979,91 +6166,22 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-static/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "node": ">= 18" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -6568,13 +6686,14 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" @@ -6607,12 +6726,6 @@ "node": ">= 0.8" } }, - "node_modules/uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6669,14 +6782,11 @@ "punycode": "^2.1.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" }, "node_modules/uuid": { "version": "8.3.2", @@ -6730,6 +6840,15 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6787,7 +6906,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/server/package.json b/server/package.json index fd29f40ea..a154bd2d9 100644 --- a/server/package.json +++ b/server/package.json @@ -10,15 +10,15 @@ "author": "", "license": "MIT", "dependencies": { - "cookie-parser": "^1.4.6", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.18.2", - "express-session": "^1.17.3", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "pg": "^8.16.3", - "sequelize": "^6.37.1" + "cors": "^2.8.6", + "crypto": "^1.0.1", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "express-session": "^1.19.0", + "googleapis": "^171.4.0", + "jsonwebtoken": "^9.0.3", + "pg": "^8.20.0", + "sequelize": "^6.37.8" }, "devDependencies": { "eslint": "^9.39.2", diff --git a/server/routes/index.js b/server/routes/index.js index 12a5fbf79..51456ee37 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); // Universal Controllers +const AuthController = require('../controllers/AuthController') const UserController = require('../controllers/UserController'); const UserLabController = require('../controllers/UserLabController'); const PageController = require('../controllers/PageController'); @@ -68,16 +69,14 @@ const TeamMemberController = require('../controllers/TeamMemberController'); // Imagine Controller const ImagineController = require('../controllers/ImagineController'); +// Auth Routes +router.get('/auth/google', AuthController.google); +router.get('/auth/google/callback', AuthController.googleCallback); + // User Routes -router.post('/url', UserController.storeURL); -router.get('/auth/google', UserController.authenticate); -router.get('/auth/google/callback', UserController.authenticateRedirect, UserController.authenticateCallback); -router.get('/logout', UserController.logout); -router.get('/user', UserController.main); -router.get('/user/:userID', UserController.getUser); -router.get('/user/:userID/development', UserController.developmentLogin); -router.get('/user/:userID/enrolled', UserController.getUserEnrolledGroups); +router.get('/user', UserController.index); router.get('/user/:userID/groups', UserController.getUserInstructingGroups); +router.get('/user/:userID/enrolled', UserController.getUserEnrolledGroups); router.get('/user/:userID/assigned', UserController.getUserAssignedLabs); router.get('/user/:userID/todo', UserController.getUserToDoLabs); router.get('/user/:userID/labrecords', UserLabController.getUserLabRecords); diff --git a/server/services/UserService.js b/server/services/UserService.js index 064352efd..e81f182cc 100644 --- a/server/services/UserService.js +++ b/server/services/UserService.js @@ -1,112 +1,5 @@ const db = require('../database'); -const updateGuestUserId = (userid, usersessionid) => { - return db.Session.findByPk(usersessionid) - .then((session) => { - if (session) { - session.userid = userid; - return session.save(); - } - return true; - }) - .catch((error) => { - console.log('unable to update guest userid:', error); - return true; - }); -}; - -const authenticate = async (data) => { - try { - const firstName = data.name.givenName; - const lastInitial = data.name.familyName.slice(0, 1); - const email = data.emails[0].value; - const userpfp = data.photos[0].value; - - // First check if user exists with this email - const existingUser = await db.Users.findOne({where: {email1: email}}); - - if (existingUser) { - if (!existingUser.userpfp) { - existingUser.set({ - userpfp: userpfp, - }); - await existingUser.save(); - } - // If user exists, create a new session - const session = await db.Session.create({userid: existingUser.userid}); - return session; - } - - // If no existing user, proceed with new account creation - const newAccount = { - firstName, - lastInitial, - email1: email, - userpfp, - }; - return await createNewAccountAndSession(newAccount); - } catch (error) { - console.error('Error while authenticating: ', error); - throw error; - } -}; - -const createNewAccountAndSession = async (newAccount) => { - try { - const user = await db.Users.create({ - firstname: newAccount.firstName, - lastinitial: newAccount.lastInitial, - email1: newAccount.email1, - userpfp: newAccount.userpfp, - }); - - const newSession = await db.Session.create({ - userid: user.userid, - }); - - return newSession; - } catch (error) { - console.error('Error creating new account and session', error); - throw error; - } -}; - -const getSession = async (token) => { - const createUserAndSession = async () => { - // Creates a brand new user and session - const user = await db.Users.create({}); - const session = await db.Session.create({userid: user.userid}); - return {user, token: session.usersessionid}; - }; - - try { - // if the request doesn't have a token, create a new user and session - if (!token) { - return createUserAndSession(); - } - - // if the request token doesn't map to a session, - // create a new user and session - const session = await db.Session.findByPk(token); - if (!session) { - return createUserAndSession(); - } - - // if the session maps to a null user, create a new user and session - // we create a new session because a session should depend on a user - // and not the other way around - const user = await db.Users.findByPk(session.userid); - if (!user) { - return createUserAndSession(); - } - - return {user, token}; - } catch (error) { - console.error('Error getting session:', error); - throw error; - } -}; - const getUserEnrolledGroups = (userid) => { return db.sequelize.query( `SELECT * FROM "enrollment" @@ -182,10 +75,7 @@ const getUser = (userid) => { module.exports = { getUserEnrolledGroups, getUser, - getSession, getUserInstructingGroups, getUserToDoLabs, getUserAssignedLabs, - authenticate, - updateGuestUserId, }; From 21774d4fa8ea63456e42d01038744b6fbfd21c29 Mon Sep 17 00:00:00 2001 From: JackyFTW Date: Thu, 26 Mar 2026 18:41:05 -0400 Subject: [PATCH 2/4] Secured many routes using custom auth middleware, frontend not tested yet --- server/auth.js | 26 ++++++- server/controllers/AuthController.js | 29 ++++---- server/controllers/GroupController.js | 77 ++++++++++----------- server/controllers/UserController.js | 19 +++-- server/controllers/UserLabController.js | 39 +++++------ server/database/models/UserSessions.js | 28 -------- server/package-lock.json | 8 --- server/package.json | 1 - server/routes/index.js | 25 ++++--- server/services/GroupService.js | 88 ++++++++++++----------- server/services/UserLabService.js | 75 ++++++++++---------- server/services/UserService.js | 92 ++++++++++++------------- 12 files changed, 239 insertions(+), 268 deletions(-) delete mode 100644 server/database/models/UserSessions.js diff --git a/server/auth.js b/server/auth.js index 8dc2459fa..8cdada8f3 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,4 +1,5 @@ const { google } = require('googleapis'); +const jwt = require('jsonwebtoken'); const oauth2Client = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, @@ -11,8 +12,31 @@ const scopes = [ 'email' ]; +const authMiddleware = async (req, res, next) => { + const authHeader = req.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(400).json({ error: 'Token missing or malformed.' }); + return; + } + + const token = authHeader.substring(7); + + try { + const verifiedToken = jwt.verify(token, process.env.KEY, { + algorithms: ['HS256'], + issuer: 'all.rit.edu' + }); + req.userId = verifiedToken.sub; + next(); + } catch { + res.status(401).json({ error: 'Unable to verify token.' }); + return; + } +}; + module.exports = { oauth2Client, - scopes + scopes, + authMiddleware }; diff --git a/server/controllers/AuthController.js b/server/controllers/AuthController.js index e3d17013f..6831eb15f 100644 --- a/server/controllers/AuthController.js +++ b/server/controllers/AuthController.js @@ -6,7 +6,7 @@ const jwt = require('jsonwebtoken'); const google = (req, res) => { const state = crypto.randomBytes(32).toString('hex'); req.session.state = state; - + const authorizationUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, @@ -18,12 +18,12 @@ const google = (req, res) => { }; const googleCallback = async (req, res) => { - if(req.query.error) { + if (req.query.error) { res.status(400).json({ error: 'Error during Google consent screen authorization.' }); return; } - if(req.query.state !== req.session.state) { + if (req.query.state !== req.session.state) { res.status(400).json({ error: 'Invalid state parameter. Aborting due to possible CSRF attack.' }); return; } @@ -31,7 +31,7 @@ const googleCallback = async (req, res) => { let ticket; try { let { tokens } = await oauth2Client.getToken(req.query.code); - + ticket = await oauth2Client.verifyIdToken({ idToken: tokens.id_token, audience: oauth2Client._clientId, @@ -40,13 +40,13 @@ const googleCallback = async (req, res) => { res.status(500).json({ error: 'Failed to exchange provided code for Google token.' }); } - let user = await db.Users.findOne({ - where: { - googleAccountId: ticket.getUserId() - } + let user = await db.Users.findOne({ + where: { + googleAccountId: ticket.getUserId() + } }); - if(!user) { + if (!user) { user = await db.Users.create({ googleAccountId: ticket.getUserId(), email: ticket.getPayload().email, @@ -58,9 +58,10 @@ const googleCallback = async (req, res) => { const iat = Math.floor(Date.now() / 1000); const token = jwt.sign({ - iss: "all.rit.edu", + iss: 'all.rit.edu', sub: user.id, aud: user.id, + exp: iat + (60 * 60 * 24), // Token valid for 1 day nbf: iat, iat: iat, googleAccountId: user.googleAccountId, @@ -72,14 +73,8 @@ const googleCallback = async (req, res) => { algorithm: 'HS256' }); - const userSession = await db.UserSessions.create({ - userId: user.id, - jwt: token, - issuedAt: new Date(), - }); - return res.status(200).json({ - jwt: userSession.jwt + token: token }); }; diff --git a/server/controllers/GroupController.js b/server/controllers/GroupController.js index 53aa6fef4..1bef616bb 100644 --- a/server/controllers/GroupController.js +++ b/server/controllers/GroupController.js @@ -1,38 +1,22 @@ const GroupService = require('../services/GroupService'); -const getGroupLabs = async (req, res) => { - try { - const data = await GroupService.getGroupLabs(req.params.groupID); - res.status(200).json(data); - } catch (error) { - console.error('Error while fetching group labs', error); - res.status(500).json({ message: error.message }); - } -}; - -const getGroupEnrolledStudents = async (req, res) => { - try { - const data = await GroupService.getGroupEnrolledStudents(req.params.groupID); - res.status(200).json(data); - } catch (error) { - console.error('Error while getting students group', error); - res.status(500).json({ message: error.message }); - } -}; - -const getCompletedGroupLabs = async (req, res) => { +const createGroup = async (req, res) => { try { - const data = await GroupService.getCompletedGroupLabs(req.params.userID, req.params.groupID); + const data = await GroupService.createGroup( + req.userId, + req.body.groupName, + req.body.color, + ); res.status(200).json(data); } catch (error) { - console.error('Error while getting completed group labs', error); - res.status(500).json({ message: error.message }); + console.error('Error while creating group', error); + res.status(500).json({ error: error.message }); } }; const enrollUserInGroup = (req, res) => { GroupService.enrollUserInGroup( - req.body.userID, + req.userId, req.body.inviteCode, ).then((response) => { // todo: figure out how to send status code along with message, @@ -52,25 +36,38 @@ const enrollUserInGroup = (req, res) => { }; const unenrollUserFromGroup = (req, res) => { - GroupService.unenrollUserFromGroup({ - userID: req.body.userID, - groupID: req.body.groupID, - }).then(() => { + GroupService.unenrollUserFromGroup(req.userId, req.body.groupId).then(() => { res.sendStatus(200); }); }; -const createGroup = async (req, res) => { +const getGroupLabs = async (req, res) => { try { - const data = await GroupService.createGroup( - req.body.userID, - req.body.groupName, - req.body.color, - ); + const data = await GroupService.getGroupLabs(req.params.groupID); res.status(200).json(data); } catch (error) { - console.error('Error while creating group', error); - res.status(500).json({ error: error.message }); + console.error('Error while fetching group labs', error); + res.status(500).json({ message: error.message }); + } +}; + +const getGroupEnrolledStudents = async (req, res) => { + try { + const data = await GroupService.getGroupEnrolledStudents(req.params.groupID); + res.status(200).json(data); + } catch (error) { + console.error('Error while getting students group', error); + res.status(500).json({ message: error.message }); + } +}; + +const getCompletedGroupLabs = async (req, res) => { + try { + const data = await GroupService.getCompletedGroupLabs(req.params.userID, req.params.groupID); + res.status(200).json(data); + } catch (error) { + console.error('Error while getting completed group labs', error); + res.status(500).json({ message: error.message }); } }; @@ -126,13 +123,13 @@ const updateGroup = async (req, res) => { }; module.exports = { + createGroup, + enrollUserInGroup, + unenrollUserFromGroup, updateGroup, deleteGroup, deleteGroupLab, addGroupLab, - createGroup, - unenrollUserFromGroup, - enrollUserInGroup, getCompletedGroupLabs, getGroupEnrolledStudents, getGroupLabs, diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 01506eda9..de97ed922 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,40 +1,39 @@ const UserService = require('../services/UserService'); -const index = (req, res) => { - UserService.getSession(req.session.token).then((data) => { - req.session.token = data.token; - res.json(data.user); +const getUser = (req, res) => { + UserService.getUser(req.userId).then((user) => { + res.json(user); }); }; const getUserInstructingGroups = (req, res) => { - UserService.getUserInstructingGroups(req.params.userID).then((records) => { + UserService.getUserInstructingGroups(req.userId).then((records) => { res.json(records); }); }; const getUserEnrolledGroups = (req, res) => { - UserService.getUserEnrolledGroups(req.params.userID).then((records) => { + UserService.getUserEnrolledGroups(req.userId).then((records) => { res.json(records); }); }; const getUserAssignedLabs = (req, res) => { - UserService.getUserAssignedLabs(req.params.userID).then((records) => { + UserService.getUserAssignedLabs(req.userId).then((records) => { res.json(records); }); }; const getUserToDoLabs = (req, res) => { - UserService.getUserToDoLabs(req.params.userID).then((records) => { + UserService.getUserToDoLabs(req.userId).then((records) => { res.json(records); }); }; module.exports = { - index, - getUserEnrolledGroups, + getUser, getUserInstructingGroups, + getUserEnrolledGroups, getUserAssignedLabs, getUserToDoLabs, }; diff --git a/server/controllers/UserLabController.js b/server/controllers/UserLabController.js index 53dba415d..c559dbf3f 100644 --- a/server/controllers/UserLabController.js +++ b/server/controllers/UserLabController.js @@ -1,5 +1,21 @@ const UserLabService = require('../services/UserLabService'); +const getUserLabRecords = async (req, res) => { + try { + const labs = await UserLabService.getUserLabRecords(req.userId); + res.status(200).json(labs); + } catch (error) { + console.error('Error while executing getUserLabRecords', error); + res.status(500).json({ error: error.message }); + } +}; + +const getUserLabCompletion = (req, res) => { + UserLabService.getUserLabCompletion(req.userId, req.params.labId).then((records) => { + res.json(records); + }); +}; + const completeAbout = (req, res) => { UserLabService.completeAbout({ labid: req.body.labid, @@ -103,26 +119,9 @@ const userCompleteQuiz = (req, res) => { }); }; -const getUserLabCompletion = (req, res) => { - UserLabService.getUserLabCompletion({ - userid: req.params.userID, - labid: req.params.labID, - }).then((records) => { - res.json(records); - }); -}; - -const getUserLabRecords = async (req, res) => { - try { - const labs = await UserLabService.getUserLabRecords(req.params.userID); - res.status(200).json(labs); - } catch (error) { - console.error('Error while executing getUserLabRecords', error); - res.status(500).json({ error: error.message }); - } -}; - module.exports = { + getUserLabRecords, + getUserLabCompletion, completeAbout, completeReading, completeExercise, @@ -133,6 +132,4 @@ module.exports = { userCompleteExercise, userCompleteReinforcement, userCompleteQuiz, - getUserLabCompletion, - getUserLabRecords, }; diff --git a/server/database/models/UserSessions.js b/server/database/models/UserSessions.js deleted file mode 100644 index 71e9cc7f6..000000000 --- a/server/database/models/UserSessions.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = (sequelize, DataTypes) => { - const UserSessions = sequelize.define( - 'UserSessions', - { - id: { - type: DataTypes.BIGINT, - unique: true, - primaryKey: true, - autoIncrement: true, - }, - userId: { - type: DataTypes.BIGINT, - }, - jwt: { - type: DataTypes.TEXT - }, - issuedAt: { - type: DataTypes.DATE - } - }, - { - tableName: 'user_sessions' - }, - ); - - UserSessions.sync(); - return UserSessions; -}; diff --git a/server/package-lock.json b/server/package-lock.json index 26d9a731d..a6140149c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "cors": "^2.8.6", - "crypto": "^1.0.1", "dotenv": "^17.3.1", "express": "^5.2.1", "express-session": "^1.19.0", @@ -2355,13 +2354,6 @@ "node": ">= 8" } }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", - "license": "ISC" - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", diff --git a/server/package.json b/server/package.json index a154bd2d9..acc192b87 100644 --- a/server/package.json +++ b/server/package.json @@ -11,7 +11,6 @@ "license": "MIT", "dependencies": { "cors": "^2.8.6", - "crypto": "^1.0.1", "dotenv": "^17.3.1", "express": "^5.2.1", "express-session": "^1.19.0", diff --git a/server/routes/index.js b/server/routes/index.js index 51456ee37..b48fce2d0 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,6 +1,9 @@ const express = require('express'); const router = express.Router(); +// Middleware +const { authMiddleware } = require('../auth'); + // Universal Controllers const AuthController = require('../controllers/AuthController') const UserController = require('../controllers/UserController'); @@ -74,18 +77,20 @@ router.get('/auth/google', AuthController.google); router.get('/auth/google/callback', AuthController.googleCallback); // User Routes -router.get('/user', UserController.index); -router.get('/user/:userID/groups', UserController.getUserInstructingGroups); -router.get('/user/:userID/enrolled', UserController.getUserEnrolledGroups); -router.get('/user/:userID/assigned', UserController.getUserAssignedLabs); -router.get('/user/:userID/todo', UserController.getUserToDoLabs); -router.get('/user/:userID/labrecords', UserLabController.getUserLabRecords); -router.get('/user/:userID/:labID', UserLabController.getUserLabCompletion); +router.get('/user', authMiddleware, UserController.getUser); +router.get('/user/groups', authMiddleware, UserController.getUserInstructingGroups); +router.get('/user/groups/enrolled', authMiddleware, UserController.getUserEnrolledGroups); +router.get('/user/assigned', authMiddleware, UserController.getUserAssignedLabs); +router.get('/user/todo', authMiddleware, UserController.getUserToDoLabs); + +// User Lab Routes +router.get('/user/records', authMiddleware, UserLabController.getUserLabRecords); +router.get('/user/:labId', authMiddleware, UserLabController.getUserLabCompletion); // Group Routes -router.post('/group/enroll', GroupController.enrollUserInGroup); -router.post('/group/unenroll', GroupController.unenrollUserFromGroup); -router.post('/group/create', GroupController.createGroup); +router.post('/group/create', authMiddleware, GroupController.createGroup); +router.post('/group/enroll', authMiddleware, GroupController.enrollUserInGroup); +router.post('/group/unenroll', authMiddleware, GroupController.unenrollUserFromGroup); router.post('/group/:groupID/add', GroupController.addGroupLab); router.put('/group/:groupID/update', GroupController.updateGroup); router.put('/group/:groupID/:labID/delete', GroupController.deleteGroupLab); diff --git a/server/services/GroupService.js b/server/services/GroupService.js index 65af6254d..406106f7d 100644 --- a/server/services/GroupService.js +++ b/server/services/GroupService.js @@ -1,31 +1,23 @@ const db = require('../database'); const crypto = require('crypto'); -const getGroupLabs = (groupid) => { - return db.sequelize.query('SELECT * FROM "labs" JOIN "group_labs" ON "group_labs"."labID"="labs"."id" WHERE "group_labs"."groupID"=(:groupID) AND "group_labs"."isActive"=true', { - replacements: { groupID: groupid }, - type: db.sequelize.QueryTypes.SELECT, - raw: true, - }); -}; - -const getGroupEnrolledStudents = (groupid) => { - return db.sequelize.query('SELECT * FROM "enrollment" JOIN "users" ON "enrollment"."userID"="users"."userid" WHERE "enrollment"."groupID"=(:groupID)', { - replacements: { groupID: groupid }, - type: db.sequelize.QueryTypes.SELECT, - raw: true, - }); -}; - -const getCompletedGroupLabs = (userid, groupid) => { - return db.sequelize.query('SELECT labs."labShortName" FROM userlabcompletion INNER JOIN labs ON labs.id = userlabcompletion.labid INNER JOIN group_labs ON group_labs."labID" = userlabcompletion.labid INNER JOIN enrollment ON enrollment."groupID" = group_labs."groupID" WHERE userlabcompletion.labcompletiontime IS NOT NULL AND userlabcompletion.userid=(:userID) AND group_labs."groupID"= (:groupID) AND enrollment."userID" = (:userID)', { - replacements: { groupID: groupid, userID: userid }, - type: db.sequelize.QueryTypes.SELECT, - raw: true, - }); +const createGroup = async (userId, groupName, color) => { + try { + const data = await db.Groups.create({ + instructorUserID: userId, + groupName: groupName, + createdDate: Date.now(), + color: color, + isActive: true, + code: crypto.randomUUID().toUpperCase().slice(1, 7), + }); + return data; + } catch (error) { + console.error('Error while creating group', error); + } }; -const enrollUserInGroup = (userid, code) => { +const enrollUserInGroup = (userId, code) => { return db.Groups .findOne({ where: { @@ -36,7 +28,7 @@ const enrollUserInGroup = (userid, code) => { // check if user is already enrolled in the group return db.Enrollment.findOne({ where: { - userID: userid, + userID: userId, groupID: group.id, isActive: true, }, @@ -49,7 +41,7 @@ const enrollUserInGroup = (userid, code) => { }; } else { return db.Enrollment.create({ - userID: userid, + userID: userId, groupID: group.id, enrolledDate: Date.now(), isActive: true, @@ -71,16 +63,14 @@ const enrollUserInGroup = (userid, code) => { ); }; -const unenrollUserFromGroup = (data) => { - const userid = data.userID; - const groupid = data.groupID; - if (userid && groupid) { +const unenrollUserFromGroup = (userId, groupId) => { + if (userId && groupId) { return db.Enrollment .findOne({ where: { - userID: userid, - groupID: groupid, + userID: userId, + groupID: groupId, isActive: true, }, }).then((enrollment) => { @@ -94,20 +84,28 @@ const unenrollUserFromGroup = (data) => { return Promise.resolve(); }; -const createGroup = async (userID, groupName, color) => { - try { - const data = await db.Groups.create({ - instructorUserID: userID, - groupName: groupName, - createdDate: Date.now(), - color: color, - isActive: true, - code: crypto.randomUUID().toUpperCase().slice(1, 7), - }); - return data; - } catch (error) { - console.error('Error while creating group', error); - } +const getGroupLabs = (groupid) => { + return db.sequelize.query('SELECT * FROM "labs" JOIN "group_labs" ON "group_labs"."labID"="labs"."id" WHERE "group_labs"."groupID"=(:groupID) AND "group_labs"."isActive"=true', { + replacements: { groupID: groupid }, + type: db.sequelize.QueryTypes.SELECT, + raw: true, + }); +}; + +const getGroupEnrolledStudents = (groupid) => { + return db.sequelize.query('SELECT * FROM "enrollment" JOIN "users" ON "enrollment"."userID"="users"."userid" WHERE "enrollment"."groupID"=(:groupID)', { + replacements: { groupID: groupid }, + type: db.sequelize.QueryTypes.SELECT, + raw: true, + }); +}; + +const getCompletedGroupLabs = (userid, groupid) => { + return db.sequelize.query('SELECT labs."labShortName" FROM userlabcompletion INNER JOIN labs ON labs.id = userlabcompletion.labid INNER JOIN group_labs ON group_labs."labID" = userlabcompletion.labid INNER JOIN enrollment ON enrollment."groupID" = group_labs."groupID" WHERE userlabcompletion.labcompletiontime IS NOT NULL AND userlabcompletion.userid=(:userID) AND group_labs."groupID"= (:groupID) AND enrollment."userID" = (:userID)', { + replacements: { groupID: groupid, userID: userid }, + type: db.sequelize.QueryTypes.SELECT, + raw: true, + }); }; const addGroupLab = async (groupID, labID) => { diff --git a/server/services/UserLabService.js b/server/services/UserLabService.js index 63cbaae67..c32e20ca8 100644 --- a/server/services/UserLabService.js +++ b/server/services/UserLabService.js @@ -1,5 +1,42 @@ const db = require('../database'); +const getUserLabRecords = async (userId) => { + try { + if (userId) { + return db.sequelize.query( + `SELECT * FROM "userlabcompletion" + JOIN "labs" ON "userlabcompletion"."labid"="labs"."id" + WHERE "userlabcompletion"."userid"=(:userId) + `, { + replacements: { userId: userId }, + type: db.sequelize.QueryTypes.SELECT, + raw: true, + }); + } + } catch (error) { + console.warn('Error getting user lab records', error); + } +}; + +const getUserLabCompletion = (userId, labId) => { + if (userId && labId) { + return db.UserLabCompletion + .findOne({ + where: + { + userid: userId, + labid: labId, + }, + }).then((userlabcompletion) => { + return userlabcompletion; + }) + .catch((err) => { + console.log(err); + }); + } + return Promise.resolve(); +}; + const completeAbout = (data) => { const usersessionid = data.usersessionid; const labid = data.labid; @@ -391,44 +428,6 @@ const userCompleteQuiz = (data) => { return Promise.resolve(); }; - -const getUserLabCompletion = (data) => { - if (data.userid) { - return db.UserLabCompletion - .findOne({ - where: - { - userid: data.userid, - labid: data.labid, - }, - }).then((userlabcompletion) => { - return userlabcompletion; - }) - .catch((err) => { - console.log(err); - }); - } - return Promise.resolve(); -}; - -const getUserLabRecords = async (userid) => { - try { - if (userid) { - return db.sequelize.query( - `SELECT * FROM "userlabcompletion" - JOIN "labs" ON "userlabcompletion"."labid"="labs"."id" - WHERE "userlabcompletion"."userid"=(:userID) - `, { - replacements: { userID: userid }, - type: db.sequelize.QueryTypes.SELECT, - raw: true, - }); - } - } catch (error) { - console.warn('Error getting user lab records', error); - } -}; - module.exports = { completeAbout, completeReading, diff --git a/server/services/UserService.js b/server/services/UserService.js index e81f182cc..6dc55eeda 100644 --- a/server/services/UserService.js +++ b/server/services/UserService.js @@ -1,81 +1,75 @@ const db = require('../database'); -const getUserEnrolledGroups = (userid) => { - return db.sequelize.query( - `SELECT * FROM "enrollment" - JOIN "groups" ON "enrollment"."groupID"="groups"."id" - WHERE "enrollment"."userID"=(:userID) AND "enrollment"."isActive"=true - `, - { - replacements: {userID: userid}, - type: db.sequelize.QueryTypes.SELECT, - raw: true, - }, - ); +const getUser = (userId) => { + return db.Users.findOne({ + where: { + id: userId, + } + }); }; -const getUserInstructingGroups = (userid) => { +const getUserInstructingGroups = (userId) => { return db.Groups.findAll({ where: { - instructorUserID: userid, + instructorUserID: userId, isActive: true, }, raw: true, }); }; -// fetches only the labs that the user has been assigned (across all groups) -// but hasn't made any progress in -const getUserToDoLabs = (userid) => { +const getUserEnrolledGroups = (userId) => { return db.sequelize.query( - ` - SELECT DISTINCT "labID", "labName" FROM "group_labs" - JOIN "enrollment" on "group_labs"."groupID" = "enrollment"."groupID" - JOIN "labs" on "labs"."id" = "group_labs" . "labID" - WHERE "enrollment"."userID"=(:userID) AND "labID" NOT IN - (SELECT "labid" FROM "userlabcompletion" - WHERE "userid"=(:userID)) - ORDER BY "labID" ASC + `SELECT * FROM "enrollment" + JOIN "groups" ON "enrollment"."groupID"="groups"."id" + WHERE "enrollment"."userID"=(:userId) AND "enrollment"."isActive"=true `, - { - replacements: {userID: userid}, - type: db.sequelize.QueryTypes.SELECT, - }, + { + replacements: { userId: userId }, + type: db.sequelize.QueryTypes.SELECT, + raw: true, + }, ); }; -const getUserAssignedLabs = (userid) => { +const getUserAssignedLabs = (userId) => { return db.sequelize.query( - `SELECT DISTINCT "labID" FROM "group_labs" + `SELECT DISTINCT "labID" FROM "group_labs" JOIN "enrollment" ON "group_labs"."groupID"="enrollment"."groupID" - WHERE "enrollment"."userID"=(:userID) + WHERE "enrollment"."userID"=(:userId) ORDER BY "labID" ASC `, - { - replacements: {userID: userid}, - type: db.sequelize.QueryTypes.SELECT, - }, + { + replacements: { userId: userId }, + type: db.sequelize.QueryTypes.SELECT, + }, ); }; -const getUser = (userid) => { - return db.Users.findOne({ - where: { - userid: userid, +// fetches only the labs that the user has been assigned (across all groups) +// but hasn't made any progress in +const getUserToDoLabs = (userId) => { + return db.sequelize.query( + ` + SELECT DISTINCT "labID", "labName" FROM "group_labs" + JOIN "enrollment" on "group_labs"."groupID" = "enrollment"."groupID" + JOIN "labs" on "labs"."id" = "group_labs" . "labID" + WHERE "enrollment"."userID"=(:userId) AND "labID" NOT IN + (SELECT "labid" FROM "userlabcompletion" + WHERE "userid"=(:userId)) + ORDER BY "labID" ASC + `, + { + replacements: { userId: userId }, + type: db.sequelize.QueryTypes.SELECT, }, - }) - .then((user) => { - return user; - }) - .catch((err) => { - console.log(err); - }); + ); }; module.exports = { - getUserEnrolledGroups, getUser, + getUserEnrolledGroups, getUserInstructingGroups, - getUserToDoLabs, getUserAssignedLabs, + getUserToDoLabs, }; From a7b2f6709af3c81742871cf0ced61b19cddf67b1 Mon Sep 17 00:00:00 2001 From: JackyFTW Date: Fri, 27 Mar 2026 09:27:57 -0400 Subject: [PATCH 3/4] More work from yesterday on securing routes --- server/controllers/GroupController.js | 66 +++++++------- server/routes/index.js | 6 +- server/services/GroupService.js | 119 ++++++++++++++++---------- 3 files changed, 115 insertions(+), 76 deletions(-) diff --git a/server/controllers/GroupController.js b/server/controllers/GroupController.js index 1bef616bb..da4416614 100644 --- a/server/controllers/GroupController.js +++ b/server/controllers/GroupController.js @@ -14,6 +14,25 @@ const createGroup = async (req, res) => { } }; +const updateGroup = async (req, res) => { + try { + const result = await GroupService.updateGroup( + req.params.groupId, + req.userId, + req.body.groupName, + req.body.groupColor, + ); + if(result.status === 'failure') { + res.status(403).json(result); + } else { + res.status(200).send('Group name/color successfully updated!'); + } + } catch (error) { + console.error('Error in updating groupName:', error); + res.status(500).json({ error: error.message }); + } +}; + const enrollUserInGroup = (req, res) => { GroupService.enrollUserInGroup( req.userId, @@ -36,11 +55,25 @@ const enrollUserInGroup = (req, res) => { }; const unenrollUserFromGroup = (req, res) => { - GroupService.unenrollUserFromGroup(req.userId, req.body.groupId).then(() => { + GroupService.unenrollUserFromGroup(req.userId, req.params.groupId).then(() => { res.sendStatus(200); }); }; +const addGroupLab = async (req, res) => { + try { + const groupLab = await GroupService.addGroupLab( + req.params.groupId, + req.userId, + req.body.labId, + ); + res.status(groupLab.status === 'failure' ? 403 : 200).json(groupLab); + } catch (error) { + console.error('Error while adding lab to group', error); + res.status(500).json({ error: error.message }); + } +}; + const getGroupLabs = async (req, res) => { try { const data = await GroupService.getGroupLabs(req.params.groupID); @@ -71,19 +104,6 @@ const getCompletedGroupLabs = async (req, res) => { } }; -const addGroupLab = async (req, res) => { - try { - const lab = await GroupService.addGroupLab( - req.body.groupID, - req.body.labID, - ); - res.status(200).json(lab); - } catch (error) { - console.error('Error while adding lab to group', error); - res.status(500).json({ error: error.message }); - } -}; - const deleteGroupLab = async (req, res) => { try { await GroupService.deleteGroupLab( @@ -108,28 +128,14 @@ const deleteGroup = async (req, res) => { } }; -const updateGroup = async (req, res) => { - try { - await GroupService.updateGroup( - req.body.groupID, - req.body.groupName, - req.body.groupColor, - ); - res.status(200).send('Group name successfully updated!'); - } catch (error) { - console.error('Error in updating groupName:', error); - res.status(500).json({ error: error.message }); - } -}; - module.exports = { createGroup, + updateGroup, enrollUserInGroup, unenrollUserFromGroup, - updateGroup, + addGroupLab, deleteGroup, deleteGroupLab, - addGroupLab, getCompletedGroupLabs, getGroupEnrolledStudents, getGroupLabs, diff --git a/server/routes/index.js b/server/routes/index.js index b48fce2d0..f40b46346 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -89,10 +89,10 @@ router.get('/user/:labId', authMiddleware, UserLabController.getUserLabCompletio // Group Routes router.post('/group/create', authMiddleware, GroupController.createGroup); +router.put('/group/:groupId/update', authMiddleware, GroupController.updateGroup); router.post('/group/enroll', authMiddleware, GroupController.enrollUserInGroup); -router.post('/group/unenroll', authMiddleware, GroupController.unenrollUserFromGroup); -router.post('/group/:groupID/add', GroupController.addGroupLab); -router.put('/group/:groupID/update', GroupController.updateGroup); +router.post('/group/:groupId/unenroll', authMiddleware, GroupController.unenrollUserFromGroup); +router.post('/group/:groupId/add', authMiddleware, GroupController.addGroupLab); router.put('/group/:groupID/:labID/delete', GroupController.deleteGroupLab); router.put('/group/:groupID/delete', GroupController.deleteGroup); router.get('/group/:groupID/labs', GroupController.getGroupLabs); diff --git a/server/services/GroupService.js b/server/services/GroupService.js index 406106f7d..784d0cca4 100644 --- a/server/services/GroupService.js +++ b/server/services/GroupService.js @@ -17,6 +17,39 @@ const createGroup = async (userId, groupName, color) => { } }; +const updateGroup = async (groupId, userId, groupName, groupColor) => { + try { + const group = await db.Groups.findOne({ + where: { + id: groupId, + instructorUserID: userId, + isActive: true, + } + }); + + console.log(group); + if (!group) { + return { + status: 'failure', + message: 'The specified group does not exist or you are not the instructor of the group.' + }; + } + + return await db.Groups.update( + { + groupName: groupName, + color: groupColor + }, + { + where: { + id: groupId, + }, + }); + } catch (error) { + console.warn('Error updating group name/color: ', error); + } +}; + const enrollUserInGroup = (userId, code) => { return db.Groups .findOne({ @@ -84,6 +117,44 @@ const unenrollUserFromGroup = (userId, groupId) => { return Promise.resolve(); }; +const addGroupLab = async (groupId, userId, labId) => { + try { + const group = await db.Groups.findOne({ + where: { + id: groupId, + instructorUserID: userId, + isActive: true, + } + }); + + if (!group) { + return { + status: 'failure', + message: 'The specified group does not exist or you are not the instructor of the group.' + }; + } + + const [groupLab, created] = await db.GroupLabs.findOrCreate({ + where: { + groupID: groupId, + labID: labId, + }, + defaults: { + isActive: true, + }, + }); + + if (!created && !groupLab.isActive) { + groupLab.isActive = true; + await groupLab.save(); + } + + return groupLab; + } catch (error) { + console.error('Error adding group lab', error); + } +}; + const getGroupLabs = (groupid) => { return db.sequelize.query('SELECT * FROM "labs" JOIN "group_labs" ON "group_labs"."labID"="labs"."id" WHERE "group_labs"."groupID"=(:groupID) AND "group_labs"."isActive"=true', { replacements: { groupID: groupid }, @@ -108,29 +179,6 @@ const getCompletedGroupLabs = (userid, groupid) => { }); }; -const addGroupLab = async (groupID, labID) => { - try { - const [groupLab, created] = await db.GroupLabs.findOrCreate({ - where: { - groupID: groupID, - labID: labID, - }, - defaults: { - isActive: true, - }, - }); - - if (!created && !groupLab.isActive) { - groupLab.isActive = true; - await groupLab.save(); - } - - return groupLab; - } catch (error) { - console.error('Error adding group lab', error); - } -}; - const deleteGroupLab = async (groupID, labID) => { try { return await db.GroupLabs.update( @@ -154,30 +202,15 @@ const deleteGroup = (groupID) => { }); }; - -const updateGroup = async (groupID, groupName, groupColor) => { - try { - return await db.Groups.update( - { groupName: groupName, color: groupColor }, - { - where: { - id: groupID, - }, - }); - } catch (error) { - console.warn('Error updating lab name: ', error); - } -}; - module.exports = { - getGroupLabs, + createGroup, updateGroup, + enrollUserInGroup, + unenrollUserFromGroup, + addGroupLab, + getGroupLabs, deleteGroup, deleteGroupLab, - addGroupLab, - createGroup, - unenrollUserFromGroup, getCompletedGroupLabs, - enrollUserInGroup, getGroupEnrolledStudents, }; From 76a9a5a477e7e333c905000c1c6bc7edf9ecc0fe Mon Sep 17 00:00:00 2001 From: JackyFTW Date: Fri, 27 Mar 2026 11:35:35 -0400 Subject: [PATCH 4/4] More work on group endpoints --- server/controllers/GroupController.js | 28 ++++++++++--------- server/routes/index.js | 16 +++++------ server/services/GroupService.js | 40 +++++++++++++++++++-------- server/services/UserService.js | 4 +-- 4 files changed, 52 insertions(+), 36 deletions(-) diff --git a/server/controllers/GroupController.js b/server/controllers/GroupController.js index da4416614..af3270fa6 100644 --- a/server/controllers/GroupController.js +++ b/server/controllers/GroupController.js @@ -22,7 +22,7 @@ const updateGroup = async (req, res) => { req.body.groupName, req.body.groupColor, ); - if(result.status === 'failure') { + if (result.status === 'failure') { res.status(403).json(result); } else { res.status(200).send('Group name/color successfully updated!'); @@ -33,6 +33,20 @@ const updateGroup = async (req, res) => { } }; +const deleteGroup = async (req, res) => { + try { + const result = await GroupService.deleteGroup( + req.params.groupId, + req.userId + ); + res.status(result.status === 'failure' ? 403 : 200).json(result); + } catch (error) { + console.error('Error while deleting group:', error); + res.status(500).json({ error: error.message }); + } +}; + + const enrollUserInGroup = (req, res) => { GroupService.enrollUserInGroup( req.userId, @@ -116,18 +130,6 @@ const deleteGroupLab = async (req, res) => { } }; -const deleteGroup = async (req, res) => { - try { - const data = await GroupService.deleteGroup( - req.body.groupID, - ); - res.status(200).json(data); - } catch (error) { - console.error('Error while deleting group:', error); - res.status(500).json({ error: error.message }); - } -}; - module.exports = { createGroup, updateGroup, diff --git a/server/routes/index.js b/server/routes/index.js index f40b46346..f907b5b18 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -83,23 +83,21 @@ router.get('/user/groups/enrolled', authMiddleware, UserController.getUserEnroll router.get('/user/assigned', authMiddleware, UserController.getUserAssignedLabs); router.get('/user/todo', authMiddleware, UserController.getUserToDoLabs); -// User Lab Routes -router.get('/user/records', authMiddleware, UserLabController.getUserLabRecords); -router.get('/user/:labId', authMiddleware, UserLabController.getUserLabCompletion); - // Group Routes -router.post('/group/create', authMiddleware, GroupController.createGroup); -router.put('/group/:groupId/update', authMiddleware, GroupController.updateGroup); +router.post('/group', authMiddleware, GroupController.createGroup); +router.put('/group/:groupId', authMiddleware, GroupController.updateGroup); +router.delete('/group/:groupId', authMiddleware, GroupController.deleteGroup); router.post('/group/enroll', authMiddleware, GroupController.enrollUserInGroup); -router.post('/group/:groupId/unenroll', authMiddleware, GroupController.unenrollUserFromGroup); +router.put('/group/:groupId/unenroll', authMiddleware, GroupController.unenrollUserFromGroup); router.post('/group/:groupId/add', authMiddleware, GroupController.addGroupLab); router.put('/group/:groupID/:labID/delete', GroupController.deleteGroupLab); -router.put('/group/:groupID/delete', GroupController.deleteGroup); router.get('/group/:groupID/labs', GroupController.getGroupLabs); router.get('/group/:groupID/labs/:userID/completed', GroupController.getCompletedGroupLabs); router.get('/group/:groupID/enrolled', GroupController.getGroupEnrolledStudents); -// user Lab Routes for lab progress and quiz +// User Lab Routes +router.get('/user/records', authMiddleware, UserLabController.getUserLabRecords); +router.get('/user/:labId', authMiddleware, UserLabController.getUserLabCompletion); router.post('/completeAbout', UserLabController.completeAbout); router.post('/completeReading', UserLabController.completeReading); router.post('/completeExercise', UserLabController.completeExercise); diff --git a/server/services/GroupService.js b/server/services/GroupService.js index 784d0cca4..d783dc93f 100644 --- a/server/services/GroupService.js +++ b/server/services/GroupService.js @@ -27,11 +27,10 @@ const updateGroup = async (groupId, userId, groupName, groupColor) => { } }); - console.log(group); if (!group) { return { status: 'failure', - message: 'The specified group does not exist or you are not the instructor of the group.' + message: 'The specified group does not exist, or you are not the instructor of the group.' }; } @@ -50,6 +49,31 @@ const updateGroup = async (groupId, userId, groupName, groupColor) => { } }; +const deleteGroup = async (groupId, userId) => { + const group = await db.Groups.findOne({ + where: { + id: groupId, + instructorUserID: userId, + isActive: true, + } + }); + + if (!group) { + return { + status: 'failure', + message: 'The specified group does not exist, or you are not the instructor of the group.' + }; + } + + return db.sequelize.query('UPDATE "group_labs" SET "isActive"=false WHERE "group_labs"."groupID"=(:groupId); UPDATE "groups" SET "isActive"=false WHERE "groups"."id"=(:groupId); UPDATE "enrollment" SET "isActive"=false WHERE "enrollment"."groupID" =(:groupId);', { + replacements: { + groupId: groupId + }, + type: db.sequelize.QueryTypes.UPDATE, + raw: true, + }); +}; + const enrollUserInGroup = (userId, code) => { return db.Groups .findOne({ @@ -130,7 +154,7 @@ const addGroupLab = async (groupId, userId, labId) => { if (!group) { return { status: 'failure', - message: 'The specified group does not exist or you are not the instructor of the group.' + message: 'The specified group does not exist, or you are not the instructor of the group.' }; } @@ -194,22 +218,14 @@ const deleteGroupLab = async (groupID, labID) => { } }; -const deleteGroup = (groupID) => { - return db.sequelize.query('UPDATE "group_labs" SET "isActive"=false WHERE "group_labs"."groupID"=(:groupID); UPDATE "groups" SET "isActive"=false WHERE "groups"."id"=(:groupID); UPDATE "enrollment" SET "isActive"=false WHERE "enrollment"."groupID" =(:groupID); ', { - replacements: { groupID: groupID }, - type: db.sequelize.QueryTypes.UPDATE, - raw: true, - }); -}; - module.exports = { createGroup, updateGroup, + deleteGroup, enrollUserInGroup, unenrollUserFromGroup, addGroupLab, getGroupLabs, - deleteGroup, deleteGroupLab, getCompletedGroupLabs, getGroupEnrolledStudents, diff --git a/server/services/UserService.js b/server/services/UserService.js index 6dc55eeda..bbc8f2a89 100644 --- a/server/services/UserService.js +++ b/server/services/UserService.js @@ -36,7 +36,7 @@ const getUserAssignedLabs = (userId) => { return db.sequelize.query( `SELECT DISTINCT "labID" FROM "group_labs" JOIN "enrollment" ON "group_labs"."groupID"="enrollment"."groupID" - WHERE "enrollment"."userID"=(:userId) + WHERE "enrollment"."userID"=(:userId) AND "enrollment"."isActive"=true ORDER BY "labID" ASC `, { @@ -54,7 +54,7 @@ const getUserToDoLabs = (userId) => { SELECT DISTINCT "labID", "labName" FROM "group_labs" JOIN "enrollment" on "group_labs"."groupID" = "enrollment"."groupID" JOIN "labs" on "labs"."id" = "group_labs" . "labID" - WHERE "enrollment"."userID"=(:userId) AND "labID" NOT IN + WHERE "enrollment"."userID"=(:userId) AND "enrollment"."isActive"=true AND "labID" NOT IN (SELECT "labid" FROM "userlabcompletion" WHERE "userid"=(:userId)) ORDER BY "labID" ASC