Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"cors": "^2.8.4",
"cron": "^1.8.2",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.1",
"dotenv": "^5.0.1",
"dropbox": "^10.34.0",
"express": "^4.22.1",
Expand Down Expand Up @@ -131,7 +132,8 @@
"twilio": "^5.5.2",
"uuid": "^3.4.0",
"ws": "^8.17.1",
"xmlrpc": "^1.3.2"
"xmlrpc": "^1.3.2",
"zod": "^4.3.6"
},
"nodemonConfig": {
"watch": [
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/educationPortal/downloadReportController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* istanbul ignore file */
// eslint-disable-next-line import/no-unresolved
const PDFDocument = require('pdfkit');
// eslint-disable-next-line import/no-unresolved
const { Parser } = require('json2csv');
const mongoose = require('mongoose');
const EducationTask = require('../../models/educationTask');
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/liveJournalPostController.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable no-console */
const crypto = require('crypto');
// eslint-disable-next-line import/no-unresolved
const xmlrpc = require('xmlrpc');
const mongoose = require('mongoose');
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
const cloudinary = require('cloudinary').v2;
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
const streamifier = require('streamifier');
const LiveJournalPost = require('../models/liveJournalPost');

Expand Down
29 changes: 23 additions & 6 deletions src/controllers/timeEntryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1558,13 +1558,26 @@ const timeEntrycontroller = function (TimeEntry) {
* recalculate the hoursByCategory for all users and update the field
*/
const recalculateHoursByCategoryAllUsers = async function (taskId) {
if (mongoose.connection.readyState === 0) {
// Check if MongoDB connection is ready before attempting to start a session
// readyState: 0 = disconnected, 1 = connected, 2 = connecting, 3 = disconnecting
if (mongoose.connection.readyState !== 1) {
const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId);
if (recalculationTask) {
recalculationTask.status = 'Failed';
recalculationTask.completionTime = new Date().toISOString();
}

logger.logInfo(
`Recalculation task ${taskId} skipped: MongoDB connection not ready (state: ${mongoose.connection.readyState})`,
);
return;
}
const session = await mongoose.startSession();
session.startTransaction();

let sesh;
try {
sesh = await mongoose.startSession();
sesh.startTransaction();

const userprofiles = await UserProfile.find({}, '_id').lean();

const recalculationPromises = userprofiles.map(async (userprofile) => {
Expand All @@ -1574,15 +1587,17 @@ const timeEntrycontroller = function (TimeEntry) {
});
await Promise.all(recalculationPromises);

await session.commitTransaction();
await sesh.commitTransaction();

const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId);
if (recalculationTask) {
recalculationTask.status = 'Completed';
recalculationTask.completionTime = new Date().toISOString();
}
} catch (err) {
await session.abortTransaction();
if (sesh) {
await sesh.abortTransaction();
}
const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId);
if (recalculationTask) {
recalculationTask.status = 'Failed';
Expand All @@ -1591,7 +1606,9 @@ const timeEntrycontroller = function (TimeEntry) {

logger.logException(err);
} finally {
session.endSession();
if (sesh) {
sesh.endSession();
}
}
};

Expand Down
1 change: 0 additions & 1 deletion src/controllers/timeEntryController.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const mongoose = require('mongoose');

const { mockRes, assertResMock } = require('../test');

const oid = () => new mongoose.Types.ObjectId().toString();
Expand Down
2 changes: 1 addition & 1 deletion src/models/userProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ const userProfileSchema = new Schema({
// differentiate between paused and separated accounts for better reporting and handling in the future
inactiveReason: {
type: String,
enum: ['Paused', 'Separated', 'ScheduledSeparation'],
enum: ['Paused', 'Separated', 'ScheduledSeparation', null],
default: undefined,
},
resetPwd: { type: String },
Expand Down
115 changes: 92 additions & 23 deletions src/services/permissionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ class PermissionService {
return permissions && typeof permissions === 'object';
}

static async checkUpdateAuthorization(requestor, userId) {
static async checkUpdateAuthorization(requestor, userId, UserProfile) {
const hasUpdatePermission = await hasPermission(requestor, 'putUserProfilePermissions');
if (!hasUpdatePermission) {
return { authorized: false, error: 'You are not authorized to update user permissions' };
}

// Special case: Owners with addDeleteEditOwners permission can update other Owners' permissions
const hasAddDeleteEditOwnersPermission = await hasPermission(requestor, 'addDeleteEditOwners');
if (hasAddDeleteEditOwnersPermission) {
const targetUser = await UserProfile.findById(userId).select('role').lean();
if (targetUser && targetUser.role === 'Owner') {
// Allow Owner with addDeleteEditOwners permission to update other Owners' permissions
return { authorized: true };
}
}

const canEditProtectedAccount = await canRequestorUpdateUser(requestor.requestorId, userId);

if (!canEditProtectedAccount) {
Expand All @@ -33,18 +43,47 @@ class PermissionService {
}

async findUserById(userId) {
const user = await this.UserProfile.findById(userId);
let user;
try {
user = await this.UserProfile.findById(userId);
} catch (findError) {
const err = new Error('Invalid user id');
err.statusCode = 400;
throw err;
}
if (!user) {
throw new Error('User not found');
const err = new Error('User not found');
err.statusCode = 404;
throw err;
}
return user;
}

static updateUserPermissions(user, permissions) {
user.permissions = {
isAcknowledged: false,
...permissions,
let existing = {};
try {
if (user.permissions) {
existing =
typeof user.permissions.toObject === 'function'
? user.permissions.toObject()
: user.permissions;
}
} catch (_) {
existing = user.permissions || {};
}
const merged = {
isAcknowledged: Boolean(permissions.isAcknowledged),
frontPermissions: Array.isArray(permissions.frontPermissions)
? permissions.frontPermissions
: (existing.frontPermissions || []),
backPermissions: Array.isArray(permissions.backPermissions)
? permissions.backPermissions
: (existing.backPermissions || []),
removedDefaultPermissions: Array.isArray(permissions.removedDefaultPermissions)
? permissions.removedDefaultPermissions
: (existing.removedDefaultPermissions || []),
};
user.permissions = merged;
user.lastModifiedDate = Date.now();
}

Expand All @@ -65,17 +104,32 @@ class PermissionService {
}

static async notifyInfringements(originalInfringements, results) {
await userHelper.notifyInfringements(
originalInfringements,
results.infringements,
results.firstName,
results.lastName,
results.email,
results.role,
results.startDate,
results.jobTitle[0],
results.weeklycommittedHours,
);
try {
if (!results) return;

const currentInfringements = results.infringements;

if (!currentInfringements) return;

const safeOriginal =
originalInfringements && typeof originalInfringements.toObject === 'function'
? originalInfringements
: currentInfringements;

await userHelper.notifyInfringements(
safeOriginal,
currentInfringements,
results.firstName,
results.lastName,
results.email,
results.role,
results.startDate,
results.jobTitle?.[0],
results.weeklycommittedHours,
);
} catch (error) {
logger.logException(error, 'Error notifying infringements after permission update');
}
}

async handlePostSaveOperations(req, user, originalInfringements, results) {
Expand All @@ -85,13 +139,24 @@ class PermissionService {
}

async updatePermissions(userId, permissions, req) {
// Validate permissions data
if (!PermissionService.validatePermissionsData(permissions)) {
throw new Error('Invalid permissions data');
const err = new Error('Invalid permissions data');
err.statusCode = 400;
throw err;
}

const requestor = req.body?.requestor;
if (!requestor || !requestor.requestorId) {
const err = new Error('Requestor not found. Ensure request is authenticated.');
err.statusCode = 401;
throw err;
}

// Check authorization
const authResult = await PermissionService.checkUpdateAuthorization(req.body.requestor, userId);
const authResult = await PermissionService.checkUpdateAuthorization(
requestor,
userId,
this.UserProfile,
);
if (!authResult.authorized) {
const error = new Error(authResult.error);
error.statusCode = 403;
Expand All @@ -108,8 +173,12 @@ class PermissionService {
// Save the user
const results = await user.save();

// Handle post-save operations
await this.handlePostSaveOperations(req, user, originalInfringements, results);
// Handle post-save operations (logging, cache); don't fail the update if these throw
try {
await this.handlePostSaveOperations(req, user, originalInfringements, results);
} catch (postSaveError) {
logger.logException(postSaveError, 'Post-save operations failed after permission update');
}

return {
message: 'Permissions updated successfully',
Expand Down
11 changes: 7 additions & 4 deletions src/utilities/liveJournalScheduler.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
// eslint-disable-next-line import/no-unresolved
const schedule = require('node-schedule');
const liveJournalPostController = require('../controllers/liveJournalPostController')();

const initializeLiveJournalScheduler = () => {
const job = schedule.scheduleJob('* * * * *', async () => {
try {
const result = await liveJournalPostController.processScheduledPosts();

if (result.processed > 0) {
console.log(`[LiveJournal Scheduler] Processed ${result.processed} posts: ${result.successful} successful, ${result.failed} failed`);
console.log(
`[LiveJournal Scheduler] Processed ${result.processed} posts: ${result.successful} successful, ${result.failed} failed`,
);
}
} catch (error) {
console.error('[LiveJournal Scheduler] Error processing scheduled posts:', error);
}
});

console.log('[LiveJournal Scheduler] Initialized - will check for scheduled posts every minute');

return job;
};

module.exports = { initializeLiveJournalScheduler };
module.exports = { initializeLiveJournalScheduler };
Loading
Loading