3july
This commit is contained in:
parent
347005322a
commit
3d1784be39
|
|
@ -77,7 +77,9 @@ BEDROCK_MODEL_ID=meta.llama3-8b-instruct-v1:0
|
||||||
# CONCIERGE_CRAWL_DELAY_MS=200
|
# CONCIERGE_CRAWL_DELAY_MS=200
|
||||||
# CONCIERGE_CRAWL_MAX_ASSET_BYTES=15728640
|
# CONCIERGE_CRAWL_MAX_ASSET_BYTES=15728640
|
||||||
|
|
||||||
# Public API base URL injected into published microsite HTML for contact forms (e.g. https://api.example.com/api or http://localhost:3001/api)
|
# HMAC secret for public form captcha tokens (falls back to JWT_SECRET if unset)
|
||||||
|
# FORM_CAPTCHA_SECRET=
|
||||||
|
|
||||||
MICROSITE_API_BASE_URL=http://localhost:3001/api
|
MICROSITE_API_BASE_URL=http://localhost:3001/api
|
||||||
# Public web app (forms, registration, default microsites)
|
# Public web app (forms, registration, default microsites)
|
||||||
# EVENTBUZZ_PUBLIC_URL=https://web.eventbuzz.live
|
# EVENTBUZZ_PUBLIC_URL=https://web.eventbuzz.live
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
/**
|
||||||
|
* Copy a quiz document into another account's event.
|
||||||
|
* Run: node scripts/copy-quiz-to-account.js
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const QuizPoll = require('../src/models/QuizPoll');
|
||||||
|
const Event = require('../src/models/Event');
|
||||||
|
const Session = require('../src/models/Session');
|
||||||
|
const User = require('../src/models/User');
|
||||||
|
const { syncQuizPollToFirebase } = require('../src/utils/quizPollFirebaseSync');
|
||||||
|
|
||||||
|
const TARGET_EMAIL = 'madhuragauri@crestindia.co.in';
|
||||||
|
|
||||||
|
/** Source quiz payload (Mongo export; ids stripped on insert). */
|
||||||
|
const SOURCE_QUIZ = {
|
||||||
|
title: 'COPD Basics',
|
||||||
|
type: 'quiz',
|
||||||
|
description: 'test',
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
'What spirometry value defines airflow limitation and confirms a COPD diagnosis after a bronchodilator is given?',
|
||||||
|
imageUrl:
|
||||||
|
'https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/quiz-polls/1782468667817-Screenshot_2026-06-01_at_10.01.48_AM.png',
|
||||||
|
type: 'single',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
text: 'FEV1/FVC greater than 0.80',
|
||||||
|
isCorrect: false,
|
||||||
|
imageUrl:
|
||||||
|
'https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/quiz-polls/1782468677271-Screenshot_2026-05-21_at_7.56.07_PM.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'FEV1/FVC less than 0.70',
|
||||||
|
isCorrect: true,
|
||||||
|
imageUrl:
|
||||||
|
'https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/quiz-polls/1782468680923-Screenshot_2026-05-21_at_7.56.07_PM.png',
|
||||||
|
},
|
||||||
|
{ text: 'FVC less than 50% predicted', isCorrect: false, imageUrl: null },
|
||||||
|
{
|
||||||
|
text: 'FEV1 increases by 12%',
|
||||||
|
isCorrect: false,
|
||||||
|
imageUrl:
|
||||||
|
'https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/quiz-polls/1782468683856-Screenshot_2026-05-21_at_7.48.20_PM.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
points: 1,
|
||||||
|
required: true,
|
||||||
|
timeLimit: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
'What structural lung change in emphysema leads to permanent air trapping and progressive shortness of breath?',
|
||||||
|
imageUrl: null,
|
||||||
|
type: 'single',
|
||||||
|
options: [
|
||||||
|
{ text: 'Reversible smooth muscle spasm', isCorrect: false, imageUrl: null },
|
||||||
|
{
|
||||||
|
text: 'Destruction of alveolar walls and loss of elastic recoil',
|
||||||
|
isCorrect: true,
|
||||||
|
imageUrl: null,
|
||||||
|
},
|
||||||
|
{ text: 'Fibrosis and narrowing of the upper airways', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'Overproduction of mucus in the large bronchi', isCorrect: false, imageUrl: null },
|
||||||
|
],
|
||||||
|
points: 1,
|
||||||
|
required: true,
|
||||||
|
timeLimit: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What is the globally recognized leading environmental cause of COPD?',
|
||||||
|
imageUrl: null,
|
||||||
|
type: 'single',
|
||||||
|
options: [
|
||||||
|
{ text: 'Alpha-1 antitrypsin deficiency', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'Outdoor air pollution', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'Cigarette smoking', isCorrect: true, imageUrl: null },
|
||||||
|
{ text: 'Occupational dust exposure', isCorrect: false, imageUrl: null },
|
||||||
|
],
|
||||||
|
points: 1,
|
||||||
|
required: true,
|
||||||
|
timeLimit: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
'Which drug class is the baseline maintenance treatment for reducing daily COPD symptoms and preventing flare-ups?',
|
||||||
|
imageUrl: null,
|
||||||
|
type: 'single',
|
||||||
|
options: [
|
||||||
|
{ text: 'Short-acting beta-agonists (SABAs)', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'Systemic corticosteroids', isCorrect: false, imageUrl: null },
|
||||||
|
{
|
||||||
|
text: 'Long-acting bronchodilators (LAMAs or LABAs)',
|
||||||
|
isCorrect: true,
|
||||||
|
imageUrl: null,
|
||||||
|
},
|
||||||
|
{ text: 'Mucolytics', isCorrect: false, imageUrl: null },
|
||||||
|
],
|
||||||
|
points: 1,
|
||||||
|
required: true,
|
||||||
|
timeLimit: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question:
|
||||||
|
'What is the minimum number of hours per day that long-term oxygen therapy must be used to improve survival rates in severe resting hypoxemia?',
|
||||||
|
imageUrl: null,
|
||||||
|
type: 'single',
|
||||||
|
options: [
|
||||||
|
{ text: 'At least 5 hours per day', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'At least 10 hours per day', isCorrect: false, imageUrl: null },
|
||||||
|
{ text: 'At least 15 hours per day', isCorrect: true, imageUrl: null },
|
||||||
|
{ text: 'Only during sleep', isCorrect: false, imageUrl: null },
|
||||||
|
],
|
||||||
|
points: 1,
|
||||||
|
required: true,
|
||||||
|
timeLimit: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
status: 'active',
|
||||||
|
isLive: false,
|
||||||
|
displayMode: 'quiz_master',
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
showResults: true,
|
||||||
|
allowMultipleAttempts: false,
|
||||||
|
timeRestricted: true,
|
||||||
|
timeLimit: 150,
|
||||||
|
rewardRules: {
|
||||||
|
quizPollSubmitted: 0,
|
||||||
|
quizCorrectPerQuestion: 5,
|
||||||
|
},
|
||||||
|
liveQuestionId: null,
|
||||||
|
randomizeQuestions: false,
|
||||||
|
showCorrectAnswer: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const uri = process.env.MONGODB_URI;
|
||||||
|
if (!uri) {
|
||||||
|
console.error('MONGODB_URI is not set');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await mongoose.connect(uri);
|
||||||
|
|
||||||
|
const user = await User.findOne({ email: TARGET_EMAIL }).lean();
|
||||||
|
if (!user) {
|
||||||
|
console.error('User not found:', TARGET_EMAIL);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await Event.findOne({ organization: user.organization, deletedAt: null })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.lean();
|
||||||
|
if (!event) {
|
||||||
|
console.error('No event found for organization', user.organization);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Session.findOne({ event: event._id }).sort({ startTime: 1 }).lean();
|
||||||
|
if (!session) {
|
||||||
|
console.error('No session found for event', event._id);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await QuizPoll.findOne({
|
||||||
|
event: event._id,
|
||||||
|
title: SOURCE_QUIZ.title,
|
||||||
|
type: 'quiz',
|
||||||
|
deletedAt: null,
|
||||||
|
}).lean();
|
||||||
|
if (existing) {
|
||||||
|
console.log('Quiz already exists — skipping create');
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
quizId: String(existing._id),
|
||||||
|
eventId: String(event._id),
|
||||||
|
eventName: event.name,
|
||||||
|
sessionId: existing.session ? String(existing.session) : null,
|
||||||
|
title: existing.title,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await mongoose.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...SOURCE_QUIZ,
|
||||||
|
event: event._id,
|
||||||
|
session: session._id,
|
||||||
|
settings: {
|
||||||
|
...SOURCE_QUIZ.settings,
|
||||||
|
presentationToken: QuizPoll.generatePresentationToken(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const item = await QuizPoll.create(payload);
|
||||||
|
await syncQuizPollToFirebase(item);
|
||||||
|
|
||||||
|
console.log('Quiz created successfully');
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
quizId: String(item._id),
|
||||||
|
title: item.title,
|
||||||
|
eventId: String(event._id),
|
||||||
|
eventName: event.name,
|
||||||
|
sessionId: String(session._id),
|
||||||
|
sessionTitle: session.title,
|
||||||
|
accountEmail: TARGET_EMAIL,
|
||||||
|
questionCount: item.questions?.length ?? 0,
|
||||||
|
presentationToken: item.settings?.presentationToken,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await mongoose.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
22
src/app.js
22
src/app.js
|
|
@ -33,6 +33,7 @@ const badgeBatchRoutes = require('./routes/badgeBatch.routes');
|
||||||
const eventTicketRoutes = require('./routes/eventTicket.routes');
|
const eventTicketRoutes = require('./routes/eventTicket.routes');
|
||||||
const publicEventTicketRoutes = require('./routes/publicEventTicket.routes');
|
const publicEventTicketRoutes = require('./routes/publicEventTicket.routes');
|
||||||
const publicMicrositeRoutes = require('./routes/publicMicrosite.routes');
|
const publicMicrositeRoutes = require('./routes/publicMicrosite.routes');
|
||||||
|
const publicCaptchaRoutes = require('./routes/publicCaptcha.routes');
|
||||||
const publicAttendanceRoutes = require('./routes/publicAttendance.routes');
|
const publicAttendanceRoutes = require('./routes/publicAttendance.routes');
|
||||||
const attendanceRoutes = require('./routes/attendance.routes');
|
const attendanceRoutes = require('./routes/attendance.routes');
|
||||||
const checkinsRoutes = require('./routes/checkins.routes');
|
const checkinsRoutes = require('./routes/checkins.routes');
|
||||||
|
|
@ -85,6 +86,22 @@ const createServer = async () => {
|
||||||
|
|
||||||
const allowedOrigins = [...new Set([...originList, ...builderExtras, ...conciergeExtras])];
|
const allowedOrigins = [...new Set([...originList, ...builderExtras, ...conciergeExtras])];
|
||||||
|
|
||||||
|
function isPublicMicrositeApiPath(path) {
|
||||||
|
if (!path) return false;
|
||||||
|
if (path.startsWith('/api/public/')) return true;
|
||||||
|
return /^\/api\/forms\/[^/]+\/submit$/.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCorsForRequest(request) {
|
||||||
|
const origin = request.headers.origin;
|
||||||
|
const allowed = resolveCorsAllowOrigin(origin);
|
||||||
|
if (allowed) return allowed;
|
||||||
|
if (origin && isPublicMicrositeApiPath(request.path)) {
|
||||||
|
return String(origin).trim().replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** In development, allow any http://localhost or http://127.0.0.1 port (Vite may use 5175, 5176, …). Production: whitelist only. */
|
/** In development, allow any http://localhost or http://127.0.0.1 port (Vite may use 5175, 5176, …). Production: whitelist only. */
|
||||||
function resolveCorsAllowOrigin(rawOrigin) {
|
function resolveCorsAllowOrigin(rawOrigin) {
|
||||||
if (!rawOrigin) return null;
|
if (!rawOrigin) return null;
|
||||||
|
|
@ -136,7 +153,7 @@ const createServer = async () => {
|
||||||
options: {
|
options: {
|
||||||
auth: false,
|
auth: false,
|
||||||
handler: (request, h) => {
|
handler: (request, h) => {
|
||||||
const allowOrigin = resolveCorsAllowOrigin(request.headers.origin);
|
const allowOrigin = resolveCorsForRequest(request);
|
||||||
const res = h.response().code(204);
|
const res = h.response().code(204);
|
||||||
if (allowOrigin) {
|
if (allowOrigin) {
|
||||||
res.header('Access-Control-Allow-Origin', allowOrigin);
|
res.header('Access-Control-Allow-Origin', allowOrigin);
|
||||||
|
|
@ -228,6 +245,7 @@ const createServer = async () => {
|
||||||
server.route(visitorRoutes);
|
server.route(visitorRoutes);
|
||||||
server.route(publicEventTicketRoutes);
|
server.route(publicEventTicketRoutes);
|
||||||
server.route(publicMicrositeRoutes);
|
server.route(publicMicrositeRoutes);
|
||||||
|
server.route(publicCaptchaRoutes);
|
||||||
server.route(publicAttendanceRoutes);
|
server.route(publicAttendanceRoutes);
|
||||||
server.route(publicConciergeRoutes);
|
server.route(publicConciergeRoutes);
|
||||||
server.route(publicQuizPollRoutes);
|
server.route(publicQuizPollRoutes);
|
||||||
|
|
@ -268,7 +286,7 @@ const createServer = async () => {
|
||||||
|
|
||||||
server.ext('onPreResponse', (request, h) => {
|
server.ext('onPreResponse', (request, h) => {
|
||||||
const response = request.response;
|
const response = request.response;
|
||||||
const allowOrigin = resolveCorsAllowOrigin(request.headers.origin);
|
const allowOrigin = resolveCorsForRequest(request);
|
||||||
|
|
||||||
if (response.isBoom && response.output) {
|
if (response.isBoom && response.output) {
|
||||||
const statusCode = response.output.statusCode;
|
const statusCode = response.output.statusCode;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,69 @@ const {
|
||||||
buildPublicEventPayload,
|
buildPublicEventPayload,
|
||||||
resolveEventForPublicForm,
|
resolveEventForPublicForm,
|
||||||
} = require('../utils/formPublicHelpers');
|
} = require('../utils/formPublicHelpers');
|
||||||
|
const { verifyCaptchaChallenge } = require('../utils/publicCaptcha');
|
||||||
|
const { getMembership } = require('../utils/participantMembership');
|
||||||
|
|
||||||
|
function throwRegError(message, code) {
|
||||||
|
const err = new Error(message);
|
||||||
|
err.code = code;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventRegistrationStatus(event) {
|
||||||
|
const settings = event.settings || {};
|
||||||
|
if (settings.requiresApproval === true) return 'pending';
|
||||||
|
const mode = String(settings.registrationMode || 'Open').trim();
|
||||||
|
if (mode === 'Approval Required') return 'pending';
|
||||||
|
return 'confirmed';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertCanRegisterForEvent(event) {
|
||||||
|
const settings = event.settings || {};
|
||||||
|
if (settings.registrationOpen === false) {
|
||||||
|
throwRegError('Registration is closed for this event', 'REGISTRATION_CLOSED');
|
||||||
|
}
|
||||||
|
const deadline = settings.registrationDeadline;
|
||||||
|
if (deadline && new Date(deadline).getTime() <= Date.now()) {
|
||||||
|
throwRegError('Registration deadline has passed', 'REGISTRATION_CLOSED');
|
||||||
|
}
|
||||||
|
const max = settings.maxParticipants;
|
||||||
|
if (max && max > 0) {
|
||||||
|
const count = await Participant.countDocuments({
|
||||||
|
eventMemberships: { $elemMatch: { event: event._id } },
|
||||||
|
});
|
||||||
|
if (count >= max) {
|
||||||
|
throwRegError('This event has reached maximum participants', 'REGISTRATION_FULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureEventMembershipForRegistrationForm(form, participant) {
|
||||||
|
const { ev: resolvedEvent } = await resolveEventForPublicForm(form);
|
||||||
|
if (!resolvedEvent?._id) return;
|
||||||
|
|
||||||
|
const event = await Event.findById(resolvedEvent._id).select('settings').lean();
|
||||||
|
if (!event) return;
|
||||||
|
|
||||||
|
const eventId = event._id;
|
||||||
|
if (getMembership(participant, eventId)) return;
|
||||||
|
|
||||||
|
await assertCanRegisterForEvent(event);
|
||||||
|
participant.eventMemberships = participant.eventMemberships || [];
|
||||||
|
participant.eventMemberships.push({
|
||||||
|
event: eventId,
|
||||||
|
status: eventRegistrationStatus(event),
|
||||||
|
registrationDate: new Date(),
|
||||||
|
});
|
||||||
|
await participant.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSubmissionEventId(form) {
|
||||||
|
const directId = extractObjectIdString(form.event);
|
||||||
|
if (directId) return directId;
|
||||||
|
const { ev } = await resolveEventForPublicForm(form);
|
||||||
|
return ev?._id ? String(ev._id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSubmitData(raw) {
|
function normalizeSubmitData(raw) {
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
|
|
@ -118,6 +181,8 @@ async function upsertParticipantFromRegistrationForm(form, tenantOrganization, d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureEventMembershipForRegistrationForm(form, p);
|
||||||
|
|
||||||
return p._id;
|
return p._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,8 +349,24 @@ const submit = async (request, h) => {
|
||||||
if (!form.settings?.isActive) return h.response(error('VALIDATION_ERROR', 'Form is not accepting submissions')).code(400);
|
if (!form.settings?.isActive) return h.response(error('VALIDATION_ERROR', 'Form is not accepting submissions')).code(400);
|
||||||
const user = request.auth?.credentials?.user;
|
const user = request.auth?.credentials?.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
if (request.payload._gotcha) {
|
||||||
|
return h.response(error('VALIDATION_ERROR', 'Invalid request')).code(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const captchaToken = request.payload.captchaToken;
|
||||||
|
if (captchaToken) {
|
||||||
|
const captchaOk = verifyCaptchaChallenge(captchaToken, request.payload.captchaAnswer);
|
||||||
|
if (!captchaOk) {
|
||||||
|
return h.response(error('VALIDATION_ERROR', 'Captcha verification failed. Please try again.')).code(400);
|
||||||
|
}
|
||||||
|
} else if (process.env.PUBLIC_FORM_CAPTCHA_REQUIRED === 'true') {
|
||||||
|
return h.response(error('VALIDATION_ERROR', 'Captcha verification failed. Please try again.')).code(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tenantOrganization = form.tenantOrganization;
|
const tenantOrganization = form.tenantOrganization;
|
||||||
const eventRef = form.event;
|
const submissionEventId = await resolveSubmissionEventId(form);
|
||||||
if (!tenantOrganization) {
|
if (!tenantOrganization) {
|
||||||
return h.response(error('VALIDATION_ERROR', 'Form is not configured for submissions (missing organization)')).code(400);
|
return h.response(error('VALIDATION_ERROR', 'Form is not configured for submissions (missing organization)')).code(400);
|
||||||
}
|
}
|
||||||
|
|
@ -300,9 +381,13 @@ const submit = async (request, h) => {
|
||||||
if (
|
if (
|
||||||
err.code === 'EMAIL_REQUIRED' ||
|
err.code === 'EMAIL_REQUIRED' ||
|
||||||
err.code === 'REGISTRATION_REQUIRED' ||
|
err.code === 'REGISTRATION_REQUIRED' ||
|
||||||
err.code === 'FORM_FIELD_CONFIG'
|
err.code === 'FORM_FIELD_CONFIG' ||
|
||||||
|
err.code === 'REGISTRATION_CLOSED' ||
|
||||||
|
err.code === 'REGISTRATION_FULL'
|
||||||
) {
|
) {
|
||||||
return h.response(error('VALIDATION_ERROR', err.message || 'Validation failed')).code(400);
|
const statusCode =
|
||||||
|
err.code === 'REGISTRATION_CLOSED' || err.code === 'REGISTRATION_FULL' ? 403 : 400;
|
||||||
|
return h.response(error(err.code, err.message || 'Validation failed')).code(statusCode);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -311,15 +396,15 @@ const submit = async (request, h) => {
|
||||||
const submission = await FormSubmission.create({
|
const submission = await FormSubmission.create({
|
||||||
form: formId,
|
form: formId,
|
||||||
tenantOrganization,
|
tenantOrganization,
|
||||||
event: eventRef || undefined,
|
event: submissionEventId || undefined,
|
||||||
user: user?._id,
|
user: user?._id,
|
||||||
participant: participantId,
|
participant: participantId,
|
||||||
data: dataObj,
|
data: dataObj,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (eventRef) {
|
if (submissionEventId) {
|
||||||
const summary = await computeEventRatingsSummary(eventRef.toString());
|
const summary = await computeEventRatingsSummary(submissionEventId);
|
||||||
await Event.findByIdAndUpdate(eventRef, {
|
await Event.findByIdAndUpdate(submissionEventId, {
|
||||||
$set: {
|
$set: {
|
||||||
'metadata.rating': summary.average,
|
'metadata.rating': summary.average,
|
||||||
'metadata.ratingCount': summary.count,
|
'metadata.ratingCount': summary.count,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
const { success } = require('../utils/response');
|
||||||
|
const { createCaptchaChallenge } = require('../utils/publicCaptcha');
|
||||||
|
|
||||||
|
const getChallenge = async (request, h) => {
|
||||||
|
const challenge = createCaptchaChallenge();
|
||||||
|
return h.response(success(challenge));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getChallenge,
|
||||||
|
};
|
||||||
|
|
@ -60,6 +60,9 @@ const formOrgCreatePayload = Joi.object({
|
||||||
const submitPayload = Joi.object({
|
const submitPayload = Joi.object({
|
||||||
participantId: Joi.string(),
|
participantId: Joi.string(),
|
||||||
data: Joi.object().pattern(Joi.string(), Joi.any()),
|
data: Joi.object().pattern(Joi.string(), Joi.any()),
|
||||||
|
captchaToken: Joi.string().allow('', null),
|
||||||
|
captchaAnswer: Joi.alternatives().try(Joi.string(), Joi.number()).allow('', null),
|
||||||
|
_gotcha: Joi.string().allow('', null),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const controller = require('../controllers/publicCaptcha.controller');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/public/captcha/challenge',
|
||||||
|
options: { auth: false, handler: controller.getChallenge },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const TTL_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
function captchaSecret() {
|
||||||
|
return process.env.FORM_CAPTCHA_SECRET || process.env.JWT_SECRET || 'eventbuzz-form-captcha-dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCaptchaChallenge() {
|
||||||
|
const a = Math.floor(Math.random() * 9) + 1;
|
||||||
|
const b = Math.floor(Math.random() * 9) + 1;
|
||||||
|
const answer = a + b;
|
||||||
|
const expires = Date.now() + TTL_MS;
|
||||||
|
const signature = crypto.createHmac('sha256', captchaSecret()).update(`${answer}:${expires}`).digest('hex');
|
||||||
|
|
||||||
|
return {
|
||||||
|
question: `${a} + ${b} = ?`,
|
||||||
|
captchaToken: `${signature}.${expires}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyCaptchaChallenge(captchaToken, captchaAnswer) {
|
||||||
|
if (!captchaToken || captchaAnswer == null || captchaAnswer === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answer = parseInt(String(captchaAnswer).trim(), 10);
|
||||||
|
if (Number.isNaN(answer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = String(captchaToken).split('.');
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [signature, expiresRaw] = parts;
|
||||||
|
const expires = parseInt(expiresRaw, 10);
|
||||||
|
if (!expires || Date.now() > expires) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = crypto.createHmac('sha256', captchaSecret()).update(`${answer}:${expires}`).digest('hex');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createCaptchaChallenge,
|
||||||
|
verifyCaptchaChallenge,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue