From c8866e31bfae1a832c334f027f6e47ab3ebe1b54 Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Tue, 29 Jul 2025 02:26:39 -0700 Subject: [PATCH 01/14] added mobile sizing to the search modal --- src/Components/ShortcutKeyModal/SearchModal.css | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Components/ShortcutKeyModal/SearchModal.css b/src/Components/ShortcutKeyModal/SearchModal.css index 2f0ccef8c..40667ac33 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.css +++ b/src/Components/ShortcutKeyModal/SearchModal.css @@ -8,8 +8,7 @@ /* blurred background */ display: flex; justify-content: center; - align-items: flex-start; - padding-top: 33vh; + align-items: center; z-index: 9999; } @@ -17,7 +16,9 @@ padding: 8px; background: white; border-radius: 8px; - width: 35rem; + width: 100%; + /* the default is the mobile size */ + max-width: 500px; } .shortcut-search-modal input { @@ -67,3 +68,11 @@ background-color: #1e293b; } } + +/* Desktop mode */ +@media (min-width: 768px) { + .shortcut-search-modal .input-wrapper { + width: 35rem; + max-width: none; + } +} \ No newline at end of file From cae685b0bfe9233ee410d1b8ca3ba6cc7776a38b Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Tue, 29 Jul 2025 03:21:30 -0700 Subject: [PATCH 02/14] used context to add a modal button to the navbar --- src/Components/Navbar/UserNavbar.js | 11 +++++++++-- src/Components/ShortcutKeyModal/SearchModal.js | 5 ++++- src/index.js | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Components/Navbar/UserNavbar.js b/src/Components/Navbar/UserNavbar.js index 9515a4c00..48d3c96e5 100644 --- a/src/Components/Navbar/UserNavbar.js +++ b/src/Components/Navbar/UserNavbar.js @@ -3,7 +3,7 @@ import { membershipState } from '../../Enums'; import { useSCE } from '../context/SceContext'; export default function UserNavbar(props) { - const { user, authenticated } = useSCE(); + const { user, authenticated, setModalOpen } = useSCE(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); let initials = ''; @@ -84,8 +84,15 @@ export default function UserNavbar(props) {
-
diff --git a/src/Components/ShortcutKeyModal/SearchModal.js b/src/Components/ShortcutKeyModal/SearchModal.js index 09a748317..cc9d33139 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.js +++ b/src/Components/ShortcutKeyModal/SearchModal.js @@ -6,7 +6,10 @@ import { useSCE } from '../context/SceContext'; import { searchUsersAndCleezyUrls } from '../../APIFunctions/ShortcutSearch'; export default function SearchModal() { - const [open, setOpen] = useState(false); + const {modalOpen, setModalOpen} = useSCE(); + //kept original open and setOpen state variables + const open = modalOpen; + const setOpen = setModalOpen; const inputRef = useRef(null); const modalRef = useRef(null); const filteredSignedOutRoutes = [...signedOutRoutes].filter(r => !r.hideFromShortcutSuggestions); diff --git a/src/index.js b/src/index.js index 312e36477..91bc7fdfb 100755 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ function App() { const [authenticated, setAuthenticated] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(true); const [user, setUser] = useState({}); + const [modalOpen, setModalOpen] = useState(false); async function getAuthStatus() { setIsAuthenticating(true); @@ -30,10 +31,10 @@ function App() { return ( !isAuthenticating && ( - + - - + + ) From 55d19280e2cdbe28bf662b777979aaaccbd15acb Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:40:37 -0700 Subject: [PATCH 03/14] added support for button on mobile devices --- src/Components/Navbar/UserNavbar.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Components/Navbar/UserNavbar.js b/src/Components/Navbar/UserNavbar.js index 48d3c96e5..b865562c7 100644 --- a/src/Components/Navbar/UserNavbar.js +++ b/src/Components/Navbar/UserNavbar.js @@ -86,17 +86,17 @@ export default function UserNavbar(props) {
+ {authenticated && user ? ( <>
From 3c713045cdc2cd32a04e16d730ccee34465e883e Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:33:54 -0700 Subject: [PATCH 04/14] changed to tailwind css classnames, moved urls to the right of the list --- .../ShortcutKeyModal/SearchModal.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Components/ShortcutKeyModal/SearchModal.js b/src/Components/ShortcutKeyModal/SearchModal.js index cc9d33139..770f4c605 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.js +++ b/src/Components/ShortcutKeyModal/SearchModal.js @@ -80,11 +80,11 @@ export default function SearchModal() { const topFiveItems = suggestions.slice(0, SHORTCUT_MAX_RESULT); return ( -
    +
      {topFiveItems.map((r, index) => (
    • setSelectItem(index)} onClick={() => { if (externalSiteRoute(r)) return; @@ -92,14 +92,20 @@ export default function SearchModal() { setOpen(false); }} > - - {r.type === 'user' ? '👤' : '📄'} - -
      - {r.pageName} -
      - {selectItem === index && (r.type === 'external_url' ? r.path : `${window.location.origin}${r.path}`)} +
      +
      + + {r.type === 'user' ? '👤' : '📄'} + + + {r.pageName} +
      + {selectItem === index && ( +
      + {r.type === 'external_url' ? r.path : window.location.origin + r.path} +
      + )}
    • ))} @@ -277,14 +283,16 @@ export default function SearchModal() { if (!open) return null; return ( -
      +
      -
      +
      + onChange={handleChanges} + className='border-[1.5px] border-gray-600 w-full rounded p-3 h-10 text-[1.2rem] bg-transparent focus:outline-sky-600' + />
      From 5559e68a3cba96c2a1900b884f234d65c29e2edb Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:27:49 -0700 Subject: [PATCH 05/14] moved urls backunderneath page name, and now always show the url --- .../ShortcutKeyModal/SearchModal.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Components/ShortcutKeyModal/SearchModal.js b/src/Components/ShortcutKeyModal/SearchModal.js index 770f4c605..f1aa76ea7 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.js +++ b/src/Components/ShortcutKeyModal/SearchModal.js @@ -80,11 +80,11 @@ export default function SearchModal() { const topFiveItems = suggestions.slice(0, SHORTCUT_MAX_RESULT); return ( -
        +
          {topFiveItems.map((r, index) => (
        • setSelectItem(index)} onClick={() => { if (externalSiteRoute(r)) return; @@ -92,20 +92,20 @@ export default function SearchModal() { setOpen(false); }} > -
          +
          {r.type === 'user' ? '👤' : '📄'} - + {r.pageName}
          - {selectItem === index && ( -
          - {r.type === 'external_url' ? r.path : window.location.origin + r.path} -
          - )} + + + {r.type === 'external_url' ? r.path : window.location.origin + r.path} + +
        • ))} @@ -285,7 +285,7 @@ export default function SearchModal() { return (
          -
          +
          ); -} +} \ No newline at end of file From 45368e4383493bc62d8ccd74d33ff776131b73db Mon Sep 17 00:00:00 2001 From: Sujan <56094667+Sujan30@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:50:39 -0700 Subject: [PATCH 06/14] Added ChatMessage schema & fixed api logic so it actually stores each chatMessage in the db (#1822) * added ChatMessage schema & modified writeMessage function to write to mongodb * modified /getLatestmessage to get the latest message, need to test it out in postman now * updated writeMessage to be async for proper mongoDB chatMessage creation * getLatestMessage now works for specific chatRoom ids, gets the userid via querying the User database by username and then extracting the userId * uncommented out some token and apikey logic * fixed lint errors yay * reverted the package-lock.json file to the one from dev branch * removed all the random styling changes, kept only the logic changes * changed from expiresAt to expireAt and added TTL index for automatic expiration * made writing to mongoDb as a seperate function * removed the extracting username logic from writeMessage. * fixed lint errors * removed writeToMongo function, just added it inside the /send route * Fixed lint errors --- api/main_endpoints/models/ChatMessage.js | 35 ++++++++++++++ api/main_endpoints/routes/Messages.js | 58 ++++++++++++------------ 2 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 api/main_endpoints/models/ChatMessage.js diff --git a/api/main_endpoints/models/ChatMessage.js b/api/main_endpoints/models/ChatMessage.js new file mode 100644 index 000000000..57bf95a5f --- /dev/null +++ b/api/main_endpoints/models/ChatMessage.js @@ -0,0 +1,35 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const ChatMessageSchema = new Schema( + { + createdAt: { + type: Date, + default: Date.now(), + }, + expiresAt: { + type: Date, + default: ()=> Date.now() + 24 * 3600 * 1000, // expires in 24 hours + }, + chatroomId: { + type: String, + required: true, + }, + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + text: { + type: String, + required: true, + } + } +); + +ChatMessageSchema.index({chatroomId: 1, createdAt: -1}); // sort by whatever is created most currently +ChatMessageSchema.index({expiresAt: 1}, {expireAfterSeconds: 0}); // TTL index for automatic expiration + +module.exports = mongoose.model('ChatMessage', ChatMessageSchema); + + diff --git a/api/main_endpoints/routes/Messages.js b/api/main_endpoints/routes/Messages.js index 82adafc8d..885afd46c 100644 --- a/api/main_endpoints/routes/Messages.js +++ b/api/main_endpoints/routes/Messages.js @@ -13,6 +13,7 @@ const logger = require('../../util/logger'); const client = require('prom-client'); const { decodeToken, decodeTokenFromBodyOrQuery } = require('../util/token-functions.js'); const { MetricsHandler, register } = require('../../util/metrics.js'); +const ChatMessage = require('../models/ChatMessage.js'); router.use(bodyParser.json()); @@ -23,7 +24,8 @@ const clients = {}; const numberOfConnections = {}; const lastMessageSent = {}; -const writeMessage = ((roomId, message, username) => { + +const writeMessage = (roomId, message, username) => { const messageObj = { timestamp: Date.now(), @@ -37,12 +39,13 @@ const writeMessage = ((roomId, message, username) => { lastMessageSent[roomId] = JSON.stringify(messageObj); + // increase the total messages sent counter MetricsHandler.totalMessagesSent.inc(); // increase the total amount of messages sent per chatroom counter MetricsHandler.totalChatMessagesPerChatRoom.labels(roomId).inc(); -}); +}; router.post('/send', async (req, res) => { @@ -65,6 +68,7 @@ router.post('/send', async (req, res) => { } let nameToUse = null; + let userId = null; if (apiKey) { try { @@ -73,20 +77,30 @@ router.post('/send', async (req, res) => { return res.sendStatus(UNAUTHORIZED); } nameToUse = result.firstName; + userId = result._id; + await writeToMongo(id, message, userId); } catch (error) { logger.error('Error in /send User.findOne: ', error); return res.sendStatus(SERVER_ERROR); } + } else { + const userObj = decodeToken(req); + if (!userObj) { + return res.sendStatus(UNAUTHORIZED); + } + nameToUse = userObj.firstName; + userId = userObj._id; } - - // Assume user passed a non null/undefined token - const userObj = decodeToken(req); - if (!userObj) { - return res.sendStatus(UNAUTHORIZED); - } - nameToUse = userObj.firstName; try { writeMessage(id, `${message}`, `${nameToUse}:`); + + ChatMessage.create({ + chatroomId: id, + text: message, + userId: userId + }).catch(err => { + logger.error('Error in /send ChatMessage.create: ', err); + }); return res.json({ status: 'Message sent' }); } catch (error) { logger.error('Error in /send writeMessage: ', error); @@ -103,33 +117,23 @@ router.get('/getLatestMessage', async (req, res) => { ]; const missingValue = required.find(({value}) => !value); - if (missingValue){ res.status(BAD_REQUEST).send(`You must specify a ${missingValue.title}`); return; } try { - User.findOne({ apiKey }, (error, result) => { - if (error) { - logger.error('/listen received an invalid API key: ', error); - res.sendStatus(SERVER_ERROR); - return; - } - - if (!result) { // return unauthorized if no api key found - return res.sendStatus(UNAUTHORIZED); - } + const user = await User.findOne({apiKey}); + if(!user){ + return res.sendStatus(UNAUTHORIZED); + } - if (!lastMessageSent[id]) { - return res.status(OK).send('Room closed'); - } + const messages = await ChatMessage.find({chatroomId: id}).sort({createdAt: -1}).limit(20).populate('userId'); - return res.status(OK).send(lastMessageSent[id]); + return res.status(OK).json(messages); - }); } catch (error) { - logger.error('Error in /get: ', error); + logger.error('Error in /getLatestMessage: ', error); res.sendStatus(SERVER_ERROR); } }); @@ -144,7 +148,6 @@ router.get('/listen', async (req, res) => { ]; const missingValue = required.find(({value}) => !value); - if (missingValue){ res.status(BAD_REQUEST).send(`You must specify a ${missingValue.title}`); return; @@ -174,7 +177,6 @@ router.get('/listen', async (req, res) => { } const { _id } = result; - numberOfConnections[_id] = numberOfConnections[_id] ? numberOfConnections[_id] + 1 : 1; const headers = { From b6a84ddde132e07916f58e295f3dfcb427f1dd19 Mon Sep 17 00:00:00 2001 From: adarshm11 Date: Wed, 30 Jul 2025 19:23:40 -0700 Subject: [PATCH 07/14] test ts From afe56958dfa385285c31ad4ae45a637467bf3b63 Mon Sep 17 00:00:00 2001 From: DavidN016 <152849233+DavidN016@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:57:46 -0700 Subject: [PATCH 08/14] updated forgot page to match login page ui (#1851) * updated forgot page to match login page ui * Fix linting issues on ForgotPassword.js * removed originalForgot.js * disable button sometimes --------- Co-authored-by: evan --- package-lock.json | 2 + src/Pages/ForgotPassword/ForgotPassword.js | 67 ++++++++++++---------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 243134f32..87c93573e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7627,6 +7627,8 @@ "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", diff --git a/src/Pages/ForgotPassword/ForgotPassword.js b/src/Pages/ForgotPassword/ForgotPassword.js index cd93bfa43..0e8603aa9 100644 --- a/src/Pages/ForgotPassword/ForgotPassword.js +++ b/src/Pages/ForgotPassword/ForgotPassword.js @@ -1,13 +1,14 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { sendPasswordReset } from '../../APIFunctions/Mailer'; -import Background from '../../Components/Background/background'; import GoogleRecaptcha from '../../Components/Captcha/GoogleRecaptcha'; -const ForgotPassword = () => { +export default function Login() { const [email, setEmail] = useState(''); const [message, setMessage] = useState(''); const [captchaValue, setCaptchaValue] = useState(null); const [captchaRef, setCaptchaRef] = useState(null); + const [loading, setLoading] = useState(false); + const [submitted, setSubmitted] = useState(false); async function handleSubmit(e) { e.preventDefault(); @@ -19,44 +20,52 @@ const ForgotPassword = () => { setMessage('Please enter a valid email address.'); return; } - + setLoading(true); captchaRef.reset(); const resetStatus = await sendPasswordReset(email, captchaValue); if (resetStatus.error) { setMessage(resetStatus.error?.response?.data?.message || 'An error occurred. Please try again later.'); } else { + setSubmitted(true); setMessage('A password reset email has been sent to you if your email exists in our system.'); } + setLoading(false); } return ( -
          -
          -
          - sce logo -
          -
          - -
          - -
          - {message &&

          +

          +
          +
          +
          +
          +

          Reset your account

          +

          + Enter your email below to reset to your account +

          +
          + + +
          + +
          + {message &&

          {message}

          } - - + + +
          +
          +
          -
          ); -}; - -export default ForgotPassword; +} From eb8721a2dc7d84b6cc3314a49982059129b5b057 Mon Sep 17 00:00:00 2001 From: ariansbahram <151223500+ariansbahram@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:34:58 -0700 Subject: [PATCH 09/14] Update user's last login date within /login endpoint (#1865) * Fix: Set lastLogin date in /login route * delete the api function * delete updateLastLoginDate * delete comment --- api/main_endpoints/routes/Auth.js | 139 +++++++++++++++--------------- src/APIFunctions/Auth.js | 2 - src/APIFunctions/User.js | 13 --- 3 files changed, 71 insertions(+), 83 deletions(-) diff --git a/api/main_endpoints/routes/Auth.js b/api/main_endpoints/routes/Auth.js index d4db4c5a9..b8c29feb4 100644 --- a/api/main_endpoints/routes/Auth.js +++ b/api/main_endpoints/routes/Auth.js @@ -146,81 +146,84 @@ router.post('/login', function(req, res) { } if (!user) { - res + return res .status(UNAUTHORIZED) .send({ message: 'Username or password does not match our records.' }); - } else { - // Check if password matches database - user.comparePassword(req.body.password, function(error, isMatch) { - if (isMatch && !error) { - if (user.accessLevel === membershipState.BANNED) { - return res - .status(UNAUTHORIZED) - .send({ - message: 'The account with email ' + + } + + // Check if password matches database + user.comparePassword(req.body.password, function(error, isMatch) { + if (!isMatch && !error) { + return res.status(UNAUTHORIZED).send({ + message: 'Username or password does not match our records.' + }); + } + + if (user.accessLevel === membershipState.BANNED) { + return res + .status(UNAUTHORIZED) + .send({ + message: 'The account with email ' + req.body.email + ' is banned', - }); - } - - // Check if the user's email has been verified - if (!user.emailVerified) { - return res - .status(UNAUTHORIZED) - .send({ message: `The email ${req.body.email} has not been verified` }); - } - - // If the username and password matches the database, assign and - // return a jwt token - const jwtOptions = { - expiresIn: '2h' - }; - - // check here to see if we should reset the pagecount. If so, do it - if (checkIfPageCountResets(user.lastLogin)) { - user.pagesPrinted = 0; - } - - // Include fields from the User model that should - // be passed to the JSON Web Token (JWT) - const userToBeSigned = { - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - accessLevel: user.accessLevel, - pagesPrinted: user.pagesPrinted, - _id: user._id - }; - user - .save() - .then(() => { - const token = jwt.sign( - userToBeSigned, config.secretKey, jwtOptions - ); - // Create audit log on successful sign-in - AuditLog.create({ - userId: user._id, - action: AuditLogActions.LOG_IN, - details: { email: user.email } - }).catch(logger.error); - - res.json({ token: 'JWT ' + token }); - }) - .catch((error) => { - logger.error('unable to login user', error); - res.sendStatus(SERVER_ERROR); - }); - } else { - res.status(UNAUTHORIZED).send({ - message: 'Username or password does not match our records.' }); - } - }); - } - } - ); + } + + // Check if the user's email has been verified + if (!user.emailVerified) { + return res + .status(UNAUTHORIZED) + .send({ message: `The email ${req.body.email} has not been verified` }); + } + + // If the username and password matches the database, assign and + // return a jwt token + const jwtOptions = { + expiresIn: '2h' + }; + + // check here to see if we should reset the pagecount. If so, do it + if (checkIfPageCountResets(user.lastLogin)) { + user.pagesPrinted = 0; + } + + // set last login date here!!!! + user.lastLogin = new Date(); + + + // Include fields from the User model that should + // be passed to the JSON Web Token (JWT) + const userToBeSigned = { + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + accessLevel: user.accessLevel, + pagesPrinted: user.pagesPrinted, + _id: user._id + }; + user + .save() + .then(() => { + const token = jwt.sign( + userToBeSigned, config.secretKey, jwtOptions + ); + // Create audit log on successful sign-in + AuditLog.create({ + userId: user._id, + action: AuditLogActions.LOG_IN, + details: { email: user.email } + }).catch(logger.error); + + res.json({ token: 'JWT ' + token }); + }) + .catch((error) => { + logger.error('unable to login user', error); + res.sendStatus(SERVER_ERROR); + }); + }); + }); }); // Verifies the users session if they have an active jwtToken. diff --git a/src/APIFunctions/Auth.js b/src/APIFunctions/Auth.js index 7a3a60f83..ce58636a8 100644 --- a/src/APIFunctions/Auth.js +++ b/src/APIFunctions/Auth.js @@ -1,5 +1,4 @@ import { UserApiResponse, ApiResponse } from './ApiResponses'; -import { updateLastLoginDate } from './User'; import { BASE_API_URL } from '../Enums'; @@ -76,7 +75,6 @@ export async function loginUser(email, password) { const result = await res.json(); if (res.ok) { status.token = result.token; - await updateLastLoginDate(email, result.token); window.location.reload(); return status; } diff --git a/src/APIFunctions/User.js b/src/APIFunctions/User.js index 6b9deaf78..cb072a556 100644 --- a/src/APIFunctions/User.js +++ b/src/APIFunctions/User.js @@ -141,19 +141,6 @@ export async function editUser(userToEdit, token) { return status; } -/** - * Updates the user's last login date when they log in. - * @param {string} email The email of the user - * @param {string} token The JWT token to allow the user to be edited - */ -export async function updateLastLoginDate(email, token) { - await editUser({ email, lastLogin: Date.now() }, { - headers: { - Authorization: `Bearer ${token}` - } - }); -} - /** * Deletes a user by an ID * @param {string} _id The ID of the user to delete From 868c37784929d5d5ec1a8d270223ce6a9c8534d1 Mon Sep 17 00:00:00 2001 From: Patrick Hoang Date: Thu, 31 Jul 2025 19:32:19 -0700 Subject: [PATCH 10/14] Audit logs frontend lightmode (#1824) * add basic audit logs page for admin dashboard * change items per page from 5 to 50 + lint updates * add formatting for audit log cards * remove unused error + loading components * move utils to components file * combine utils into AuditLogCard.js * lint fix * implement filtering and updated pagination * update activityTypes.js to match mongodb schema * ensure filtering takes into account 0-indexed currentPage + adjust useEffect to trigger when applying filter * lint fix * change filtering to one input that searches for first name, last name, and email * add key per auditLogCard + combine FilterActivityTypes to AuditLog.js page * lint fix * implement colors for light mode + redirect to user edit when pressing on audit log username * minor css change --- src/Pages/AuditLog/AuditLog.js | 28 +++++++++--------- src/Pages/AuditLog/Components/AuditLogCard.js | 29 ++++++++++--------- src/Pages/AuditLog/Components/Pagination.js | 12 ++++---- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Pages/AuditLog/AuditLog.js b/src/Pages/AuditLog/AuditLog.js index dfa83478c..9e7d60aa7 100644 --- a/src/Pages/AuditLog/AuditLog.js +++ b/src/Pages/AuditLog/AuditLog.js @@ -89,14 +89,14 @@ export default function AuditLogPage() { return (
          - Loading audit logs... + Loading audit logs...
          ); } if (error) { return ( -
          +
          Error: {error}
          @@ -107,8 +107,8 @@ export default function AuditLogPage() { return (
          📋
          -

          No audit logs found

          -

          There are no audit logs to display at this time.

          +

          No audit logs found

          +

          There are no audit logs to display at this time.

          ); } @@ -135,12 +135,12 @@ export default function AuditLogPage() { Audit Logs
          -
          +
          - +
          - + {isDropdownOpen && ( -
          +
          {activityTypes.map(activity => ( ))}
          diff --git a/src/Pages/AuditLog/Components/AuditLogCard.js b/src/Pages/AuditLog/Components/AuditLogCard.js index 8f2484dd5..cb36705af 100644 --- a/src/Pages/AuditLog/Components/AuditLogCard.js +++ b/src/Pages/AuditLog/Components/AuditLogCard.js @@ -8,12 +8,12 @@ const AuditLogCard = ({ log, index }) => { return null; } return ( -
          -

          Details:

          +
          +

          Details:

          {Object.entries(details).map(([key, value]) => ( -
          - {key}: {String(value)} +
          + {key}: {String(value)}
          ))}
          @@ -71,7 +71,7 @@ const AuditLogCard = ({ log, index }) => { }; return ( -
          +
          @@ -79,22 +79,25 @@ const AuditLogCard = ({ log, index }) => {
          -

          - +

          + {log.userId ? `${log.userId.firstName} ${log.userId.lastName}` : 'Unknown User'} - {' '} - {getActionDescription(log)} + {' '} + {getActionDescription(log)}

          {log.documentId && log.documentId !== log.userId && ( -

          - Target: User {log.documentId} +

          + Target: User {log.documentId}

          )}
          -
          +
          @@ -102,7 +105,7 @@ const AuditLogCard = ({ log, index }) => {
          - + {log.action.replace(/_/g, ' ')}
          diff --git a/src/Pages/AuditLog/Components/Pagination.js b/src/Pages/AuditLog/Components/Pagination.js index 186b1d16a..1687a4a72 100644 --- a/src/Pages/AuditLog/Components/Pagination.js +++ b/src/Pages/AuditLog/Components/Pagination.js @@ -1,25 +1,25 @@ const Pagination = ({ currentPage, totalPages, goToPage, startIndex, endIndex, totalResults }) => { function getPreviousButtonClassName(currentPage) { return currentPage === 0 - ? 'px-3 py-2 rounded-md text-sm font-medium bg-gray-700 text-gray-500 cursor-not-allowed' - : 'px-3 py-2 rounded-md text-sm font-medium bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-600'; + ? 'px-3 py-2 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' + : 'px-3 py-2 rounded-md text-sm font-medium bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 border border-gray-300 dark:border-gray-600'; } function getPageButtonClassName(pageNum, currentPage) { return currentPage === pageNum ? 'px-3 py-2 rounded-md text-sm font-medium bg-blue-600 text-white' - : 'px-3 py-2 rounded-md text-sm font-medium bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-600'; + : 'px-3 py-2 rounded-md text-sm font-medium bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 border border-gray-300 dark:border-gray-600'; } function getNextButtonClassName(currentPage, totalPages) { return currentPage === totalPages - 1 - ? 'px-3 py-2 rounded-md text-sm font-medium bg-gray-700 text-gray-500 cursor-not-allowed' - : 'px-3 py-2 rounded-md text-sm font-medium bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-600'; + ? 'px-3 py-2 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' + : 'px-3 py-2 rounded-md text-sm font-medium bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 border border-gray-300 dark:border-gray-600'; } return (
          -
          +
          Showing {startIndex + 1} to {endIndex} of {totalResults} results
          From c688fc4e267c7b8d1452f5689f0bf2308b078be0 Mon Sep 17 00:00:00 2001 From: marvzhai Date: Thu, 31 Jul 2025 19:50:31 -0700 Subject: [PATCH 11/14] Audit on update ledsign (#1858) * add audit logs on led sign update + tests * lint fix * lint fix * remove editedBy field in audit log details Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * fix api test --------- Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> --- api/main_endpoints/routes/LedSign.js | 15 ++++- api/main_endpoints/util/auditLogActions.js | 1 + test/api/LedSign.js | 77 +++++++++++++++++++++- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/api/main_endpoints/routes/LedSign.js b/api/main_endpoints/routes/LedSign.js index 47a67fd3f..a04761432 100644 --- a/api/main_endpoints/routes/LedSign.js +++ b/api/main_endpoints/routes/LedSign.js @@ -11,6 +11,8 @@ const { } = 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 runningInDevelopment = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'; @@ -36,7 +38,8 @@ router.post('/updateSignText', async (req, res) => { logger.warn('/updateSignText was requested without a token'); return res.sendStatus(UNAUTHORIZED); } - if (!await decodeToken(req)) { + const user = await decodeToken(req); // Store the user here + if (!user) { logger.warn('/updateSignText was requested with an invalid token'); return res.sendStatus(UNAUTHORIZED); } @@ -56,8 +59,16 @@ router.post('/updateSignText', async (req, res) => { if(!result) { status = SERVER_ERROR; } + + AuditLog.create({ + userId: user._id, + action: AuditLogActions.UPDATE_SIGN, + details: { + newSignText: req.body.text, + } + }).catch(logger.error); + return res.sendStatus(status); }); - module.exports = router; diff --git a/api/main_endpoints/util/auditLogActions.js b/api/main_endpoints/util/auditLogActions.js index b53ceaf0f..1c8d987f7 100644 --- a/api/main_endpoints/util/auditLogActions.js +++ b/api/main_endpoints/util/auditLogActions.js @@ -7,6 +7,7 @@ const AuditLogActions = { EMAIL_SENT: 'EMAIL_SENT', CHANGE_PW: 'CHANGE_PW', RESET_PW: 'RESET_PW', + UPDATE_SIGN: 'UPDATE_SIGN', VERIFY_CARD: 'VERIFY_CARD', ADD_CARD: 'ADD_CARD', DELETE_CARD: 'DELETE_CARD', diff --git a/test/api/LedSign.js b/test/api/LedSign.js index ebf1059fd..2ecd3c1a5 100644 --- a/test/api/LedSign.js +++ b/test/api/LedSign.js @@ -17,7 +17,9 @@ const { restoreTokenMock, } = require('../util/mocks/TokenValidFunctions'); const SshTunnelFunctions = require('../../api/main_endpoints/util/LedSign'); - +const AuditLog = require('../../api/main_endpoints/models/AuditLog'); +const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions'); +const mongoose = require('mongoose'); let app = null; let test = null; @@ -91,6 +93,79 @@ describe('LED Sign', () => { '/api/LedSign/updateSignText'); expect(result).to.have.status(OK); }); + + describe('tests for audit log on sign text update', () => { + const userId = new mongoose.Types.ObjectId(); + + beforeEach(async () => { + await AuditLog.deleteMany({}); + + // test user + setTokenStatus(true, { + _id: userId, + email: 'admin@test.com', + accessLevel: 'ADMIN' + }); + }); + + afterEach(async () => { + await AuditLog.deleteMany({}); + }); + + it('Should create audit log when LED sign is successfully updated', async () => { + updateSignStub.resolves(true); + + const signData = { + text: 'Welcome to SCE!', + duration: 5000 + }; + + const result = await test.sendPostRequestWithToken(token, + '/api/LedSign/updateSignText', signData); + + expect(result).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.UPDATE_SIGN + }).lean(); + + expect(auditEntry).to.exist; + expect(auditEntry.action).to.equal(AuditLogActions.UPDATE_SIGN); + expect(auditEntry.details).to.have.property('newSignText', 'Welcome to SCE!'); + }); + + it('Should create audit log with user information who updated the sign', async () => { + updateSignStub.resolves(true); + + setTokenStatus(true, { + _id: userId, + email: 'admin@sce.edu', + firstName: 'Ad', + lastName: 'Min', + accessLevel: 'ADMIN' + }); + + const signData = { + text: 'Updated by admin', + duration: 4000 + }; + + const result = await test.sendPostRequestWithToken(token, + '/api/LedSign/updateSignText', signData); + + expect(result).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.UPDATE_SIGN + }).lean(); + + expect(auditEntry).to.exist; + expect(auditEntry.userId.toString()).to.equal(userId.toString()); + }); + + }); }); describe('/GET healthCheck', () => { From 1603d1b5f5b3f66a949d57fff7a83523a5040983 Mon Sep 17 00:00:00 2001 From: marvzhai Date: Thu, 31 Jul 2025 19:54:29 -0700 Subject: [PATCH 12/14] Add audit logs for advertisement creation/deletion (#1859) * add audit logs for ad creation/deletion * i accidentally changed the wrong file lol * remove createdBy field in audit log details Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * remove redundant fields in audit logs * lint fix * Update api/main_endpoints/routes/Advertisement.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> --------- Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> --- api/main_endpoints/routes/Advertisement.js | 77 ++++++--- api/main_endpoints/util/auditLogActions.js | 2 + api/util/metrics.js | 16 +- test/api/Advertisement.js | 177 ++++++++++++++++++++- 4 files changed, 243 insertions(+), 29 deletions(-) diff --git a/api/main_endpoints/routes/Advertisement.js b/api/main_endpoints/routes/Advertisement.js index 93a199e4a..a726a992c 100644 --- a/api/main_endpoints/routes/Advertisement.js +++ b/api/main_endpoints/routes/Advertisement.js @@ -5,7 +5,10 @@ const { decodeToken, checkIfTokenSent, } = 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'); router.get('/', async (req, res) => { const count = await Advertisement.countDocuments(); @@ -36,21 +39,35 @@ router.get('/getAllAdvertisements', async (req, res) => { router.post('/createAdvertisement', async (req, res) => { if (!checkIfTokenSent(req)) { return res.sendStatus(FORBIDDEN); - } else if (!await decodeToken(req)) { + } + + const user = await decodeToken(req); + if (!user) { return res.sendStatus(UNAUTHORIZED); } + const newAd = new Advertisement({ message: req.body.message, expireDate: req.body.expireDate }); - Advertisement.create(newAd) - .then((post) => { - return res.json(post); - }) - .catch( - (error) => res.sendStatus(BAD_REQUEST) - ); + try { + const createdAd = await Advertisement.create(newAd); + AuditLog.create({ + userId: user._id, + action: AuditLogActions.CREATE_AD, + details: { + message: createdAd.message, + expireDate: createdAd.expireDate, + advertisementId: createdAd._id + } + }).catch(logger.error); + + res.status(OK).send(createdAd); + } catch (error) { + logger.error('Error creating ad:', error); + res.sendStatus(BAD_REQUEST); + } }); router.post('/deleteAdvertisement', async (req, res) => { @@ -59,17 +76,41 @@ router.post('/deleteAdvertisement', async (req, res) => { } else if (!await decodeToken(req)) { return res.sendStatus(UNAUTHORIZED); } - Advertisement.deleteOne({ _id: req.body._id }) - .then(result => { - if (result.n < 1) { - res.sendStatus(NOT_FOUND); - } else { - res.sendStatus(OK); + + const user = await decodeToken(req); + if (!user) { + return res.sendStatus(UNAUTHORIZED); + } + + try { + const adToDelete = await Advertisement.findById(req.body._id); + + if (!adToDelete) { + return res.sendStatus(NOT_FOUND); + } + + const deleteResult = await Advertisement.deleteOne({_id: req.body._id}); + + if(deleteResult.deletedCount < 1) { + return res.sendStatus(NOT_FOUND); + } + + AuditLog.create({ + userId: user._id, + action: AuditLogActions.DELETE_AD, + details: { + deletedAd: { + id: adToDelete._id, + message: adToDelete.message, + } } - }) - .catch(() => { - res.sendStatus(BAD_REQUEST); - }); + }).catch(logger.error); + + res.sendStatus(OK); + } catch (error) { + logger.error('Error deleting ad:', error); + res.sendStatus(BAD_REQUEST); + } }); module.exports = router; diff --git a/api/main_endpoints/util/auditLogActions.js b/api/main_endpoints/util/auditLogActions.js index 1c8d987f7..3b18bd9e8 100644 --- a/api/main_endpoints/util/auditLogActions.js +++ b/api/main_endpoints/util/auditLogActions.js @@ -7,6 +7,8 @@ const AuditLogActions = { EMAIL_SENT: 'EMAIL_SENT', CHANGE_PW: 'CHANGE_PW', RESET_PW: 'RESET_PW', + CREATE_AD: 'CREATE_AD', + DELETE_AD: 'DELETE_AD', UPDATE_SIGN: 'UPDATE_SIGN', VERIFY_CARD: 'VERIFY_CARD', ADD_CARD: 'ADD_CARD', diff --git a/api/util/metrics.js b/api/util/metrics.js index 039d1d9aa..31b17a088 100644 --- a/api/util/metrics.js +++ b/api/util/metrics.js @@ -2,28 +2,28 @@ const client = require('prom-client'); const register = new client.Registry(); class MetricsHandler { - endpointHits = new client.Counter({ + endpointHits = new client.Counter({ name: 'endpoint_hits', help: 'Counter for tracking endpoint hits with status codes', labelNames: ['method', 'route', 'statusCode'], - }); + }); - emailSent = new client.Counter({ + emailSent = new client.Counter({ name: 'email_sent', help: 'Counter for tracking emails sent', labelNames: ['type'], - }); + }); - captchaVerificationErrors = new client.Counter({ + captchaVerificationErrors = new client.Counter({ name: 'captcha_verification_errors', help: 'Counter for tracking captcha verification errors', - }); + }); - sshTunnelErrors = new client.Counter({ + sshTunnelErrors = new client.Counter({ name: 'ssh_tunnel_errors', help: 'Counter for tracking ssh tunnel errors', labelNames: ['type'], - }); + }); totalMessagesSent = new client.Counter({ name: 'total_messages_sent', diff --git a/test/api/Advertisement.js b/test/api/Advertisement.js index f66c827e1..9e48aecf9 100644 --- a/test/api/Advertisement.js +++ b/test/api/Advertisement.js @@ -4,8 +4,17 @@ const Advertisement = require('../../api/main_endpoints/models/Advertisement'); const chai = require('chai'); const chaiHttp = require('chai-http'); const constants = require('../../api/util/constants'); -const { OK, BAD_REQUEST } = constants.STATUS_CODES; +const { OK, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND } = constants.STATUS_CODES; const SceApiTester = require('../../test/util/tools/SceApiTester'); +const { + initializeTokenMock, + setTokenStatus, + resetTokenMock, + restoreTokenMock, +} = require('../util/mocks/TokenValidFunctions'); +const AuditLog = require('../../api/main_endpoints/models/AuditLog'); +const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions'); +const mongoose = require('mongoose'); let app = null; let test = null; @@ -15,8 +24,11 @@ const tools = require('../util/tools/tools.js'); chai.should(); chai.use(chaiHttp); +const token = ''; + describe('Advertisement', () => { before(done => { + initializeTokenMock(); app = tools.initializeServer( __dirname + '/../../api/main_endpoints/routes/Advertisement.js'); test = new SceApiTester(app); @@ -25,18 +37,177 @@ describe('Advertisement', () => { }); after(done => { + restoreTokenMock(); tools.terminateServer(done); }); + beforeEach(() => { + setTokenStatus(false); + }); + + afterEach(() => { + resetTokenMock(); + }); + const INVALID_ADVERTISEMENT = { createDate: new Date('01/01/2001') }; const VALID_ADVERTISEMENT = { - pictureUrl: - 'https://www.fosi.org/', + message: 'Shameless plug', createDate: new Date('01/01/2001'), expireDate: new Date('10/10/2001') }; + describe('/POST createAdvertisement', () => { + + it('Should return 403 when token is not sent', async () => { + const res = await test.sendPostRequest('/api/Advertisement/createAdvertisement', VALID_ADVERTISEMENT); + expect(res).to.have.status(FORBIDDEN); + }); + + it('Should return 401 when invalid token is sent', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/createAdvertisement', VALID_ADVERTISEMENT); + expect(res).to.have.status(UNAUTHORIZED); + }); + + describe('audit log tests for creating ads', () => { + + const userId = new mongoose.Types.ObjectId(); + + beforeEach(async () => { + await Advertisement.deleteMany({}); + await AuditLog.deleteMany({}); + + setTokenStatus(true, { + _id: userId, + email: 'admin@test.com', + accessLevel: 'ADMIN' + }); + }); + + afterEach(async () => { + await Advertisement.deleteMany({}); + await AuditLog.deleteMany({}); + }); + + it('Should create audit log when ad is succesfully created' + 'with user info of who created the ad', async () => { + + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/createAdvertisement', VALID_ADVERTISEMENT); + expect(res).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.CREATE_AD + }).lean(); + + expect(auditEntry).to.exist; + expect(auditEntry.action).to.equal(AuditLogActions.CREATE_AD); + expect(auditEntry.details).to.have.property('message', 'Shameless plug'); + expect(auditEntry.details).to.have.property('advertisementId'); + expect(auditEntry.userId.toString()).to.equal(userId.toString()); + }); + + it('Should not create audit logs for invalid advertisement creation', async () => { + + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/createAdvertisement', INVALID_ADVERTISEMENT); + + expect(res).to.have.status(BAD_REQUEST); + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.CREATE_AD + }).lean(); + + expect(auditEntry).to.not.exist; + }); + }); + }); + + describe('/POST deleteAdvertisement', () => { + it('Should return 403 if no token is sent', async () => { + const res = await test.sendPostRequest('/api/Advertisement/deleteAdvertisement', { _id: VALID_ADVERTISEMENT._id }); + expect(res).to.have.status(FORBIDDEN); + }); + + it('Should return 401 if invalid token is sent', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/deleteAdvertisement', { _id: VALID_ADVERTISEMENT._id }); + expect(res).to.have.status(UNAUTHORIZED); + }); + + it('Should return 404 if ad is not found', async () => { + const fakeId = new mongoose.Types.ObjectId(); + + const userId = new mongoose.Types.ObjectId(); + setTokenStatus(true, { + _id: userId, + email: 'admin@test.com', + accessLevel: 'ADMIN' + }); + + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/deleteAdvertisement', { _id: fakeId }); + expect(res).to.have.status(NOT_FOUND); + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.DELETE_AD + }).lean(); + + expect(auditEntry).to.not.exist; + }); + + it('Should return 404 if ad fails to delete (e.g., already deleted)', async () => { + setTokenStatus(true); + + await Advertisement.deleteOne({ _id: VALID_ADVERTISEMENT._id }); + + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/deleteAdvertisement', { _id: VALID_ADVERTISEMENT._id }); + expect(res).to.have.status(NOT_FOUND); + }); + + describe('audit log tests for deleting ads', () => { + + const userId = new mongoose.Types.ObjectId(); + let createdAd = null; + + beforeEach(async () => { + await Advertisement.deleteMany({}); + await AuditLog.deleteMany({}); + + setTokenStatus(true, { + _id: userId, + email: 'admin@test.com', + accessLevel: 'ADMIN' + }); + + createdAd = await Advertisement.create({ + message: 'Delete me!', + expireDate: new Date('10/12/2099') + }); + }); + + afterEach(async () => { + await Advertisement.deleteMany({}); + await AuditLog.deleteMany({}); + }); + + it('Should create an audit log for succesful ad deletion', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/Advertisement/deleteAdvertisement', { _id: createdAd._id }); + expect(res).to.have.status(OK); + + const ad = await Advertisement.findById(createdAd._id); + expect(ad).to.be.null; + + const auditEntry = await AuditLog.findOne({ + userId: userId, + action: AuditLogActions.DELETE_AD + }).lean(); + + expect(auditEntry).to.exist; + expect(auditEntry.userId.toString()).to.equal(userId.toString()); + expect(auditEntry.details.deletedAd.message).to.equal('Delete me!'); + expect(auditEntry.details.deletedAd.id.toString()).to.equal(createdAd._id.toString()); + }); + }); + }); }); From ddbcf18f8c79bafa9001e01bf3963efc75d2ee7f Mon Sep 17 00:00:00 2001 From: marvzhai Date: Thu, 31 Jul 2025 20:30:07 -0700 Subject: [PATCH 13/14] Audit on user password + profile change (#1800) * add audit on user password/profile change * Update api/main_endpoints/routes/User.js Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> * delete unused endpoint * fix User tests + lint --------- Co-authored-by: Evan Ugarte <36345325+evanugarte@users.noreply.github.com> --- api/main_endpoints/routes/User.js | 63 ++++++-- test/api/User.js | 235 +++++++++++++++++++++++------- 2 files changed, 226 insertions(+), 72 deletions(-) diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index 0b886be9e..1dffc8daf 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -24,6 +24,8 @@ const { SERVER_ERROR, } = require('../../util/constants').STATUS_CODES; const membershipState = require('../../util/constants').MEMBERSHIP_STATE; +const AuditLog = require('../models/AuditLog.js'); +const AuditLogActions = require('../util/auditLogActions.js'); const logger = require('../../util/logger'); @@ -32,9 +34,6 @@ const crypto = require('crypto'); const ROWS_PER_PAGE = 20; -const AuditLogActions = require('../util/auditLogActions.js'); -const AuditLog = require('../models/AuditLog.js'); - // Delete a member router.post('/delete', async (req, res) => { if (!checkIfTokenSent(req)) { @@ -198,6 +197,25 @@ router.post('/edit', async (req, res) => { const query = { _id: req.body._id }; let user = req.body; + // 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.' }); + } + + // Track field changes + 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] + }; + } + }); + if (typeof req.body.numberOfSemestersToSignUpFor !== 'undefined') { user.membershipValidUntil = getMemberExpirationDate( parseInt(req.body.numberOfSemestersToSignUpFor) @@ -213,6 +231,14 @@ router.post('/edit', async (req, res) => { return res.sendStatus(SERVER_ERROR); } user.password = result; + + // create audit log for password change + AuditLog.create({ + userId: decoded._id, + action: AuditLogActions.CHANGE_PW, + details: { email: existingUser.email, userId: decoded._id }, + }).catch(logger.error); + } else { // omit password from the object if it is falsy // i.e. an empty string, undefined or null @@ -236,24 +262,28 @@ router.post('/edit', async (req, res) => { if (result.nModified < 1) { return res .status(NOT_FOUND) - .send({ message: `${query.email} not found.` }); + .send({ message: `${existingUser.email} not found.` }); } - const sanitizedUser = {...user}; // shallow copy of the user; doesn't affect original + if (Object.keys(fieldChanges).length > 0) { + const sanitizedUser = {...user}; + if ('password' in sanitizedUser) { + sanitizedUser.password = true; + } - if ('password' in sanitizedUser) { - sanitizedUser.password = true; + AuditLog.create({ + userId: decoded._id, // person who did modification + action: AuditLogActions.UPDATE_USER, + documentId: user._id, + details: { + updatedInfo: sanitizedUser, + fieldChanges: fieldChanges + } + }).catch(logger.error); } - AuditLog.create({ - userId: decoded._id, // the user making the update - action: AuditLogActions.UPDATE_USER, - documentId: user._id, // the user affected by the update - details: {updatedInfo: sanitizedUser} - }).catch(logger.error); - return res.status(OK).send({ - message: `${query.email} was updated.`, + message: `${existingUser.email} was updated.`, membershipValidUntil: user.membershipValidUntil }); }); @@ -272,8 +302,9 @@ router.post('/getPagesPrintedCount', (req, res) => { apiEndpoint: 'user/PagesPrintedCount', errorDescription: error }; + logger.error(info); - res.status(BAD_REQUEST).send({ message: 'Bad Request.' }); + return res.status(BAD_REQUEST).send({ message: 'Bad Request.' }); } if (!result) { diff --git a/test/api/User.js b/test/api/User.js index 25c0a8f8a..01a8cc088 100644 --- a/test/api/User.js +++ b/test/api/User.js @@ -21,6 +21,8 @@ const sinon = require('sinon'); const SceApiTester = require('../util/tools/SceApiTester'); const {mockDayMonthAndYear, revertClock} = require('../util/mocks/Date.js'); +const AuditLog = require('../../api/main_endpoints/models/AuditLog.js'); +const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js'); let app = null; let test = null; @@ -44,9 +46,6 @@ const { const { MEMBERSHIP_STATE } = require('../../api/util/constants'); const { getMemberExpirationDate } = require('../../api/main_endpoints/util/userHelpers.js'); -const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js'); -const AuditLog = require('../../api/main_endpoints/models/AuditLog.js'); - chai.should(); chai.use(chaiHttp); @@ -212,51 +211,158 @@ describe('User', () => { expect(result).to.have.status(NOT_FOUND); }); - it('Should return statusCode 200 and a message ' + - 'if a user was edited', async () => { - const user = { - _id: id, - email: 'd@e.f', - token: token, - firstName: 'pinkUnicorn', - discordID: '0987654321', - numberOfSemestersToSignUpFor: undefined - }; - setTokenStatus(true); - const result = await test.sendPostRequestWithToken( - token, '/api/User/edit', user); - expect(result).to.have.status(OK); - result.body.should.be.a('object'); - result.body.should.have.property('message'); - }); + describe('create audit log on user change', async () => { - it('Should create an audit log when a user is updated', async () => { - // ensure Audit log DB starts fresh before this test - await AuditLog.deleteMany({}); - // update email, firstname, password, and discordID - const user = { - _id: id, - email: 'newemail@gmail.com', - password: 'newPassword', - token: token, - firstName: 'Newname', - discordID: '421482148', - numberOfSemestersToSignUpFor: undefined - }; - setTokenStatus(true, user); + // create clean testUser before each test + let testUser; - const result = await test.sendPostRequestWithToken( - token, '/api/User/edit', user - ); - expect(result).to.have.status(OK); + beforeEach(async () => { + await User.deleteMany({}); + testUser = await new User({ + email: 'a@b.c', + password: 'Passw0rd', + firstName: 'first-name', + lastName: 'last-name', + major: 'Computer Science' + }).save(); + + setTokenStatus(true, testUser); + }); + + afterEach(async () => { + await AuditLog.deleteMany({}); + }); + + it('Should create an audit log when a user is updated (no password change)' + 'not create an audit log for password change', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/User/edit', { + _id: testUser._id.toString(), + firstName: 'Newname', + email: 'a@b.c', + token + }); + + expect(res).to.have.status(OK); + res.body.should.be.a('object'); + res.body.should.have.property('message'); + + const auditEntry = await AuditLog.findOne({ userId: testUser._id }).lean(); + expect(auditEntry).to.exist; + expect(auditEntry.details.fieldChanges.firstName).to.have.deep.equal({ + from: 'first-name', + to: 'Newname' + }); + + // make sure pw change log doesn't exist + const changePWlog = await AuditLog.findOne({ + userId: id, + action: AuditLogActions.CHANGE_PW + }).lean(); + expect(changePWlog).to.not.exist; + }); - const auditEntry = await AuditLog.findOne().lean(); + it('Should create an audit log when a user changes their password (no profile update)', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/User/edit', { + _id: testUser._id.toString(), + firstName: 'first-name', + email: 'a@b.c', + password: 'Newpassw0rd', + token + }); + + expect(res).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ userId: testUser._id }).lean(); + expect(auditEntry).to.exist; + expect(auditEntry.action).to.equal(AuditLogActions.CHANGE_PW); + expect(auditEntry).to.not.have.property('password'); + }); + + it('Should create both audit logs when password and profile info are updated', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/User/edit', { + _id: testUser._id.toString(), + firstName: 'Newname', + email: 'a@b.c', + password: 'Newpassword1', + discordID: 'anotherID', + token + }); + + expect(res).to.have.status(OK); + + const changePwLog = await AuditLog.findOne({ action: AuditLogActions.CHANGE_PW }).lean(); + const updateUserLog = await AuditLog.findOne({ action: AuditLogActions.UPDATE_USER }).lean(); + + expect(changePwLog).to.exist; + expect(updateUserLog).to.exist; + }); + + it('Should track profile changes in audit log with correct from/to values', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/User/edit', { + _id: testUser._id.toString(), + firstName: 'Newname', + lastName: 'Newlastname', + email: 'a@b.c', + password: 'Newpassword1', + discordID: 'anotherID', + major: 'Software Engineering', + token + }); + + expect(res).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + action: AuditLogActions.UPDATE_USER, + documentId: testUser._id + }).lean(); + + expect(auditEntry).to.exist; + expect(auditEntry.details).to.have.property('fieldChanges'); + + // track firstName change + expect(auditEntry.details.fieldChanges).to.have.property('firstName'); + expect(auditEntry.details.fieldChanges.firstName).to.have.deep.equal({ + from: 'first-name', + to: 'Newname' + }); + + // track lastName change + expect(auditEntry.details.fieldChanges).to.have.property('lastName'); + expect(auditEntry.details.fieldChanges.lastName).to.have.deep.equal({ + from: 'last-name', + to: 'Newlastname' + }); + + // track major change + expect(auditEntry.details.fieldChanges).to.have.property('major'); + expect(auditEntry.details.fieldChanges.major).to.have.deep.equal({ + from: 'Computer Science', + to: 'Software Engineering' + }); + + // Should NOT track unchanged fields + password field + expect(auditEntry.details.fieldChanges).to.not.have.property('password'); + expect(auditEntry.details.fieldChanges).to.not.have.property('email'); + }); - expect(auditEntry).to.exist; - expect(auditEntry).to.have.property('userId'); - expect(auditEntry).to.have.property('action', AuditLogActions.UPDATE_USER); - expect(auditEntry.details.updatedInfo).to.have.property('password', true); - await AuditLog.deleteMany({}); + it('Should not create audit log when no fields actually change', async () => { + const res = await test.sendPostRequestWithToken(token, '/api/User/edit', { + _id: testUser._id.toString(), + email: 'a@b.c', + password: 'Passw0rd', + firstName: 'first-name', + lastName: 'last-name', + major: 'Computer Science' + }); + + expect(res).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + action: AuditLogActions.UPDATE_USER || AuditLogActions.CHANGE_PW, + documentId: testUser._id + }).lean(); + + expect(auditEntry).to.not.exist; + }); }); }); @@ -288,14 +394,27 @@ describe('User', () => { expect(result).to.have.status(NOT_FOUND); }); it('Should return status code 200 if user is found', async () => { - const user = { - userID: id, - token: token - }; - setTokenStatus(true); - const result = await test.sendPostRequestWithToken(token, '/api/User/getUserById', user); - expect(result).to.have.status(OK); - result.body.should.not.have.property('password'); + + const testUser = await new User({ + email: 'getuser@test.com', + password: 'Passw0rd', + firstName: 'Get', + lastName: 'User', + accessLevel: MEMBERSHIP_STATE.ADMIN, + emailVerified: true + }).save(); + + // Set token mock for this user + setTokenStatus(true, { _id: testUser._id, accessLevel: MEMBERSHIP_STATE.ADMIN }); + + const res = await test.sendPostRequestWithToken('', '/api/User/getUserById', { + userID: testUser._id, + token: '' + }); + + expect(res).to.have.status(OK); + res.body.should.have.property('email').eql('getuser@test.com'); + res.body.should.not.have.property('password'); }); }); @@ -351,13 +470,17 @@ describe('User', () => { it('Should return statusCode 200 and a message ' + 'if a user was deleted', async () => { - const user = { + const user = await new User({ _id : id, + email: 'delete@test.com', + password: 'Passw0rd', + firstName: 'Delete', + lastName: 'Me', token: token - }; - setTokenStatus(true); + }).save(); + setTokenStatus(true, { _id: user._id, accesslevel: MEMBERSHIP_STATE.ADMIN }); const result = await test.sendPostRequestWithToken( - token, '/api/User/delete', user); + token, '/api/User/delete', { _id: user._id, token: token } ); expect(result).to.have.status(OK); }); From d655adbcdccca075b9f149b090931421bd4a14df Mon Sep 17 00:00:00 2001 From: luSteven01 <146397397+luSteven01@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:07:50 -0700 Subject: [PATCH 14/14] declared bankruptcy for changing classnames to tailwind css, removed light mode for now cause it was bugging out on me, and changed the input text to white --- src/Components/ShortcutKeyModal/SearchModal.css | 2 +- src/Components/ShortcutKeyModal/SearchModal.js | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Components/ShortcutKeyModal/SearchModal.css b/src/Components/ShortcutKeyModal/SearchModal.css index 40667ac33..3017c9167 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.css +++ b/src/Components/ShortcutKeyModal/SearchModal.css @@ -75,4 +75,4 @@ width: 35rem; max-width: none; } -} \ No newline at end of file +} diff --git a/src/Components/ShortcutKeyModal/SearchModal.js b/src/Components/ShortcutKeyModal/SearchModal.js index f1aa76ea7..e431b6189 100644 --- a/src/Components/ShortcutKeyModal/SearchModal.js +++ b/src/Components/ShortcutKeyModal/SearchModal.js @@ -7,7 +7,7 @@ import { searchUsersAndCleezyUrls } from '../../APIFunctions/ShortcutSearch'; export default function SearchModal() { const {modalOpen, setModalOpen} = useSCE(); - //kept original open and setOpen state variables + // kept original open and setOpen state variables const open = modalOpen; const setOpen = setModalOpen; const inputRef = useRef(null); @@ -105,7 +105,6 @@ export default function SearchModal() { {r.type === 'external_url' ? r.path : window.location.origin + r.path} -
          ))} @@ -290,9 +289,9 @@ export default function SearchModal() { ref={inputRef} placeholder="Search here... (Ctrl + k)" value={keyword} - onChange={handleChanges} - className='border-[1.5px] border-gray-600 w-full rounded p-3 h-10 text-[1.2rem] bg-transparent focus:outline-sky-600' - /> + onChange={handleChanges} + className='border-[1.5px] text-white border-gray-600 w-full rounded p-3 h-10 text-[1.2rem] bg-transparent focus:outline-sky-600' + />
          @@ -301,4 +300,4 @@ export default function SearchModal() {
          ); -} \ No newline at end of file +}