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
174 changes: 101 additions & 73 deletions backend/controllers/referral.controller.js
Original file line number Diff line number Diff line change
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 @@ -561,6 +542,15 @@ async function flagReferralForSpam(req, res, next) {
}
});

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 Down Expand Up @@ -594,6 +584,15 @@ async function hideReferral(req, res, next) {
}
});

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 Down Expand Up @@ -627,6 +626,15 @@ async function restoreReferral(req, res, next) {
}
});

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 Down Expand Up @@ -672,6 +680,15 @@ async function suspendReferralPoster(req, res, next) {
}
});

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 +705,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 +726,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 +841,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 +875,6 @@ module.exports = {
getMyReferrals,
getReferralById,
getReferralTimeline,
computeReferralBonus
computeReferralBonus,
checkReferralAccessMiddleware
};

7 changes: 7 additions & 0 deletions backend/controllers/resumeAnalyzer.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,18 @@ function buildSkillExplanations({ resumeText = '', resumeSkills = [], requiredSk
matchedBecause.push(`resume does not include '${skillLabel}'`);
}

const confidence = matchedType === 'exact' ? 'exact' : (matchedType === 'partial' ? 'partial' : 'low');

if (matchedBecause.length === 0) {
matchedBecause.push(`no confirmation information available for '${skillLabel}'`);
}

return {
skill: skillLabel,
status,
matchedType,
matchedBecause,
confidence,
source,
};
});
Expand Down
5 changes: 5 additions & 0 deletions backend/models/ResumeAnalysis.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const resumeAnalysisSchema = new mongoose.Schema(
type: String,
default: 'jobRequirements',
},
confidence: {
type: String,
enum: ['exact', 'partial', 'low'],
default: 'low',
},
},
],
default: [],
Expand Down
30 changes: 17 additions & 13 deletions backend/routes/referral.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const {
getMyReferrals,
getReferralById,
getReferralTimeline,
computeReferralBonus
computeReferralBonus,
checkReferralAccessMiddleware
} = require('../controllers/referral.controller');
const { authenticate, isStudent } = require('../middlewares/auth.middleware');

Expand All @@ -35,31 +36,34 @@ router.post('/', authenticate, createReferral);
// Get all referrals
router.get('/', authenticate, getReferrals);

// Get my referrals
router.get('/my-referrals', authenticate, getMyReferrals);

// Check access for all ID-based subroutes
router.use('/:id', authenticate, checkReferralAccessMiddleware);

// Apply for referral
router.post('/:id/apply', authenticate, isStudent, applyForReferral);
router.post('/:id/apply', isStudent, applyForReferral);

// Referral Q&A / messaging
router.post('/:id/messages', authenticate, sendReferralMessage);
router.get('/:id/messages', authenticate, getReferralMessages);
router.post('/:id/messages', sendReferralMessage);
router.get('/:id/messages', getReferralMessages);

// Referral bonus computation
router.post('/:id/compute-bonus', authenticate, allowBonusCompute, computeReferralBonus);
router.post('/:id/compute-bonus', allowBonusCompute, computeReferralBonus);

// Manage applicants (only poster)
router.put('/:id/applicants/:applicantId/accept', authenticate, acceptReferral);
router.put('/:id/applicants/:applicantId/reject', authenticate, rejectReferral);
router.put('/:id/applicants/:applicantId/accept', acceptReferral);
router.put('/:id/applicants/:applicantId/reject', rejectReferral);

// Close referral (poster/admin)
router.patch('/:id/close', authenticate, closeReferral);
router.patch('/:id/close', closeReferral);

// Get referral timeline
router.get('/:id/timeline', authenticate, getReferralTimeline);

// Get my referrals
router.get('/my-referrals', authenticate, getMyReferrals);
router.get('/:id/timeline', getReferralTimeline);

// Get single referral
router.get('/:id', authenticate, getReferralById);
router.get('/:id', getReferralById);

module.exports = router;

Loading
Loading