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/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/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/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/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 = {
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/api/main_endpoints/util/auditLogActions.js b/api/main_endpoints/util/auditLogActions.js
index b53ceaf0f..3b18bd9e8 100644
--- a/api/main_endpoints/util/auditLogActions.js
+++ b/api/main_endpoints/util/auditLogActions.js
@@ -7,6 +7,9 @@ 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',
DELETE_CARD: 'DELETE_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/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/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
diff --git a/src/Components/Navbar/UserNavbar.js b/src/Components/Navbar/UserNavbar.js
index 9515a4c00..b865562c7 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,12 +84,19 @@ export default function UserNavbar(props) {
+
{authenticated && user ? (
<>
diff --git a/src/Components/ShortcutKeyModal/SearchModal.css b/src/Components/ShortcutKeyModal/SearchModal.css
index 2f0ccef8c..3017c9167 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;
+ }
+}
diff --git a/src/Components/ShortcutKeyModal/SearchModal.js b/src/Components/ShortcutKeyModal/SearchModal.js
index 09a748317..e431b6189 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);
@@ -77,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;
@@ -89,14 +92,19 @@ 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}
+
+
+
+ {r.type === 'external_url' ? r.path : window.location.origin + r.path}
+
))}
@@ -274,14 +282,16 @@ export default function SearchModal() {
if (!open) return null;
return (
-
+
-
+
+ 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'
+ />
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 }) => {
-
+
@@ -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
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 (
-
-
-
-

-
-
);
-};
-
-export default ForgotPassword;
+}
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 && (
-
+
-
-
+
+
)
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());
+ });
+ });
+ });
});
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', () => {
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);
});