Add form reply mail support

This commit is contained in:
Tanuja Pansare 2026-07-10 12:06:04 +05:30
parent 399f14249a
commit ce84277004
7 changed files with 107 additions and 2 deletions

View File

@ -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,
};

View File

@ -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,

View File

@ -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: {

View File

@ -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: {

View File

@ -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 },
},
];

View File

@ -0,0 +1,29 @@
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 = `
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #111827;">
<p><strong>From:</strong> ${escapeHtml(from || 'EventBuzz')}</p>
<p><strong>To:</strong> ${escapeHtml(to || '')}</p>
<p><strong>Subject:</strong> ${escapeHtml(safeSubject)}</p>
<div style="white-space: pre-wrap; margin-top: 12px;">${escapeHtml(text)}</div>
</div>
`;
return {
subject: safeSubject,
text,
html,
};
}
module.exports = { buildSubmissionReplyEmail };

View File

@ -0,0 +1,17 @@
const { buildSubmissionReplyEmail } = require('./formReplyMail');
describe('buildSubmissionReplyEmail', () => {
it('builds a reply email with the provided subject and body', () => {
const message = buildSubmissionReplyEmail({
to: 'attendee@example.com',
from: 'organizer@example.com',
subject: 'Re: Your registration',
body: 'Thanks for signing up!',
});
expect(message.subject).toBe('Re: Your registration');
expect(message.text).toContain('Thanks for signing up!');
expect(message.html).toContain('Thanks for signing up!');
expect(message.html).toContain('organizer@example.com');
});
});