diff --git a/src/controllers/form.controller.js b/src/controllers/form.controller.js index b40fa4e..e777650 100644 --- a/src/controllers/form.controller.js +++ b/src/controllers/form.controller.js @@ -16,6 +16,8 @@ const { verifyCaptchaChallenge } = require('../utils/publicCaptcha'); const { getMembership } = require('../utils/participantMembership'); const { publicRegistrationFields } = require('../utils/registrationFormFields'); const { processFormSubmissionFileUploads } = require('../utils/formSubmissionFiles'); +const { buildSubmissionReplyEmail } = require('../utils/formReplyMail'); +const { sendOrgMail } = require('../utils/mail'); function throwRegError(message, code) { const err = new Error(message); @@ -581,6 +583,39 @@ const listSubmissions = async (request, h) => { }; /** Public: form definition for rendering (no auth). Only when form accepts responses. */ +const replyToSubmission = async (request, h) => { + const form = await Form.findById(request.params.formId); + if (!form) return h.response(error('NOT_FOUND', 'Form not found')).code(404); + + const allowed = await formBelongsToUserOrg(form, request.auth.credentials.user); + if (!allowed) return h.response(error('FORBIDDEN', 'Access denied')).code(403); + + const { to, subject, body } = request.payload || {}; + if (!to) return h.response(error('VALIDATION_ERROR', 'Recipient email is required')).code(400); + + const orgId = form.tenantOrganization || (form.event ? String(form.event) : null); + const message = buildSubmissionReplyEmail({ + to, + from: request.auth.credentials.user?.email || 'EventBuzz', + subject, + body, + }); + + const result = await sendOrgMail({ + organizationId: orgId, + to, + subject: message.subject, + html: message.html, + text: message.text, + }); + + if (!result?.ok && !result?.skipped) { + return h.response(error('MAIL_FAILED', result?.error || 'Could not send email')).code(502); + } + + return h.response(success({ sent: true })); +}; + const getPublicForm = async (request, h) => { const form = await Form.findById(request.params.formId).lean(); if (!form) return h.response(error('NOT_FOUND', 'Form not found')).code(404); @@ -618,5 +653,6 @@ module.exports = { remove, submit, listSubmissions, + replyToSubmission, getPublicForm, }; diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js index 30f7600..ba93656 100644 --- a/src/controllers/media.controller.js +++ b/src/controllers/media.controller.js @@ -91,10 +91,11 @@ const list = async (request, h) => { const filter = { ...orgRes.filter }; if (request.query.type) filter.type = request.query.type; if (request.query.collectionName) filter.collectionName = request.query.collectionName; + if (request.query.eventId || request.query.event_id) filter.eventId = request.query.eventId || request.query.event_id; const { page = 1, limit = 20 } = request.query; const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); const [media, total] = await Promise.all([ - Content.find(filter).skip(skip).limit(parseInt(limit, 10)).lean(), + Content.find(filter).populate({ path: 'uploadedBy', select: 'firstName lastName email phone' }).skip(skip).limit(parseInt(limit, 10)).lean(), Content.countDocuments(filter), ]); return h.response(success({ media, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) @@ -111,7 +112,7 @@ const listLibrary = async (request, h) => { const { page = 1, limit = 20 } = request.query; const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); const [media, total] = await Promise.all([ - LibraryContent.find(filter).sort({ createdAt: -1 }).skip(skip).limit(parseInt(limit, 10)).lean(), + LibraryContent.find(filter).populate({ path: 'uploadedBy', select: 'firstName lastName email phone' }).sort({ createdAt: -1 }).skip(skip).limit(parseInt(limit, 10)).lean(), LibraryContent.countDocuments(filter), ]); return h.response(success({ media, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) @@ -183,6 +184,7 @@ const upload = async (request, h) => { const baseDoc = { organization: orgId, + eventId: eventId || undefined, name: payload.name || extracted?.filename || 'Untitled', description, visibility, diff --git a/src/models/Content.js b/src/models/Content.js index a76a84d..60c510b 100644 --- a/src/models/Content.js +++ b/src/models/Content.js @@ -7,6 +7,11 @@ const contentSchema = new mongoose.Schema( ref: 'Organization', required: true, }, + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + index: true, + }, name: String, description: { type: String, default: '' }, visibility: { diff --git a/src/models/LibraryContent.js b/src/models/LibraryContent.js index 6a503d9..acffac6 100644 --- a/src/models/LibraryContent.js +++ b/src/models/LibraryContent.js @@ -8,6 +8,11 @@ const libraryContentSchema = new mongoose.Schema( ref: 'Organization', required: true, }, + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + index: true, + }, name: String, description: { type: String, default: '' }, visibility: { diff --git a/src/routes/form.routes.js b/src/routes/form.routes.js index e5b5cb1..918bd8c 100644 --- a/src/routes/form.routes.js +++ b/src/routes/form.routes.js @@ -70,6 +70,12 @@ const submitPayload = Joi.object({ _gotcha: Joi.string().allow('', null), }); +const replyPayload = Joi.object({ + to: Joi.string().email().required(), + subject: Joi.string().allow('', null), + body: Joi.string().allow('', null), +}); + module.exports = [ { method: 'GET', path: '/api/forms', options: { handler: controller.listByOrganization } }, { @@ -93,4 +99,9 @@ module.exports = [ options: { auth: false, handler: controller.getPublicForm }, }, { method: 'GET', path: '/api/forms/{formId}/submissions', options: { handler: controller.listSubmissions } }, + { + method: 'POST', + path: '/api/forms/{formId}/submissions/reply', + options: { validate: { payload: replyPayload }, handler: controller.replyToSubmission }, + }, ]; diff --git a/src/utils/formReplyMail.js b/src/utils/formReplyMail.js new file mode 100644 index 0000000..891e0e7 --- /dev/null +++ b/src/utils/formReplyMail.js @@ -0,0 +1,29 @@ +function escapeHtml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function buildSubmissionReplyEmail({ to, from, subject, body }) { + const safeSubject = String(subject || 'Re: Form response').trim() || 'Re: Form response'; + const safeBody = String(body || '').trim(); + const text = safeBody || '(No message)'; + const html = ` +
From: ${escapeHtml(from || 'EventBuzz')}
+To: ${escapeHtml(to || '')}
+Subject: ${escapeHtml(safeSubject)}
+