From fdd0d5cf89973f3114d9d9a3223132ec23d2ec9a Mon Sep 17 00:00:00 2001 From: Test Date: Sun, 31 May 2026 15:01:34 -0400 Subject: [PATCH] feat: add public job application submit endpoint with file upload Expose POST /api/jobforms/:formId/responses with multer so applicants can submit answers and attachments without auth. Co-authored-by: Cursor --- src/controllers/collaborationController.js | 90 ++++++++++++++++++++++ src/routes/collaborationRouter.js | 7 ++ src/startup/middleware.js | 5 ++ 3 files changed, 102 insertions(+) diff --git a/src/controllers/collaborationController.js b/src/controllers/collaborationController.js index 518e880cf..144630ac9 100644 --- a/src/controllers/collaborationController.js +++ b/src/controllers/collaborationController.js @@ -1,5 +1,7 @@ +const mongoose = require('mongoose'); const Form = require('../models/JobFormsModel'); const Response = require('../models/jobApplicationsModel'); +const upload = require('../middleware/multerMiddleware'); // Create a new form exports.createForm = async (req, res) => { @@ -68,6 +70,94 @@ exports.updateFormFormat = async (req, res) => { } }; +// Submit a job application for a form (public — applicants) +exports.submitJobApplication = async (req, res) => { + try { + const { formId } = req.params; + let payload = {}; + try { + payload = JSON.parse(req.body.payload || '{}'); + } catch (parseError) { + return res.status(400).json({ message: 'Invalid application payload.' }); + } + + const form = await Form.findById(formId); + if (!form) { + return res.status(404).json({ message: 'Form not found.' }); + } + + const { applicantName, applicantEmail, answers: answersPayload = [], profile = {} } = payload; + if (!applicantEmail || !String(applicantEmail).trim()) { + return res.status(400).json({ message: 'Email is required.' }); + } + + const fileByField = {}; + (req.files || []).forEach((file) => { + fileByField[file.fieldname] = file; + }); + + const builtAnswers = answersPayload.map(({ questionId, answer }) => { + const qIdStr = questionId ? String(questionId) : ''; + const uploaded = qIdStr ? fileByField[`questionFile_${qIdStr}`] : null; + if (uploaded) { + return { + questionId: questionId || new mongoose.Types.ObjectId(), + answer: { + fileName: uploaded.originalname, + mimeType: uploaded.mimetype, + size: uploaded.size, + }, + }; + } + return { + questionId: questionId || new mongoose.Types.ObjectId(), + answer, + }; + }); + + const { resume } = fileByField; + if (resume) { + builtAnswers.push({ + questionId: new mongoose.Types.ObjectId(), + answer: { + type: 'resume', + fileName: resume.originalname, + mimeType: resume.mimetype, + size: resume.size, + }, + }); + } + + builtAnswers.push({ + questionId: new mongoose.Types.ObjectId(), + answer: { + type: 'applicantProfile', + applicantName, + applicantEmail, + ...profile, + }, + }); + + const submission = new Response({ + formId, + respondent: String(applicantEmail).trim(), + answers: builtAnswers, + }); + + await submission.save(); + + res.status(201).json({ + message: 'Application submitted successfully.', + responseId: submission._id, + }); + } catch (error) { + console.error('Error submitting job application:', error); + res.status(500).json({ message: 'Error submitting application.', error: error.message }); + } +}; + +exports.submitJobApplicationMiddleware = upload.any(); + // Get all responses of a form exports.getFormResponses = async (req, res) => { try { diff --git a/src/routes/collaborationRouter.js b/src/routes/collaborationRouter.js index cf2c52372..c2ee7be1c 100644 --- a/src/routes/collaborationRouter.js +++ b/src/routes/collaborationRouter.js @@ -17,6 +17,13 @@ router.get('/jobforms/:formId', formController.getFormFormat); // Get all responses of a form router.get('/jobforms/:formId/responses', formController.getFormResponses); +// Submit a job application (public) +router.post( + '/jobforms/:formId/responses', + formController.submitJobApplicationMiddleware, + formController.submitJobApplication, +); + // Question management routes router.post('/jobforms/:formId/questions', formController.addQuestion); router.patch('/jobforms/:formId/questions/:questionIndex', formController.updateQuestion); diff --git a/src/startup/middleware.js b/src/startup/middleware.js index d6f14cee4..11e6059b1 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -79,6 +79,11 @@ module.exports = function (app) { return; } + if (/^\/api\/jobforms\/[^/]+\/responses$/.test(req.originalUrl) && req.method === 'POST') { + next(); + return; + } + if (req.originalUrl.startsWith('/api/bluesky')) { next(); return;