diff --git a/api/config/config.example.json b/api/config/config.example.json index adb32153e..60c2ded22 100644 --- a/api/config/config.example.json +++ b/api/config/config.example.json @@ -29,5 +29,8 @@ "officeAccessCard": { "API_KEY": "NOTHING_REALLY" }, + "LED_SIGN": { + "ENABLED": false + }, "secretKey": "super duper secret key" } \ No newline at end of file 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..12e2c4633 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) { @@ -236,19 +231,15 @@ router.post('/login', function(req, res) { }); }); -// Verifies the users session if they have an active jwtToken. +// Verifies the users session if they have an active jwt. // 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..e634f55f5 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 { MEMBERSHIP_STATE } = require('../../util/constants.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, MEMBERSHIP_STATE.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, MEMBERSHIP_STATE.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, MEMBERSHIP_STATE.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 a04761432..89a309ee3 100644 --- a/api/main_endpoints/routes/LedSign.js +++ b/api/main_endpoints/routes/LedSign.js @@ -5,17 +5,17 @@ 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'); const AuditLog = require('../models/AuditLog.js'); +const { + LED_SIGN = {} +} = require('../../config/config.json'); +const { MEMBERSHIP_STATE } = require('../../util/constants.js'); -const runningInDevelopment = process.env.NODE_ENV !== 'production' - && process.env.NODE_ENV !== 'test'; +const runningInTest = process.env.NODE_ENV === 'test'; router.get('/healthCheck', async (req, res) => { @@ -23,7 +23,8 @@ router.get('/healthCheck', async (req, res) => { * How these work with Quasar: * https://github.com/SCE-Development/Quasar/wiki/How-do-Health-Checks-Work%3F */ - if (runningInDevelopment) { + if (!LED_SIGN.ENABLED && !runningInTest) { + logger.warn('led sign is disabled, returning 200 by default'); return res.sendStatus(OK); } const dataFromSign = await healthCheck(); @@ -34,16 +35,13 @@ 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) { + const decoded = await decodeToken(req, MEMBERSHIP_STATE.OFFICER); + if (decoded.status !== OK) { logger.warn('/updateSignText was requested with an invalid token'); - return res.sendStatus(UNAUTHORIZED); + return res.sendStatus(decoded.status); } - if (runningInDevelopment) { + if (!LED_SIGN.ENABLED && !runningInTest) { + logger.warn('led sign is disabled, returning 200 by default'); return res.sendStatus(OK); } // need to make this its own api endpoint @@ -60,8 +58,8 @@ router.post('/updateSignText', async (req, res) => { status = SERVER_ERROR; } - AuditLog.create({ - userId: user._id, + await AuditLog.create({ + 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..cdd93e271 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,17 +14,13 @@ 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, generateAlias, deleteCard, + editAlias, } = require('../util/OfficeAccessCard.js'); const AuditLogActions = require('../util/auditLogActions.js'); const AuditLog = require('../models/AuditLog.js'); @@ -87,10 +83,10 @@ router.get('/verify', async (req, res) => { if (apiKey !== API_KEY) { writeLogToClient(req.method, { - statusCode: UNAUTHORIZED, + statusCode: FORBIDDEN, message: `Invalid API key: ${apiKey}`, }); - return res.sendStatus(UNAUTHORIZED); + return res.sendStatus(FORBIDDEN); } const cardExists = await checkIfCardExists({ cardBytes }); @@ -142,9 +138,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 +169,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 +185,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; @@ -215,10 +210,60 @@ router.post('/getAllCards', async (req, res) => { } }); +router.post('/edit', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const { _id, alias } = req.body; + + const required = [ + { value: _id && /^[0-9a-fA-F]{24}$/.test(_id) ? _id : null, title: 'Valid, alphanumeric Card ID', }, + { value: alias?.trim(), title: 'New card alias', }, + ]; + + const missingValue = required.find(({ value }) => !value); + if (missingValue) { + writeLogToClient(req.method, { + statusCode: BAD_REQUEST, + message: `${missingValue.title} missing from request`, + }); + return res.status(BAD_REQUEST).send(`${missingValue.title} missing from request`); + } + + try { + const updatedCard = await editAlias(_id, alias); + + if (!updatedCard) { + return res.sendStatus(NOT_FOUND); + } + + // Log the edit action + AuditLog.create({ + userId: decoded.token._id, + action: AuditLogActions.EDIT_CARD, + details: { + newAlias: alias, + _id, + } + }); + + logger.info(`Card alias updated successfully for card ID: ${_id}`); + return res.status(OK).json({ + message: 'Card alias updated successfully', + card: updatedCard + }); + } catch (error) { + logger.error('Error updating card alias: ', error); + return res.status(SERVER_ERROR).send('Error updating card alias'); + } +}); + 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 73a5c97f7..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,14 +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); - } - if (!await decodeToken(req)) { + 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/LedSign.js b/api/main_endpoints/util/LedSign.js index b5cb2860e..bd3b409c2 100644 --- a/api/main_endpoints/util/LedSign.js +++ b/api/main_endpoints/util/LedSign.js @@ -29,7 +29,7 @@ let LED_SIGN_URL = process.env.LED_SIGN_URL async function updateSign(data) { return new Promise((resolve) => { axios - .post(LED_SIGN_URL + '/api/update-sign', data) + .post(LED_SIGN_URL + '/update-sign', data) .then(() => { resolve(true); }).catch((err) => { @@ -46,7 +46,7 @@ async function updateSign(data) { async function turnOffSign() { return new Promise((resolve) => { axios - .get(LED_SIGN_URL + '/api/turn-off') + .get(LED_SIGN_URL + '/turn-off') .then(() => { resolve(true); }).catch((err) => { @@ -77,7 +77,7 @@ async function turnOffSign() { async function healthCheck() { return new Promise((resolve) => { axios - .get(LED_SIGN_URL + '/api/health-check') + .get(LED_SIGN_URL + '/health-check') .then(({ data }) => { resolve(data); }).catch((err) => { diff --git a/api/main_endpoints/util/OfficeAccessCard.js b/api/main_endpoints/util/OfficeAccessCard.js index c9b31cc2b..d24866564 100644 --- a/api/main_endpoints/util/OfficeAccessCard.js +++ b/api/main_endpoints/util/OfficeAccessCard.js @@ -20,8 +20,8 @@ function checkIfCardExists({ cardBytes = null, alias = null } = {}) { return resolve(false); } if (!result) { - const { description } = body; - logger.info(`Card:${description} not found in the database`); + const description = cardBytes !== null ? cardBytes : alias; + logger.info(`Card: ${description} not found in the database`); } return resolve(result); // return the document }); @@ -87,4 +87,30 @@ function deleteCard(alias) { }); } -module.exports = { checkIfCardExists, generateAlias, deleteCard }; +function editAlias(_id, newAlias) { + return new Promise((resolve) => { + try { + OfficeAccessCard.findByIdAndUpdate( + _id, + { $set: { alias: newAlias } }, + { new: true, useFindAndModify: false }, + (error, result) => { + if (error) { + logger.error('editAlias got an error querying mongodb: ', error); + return resolve(false); + } + if (!result) { + logger.info(`Card with id ${_id} not found in the database`); + return resolve(false); + } + return resolve(result); + } + ); + } catch (error) { + logger.error('editAlias caught an error: ', error); + return resolve(false); + } + }); +} + +module.exports = { checkIfCardExists, generateAlias, deleteCard, editAlias }; diff --git a/api/main_endpoints/util/auditLogActions.js b/api/main_endpoints/util/auditLogActions.js index b1da29785..13e7a3d5b 100644 --- a/api/main_endpoints/util/auditLogActions.js +++ b/api/main_endpoints/util/auditLogActions.js @@ -14,6 +14,7 @@ const AuditLogActions = { VERIFY_CARD: 'VERIFY_CARD', ADD_CARD: 'ADD_CARD', DELETE_CARD: 'DELETE_CARD', + EDIT_CARD: 'EDIT_CARD', }; module.exports = AuditLogActions; 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/CreateUserScript.js b/api/util/CreateUserScript.js index 4f957548d..838a32aaa 100644 --- a/api/util/CreateUserScript.js +++ b/api/util/CreateUserScript.js @@ -66,10 +66,6 @@ inquirer name: 'Banned', value: membershipState.BANNED }, - { - name: 'Alumni', - value: membershipState.ALUMNI - } ], filter: function(val) { console.debug(val); diff --git a/api/util/SceHttpServer.js b/api/util/SceHttpServer.js index f149379e3..0d6579ee4 100644 --- a/api/util/SceHttpServer.js +++ b/api/util/SceHttpServer.js @@ -87,7 +87,9 @@ class SceHttpServer { requireList.map((route) => { try { this.app.use(this.prefix + route.endpointName, require(route.filePath)); + MetricsHandler.errorLoadingExpressRoute.labels(route.endpointName).set(0); } catch (e) { + MetricsHandler.errorLoadingExpressRoute.labels(route.endpointName).set(1); logger.error( `error importing ${route.filePath} to handle: ${route.endpointName}:`, e diff --git a/api/util/constants.js b/api/util/constants.js index f1e07e7b0..7b7e6b68a 100644 --- a/api/util/constants.js +++ b/api/util/constants.js @@ -12,7 +12,6 @@ const MEMBERSHIP_STATE = { BANNED: -2, PENDING: -1, NON_MEMBER: 0, - ALUMNI: 0.5, MEMBER: 1, OFFICER: 2, ADMIN: 3, diff --git a/api/util/metrics.js b/api/util/metrics.js index 31b17a088..bb6c75c9a 100644 --- a/api/util/metrics.js +++ b/api/util/metrics.js @@ -57,6 +57,12 @@ class MetricsHandler { help: 'Total number of bytes from expired chunks that have been deleted' }); + errorLoadingExpressRoute = new client.Gauge({ + name: 'error_loading_express_route', + help: 'Shows if all routes are fully loaded (0 = loaded, 1 = not loaded)', + labelNames: ['endpointName'] + }) + constructor() { register.setDefaultLabels({ app: 'sce-core', 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/docker-compose.dev.yml b/docker-compose.dev.yml index c43e53df4..5084fc350 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -48,7 +48,7 @@ services: - MAIN_ENDPOINT_URL=sce-main-endpoints-dev:8080 - CLEEZY_URL=http://host.docker.internal:8000 - PRINTER_URL=http://host.docker.internal:14000 - - LED_SIGN_URL=http://host.docker.internal:11000 + - LED_SIGN_URL=http://host.docker.internal:10000 - DISCORD_REDIRECT_URI=http://localhost/api/user/callback - MAILER_API_URL=http://sce-cloud-api-dev:8082/cloudapi - DATABASE_HOST=sce-mongodb-dev diff --git a/docker-compose.yml b/docker-compose.yml index 4f2a79e26..f9a0395bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,11 @@ services: dockerfile: ./Dockerfile command: - --hosts - - http://host.docker.internal:11000/api/health-check + # led sign + - http://host.docker.internal:10000/health-check + # printer - http://host.docker.internal:14000/healthcheck/printer + # status page - http://host.docker.internal:17000/hello extra_hosts: - "host.docker.internal:host-gateway" @@ -44,7 +47,7 @@ services: environment: - CLEEZY_URL=http://cleezy-app.sce:8000 - PRINTER_URL=http://host.docker.internal:14000 - - LED_SIGN_URL=http://host.docker.internal:11000 + - LED_SIGN_URL=http://host.docker.internal:10000 - DISCORD_REDIRECT_URI=https://sce.sjsu.edu/api/user/callback - MAILER_API_URL=http://sce-cloud-api:8082/cloudapi - DATABASE_HOST=sce-mongodb diff --git a/package-lock.json b/package-lock.json index af1711335..eae699744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20105,6 +20105,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "license": "MIT", "dependencies": { "@types/cookie": "^0.3.3", "cookie": "^0.4.0" diff --git a/src/APIFunctions/Auth.js b/src/APIFunctions/Auth.js index ce58636a8..efe2351b8 100644 --- a/src/APIFunctions/Auth.js +++ b/src/APIFunctions/Auth.js @@ -1,5 +1,8 @@ import { UserApiResponse, ApiResponse } from './ApiResponses'; import { BASE_API_URL } from '../Enums'; +import Cookies from 'universal-cookie'; + +const cookies = new Cookies(); // cookie handler /** @@ -88,18 +91,16 @@ export async function loginUser(email, password) { } /** - * Checks if the user is signed in by evaluating a jwt token in local storage. + * Checks if the user is signed in by evaluating a jwt token in cookies. * @returns {UserApiResponse} Containing information for * whether the user is signed or not */ export async function checkIfUserIsSignedIn() { let status = new UserApiResponse(); - const token = window.localStorage - ? window.localStorage.getItem('jwtToken') - : ''; + const token = cookies.get('jwt') ?? ''; - // If there is not token in local storage, + // If there is not token in cookies, // we cant do anything and return if (!token) { status.error = true; @@ -203,3 +204,11 @@ export async function validatePasswordReset(resetToken) { } return status; } + +export function setJwtCookie(token) { + cookies.set('jwt', token, { maxAge: 60 * 60 * 2, secure: true, sameSite: 'strict' }); // expire cookies after 2 hours +} + +export function deleteJwtCookie() { + cookies.remove('jwt'); +} diff --git a/src/APIFunctions/CardReader.js b/src/APIFunctions/CardReader.js index 25c1e4a4b..1f7fdfcee 100644 --- a/src/APIFunctions/CardReader.js +++ b/src/APIFunctions/CardReader.js @@ -55,3 +55,28 @@ export async function deleteCardFromDb(token, alias) { } return status; } + +export async function editCardAlias(token, _id, alias) { + let status = new ApiResponse(); + try { + const url = new URL('/api/OfficeAccessCard/edit', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ _id, alias }), + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index 3221d9eb4..977855a13 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -1,5 +1,6 @@ import React from 'react'; import { useSCE } from '../context/SceContext'; +import { deleteJwtCookie } from '../../APIFunctions/Auth'; export default function UserNavBar(props) { const { user, setAuthenticated } = useSCE(); @@ -16,7 +17,7 @@ export default function UserNavBar(props) { function handleLogout() { setAuthenticated(false); - window.localStorage.removeItem('jwtToken'); + deleteJwtCookie(); window.location.reload(); } diff --git a/src/Components/Navbar/NavBarWrapper.js b/src/Components/Navbar/NavBarWrapper.js index bc096fea3..5cd82bf5f 100755 --- a/src/Components/Navbar/NavBarWrapper.js +++ b/src/Components/Navbar/NavBarWrapper.js @@ -2,6 +2,7 @@ import React from 'react'; import UserNavbar from './UserNavbar'; import AdminNavbar from './AdminNavbar'; import { useSCE } from '../context/SceContext'; +import { deleteJwtCookie } from '../../APIFunctions/Auth'; function NavBarWrapper({ enableAdminNavbar = false, @@ -13,7 +14,7 @@ function NavBarWrapper({ function handleLogout() { setAuthenticated(false); setUser({}); - window.localStorage.removeItem('jwtToken'); + deleteJwtCookie(); window.location.reload(); } diff --git a/src/Enums.js b/src/Enums.js index 71ba63922..fc9e9a6f0 100644 --- a/src/Enums.js +++ b/src/Enums.js @@ -24,7 +24,6 @@ const membershipState = { BANNED: -2, PENDING: -1, NON_MEMBER: 0, - ALUMNI: 0.5, MEMBER: 1, OFFICER: 2, ADMIN: 3, @@ -34,17 +33,12 @@ const membershipStatusArray = [ 'Ban', 'Pending', 'Nonmember', - 'Alumni', 'Member', 'Officer', 'Admin', ]; function membershipStateToString(accessLevel) { - if (accessLevel === membershipState.ALUMNI) - return 'Alumni'; - else if (accessLevel > 0) - return membershipStatusArray[accessLevel + 3]; return membershipStatusArray[accessLevel + 2]; } diff --git a/src/Pages/CardReader/CardReader.js b/src/Pages/CardReader/CardReader.js index d068ffc72..5bb60e9bc 100644 --- a/src/Pages/CardReader/CardReader.js +++ b/src/Pages/CardReader/CardReader.js @@ -1,9 +1,9 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { BASE_API_URL } from '../../Enums'; import { useSCE } from '../../Components/context/SceContext'; -import { getAllCardsFromDb, deleteCardFromDb } from '../../APIFunctions/CardReader'; +import { getAllCardsFromDb, deleteCardFromDb, editCardAlias } from '../../APIFunctions/CardReader'; import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal'; -import { trashcanSymbol } from '../Overview/SVG'; +import { trashcanSymbol, pencilSymbol, checkSymbol, cancelSymbol } from '../Overview/SVG'; const header = [ 'Time'.padEnd(29), @@ -17,10 +17,13 @@ const header = [ export default function CardReader() { const { user } = useSCE(); const token = user.token; + const [logs, setLogs] = useState([]); const [cards, setCards] = useState([]); const [toggleDelete, setToggleDelete] = useState(false); const [cardToDelete, setCardToDelete] = useState({}); + const [editingCardId, setEditingCardId] = useState(null); + const [editedAlias, setEditedAlias] = useState(''); const [tab, setTab] = useState(() => { const params = new URLSearchParams(window.location.search); return params.get('tab') || 'registry'; @@ -44,9 +47,9 @@ export default function CardReader() { const getColumnClassName = (columnName) => { let className = 'px-6 py-3 whitespace-nowrap '; - if(columnName === 'lastVerifiedAt' | columnName === 'registrationDate'){ + if (['lastVerifiedAt', 'registrationDate'].includes(columnName)) { className += 'hidden md:table-cell '; - } else if (columnName === 'verifiedCount'){ + } else if (columnName === 'verifiedCount') { className += 'hidden lg:table-cell'; } return className; @@ -89,12 +92,78 @@ export default function CardReader() { setCardToDelete(card); } + function handleEditClick(card) { + if (editingCardId === card._id) { + setEditingCardId(null); + setEditedAlias(''); + } else { + setEditingCardId(card._id); + setEditedAlias(card.alias); + } + } + + async function handleSaveEdit(cardId) { + if (!editedAlias.trim()) { + return; // Don't save empty alias + } + + try { + const response = await editCardAlias(token, cardId, editedAlias.trim()); + if (!response.error) { + // Refetch all cards from database to ensure UI matches server reality + await getAllCards(); + setEditingCardId(null); + setEditedAlias(''); + } + } catch (error) { + setLogs( + (currLogs) => [ + '[error] unable to update card alias, check browser logs: \n' + error, + ...currLogs, + ] + ); + } + } + + function handleEditKeyDown(key) { + if (key === 'Enter') { + handleSaveEdit(editingCardId); + } else if (key === 'Escape') { + setEditingCardId(null); + setEditedAlias(''); + } + } + + function renderInputOrAlias(card) { + if (editingCardId === card._id) { + return ( + setEditedAlias(e.target.value)} + onKeyDown={(e) => handleEditKeyDown(e.key)} + className='bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded text-gray-700 dark:text-white font-medium m-0 px-1 py-0 focus:outline-none focus:ring-1 focus:ring-blue-500' + style={{ width: '16ch' }} + autoFocus + /> + ); + } + return ( +
The current sign message will expire on {humanizedExpiration}
; + } + + function getExpirationButtonOrInput() { + if (showInput) { + return <> ++ Your selected expiration is considered behind the current time of {getFormattedTime()}. +
++ Submitting a message with this expiration will not update the sign. +
+