diff --git a/api/main_endpoints/routes/Advertisement.js b/api/main_endpoints/routes/Advertisement.js index 09bb7bb75..0339e481d 100644 --- a/api/main_endpoints/routes/Advertisement.js +++ b/api/main_endpoints/routes/Advertisement.js @@ -1,14 +1,12 @@ const express = require('express'); const router = express.Router(); const { OK, BAD_REQUEST, FORBIDDEN, UNAUTHORIZED, NOT_FOUND } = require('../../util/constants').STATUS_CODES; -const { - decodeToken, - checkIfTokenSent, -} = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const logger = require('../../util/logger'); const Advertisement = require('../models/Advertisement'); const AuditLog = require('../models/AuditLog.js'); const AuditLogActions = require('../util/auditLogActions.js'); +const membershipState = require('../../util/constants.js').MEMBERSHIP_STATE; router.get('/', async (req, res) => { const count = await Advertisement.countDocuments(); @@ -26,10 +24,9 @@ router.get('/', async (req, res) => { router.get('/getAllAdvertisements', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } Advertisement.find() .sort({ createdAt: -1 }) @@ -41,13 +38,9 @@ router.get('/getAllAdvertisements', async (req, res) => { }); router.post('/createAdvertisement', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } - - const user = await decodeToken(req); - if (!user) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const newAd = new Advertisement({ @@ -58,7 +51,7 @@ router.post('/createAdvertisement', async (req, res) => { try { const createdAd = await Advertisement.create(newAd); AuditLog.create({ - userId: user._id, + userId: decoded.token._id, action: AuditLogActions.CREATE_AD, details: { message: createdAd.message, @@ -75,15 +68,9 @@ router.post('/createAdvertisement', async (req, res) => { }); router.post('/deleteAdvertisement', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { - return res.sendStatus(UNAUTHORIZED); - } - - const user = await decodeToken(req); - if (!user) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } try { @@ -94,7 +81,7 @@ router.post('/deleteAdvertisement', async (req, res) => { } AuditLog.create({ - userId: user._id, + userId: decoded.token._id, action: AuditLogActions.DELETE_AD, details: { deletedAd: { diff --git a/api/main_endpoints/routes/AuditLog.js b/api/main_endpoints/routes/AuditLog.js index edf306ab3..05b6b429b 100644 --- a/api/main_endpoints/routes/AuditLog.js +++ b/api/main_endpoints/routes/AuditLog.js @@ -3,25 +3,18 @@ const router = express.Router(); const AuditLog = require('../models/AuditLog'); const { OK, UNAUTHORIZED, SERVER_ERROR } = require('../../util/constants').STATUS_CODES; -const { OFFICER } = require('../../util/constants.js').MEMBERSHIP_STATE; +const membershipState = require('../../util/constants.js').MEMBERSHIP_STATE; -const { checkIfTokenSent, checkIfTokenValid, decodeTokenFromBodyOrQuery } = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const logger = require('../../util/logger'); const User = require('../models/User.js'); let { clients } = require('../util/AuditLog.js'); router.get('/getAuditLogs', async (req, res) => { - if (!checkIfTokenSent(req)) { - logger.warn('/getAuditLogs was requested without a token'); - return res.sendStatus(UNAUTHORIZED); - } - - const isValid = checkIfTokenValid(req, OFFICER); - - if (!isValid) { - logger.warn('/getAuditLogs was requested with an invalid or unauthorized token'); - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const itemsPerPage = 50; @@ -73,9 +66,9 @@ router.get('/getAuditLogs', async (req, res) => { }); router.get('/listen', async (req, res) => { - const decoded = await decodeTokenFromBodyOrQuery(req); - if (!Object.keys(decoded) || decoded.accessLevel < OFFICER) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const headers = { diff --git a/api/main_endpoints/routes/Auth.js b/api/main_endpoints/routes/Auth.js index 98f945c12..74c55053a 100644 --- a/api/main_endpoints/routes/Auth.js +++ b/api/main_endpoints/routes/Auth.js @@ -11,11 +11,7 @@ const PasswordReset = require('../models/PasswordReset.js'); const logger = require('../../util/logger'); const { registerUser, testPasswordStrength } = require('../util/userHelpers'); const { verifyCaptcha } = require('../util/captcha'); -const { - checkIfTokenSent, - checkIfTokenValid, - decodeToken -} = require('../util/token-functions'); +const { decodeToken } = require('../util/token-functions'); const jwt = require('jsonwebtoken'); const { OK, @@ -60,10 +56,9 @@ router.post('/register', async (req, res) => { }); router.post('/resendVerificationEmail', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req, membershipState.OFFICER)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const maybeUser = await userWithEmailExists(req.body.email); if (!maybeUser) { @@ -239,16 +234,12 @@ router.post('/login', function(req, res) { // Verifies the users session if they have an active jwtToken. // Used on the inital load of root '/' // Returns the name and accesslevel of the user w/ the given access token -router.post('/verify', function(req, res) { - if (!checkIfTokenSent(req)) { - return res.status(UNAUTHORIZED).json({}); - } - const token = decodeToken(req); - if (token === null || Object.keys(token).length === 0) { - res.status(UNAUTHORIZED).json({}); - } else { - res.status(OK).json(token); +router.post('/verify', async function(req, res) { + const decoded = await decodeToken(req); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } + res.status(OK).json(decoded.token); }); router.post('/generateHashedId', async (req, res) => { diff --git a/api/main_endpoints/routes/Cleezy.js b/api/main_endpoints/routes/Cleezy.js index 8c2091446..1de62d989 100644 --- a/api/main_endpoints/routes/Cleezy.js +++ b/api/main_endpoints/routes/Cleezy.js @@ -1,10 +1,7 @@ const express = require('express'); const axios = require('axios'); const router = express.Router(); -const { - decodeToken, - checkIfTokenSent, -} = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const { OK, UNAUTHORIZED, @@ -15,6 +12,7 @@ const logger = require('../../util/logger'); const { Cleezy } = require('../../config/config.json'); const { ENABLED } = Cleezy; const cleezyHelpers = require('../util/cleezyHelpers.js'); +const { membershipState } = require('../../../src/Enums.js'); let CLEEZY_URL = process.env.CLEEZY_URL || 'http://localhost:8000'; @@ -28,10 +26,9 @@ router.get('/list', async (req, res) => { }); } const { page = 0, search, sortColumn = 'created_at', sortOrder = 'DESC'} = req.query; - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } try { const returnData = await cleezyHelpers.searchCleezyUrls({ page, search, sortColumn, sortOrder }); @@ -47,10 +44,9 @@ router.get('/list', async (req, res) => { }); router.post('/createUrl', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const { url, alias, expiresAt } = req.body; let jsonbody = { url, alias: alias || null }; @@ -68,10 +64,9 @@ router.post('/createUrl', async (req, res) => { }); router.post('/deleteUrl', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const { alias } = req.body; axios diff --git a/api/main_endpoints/routes/LedSign.js b/api/main_endpoints/routes/LedSign.js index bf68e1cff..551dec994 100644 --- a/api/main_endpoints/routes/LedSign.js +++ b/api/main_endpoints/routes/LedSign.js @@ -5,10 +5,7 @@ const { SERVER_ERROR, UNAUTHORIZED } = require('../../util/constants').STATUS_CODES; -const { - decodeToken, - checkIfTokenSent -} = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const logger = require('../../util/logger'); const { updateSign, healthCheck, turnOffSign } = require('../util/LedSign.js'); const AuditLogActions = require('../util/auditLogActions.js'); @@ -16,6 +13,7 @@ const AuditLog = require('../models/AuditLog.js'); const { LED_SIGN = {} } = require('../../config/config.json'); +const { membershipState } = require('../../../src/Enums.js'); const runningInTest = process.env.NODE_ENV === 'test'; @@ -37,14 +35,10 @@ router.get('/healthCheck', async (req, res) => { }); router.post('/updateSignText', async (req, res) => { - if (!checkIfTokenSent(req)) { - logger.warn('/updateSignText was requested without a token'); - return res.sendStatus(UNAUTHORIZED); - } - const user = await decodeToken(req); // Store the user here - if (!user || Object.keys(user) === 0) { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { logger.warn('/updateSignText was requested with an invalid token'); - return res.sendStatus(UNAUTHORIZED); + return res.sendStatus(decoded.status); } if (!LED_SIGN.ENABLED && !runningInTest) { logger.warn('led sign is disabled, returning 200 by default'); @@ -65,7 +59,7 @@ router.post('/updateSignText', async (req, res) => { } await AuditLog.create({ - userId: user._id, + userId: decoded.token._id, action: AuditLogActions.UPDATE_SIGN, details: { newSignText: req.body.text, diff --git a/api/main_endpoints/routes/Messages.js b/api/main_endpoints/routes/Messages.js index 033cb3133..83e9e8183 100644 --- a/api/main_endpoints/routes/Messages.js +++ b/api/main_endpoints/routes/Messages.js @@ -11,7 +11,7 @@ const bodyParser = require('body-parser'); const User = require('../models/User.js'); const logger = require('../../util/logger'); const client = require('prom-client'); -const { decodeToken, decodeTokenFromBodyOrQuery } = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const { MetricsHandler, register } = require('../../util/metrics.js'); @@ -80,11 +80,11 @@ router.post('/send', async (req, res) => { } // Assume user passed a non null/undefined token - const userObj = decodeToken(req); - if (!userObj) { + const userObj = await decodeToken(req); + if (!userObj.token) { return res.sendStatus(UNAUTHORIZED); } - nameToUse = userObj.firstName; + nameToUse = userObj.token.firstName; try { writeMessage(id, `${message}`, `${nameToUse}:`); return res.json({ status: 'Message sent' }); @@ -152,11 +152,11 @@ router.get('/listen', async (req, res) => { let filterQuery = {}; // filter to find user in the database if (token) { - let userObj = decodeTokenFromBodyOrQuery(req); - if (!Object.keys(userObj)) { + const userObj = await decodeToken(req); + if (!userObj.token) { return res.sendStatus(UNAUTHORIZED); } - filterQuery._id = userObj._id; + filterQuery._id = userObj.token._id; } else { filterQuery.apiKey = apiKey; } diff --git a/api/main_endpoints/routes/OfficeAccessCard.js b/api/main_endpoints/routes/OfficeAccessCard.js index 31531e7f0..53dabd032 100644 --- a/api/main_endpoints/routes/OfficeAccessCard.js +++ b/api/main_endpoints/routes/OfficeAccessCard.js @@ -6,7 +6,7 @@ const { OK, FORBIDDEN, } = require('../../util/constants').STATUS_CODES; -const { OFFICER } = require('../../util/constants').MEMBERSHIP_STATE; +const membershipState = require('../../util/constants').MEMBERSHIP_STATE; const express = require('express'); const router = express.Router(); const bodyParser = require('body-parser'); @@ -14,12 +14,7 @@ const OfficeAccessCard = require('../models/OfficeAccessCard.js'); const logger = require('../../util/logger'); const { officeAccessCard = {} } = require('../../config/config.json'); const { API_KEY = 'NOTHING_REALLY' } = officeAccessCard; -const { - decodeTokenFromBodyOrQuery, - decodeToken, - checkIfTokenSent, - checkIfTokenValid -} = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const ROWS_PER_PAGE = 25; const { checkIfCardExists, @@ -142,9 +137,9 @@ router.get('/verify', async (req, res) => { }); router.post('/delete', async (req, res) => { - const decoded = decodeToken(req); - if (!decoded) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const { alias } = req.body; @@ -173,7 +168,7 @@ router.post('/delete', async (req, res) => { statusCode: OK, }); AuditLog.create({ - userId: decoded._id, + userId: decoded.token._id, action: AuditLogActions.DELETE_CARD, details: { alias } }); @@ -189,10 +184,9 @@ router.post('/delete', async (req, res) => { }); router.post('/getAllCards', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const skip = Math.max(Number(req.body.page) || 0, 0) * ROWS_PER_PAGE; @@ -216,9 +210,9 @@ router.post('/getAllCards', async (req, res) => { }); router.get('/listen', async (req, res) => { - const decoded = await decodeTokenFromBodyOrQuery(req); - if (!Object.keys(decoded) || decoded.accessLevel < OFFICER) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); } const headers = { diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index a0621b7e8..da9c82bd1 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -8,10 +8,7 @@ const path = require('path'); const { MetricsHandler, register } = require('../../util/metrics.js'); const { cleanUpChunks, cleanUpExpiredChunks, recordPrintingFolderSize } = require('../util/Printer.js'); -const { - decodeToken, - checkIfTokenSent, -} = require('../util/token-functions.js'); +const { decodeToken } = require('../util/token-functions.js'); const { OK, UNAUTHORIZED, @@ -74,16 +71,12 @@ router.get('/healthCheck', async (req, res) => { }); router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { - if (!checkIfTokenSent(req)) { - logger.warn('/sendPrintRequest was requested without a token'); - return res.sendStatus(UNAUTHORIZED); - } - - const decodedToken = await decodeToken(req); - if (!decodedToken || Object.keys(decodedToken) === 0) { + const decoded = await decodeToken(req); + if (!decoded.token) { logger.warn('/sendPrintRequest was requested with an invalid token'); - return res.sendStatus(UNAUTHORIZED); + return res.sendStatus(decoded.status); } + // this makes printing pass in unit tests, at some point need to test axios call if (!PRINTING.ENABLED) { logger.warn('Printing is disabled, returning 200 and dummy print id to mock the printing server'); return res.status(OK).send({ printId: null }); diff --git a/api/main_endpoints/routes/ShortcutSearch.js b/api/main_endpoints/routes/ShortcutSearch.js index e2e854c70..20e60503a 100644 --- a/api/main_endpoints/routes/ShortcutSearch.js +++ b/api/main_endpoints/routes/ShortcutSearch.js @@ -3,10 +3,7 @@ const express = require('express'); const router = express.Router(); const User = require('../models/User.js'); -const { - checkIfTokenSent, - checkIfTokenValid, -} = require('../util/token-functions'); +const { decodeToken } = require('../util/token-functions'); const { OK, UNAUTHORIZED, @@ -23,10 +20,9 @@ const MAX_RESULT = 5; // Search for all members using either first name, last name or email // Search for all cleezy urls using either alias or url router.post('/', async function(req, res) { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req, membershipState.OFFICER)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (!decoded.token) { + return res.sendStatus(decoded.status); } if (!req.body.query) { @@ -122,10 +118,14 @@ router.post('/', async function(req, res) { search: req.body.query, limit: MAX_RESULT }); + let cleezyData = []; + if (cleezyRes.data) { + cleezyData = cleezyRes.data; + } return res.status(OK).send({ items: { users, - cleezyData: cleezyRes.data, + cleezyData, } }); } catch (error) { diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index a9c0d37b5..1a48d45f7 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -9,11 +9,7 @@ const { getMemberExpirationDate, hashPassword, } = require('../util/userHelpers'); -const { - checkIfTokenSent, - checkIfTokenValid, - decodeToken, -} = require('../util/token-functions'); +const { decodeToken } = require('../util/token-functions'); const { OK, BAD_REQUEST, @@ -36,31 +32,31 @@ const ROWS_PER_PAGE = 20; // Delete a member router.post('/delete', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req); + if (!decoded.token) { + return res.sendStatus(decoded.status); } - const decoded = decodeToken(req); const targetUser = await User.findById(req.body._id); if (!targetUser) { return res.sendStatus(NOT_FOUND); } - // Check if req has lower privilege than the account they wish to delete - if (targetUser.accessLevel !== 'undefined') { - if (decoded.accessLevel < targetUser.accessLevel) { + + // If not officer, only allow deletion of own account + if (decoded.token.accessLevel < membershipState.OFFICER) { + if (req.body._id && req.body._id !== decoded.token._id) { return res .status(FORBIDDEN) - .json( { message: 'you must have higher privileges to delete users with lower privileges'}); + .json({ message: 'you must be an officer or admin to delete other users' }); } } - // If not officer, only allow deletion of own account - if (decoded.accessLevel < membershipState.OFFICER) { - if (req.body._id && req.body._id !== decoded._id) { + + // Check if req has lower privilege than the account they wish to delete + if (targetUser.accessLevel !== 'undefined') { + if (decoded.token.accessLevel < targetUser.accessLevel) { return res .status(FORBIDDEN) - .json({ message: 'you must be an officer or admin to delete other users' }); + .json( { message: 'you must have higher privileges to delete users with lower privileges'}); } } @@ -78,21 +74,21 @@ router.post('/delete', async (req, res) => { }); // Search for a member -router.post('/search', function(req, res) { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req, membershipState.ALUMNI)) { - return res.sendStatus(UNAUTHORIZED); +router.post('/search', async function(req, res) { + const decoded = await decodeToken(req, membershipState.OFFICER); + + if (!decoded.token) { + return res.sendStatus(decoded.status); } + User.findOne({ email: req.body.email }, function(error, result) { if (error) { - res.status(BAD_REQUEST).send({ message: 'Bad Request.' }); + return res.sendStatus(BAD_REQUEST); } if (!result) { return res - .status(NOT_FOUND) - .send({ message: `${req.body.email} not found.` }); + .sendStatus(NOT_FOUND); } const user = { @@ -121,11 +117,11 @@ router.post('/search', function(req, res) { // Search for all members router.post('/users', async function(req, res) { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (!decoded.token) { + return res.sendStatus(decoded.status); } + let maybeOr = {}; if (req.body.query) { maybeOr = { @@ -164,136 +160,138 @@ router.post('/users', async function(req, res) { // Edit/Update a member record router.post('/edit', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req); + if (!decoded.token) { + return res.sendStatus(decoded.status); } - if (!req.body._id) { + const { accessLevel, _id: tokenId, email: tokenEmail } = decoded.token; + const { _id: targetId, password, numberOfSemestersToSignUpFor, ...userData } = req.body; + const isOfficer = accessLevel >= membershipState.OFFICER; + const isTargetAdmin = accessLevel === membershipState.ADMIN; + + if (!targetId) { return res.sendStatus(BAD_REQUEST); } - let decoded = decodeToken(req); - if (decoded.accessLevel < membershipState.OFFICER) { - if (req.body.email && req.body.email != decoded.email) { - return res - .status(UNAUTHORIZED) - .send('Unauthorized to edit another user'); - } - if (req.body.accessLevel && req.body.accessLevel !== decoded.accessLevel) { - return res - .status(UNAUTHORIZED) - .send('Unauthorized to change access level'); - } + const existingUser = await User.findById(targetId); + if (!existingUser) { + return res.status(NOT_FOUND).send({ message: 'User not found.' }); } - if (decoded.accessLevel === membershipState.OFFICER) { - if (req.body.accessLevel && req.body.accessLevel == membershipState.ADMIN) { - return res.sendStatus(UNAUTHORIZED); - } + if (!isOfficer && targetId.toString() !== tokenId.toString()) { + return res + .status(FORBIDDEN) + .send('Unauthorized to edit another user'); } - const query = { _id: req.body._id }; - let user = req.body; + // Members cannot change email or accessLevel + if (!isOfficer && (userData.email || userData.accessLevel)) { + return res.status(UNAUTHORIZED).send('Unauthorized to change sensitive fields'); + } - // keep track of user in db - const existingUser = await User.findById(req.body._id); - if (!existingUser) { - return res.status(NOT_FOUND).send({ message: 'User not found.' }); + // Officers cannot change accessLevel to ADMIN + if (isOfficer && userData.accessLevel === membershipState.ADMIN && !isTargetAdmin) { + return res.sendStatus(UNAUTHORIZED); } - // Track field changes + // Prepare Data for Update (Sanitization) + const allowedFields = [ + 'firstName', 'lastName', 'email', 'accessLevel', 'major', + 'discordID', 'emailOptIn', 'membershipValidUntil' + ]; + + const dataToUpdate = {}; const fieldChanges = {}; - const fieldsToTrack = ['firstName', 'lastName', 'email', 'accessLevel', 'major', 'discordID', 'emailOptIn', 'membershipValidUntil']; - fieldsToTrack.forEach(field => { - if (user[field] !== undefined && user[field] !== existingUser[field]) { - fieldChanges[field] = { - from: existingUser[field], - to: user[field] - }; + // Iterate through allowed fields and build the update object and audit log + allowedFields.forEach(field => { + // Only include the field if it was provided in the request body + if (userData[field] !== undefined) { + // Check if value actually changed for audit + if (userData[field] !== existingUser[field]) { + fieldChanges[field] = { from: existingUser[field], to: userData[field] }; + } + dataToUpdate[field] = userData[field]; } }); - if (typeof req.body.numberOfSemestersToSignUpFor !== 'undefined') { - user.membershipValidUntil = getMemberExpirationDate( - parseInt(req.body.numberOfSemestersToSignUpFor) + // Handle special membership duration field + if (typeof numberOfSemestersToSignUpFor !== 'undefined' && isOfficer) { + dataToUpdate.membershipValidUntil = getMemberExpirationDate( + parseInt(numberOfSemestersToSignUpFor) ); + // Audit the implicit change + if (existingUser.membershipValidUntil !== dataToUpdate.membershipValidUntil) { + fieldChanges.membershipValidUntil = { + from: existingUser.membershipValidUntil, + to: dataToUpdate.membershipValidUntil + }; + } } - delete user.numberOfSemestersToSignUpFor; - - if (!!user.password) { - // hash the password before storing - const result = await hashPassword(user.password); - if (!result) { + // Handle Password Hashing and Audit + if (password) { + const hashedPassword = await hashPassword(password); + if (!hashedPassword) { return res.sendStatus(SERVER_ERROR); } - user.password = result; + dataToUpdate.password = hashedPassword; - // create audit log for password change + // Create audit log for password change AuditLog.create({ - userId: decoded._id, + userId: tokenId, action: AuditLogActions.CHANGE_PW, - details: { email: existingUser.email, userId: decoded._id }, + details: { email: existingUser.email, userId: tokenId }, }).catch(logger.error); - - } else { - // omit password from the object if it is falsy - // i.e. an empty string, undefined or null - delete user.password; } - // Remove the auth token from the form getting edited - delete user.token; - - User.updateOne(query, { ...user }, function(error, result) { - if (error) { - const info = { - errorTime: new Date(), - apiEndpoint: 'user/edit', - errorDescription: error - }; - - res.status(BAD_REQUEST).send({ message: 'Bad Request.' }); - } + // If no fields are actually changing (excluding the token, which was removed above) + if (Object.keys(dataToUpdate).length === 0 && !password) { + return res.status(OK).send({ message: 'No changes submitted.' }); + } - if (result.nModified < 1) { - return res - .status(NOT_FOUND) - .send({ message: `${existingUser.email} not found.` }); + try { + const result = await User.updateOne({ _id: targetId }, dataToUpdate); + + // Check if the update actually modified a document + if (result.nModified < 1 && result.matchedCount > 0) { + // Matched but not modified means no fields actually changed. + // We can safely treat this as a success if no error occurred. + } else if (result.nModified < 1 && result.matchedCount < 1) { + return res.status(NOT_FOUND).send({ message: `${existingUser.email} not found.` }); } if (Object.keys(fieldChanges).length > 0) { - const sanitizedUser = {...user}; - if ('password' in sanitizedUser) { - sanitizedUser.password = true; - } + // Create a simplified log of what was updated + const auditDetails = { + updatedInfo: JSON.stringify({ ...dataToUpdate, password: !!password }), // true/false for password + fieldChanges: JSON.stringify(fieldChanges) + }; AuditLog.create({ - userId: decoded._id, // person who did modification + userId: tokenId, action: AuditLogActions.UPDATE_USER, - documentId: user._id, - details: { - updatedInfo: JSON.stringify(sanitizedUser), - fieldChanges: JSON.stringify(fieldChanges) - } + documentId: targetId, + details: auditDetails }).catch(logger.error); } return res.status(OK).send({ message: `${existingUser.email} was updated.`, - membershipValidUntil: user.membershipValidUntil + membershipValidUntil: dataToUpdate.membershipValidUntil || existingUser.membershipValidUntil }); - }); + + } catch (error) { + logger.error('/edit had an error:', error); + return res.status(BAD_REQUEST).send({ message: 'Bad Request: Unable to update user.' }); + } }); -router.post('/getPagesPrintedCount', (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); +router.post('/getPagesPrintedCount', async (req, res) => { + const decoded = await decodeToken(req); + if (!decoded.token) { + return res.sendStatus(decoded.status); } User.findOne({ email: req.body.email }, function(error, result) { if (error) { @@ -317,20 +315,30 @@ router.post('/getPagesPrintedCount', (req, res) => { }); router.post('/getUserById', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req); + if (!decoded.token) { + return res.sendStatus(decoded.status); } + let targetUserId = req.body.userID; + // If not officer, only allow reading of own account - let decoded = decodeToken(req); - if (decoded.accessLevel < membershipState.OFFICER) { - if (req.body.userID && req.body.userID !== decoded._id) { + if (decoded.token.accessLevel < membershipState.OFFICER) { + // 1. Force the lookup ID to be the logged-in user's ID + targetUserId = decoded.token._id; + + // 2. If the user tried to request a *different* ID, explicitly block them + if (req.body.userID && req.body.userID !== decoded.token._id) { return res .status(FORBIDDEN) .json({ message: 'you must be an officer or admin to read other users\' data' }); } } + + // If no ID was provided in the request body, use the token ID as a fallback + if (!targetUserId) { + targetUserId = decoded.token._id; + } + User.findOne({ _id: req.body.userID}, (err, result) => { if (err) { return res.sendStatus(BAD_REQUEST); @@ -392,11 +400,10 @@ router.post('/getUserDataByEmail', (req, res) => { }); // Search for all members with verified emails and subscribed -router.post('/usersSubscribedAndVerified', function(req, res) { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); +router.post('/usersSubscribedAndVerified', async function(req, res) { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (!decoded.token) { + return res.sendStatus(decoded.status); } User.find({ emailVerified: true, emailOptIn: true }) .then((users) => { @@ -418,11 +425,10 @@ router.post('/usersSubscribedAndVerified', function(req, res) { }); // Search for all members with verified emails, subscribed, and not banned or pending -router.post('/usersValidVerifiedAndSubscribed', function(req, res) { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req, membershipState.OFFICER)) { - return res.sendStatus(UNAUTHORIZED); +router.post('/usersValidVerifiedAndSubscribed', async function(req, res) { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (!decoded.token) { + return res.sendStatus(decoded.status); } User.find({ emailVerified: true, @@ -446,13 +452,11 @@ router.post('/usersValidVerifiedAndSubscribed', function(req, res) { // Generate an API key for the Messages API if the user does not have an API key; otherwise, return the existing API key router.post('/apikey', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } - if (!checkIfTokenValid(req)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req); + if (!decoded.token) { + return res.sendStatus(decoded.status); } - let { _id } = decodeToken(req); + let { _id } = decoded.token; User.findOne({_id}) .then((user) => { @@ -482,10 +486,9 @@ router.post('/apikey', async (req, res) => { // Finds number of those signups who've paid for annual plan // Assumes members who have paid have been assigned an expiration date router.get('/getNewPaidMembersThisSemester', async (req, res) => { - if (!checkIfTokenSent(req)) { - return res.sendStatus(FORBIDDEN); - } else if (!checkIfTokenValid(req, membershipState.OFFICER)) { - return res.sendStatus(UNAUTHORIZED); + const decoded = await decodeToken(req, membershipState.OFFICER); + if (!decoded.token) { + return res.sendStatus(decoded.status); } const today = new Date(); diff --git a/api/main_endpoints/util/token-functions.js b/api/main_endpoints/util/token-functions.js index 7d7fb43d4..22db1475d 100644 --- a/api/main_endpoints/util/token-functions.js +++ b/api/main_endpoints/util/token-functions.js @@ -5,75 +5,62 @@ const membershipState = require('../../util/constants').MEMBERSHIP_STATE; require('./passport')(passport); +const { UNAUTHORIZED, OK, FORBIDDEN } = require('../../util/constants').STATUS_CODES; +const logger = require('../../util/logger'); -/** - * Check if the request body contains a token - * @param {object} request the HTTP request from the client - * @returns {boolean} if the token exists in the request body - */ -function checkIfTokenSent(request) { - try { - return !!request.headers.authorization; - } catch(_) { - return false; +class SceStatusOrToken { + constructor() { + this.token = null; + this.status = null; } } /** * @param {object} request the HTTP request from the client */ -function decodeToken(request){ +async function decodeToken(request, requiredAccessLevel = membershipState.NON_MEMBER) { + const decodedResponse = new SceStatusOrToken(); + let token = null; + try { - let decodedResponse = {}; - if (!request.headers.authorization || !request.headers.authorization.length) { + if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) { + token = request.headers.authorization.split('Bearer ')[1]; + } else if (request.query && request.query.token) { + token = request.query.token; + } + + if (!token) { + decodedResponse.status = UNAUTHORIZED; return decodedResponse; } - const token = request.headers.authorization.split('Bearer ')[1]; + const userToken = token.replace(/^JWT\s/, ''); - jwt.verify(userToken, secretKey, function(error, decoded) { - if (!error && decoded) { - decodedResponse = decoded; - } + const decoded = await new Promise((resolve, reject) => { + jwt.verify(userToken, secretKey, (error, payload) => { + if (error || !payload) { + return reject(error || new Error('Token verification failed.')); + } + resolve(payload); + }); }); + + const hasRequiredAccess = decoded.accessLevel >= requiredAccessLevel; + + decodedResponse.status = hasRequiredAccess ? OK : FORBIDDEN; + decodedResponse.token = decoded; + + return decodedResponse; + + } catch (err) { + logger.error('Token validation failed:', err); + decodedResponse.status = FORBIDDEN; + return decodedResponse; - } catch (_) { - return null; } } -/** -* @param {object} request the HTTP request from the client -*/ -function decodeTokenFromBodyOrQuery(request){ - const token = request.body.token || request.query.token; - const userToken = token.replace(/^JWT\s/, ''); - let decodedResponse = {}; - jwt.verify(userToken, secretKey, function(error, decoded) { - if (!error && decoded) { - decodedResponse = decoded; - } - }); - return decodedResponse; -} -/** - * Checks if the request token is valid and returns either a valid response - * or undefined - * @param {object} request the HTTP request from the client - * @param {number} accessLevel the minimum access level to consider the token valid - * @param {boolean} returnDecoded optional parameter to return the decoded - * response to the user - * @returns {boolean} whether the user token is valid or not - */ -function checkIfTokenValid(request, accessLevel = membershipState.NON_MEMBER) { - let decoded = decodeToken(request); - let response = decoded && decoded.accessLevel >= accessLevel; - return response; -} module.exports = { - checkIfTokenSent, - checkIfTokenValid, decodeToken, - decodeTokenFromBodyOrQuery }; diff --git a/api/util/token-verification.js b/api/util/token-verification.js deleted file mode 100644 index 7f13ca9b9..000000000 --- a/api/util/token-verification.js +++ /dev/null @@ -1,11 +0,0 @@ -const { DISCORD_COREV4_KEY } = require('../config/config.json'); - -/** - * Checks API key value and return true or false depending on if it matches - * @param {String} apiKey - * @returns {boolean} whether the api key was valid or not - */ -function checkDiscordKey(apiKey) { - return apiKey === DISCORD_COREV4_KEY; -} -module.exports = { checkDiscordKey }; diff --git a/test/api/Advertisement.js b/test/api/Advertisement.js index 9e48aecf9..47e8bb61d 100644 --- a/test/api/Advertisement.js +++ b/test/api/Advertisement.js @@ -61,9 +61,9 @@ describe('Advertisement', () => { describe('/POST createAdvertisement', () => { - it('Should return 403 when token is not sent', async () => { + it('Should return 401 when token is not sent', async () => { const res = await test.sendPostRequest('/api/Advertisement/createAdvertisement', VALID_ADVERTISEMENT); - expect(res).to.have.status(FORBIDDEN); + expect(res).to.have.status(UNAUTHORIZED); }); it('Should return 401 when invalid token is sent', async () => { @@ -125,9 +125,9 @@ describe('Advertisement', () => { }); describe('/POST deleteAdvertisement', () => { - it('Should return 403 if no token is sent', async () => { + it('Should return 401 if no token is sent', async () => { const res = await test.sendPostRequest('/api/Advertisement/deleteAdvertisement', { _id: VALID_ADVERTISEMENT._id }); - expect(res).to.have.status(FORBIDDEN); + expect(res).to.have.status(UNAUTHORIZED); }); it('Should return 401 if invalid token is sent', async () => { diff --git a/test/api/Auth.js b/test/api/Auth.js index 7c67f565a..12ea2ea4d 100644 --- a/test/api/Auth.js +++ b/test/api/Auth.js @@ -180,19 +180,16 @@ describe('Auth', () => { } }; - const decodedPayload = decodeToken(mockRequest); + const decodedPayload = await decodeToken(mockRequest, MEMBERSHIP_STATE.PENDING); const expectedPayload = { - firstName: 'Test', - lastName: 'User', - email: 'logintest@gmail.com', - accessLevel: MEMBERSHIP_STATE.PENDING, - pagesPrinted: 0, - _id: decodedPayload._id, - iat: decodedPayload.iat, - exp: decodedPayload.exp, + 'accessLevel': -1, + 'email': 'logintest@gmail.com', + 'firstName': 'Test', + 'lastName': 'User', + 'pagesPrinted': 0, }; - expect(decodedPayload).to.deep.equal(expectedPayload); + expect(decodedPayload.token).to.deep.include(expectedPayload); } finally { await User.deleteOne({email: user.email}); } diff --git a/test/api/OfficeAccessCard.js b/test/api/OfficeAccessCard.js index b0106e5b1..74145b3d7 100644 --- a/test/api/OfficeAccessCard.js +++ b/test/api/OfficeAccessCard.js @@ -211,9 +211,9 @@ describe('OfficeAccessCard', () => { }); describe('POST getAllCards', () => { - it('Should return 403 when token is not sent', async () => { + it('Should return 401 when token is not sent', async () => { const result = await test.sendPostRequest(GET_ALL_CARDS_API_PATH); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return 401 when invalid token is sent', async () => { diff --git a/test/api/ShortcutSearch.js b/test/api/ShortcutSearch.js index faa61dcde..19ed9087c 100644 --- a/test/api/ShortcutSearch.js +++ b/test/api/ShortcutSearch.js @@ -9,7 +9,6 @@ const chaiHttp = require('chai-http'); const { OK, UNAUTHORIZED, - FORBIDDEN } = require('../../api/util/constants').STATUS_CODES; const SceApiTester = require('../util/tools/SceApiTester'); @@ -25,12 +24,6 @@ const { initializeTokenMock } = require('../util/mocks/TokenValidFunctions'); -const { - setDiscordAPIStatus, - resetDiscordAPIMock, - restoreDiscordAPIMock, - initializeDiscordAPIMock -} = require('../util/mocks/DiscordApiFunction'); const { MEMBERSHIP_STATE } = require('../../api/util/constants'); const { getMemberExpirationDate } = require('../../api/main_endpoints/util/userHelpers.js'); @@ -41,7 +34,6 @@ chai.use(chaiHttp); describe('ShortcutSearch', () => { before(async () => { initializeTokenMock(); - initializeDiscordAPIMock(); app = tools.initializeServer([ __dirname + '/../../api/main_endpoints/routes/ShortcutSearch.js', ]); @@ -60,18 +52,15 @@ describe('ShortcutSearch', () => { after(done => { restoreTokenMock(); - restoreDiscordAPIMock(); tools.terminateServer(done); }); beforeEach(() => { setTokenStatus(false); - setDiscordAPIStatus(false); }); afterEach(() => { resetTokenMock(); - resetDiscordAPIMock(); }); const token = ''; @@ -81,10 +70,10 @@ describe('ShortcutSearch', () => { const fiveMatchUsers = { query: 'Lot' }; const url = '/api/ShortcutSearch/'; - it('Should return status code 403 if no token is passed through', async () => { + it('Should return status code 401 if no token is passed through', async () => { setTokenStatus(false); const result = await test.sendPostRequest(url, queryUser); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return status code 401 if access level is invalid', async () => { @@ -256,6 +245,7 @@ describe('ShortcutSearch', () => { ]; for (const payload of injectionPayloads) { const result = await test.sendPostRequestWithToken(token, url, { query: String(payload)}); + expect(result).to.have.status(OK); expect(result.body.items.users.length).at.most(5); expect(result.body.items.cleezyData.length).at.most(5); diff --git a/test/api/TokenFunctions.js b/test/api/TokenFunctions.js index 194f6c403..c1dc5d13e 100644 --- a/test/api/TokenFunctions.js +++ b/test/api/TokenFunctions.js @@ -4,6 +4,8 @@ const sinon = require('sinon'); const chai = require('chai'); const expect = chai.expect; const proxyquire = require('proxyquire'); +const { OK, FORBIDDEN, UNAUTHORIZED } = require('../../api/util/constants').STATUS_CODES; +const membershipState = require('../../api/util/constants').MEMBERSHIP_STATE; const requestWithToken = { headers: { @@ -14,6 +16,7 @@ const requestWithToken = { } }; const requestWithoutToken = { + headers: {}, body: {} }; let tokenFunctions; @@ -31,32 +34,41 @@ describe('TokenFunctions', () => { }); done(); }); - describe('checkIfTokenSent', () => { - it('Should return true if a token field exists in the request', done => { - expect(tokenFunctions.checkIfTokenSent(requestWithToken)).to.equal(true); - done(); + describe('decodeToken', () => { + it('Should resolve with UNAUTHORIZED if no token is sent', done => { + tokenFunctions.decodeToken(requestWithoutToken) + .then(decodedResponse => { + expect(decodedResponse.status).to.equal(UNAUTHORIZED); + done(); + }); }); - it('Should return false if a token field does ' + - 'not exist in the request', done => { - expect(tokenFunctions.checkIfTokenSent(requestWithoutToken)) - .to.equal(false); - done(); - }); - }); - - describe('checkIfTokenValid', () => { - it('Should return the decoded response ', done => { - jwtStub.yields(false, requestWithToken.body); - expect(tokenFunctions.checkIfTokenValid(requestWithToken)) - .to.equal(true); - done(); - }); - it('Should return false if a token field ' + - 'does not exist in the request', done => { - jwtStub.yields(true, false); - expect(tokenFunctions.checkIfTokenValid(requestWithToken)) - .to.equal(false); - done(); + it('Should resolve with FORBIDDEN if token is invalid', done => { + jwtStub.yields(new Error('invalid token'), null); + tokenFunctions.decodeToken(requestWithToken) + .then(decodedResponse => { + expect(decodedResponse.status).to.equal(FORBIDDEN); + done(); + }); }); + it('Should resolve with FORBIDDEN if access level is insufficient', + done => { + jwtStub.yields(null, { accessLevel: membershipState.MEMBER }); + tokenFunctions.decodeToken(requestWithToken, membershipState.OFFICER) + .then(decodedResponse => { + expect(decodedResponse.status).to.equal(FORBIDDEN); + done(); + }); + }); + it('Should resolve with OK and the decoded token if token is valid and access level is sufficient', + done => { + const decodedToken = { accessLevel: membershipState.OFFICER, firstName: 'Test' }; + jwtStub.yields(null, decodedToken); + tokenFunctions.decodeToken(requestWithToken, membershipState.OFFICER) + .then(decodedResponse => { + expect(decodedResponse.status).to.equal(OK); + expect(decodedResponse.token).to.equal(decodedToken); + done(); + }); + }); }); }); diff --git a/test/api/User.js b/test/api/User.js index cd97047cb..135c04041 100644 --- a/test/api/User.js +++ b/test/api/User.js @@ -12,7 +12,6 @@ let id = new mongoose.Types.ObjectId(); const chaiHttp = require('chai-http'); const { OK, - BAD_REQUEST, UNAUTHORIZED, NOT_FOUND, FORBIDDEN @@ -37,12 +36,6 @@ const { initializeTokenMock } = require('../util/mocks/TokenValidFunctions'); -const { - setDiscordAPIStatus, - resetDiscordAPIMock, - restoreDiscordAPIMock, - initializeDiscordAPIMock -} = require('../util/mocks/DiscordApiFunction'); const { MEMBERSHIP_STATE } = require('../../api/util/constants'); const { getMemberExpirationDate } = require('../../api/main_endpoints/util/userHelpers.js'); @@ -53,7 +46,6 @@ chai.use(chaiHttp); describe('User', () => { before(done => { initializeTokenMock(); - initializeDiscordAPIMock(); app = tools.initializeServer([ __dirname + '/../../api/main_endpoints/routes/User.js', __dirname + '/../../api/main_endpoints/routes/Auth.js' @@ -74,30 +66,27 @@ describe('User', () => { after(done => { restoreTokenMock(); - restoreDiscordAPIMock(); tools.terminateServer(done); }); beforeEach(() => { setTokenStatus(false); - setDiscordAPIStatus(false); }); afterEach(() => { resetTokenMock(); - resetDiscordAPIMock(); }); const token = ''; describe('/POST search', () => { - it('Should return statusCode 403 if no token is passed in', async () => { + it('Should return statusCode 401 if no token is passed in', async () => { const user = { email: 'a@b.c' }; const result = await test.sendPostRequest( '/api/User/users', user); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return statusCode 401 if an invalid ' + @@ -124,13 +113,13 @@ describe('User', () => { }); describe('/POST searchFor', () => { - it('Should return statusCode 403 if no token is passed in', async () => { + it('Should return statusCode 401 if no token is passed in', async () => { const user = { email: 'a@b.c' }; const result = await test.sendPostRequest( '/api/User/search', user); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return statusCode 401 if an invalid ' + @@ -180,13 +169,13 @@ describe('User', () => { }); describe('/POST edit', () => { - it('Should return statusCode 403 if no token is passed in', async () => { + it('Should return statusCode 401 if no token is passed in', async () => { const user = { _id: id, }; const result = await test.sendPostRequest( '/api/User/edit', user); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return statusCode 401 if an invalid ' + @@ -223,7 +212,8 @@ describe('User', () => { password: 'Passw0rd', firstName: 'first-name', lastName: 'last-name', - major: 'Computer Science' + major: 'Computer Science', + accessLevel: MEMBERSHIP_STATE.OFFICER, }).save(); setTokenStatus(true, testUser); @@ -369,14 +359,14 @@ describe('User', () => { }); describe('/POST getUserById', () => { - it('Should return status code 403 if no token was passed in', async () => { + it('Should return status code 401 if no token was passed in', async () => { const user = { userID: id, }; const result = await test.sendPostRequest('/api/user/getUserById', user); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); - it('Should return status code 403 if' + + it('Should return status code 401 if' + ' an invalid token was passed in', async () => { const user = { userID: id, @@ -439,13 +429,13 @@ describe('User', () => { await userAdmin.save(); }); - it('Should return statusCode 403 if no token is passed in', async () => { + it('Should return statusCode 401 if no token is passed in', async () => { const user = { _id : id }; const result = await test.sendPostRequest( '/api/User/delete', user); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return statusCode 403 if an invalid ' + @@ -487,7 +477,7 @@ describe('User', () => { }); it('Should return statusCode 200 if user deletes themself', async () => { - setTokenStatus(true); + setTokenStatus(true, {accessLevel: MEMBERSHIP_STATE.MEMBER}); const deleteUser = { email: 'h@i.j', password: 'Passw0rd', @@ -505,33 +495,66 @@ describe('User', () => { _id: getUser.body._id, token: token }; + setTokenStatus(true, {accessLevel: MEMBERSHIP_STATE.MEMBER, _id: getUser.body._id}); const result = await test.sendPostRequestWithToken( token, '/api/User/delete', user); expect(result).to.have.status(OK); }); - it('Should return statusCode 200 if user deletes themself as a member', async () => { - setTokenStatus(true, {accessLevel: MEMBERSHIP_STATE.MEMBER}); - const deleteUser = { - email: 'h@i.j', + it('Should return statusCode 403 if a member deletes another member', async () => { + setTokenStatus(true, { accessLevel: MEMBERSHIP_STATE.MEMBER }); + // Define credentials for the Member (the deleting user) + const memberCredentials = { + email: 'member@test.com', password: 'Passw0rd', - firstName: 'first-name', - lastName: 'last-name', + firstName: 'Member', + lastName: 'User', }; - const searchUser = { - email: 'h@i.j', - token: token + + // Define credentials for the Target User (the user being deleted) + const targetCredentials = { + email: 'target@test.com', + password: 'TargetPassw0rd', + firstName: 'Target', + lastName: 'User', }; - await test.sendPostRequest('/api/Auth/register', deleteUser); - const getUser = await test.sendPostRequestWithToken( - token, '/api/User/search', searchUser); - const user = { - _id: getUser.body._id, - token: token + + // Register the Target User (the one to be deleted) + await test.sendPostRequest('/api/Auth/register', targetCredentials); + + // Register the Member and get their JWT and decoded token data + await test.sendPostRequest('/api/Auth/register', memberCredentials); + + // Find the Target User to get their _id + // Use the *real* member token to perform the search + const targetSearchResponse = await test.sendPostRequestWithToken( + token, + '/api/User/search', + { email: targetCredentials.email } + ); + const memberSearchResponse = await test.sendPostRequestWithToken( + token, + '/api/User/search', + { email: memberCredentials.email } + ); + + // The target user ID is what we want to delete + const targetUserId = targetSearchResponse.body._id; + const memberUserId = memberSearchResponse.body._id; + + + // Member attempts to delete the Target User using the Target User's ID + const deletePayload = { + _id: targetUserId, // ID of the user to delete (NOT the member's ID) }; + + setTokenStatus(true, { accessLevel: MEMBERSHIP_STATE.MEMBER, _id: memberUserId }); + const result = await test.sendPostRequestWithToken( - token, '/api/User/delete', user); - expect(result).to.have.status(FORBIDDEN); + token, '/api/User/delete', deletePayload); + + // Verification + expect(result).to.have.status(FORBIDDEN); // Expect 403 result.body.should.have.property('message'); result.body.message.should.equal( 'you must be an officer or admin to delete other users', @@ -589,9 +612,9 @@ describe('User', () => { }); // no token - it('Should return status code 403 if no token is passed through', async () => { + it('Should return status code 401 if no token is passed through', async () => { const result = await test.sendPostRequest('/api/user/apikey', {}); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); // invalid token @@ -614,9 +637,9 @@ describe('User', () => { expect(result).to.have.status(OK); }); - it('Should return statusCode 403 if no token is passed in', async () => { + it('Should return statusCode 401 if no token is passed in', async () => { const result = await test.sendGetRequest('/api/user/getNewPaidMembersThisSemester'); - expect(result).to.have.status(FORBIDDEN); + expect(result).to.have.status(UNAUTHORIZED); }); it('Should return statusCode 401 if an invalid' + diff --git a/test/util/mocks/DiscordApiFunction.js b/test/util/mocks/DiscordApiFunction.js deleted file mode 100644 index c9b310394..000000000 --- a/test/util/mocks/DiscordApiFunction.js +++ /dev/null @@ -1,45 +0,0 @@ -const DiscordValidation = require('../../../api/util/token-verification'); -const sinon = require('sinon'); - -let discordApiKeyMock = null; - -/** - * Initialize the stub to be used in other functions. - */ -function initializeDiscordAPIMock() { - discordApiKeyMock = sinon.stub(DiscordValidation, 'checkDiscordKey'); -} - -/** - * Restore sinon's stub, function returned to its original state - */ -function restoreDiscordAPIMock() { - discordApiKeyMock.restore(); -} - -/** - * Reset sinon-stub's call, reset onCall-function back to the beginning - */ -function resetDiscordAPIMock() { - discordApiKeyMock.reset(); -} - -/** - * - * @param {Boolean} returnValue - * @returns the value of the boolean param. - */ -function setDiscordAPIStatus(returnValue) { - if (returnValue) { - discordApiKeyMock.returns(true); - } else { - discordApiKeyMock.returns(false); - } -} - -module.exports = { - setDiscordAPIStatus, - resetDiscordAPIMock, - restoreDiscordAPIMock, - initializeDiscordAPIMock -}; diff --git a/test/util/mocks/TokenValidFunctions.js b/test/util/mocks/TokenValidFunctions.js index f3d1e36f9..3854fa8c7 100644 --- a/test/util/mocks/TokenValidFunctions.js +++ b/test/util/mocks/TokenValidFunctions.js @@ -1,15 +1,14 @@ const TokenFunctions = require( '../../../api/main_endpoints/util/token-functions'); const sinon = require('sinon'); +const { OK, FORBIDDEN, UNAUTHORIZED } = require('../../../api/util/constants').STATUS_CODES; -let checkifTokenValidMock = null; let decodeTokenValidMock = null; /** * Initialize the stub to be used in other functions. */ function initializeTokenMock() { - checkifTokenValidMock = sinon.stub(TokenFunctions, 'checkIfTokenValid'); decodeTokenValidMock = sinon.stub(TokenFunctions, 'decodeToken'); } @@ -17,7 +16,6 @@ function initializeTokenMock() { * Restore sinon's stub, function returned to its original state */ function restoreTokenMock() { - checkifTokenValidMock.restore(); decodeTokenValidMock.restore(); } @@ -25,7 +23,6 @@ function restoreTokenMock() { * Reset sinon-stub's call, reset onCall-function back to the beginning */ function resetTokenMock() { - checkifTokenValidMock.reset(); decodeTokenValidMock.reset(); } @@ -38,15 +35,18 @@ function resetTokenMock() { * @returns return parameter (above) */ function setTokenStatus( - returnValue, + isSuccessful, data = {}, ) { - checkifTokenValidMock.returns(returnValue); - if (returnValue) { - decodeTokenValidMock.returns(data); - } else { - decodeTokenValidMock.returns(null); - } + const status = isSuccessful ? OK : UNAUTHORIZED; + const tokenPayload = isSuccessful ? data : null; + + decodeTokenValidMock.returns( + Promise.resolve({ + status: status, + token: tokenPayload, + }) + ); } module.exports = {