Add form reply mail support
This commit is contained in:
parent
399f14249a
commit
ce84277004
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.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 = `
|
||||
<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 };
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue