Skip to content
Merged
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
198 changes: 115 additions & 83 deletions backend/controllers/referral.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async function moderateReferral({ referralId, adminId, actionType, reason, refer
adminId,
referralId: referral._id,
actionType,
reason: normalizeModerationReason(reason),
reason: reason,
targetUserId: referral.postedBy?._id || referral.postedBy || null
});
});
Expand Down Expand Up @@ -104,6 +104,30 @@ async function getReferralAccess(referralId, userId) {
};
}

async function checkReferralAccessMiddleware(req, res, next) {
try {
const { id } = req.params;
if (!id || !mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ message: 'Invalid referral ID format' });
}

const accessInfo = await getReferralAccess(id, req.user.id);
if (!accessInfo.referral) {
return res.status(404).json({ message: 'Referral not found' });
}

// Hide referrals that are hidden/removed from non-admins
if (isReferralHidden(accessInfo.referral) && accessInfo.currentUser?.type !== 'admin') {
return res.status(404).json({ message: 'Referral not found' });
}

req.referralAccess = accessInfo;
next();
} catch (err) {
next(err);
}
}

// Create a new job referral opportunity
async function createReferral(req, res, next) {
try {
Expand Down Expand Up @@ -223,22 +247,12 @@ async function getReferrals(req, res, next) {
// Apply for a referral
async function applyForReferral(req, res, next) {
try {
const { id } = req.params;
const currentUser = await getCurrentUserSummary(req.user.id);
const { referral, currentUser } = req.referralAccess;

if (!currentUser || currentUser.type !== 'student') {
return res.status(403).json({ message: 'Only students can apply for referrals' });
}

const referral = await JobReferral.findById(id);
if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}

if (isReferralHidden(referral) && currentUser?.type !== 'admin') {
return res.status(404).json({ message: 'Referral not found' });
}

if (referral.status !== 'open') {
return res.status(400).json({ message: 'Referral is no longer open' });
}
Expand Down Expand Up @@ -287,16 +301,9 @@ async function applyForReferral(req, res, next) {
// Accept a referral applicant
async function acceptReferral(req, res, next) {
try {
const { id, applicantId } = req.params;
const currentUser = await getCurrentUserSummary(req.user.id);
const { applicantId } = req.params;
const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;

const referral = await JobReferral.findById(id);
if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}

const isOwner = referral.postedBy.toString() === req.user.id;
const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
Expand Down Expand Up @@ -346,16 +353,9 @@ async function acceptReferral(req, res, next) {
// Reject a referral applicant
async function rejectReferral(req, res, next) {
try {
const { id, applicantId } = req.params;
const currentUser = await getCurrentUserSummary(req.user.id);
const { applicantId } = req.params;
const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;

const referral = await JobReferral.findById(id);
if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}

const isOwner = referral.postedBy.toString() === req.user.id;
const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
Expand Down Expand Up @@ -399,15 +399,10 @@ async function rejectReferral(req, res, next) {
// Get single referral by ID
async function getReferralById(req, res, next) {
try {
const { referral, access } = await getReferralAccess(req.params.id, req.user.id);

if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}
const { referral, access } = req.referralAccess;

if (!access) {
// Do not leak referral existence.
return res.status(404).json({ message: 'Referral not found' });
return res.status(403).json({ message: 'Access denied' });
}

// Keep response structure consistent with prior implementation.
Expand All @@ -422,15 +417,10 @@ async function getReferralById(req, res, next) {

async function getReferralTimeline(req, res, next) {
try {
const { referral, access } = await getReferralAccess(req.params.id, req.user.id);

if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}
const { referral, access } = req.referralAccess;

if (!access) {
// Do not leak referral existence.
return res.status(404).json({ message: 'Referral not found' });
return res.status(403).json({ message: 'Access denied' });
}

const timeline = [...(referral.timeline || [])].sort(
Expand All @@ -445,18 +435,9 @@ async function getReferralTimeline(req, res, next) {


async function closeReferral(req, res, next) {

try {
const { id } = req.params;
const currentUser = await getCurrentUserSummary(req.user.id);

const referral = await JobReferral.findById(id);
if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}
const { referral, isOwner, isAdmin, currentUser } = req.referralAccess;

const isOwner = referral.postedBy.toString() === req.user.id;
const isAdmin = currentUser?.type === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ message: 'Access denied' });
}
Expand Down Expand Up @@ -545,22 +526,32 @@ async function flagReferralForSpam(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
const normalizedReason = normalizeModerationReason(reason, 'Flagged for review');

const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'flag',
reason: normalizeModerationReason(reason, 'Flagged for review'),
reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'flagged',
reason: normalizeModerationReason(reason, 'Flagged for review'),
reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});

const io = req.app.get('io');
if (io) {
io.emit('referralModerationUpdated', {
referralId: id,
status: referral.moderation?.status || 'visible',
referral
});
}

res.json({
message: 'Referral flagged successfully',
referral,
Expand All @@ -578,22 +569,32 @@ async function hideReferral(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
const normalizedReason = normalizeModerationReason(reason, 'Hidden by administrator');

const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'hide',
reason: normalizeModerationReason(reason, 'Hidden by administrator'),
reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'hidden',
reason: normalizeModerationReason(reason, 'Hidden by administrator'),
reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});

const io = req.app.get('io');
if (io) {
io.emit('referralModerationUpdated', {
referralId: id,
status: referral.moderation?.status || 'visible',
referral
});
}

res.json({
message: 'Referral hidden successfully',
referral,
Expand All @@ -611,22 +612,32 @@ async function restoreReferral(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
const normalizedReason = normalizeModerationReason(reason, 'Restored by administrator');

const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'restore',
reason: normalizeModerationReason(reason, 'Restored by administrator'),
reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'visible',
reason: normalizeModerationReason(reason, 'Restored by administrator'),
reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
}
});

const io = req.app.get('io');
if (io) {
io.emit('referralModerationUpdated', {
referralId: id,
status: referral.moderation?.status || 'visible',
referral
});
}

res.json({
message: 'Referral restored successfully',
referral,
Expand All @@ -644,16 +655,17 @@ async function suspendReferralPoster(req, res, next) {
try {
const { id } = req.params;
const { reason } = req.body;
const normalizedReason = normalizeModerationReason(reason, 'Poster suspended by administrator');

const { referral, moderationAction } = await moderateReferral({
referralId: id,
adminId: req.user.id,
actionType: 'suspend_poster',
reason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
reason: normalizedReason,
referralMutator: async (referralDoc) => {
referralDoc.moderation = {
status: 'hidden',
reason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
reason: normalizedReason,
moderatedBy: req.user.id,
moderatedAt: new Date()
};
Expand All @@ -666,12 +678,21 @@ async function suspendReferralPoster(req, res, next) {
await User.findByIdAndUpdate(posterId, {
referralPostingSuspended: true,
referralPostingSuspendedAt: new Date(),
referralPostingSuspendedReason: normalizeModerationReason(reason, 'Poster suspended by administrator'),
referralPostingSuspendedReason: normalizedReason,
referralPostingSuspendedBy: req.user.id
}, { session });
}
});

const io = req.app.get('io');
if (io) {
io.emit('referralModerationUpdated', {
referralId: id,
status: referral.moderation?.status || 'visible',
referral
});
}

res.json({
message: 'Referral poster suspended successfully',
referral,
Expand All @@ -688,11 +709,7 @@ async function suspendReferralPoster(req, res, next) {
async function getReferralMessages(req, res, next) {
try {
const { id } = req.params;
const { referral, access } = await getReferralAccess(id, req.user.id);

if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}
const { referral, access } = req.referralAccess;

if (!access) {
return res.status(403).json({ message: 'Access denied' });
Expand All @@ -713,19 +730,40 @@ async function sendReferralMessage(req, res, next) {
try {
const { id } = req.params;
const { body, recipientId } = req.body;
const { referral, currentUser, access, isOwner, isApplicant } = await getReferralAccess(id, req.user.id);

if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
const validationErrors = {};

// Validate referral ID (id)
if (!mongoose.Types.ObjectId.isValid(id)) {
validationErrors.id = 'Invalid referral ID format';
}

if (!access) {
return res.status(403).json({ message: 'Access denied' });
// Validate recipientId if provided
if (recipientId !== undefined && recipientId !== null && String(recipientId).trim() !== '') {
if (!mongoose.Types.ObjectId.isValid(recipientId)) {
validationErrors.recipientId = 'Recipient ID must be a valid ObjectId';
}
}

// Validate body
const trimmedBody = String(body || '').trim();
if (!trimmedBody) {
return res.status(400).json({ message: 'Message body is required' });
validationErrors.body = 'Message body is required';
} else if (trimmedBody.length > 2000) {
validationErrors.body = 'Message body cannot exceed 2000 characters';
}

if (Object.keys(validationErrors).length > 0) {
return res.status(400).json({
message: 'Validation failed',
errors: validationErrors
});
}

const { referral, currentUser, access, isOwner, isApplicant } = req.referralAccess;

if (!access) {
return res.status(403).json({ message: 'Access denied' });
}

let recipientUser = null;
Expand Down Expand Up @@ -807,13 +845,7 @@ async function getMyReferrals(req, res, next) {

async function computeReferralBonus(req, res, next) {
try {
const referral = await JobReferral.findById(req.params.id)
.populate('postedBy', 'name email alumnus_bio')
.populate('applicants.user', 'name email alumnus_bio');

if (!referral) {
return res.status(404).json({ message: 'Referral not found' });
}
const { referral } = req.referralAccess;

const { referral: persistedReferral, bonus } = await recomputeAndPersistReferralBonus(referral, {
computedBy: req.user?.id || null
Expand Down Expand Up @@ -847,6 +879,6 @@ module.exports = {
getMyReferrals,
getReferralById,
getReferralTimeline,
computeReferralBonus
computeReferralBonus,
checkReferralAccessMiddleware
};

Loading
Loading