Your commit message
This commit is contained in:
parent
11c511bde2
commit
399f14249a
15
.env.example
15
.env.example
|
|
@ -2,6 +2,8 @@ NODE_ENV=development
|
|||
PORT=3001
|
||||
# Max JSON body size (bytes). Default 25MB if unset — needed for event create with base64 logo/banner.
|
||||
# PAYLOAD_MAX_BYTES=26214400
|
||||
# Max size per form file upload after base64 decode (default 10MB)
|
||||
# FORM_SUBMISSION_MAX_FILE_BYTES=10485760
|
||||
MONGODB_URI=mongodb://localhost:27017/eventbuzz
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this
|
||||
JWT_REFRESH_SECRET=your-refresh-secret-key-change-this
|
||||
|
|
@ -87,8 +89,21 @@ MICROSITE_API_BASE_URL=http://localhost:3001/api
|
|||
# MICROSITE_REGISTRATION_URL_TEMPLATE=https://web.eventbuzz.live/event/{eventId}/register
|
||||
# Published microsite folder base (custom microsite static HTML)
|
||||
# MICROSITE_PUBLIC_BASE_URL=https://web.eventbuzz.live
|
||||
|
||||
# --- Microsite V2 (Handlebars static microsites on S3 + subdomain) ---
|
||||
# S3 key prefix for template and published site objects (default: eventbuzz/microsite-v2)
|
||||
# MICROSITE_V2_S3_PREFIX=eventbuzz/microsite-v2
|
||||
# Public URL pattern for event subdomains; {subdomain} is replaced at runtime
|
||||
# MICROSITE_V2_PUBLIC_BASE_URL=https://{subdomain}.eventbuzz.live
|
||||
# Infra: wildcard DNS *.eventbuzz.live → CloudFront; map Host header to
|
||||
# s3://{AWS_BUCKET}/{MICROSITE_V2_S3_PREFIX}/sites/{subdomain}/index.html
|
||||
# Dev fallback: open published HTML via CloudFront URL of index.html key directly.
|
||||
# Seed first template: npm run seed:microsite-v2-template
|
||||
|
||||
# Comma-separated CORS origins (include console + public web app)
|
||||
# CORS_ORIGIN=https://console.eventbuzz.live,https://web.eventbuzz.live
|
||||
# Public routes under /api/public/* also echo the request Origin (any domain) when the API is up.
|
||||
# Browser "access control checks" errors on api.eventbuzz.live often mean nginx 502 (Node not running), not CORS config.
|
||||
|
||||
# Firebase Realtime Database (optional — quiz/poll sync). Paths relative to backend/ when starting the API from backend/
|
||||
FIREBASE_DATABASE_URL=https://eventbuzz-live-default-rtdb.firebaseio.com
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
/** Minimal runtime bridge for SSR-published EventUp pages (no site-config.js DOM hydration). */
|
||||
function readEmbeddedConfig() {
|
||||
var el = document.getElementById('eventup-microsite-config');
|
||||
if (!el || !el.textContent) return null;
|
||||
try {
|
||||
return JSON.parse(el.textContent);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (!theme) return;
|
||||
var root = document.documentElement;
|
||||
if (theme.accent) {
|
||||
root.style.setProperty('--eventup-accent', theme.accent);
|
||||
root.style.setProperty('--eventup-accent-start', theme.accent);
|
||||
}
|
||||
var headingColor = (theme.heading && theme.heading.color) || theme.headingColor;
|
||||
if (headingColor) {
|
||||
root.style.setProperty('--eventup-heading-color', headingColor);
|
||||
}
|
||||
}
|
||||
|
||||
function initCountdown(hero) {
|
||||
if (!hero || !hero.countdown || hero.countdown.show === false) {
|
||||
var countSection = document.querySelector('[data-site-component="countdown"]');
|
||||
if (countSection) countSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (!window.jQuery || !window.jQuery.fn.countdown) return;
|
||||
var date = hero.countdown.date;
|
||||
var time = hero.countdown.time || '12:00';
|
||||
if (!date) return;
|
||||
var target = new Date(date + 'T' + time + ':00');
|
||||
window.jQuery('#clock').countdown(target, function (event) {
|
||||
window.jQuery(this).html(event.strftime('%D days %H:%M:%S'));
|
||||
});
|
||||
}
|
||||
|
||||
function applyLinkTarget(linkEl, item) {
|
||||
if (!linkEl || !item || !item.url) return;
|
||||
linkEl.href = item.url;
|
||||
if (item.target === '_blank') {
|
||||
linkEl.target = '_blank';
|
||||
linkEl.rel = 'noopener noreferrer';
|
||||
return;
|
||||
}
|
||||
linkEl.removeAttribute('target');
|
||||
if (item.target === '_self' || !item.target) {
|
||||
linkEl.removeAttribute('rel');
|
||||
}
|
||||
}
|
||||
|
||||
function bindCtaLinks(config) {
|
||||
var components = config.components || config.sections || {};
|
||||
var heroCta = components.hero && components.hero.cta;
|
||||
var heroLinks = document.querySelectorAll('[data-hero-cta-link], [data-nav-cta-link]');
|
||||
|
||||
if (heroCta && heroCta.url) {
|
||||
if (heroCta.target === 'modal' && window.EventUpRegistrationModal && window.EventUpRegistrationModal.bindLink) {
|
||||
heroLinks.forEach(function (a) {
|
||||
window.EventUpRegistrationModal.bindLink(a, heroCta, config);
|
||||
});
|
||||
} else {
|
||||
heroLinks.forEach(function (a) {
|
||||
applyLinkTarget(a, heroCta);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var footer = components.footer;
|
||||
if (!footer || !footer.links || !footer.links.length) return;
|
||||
|
||||
var footerLinkEls = document.querySelectorAll('[data-footer-link]');
|
||||
footer.links.forEach(function (item, index) {
|
||||
var linkEl = footerLinkEls[index];
|
||||
if (!linkEl || !item.url) return;
|
||||
|
||||
if (item.target === 'modal' && window.EventUpRegistrationModal && window.EventUpRegistrationModal.bindLink) {
|
||||
window.EventUpRegistrationModal.bindLink(linkEl, item, config);
|
||||
return;
|
||||
}
|
||||
|
||||
applyLinkTarget(linkEl, item);
|
||||
});
|
||||
}
|
||||
|
||||
function boot(config) {
|
||||
if (!config) return;
|
||||
applyTheme(config.theme || {});
|
||||
initCountdown((config.components || config.sections || {}).hero);
|
||||
bindCtaLinks(config);
|
||||
|
||||
var components = config.components || config.sections || {};
|
||||
if (window.EventUpContactForm && components.contact) {
|
||||
window.EventUpContactForm.init(components.contact, config);
|
||||
}
|
||||
if (window.EventUpFeedbackFab && components.feedback) {
|
||||
window.EventUpFeedbackFab.init(components.feedback, config);
|
||||
}
|
||||
if (window.EventUpMarketingPopup && components.marketing_popup) {
|
||||
window.EventUpMarketingPopup.init(components.marketing_popup, config);
|
||||
}
|
||||
|
||||
document.documentElement.classList.add('site-config-loaded');
|
||||
document.dispatchEvent(new CustomEvent('site-config-loaded'));
|
||||
if (typeof window.updateNavbarSpacer === 'function') {
|
||||
window.updateNavbarSpacer();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
var config = readEmbeddedConfig();
|
||||
if (config) {
|
||||
boot(config);
|
||||
return;
|
||||
}
|
||||
fetch('config.json', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (json) {
|
||||
if (json) {
|
||||
if (!json.components && json.sections) json.components = json.sections;
|
||||
boot(json);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>{{meta.title}}</title>
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/css/bootstrap.min.css'}}">
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/fonts/line-icons.css'}}">
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/css/nivo-lightbox.css'}}">
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/css/animate.css'}}">
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/css/main.css'}}">
|
||||
<link rel="stylesheet" href="{{assetUrl assetsBaseUrl 'assets/css/responsive.css'}}">
|
||||
<style>
|
||||
:root {
|
||||
--eventup-accent: {{theme.accent}};
|
||||
--eventup-accent-start: {{theme.accent}};
|
||||
--eventup-heading-color: {{#if theme.heading.color}}{{theme.heading.color}}{{else}}{{theme.headingColor}}{{/if}};
|
||||
}
|
||||
{{#if sections.hero.image.url}}
|
||||
#hero-area.hero-area-bg { background-image: url('{{resolveAsset assetsBaseUrl sections.hero.image.url}}'); }
|
||||
{{/if}}
|
||||
</style>
|
||||
<script id="eventup-microsite-config" type="application/json">{{json this}}</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{#if_show sections.logo}}
|
||||
<nav class="navbar navbar-expand-lg bg-inverse fixed-top scrolling-navbar">
|
||||
<div class="container">
|
||||
<a href="#header-wrap" class="navbar-brand">
|
||||
{{#if sections.logo.header.url}}
|
||||
<img src="{{resolveAsset assetsBaseUrl sections.logo.header.url}}" alt="{{sections.logo.header.alt}}" style="max-height:{{sections.logo.header.maxHeight}}px">
|
||||
{{/if}}
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<i class="lni-menu"></i>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto w-100 justify-content-end">
|
||||
<li class="nav-item active"><a class="nav-link" href="#header-wrap">Home</a></li>
|
||||
{{#if_show sections.about}}<li class="nav-item"><a class="nav-link" href="#about">About</a></li>{{/if_show}}
|
||||
{{#if_show sections.speakers}}<li class="nav-item"><a class="nav-link" href="#team">Speakers</a></li>{{/if_show}}
|
||||
{{#if_show sections.event_schedule}}<li class="nav-item"><a class="nav-link" href="#schedules">Schedule</a></li>{{/if_show}}
|
||||
{{#if_show sections.sponsors}}<li class="nav-item"><a class="nav-link" href="#sponsors">Sponsors</a></li>{{/if_show}}
|
||||
{{#if_show sections.contact}}<li class="nav-item"><a class="nav-link" href="#contact-map">Contact</a></li>{{/if_show}}
|
||||
{{#if sections.hero.cta.text}}
|
||||
<li class="nav-item nav-item--register">
|
||||
<a class="btn btn-common" href="{{sections.hero.cta.url}}" data-nav-cta-link{{#if (eq sections.hero.cta.target "_blank")}} target="_blank" rel="noopener noreferrer"{{/if}}>{{sections.hero.cta.text}}</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="navbar-spacer" aria-hidden="true"></div>
|
||||
{{/if_show}}
|
||||
|
||||
<header id="header-wrap">
|
||||
{{#if_show sections.hero}}
|
||||
<div id="hero-area" class="hero-area-bg" data-site-component="hero">
|
||||
<div class="overlay"></div>
|
||||
<div class="container-fluid hero-split-container">
|
||||
<div class="row hero-split align-items-center no-gutters">
|
||||
<div class="col-lg-6 col-md-6 col-12 hero-split__media">
|
||||
{{#if sections.logo.hero.url}}
|
||||
<div class="hero-logo-wrap">
|
||||
<img class="hero-logo" src="{{resolveAsset assetsBaseUrl sections.logo.hero.url}}" alt="{{sections.logo.hero.alt}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-12 hero-split__content">
|
||||
<div class="hero-split__text">
|
||||
<h2 class="head-title hero-brand-title">{{sections.hero.event_name}}</h2>
|
||||
<p class="banner-info">{{sections.hero.date_venue}}</p>
|
||||
{{#if sections.hero.cta.text}}
|
||||
<div class="banner-btn">
|
||||
<a href="{{sections.hero.cta.url}}" class="btn btn-common" data-hero-cta-link{{#if (eq sections.hero.cta.target "_blank")}} target="_blank" rel="noopener noreferrer"{{/if}}>{{sections.hero.cta.text}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if_show}}
|
||||
</header>
|
||||
|
||||
{{#if sections.hero.countdown.show}}
|
||||
<section id="count" data-site-component="countdown">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
<div class="count-wrapper text-center">
|
||||
<div class="time-countdown wow fadeInUp" data-wow-delay="0.2s">
|
||||
<div id="clock" class="time-count"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if_show sections.about}}
|
||||
<section id="about" class="section-padding" data-site-component="about">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="about-content">
|
||||
<div class="about-text">
|
||||
<h2>{{sections.about.heading}}</h2>
|
||||
<div>{{{sections.about.content}}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.video}}
|
||||
<div class="ready-to-play" data-site-component="video">
|
||||
<div class="container">
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title wow fadeInUp">{{sections.video.heading}}</h2>
|
||||
</div>
|
||||
<div class="video-grid row">
|
||||
{{#each sections.video.items}}
|
||||
<div class="col-md-6 mb-4">
|
||||
<video class="w-100" controls preload="metadata" src="{{resolveAsset @root.assetsBaseUrl this.url}}"></video>
|
||||
{{#if this.title}}<p class="mt-2 text-center">{{this.title}}</p>{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.informationBar}}
|
||||
<section id="information-bar" data-site-component="information-bar">
|
||||
<div class="container">
|
||||
<div class="row inforation-wrapper">
|
||||
{{#each sections.informationBar.items}}
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<div class="info-bar-item">
|
||||
<i class="{{this.icon}} info-bar-item__icon"></i>
|
||||
<div class="info-bar-item__text">
|
||||
<b class="info-bar-item__label">{{this.label}}</b>
|
||||
<span class="info-bar-item__value">{{this.value}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.event_highlights}}
|
||||
<section id="intro" class="intro section-padding" data-site-component="event_highlights">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title wow fadeInUp">{{sections.event_highlights.heading}}</h2>
|
||||
{{#if sections.event_highlights.subtitle}}<p class="wow fadeInDown">{{sections.event_highlights.subtitle}}</p>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row intro-wrapper">
|
||||
{{#each sections.event_highlights.items}}
|
||||
<div class="col-lg-4 col-md-6 col-xs-12">
|
||||
<div class="single-intro-text mb-70">
|
||||
<i class="{{this.icon}}"></i>
|
||||
<h3>{{this.title}}</h3>
|
||||
<p>{{this.description}}</p>
|
||||
<span class="count-number">0{{@index}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.event_stats}}
|
||||
<section class="counter-section section-padding" data-site-component="event_stats">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{{#each sections.event_stats.items}}
|
||||
<div class="col-lg-3 col-md-6 col-xs-12 work-counter-widget">
|
||||
<div class="counter">
|
||||
<div class="icon"><i class="{{this.icon}}"></i></div>
|
||||
<div class="counter-content">
|
||||
<div class="counterUp">{{this.value}}</div>
|
||||
<p>{{this.label}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.speakers}}
|
||||
<section id="team" class="section-padding text-center speakers" data-site-component="speakers">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title wow fadeInUp">{{sections.speakers.heading}}</h2>
|
||||
{{#if sections.speakers.subtitle}}<p class="wow fadeInDown">{{sections.speakers.subtitle}}</p>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row speakers-area wow fadeInDown">
|
||||
{{#each sections.speakers.items}}
|
||||
<div class="col-lg-3 col-md-6 col-xs-12 mb-4">
|
||||
<div class="speaker-card">
|
||||
{{#if this.image}}<img src="{{resolveAsset @root.assetsBaseUrl this.image}}" alt="{{this.name}}" class="img-fluid rounded-circle mb-3" style="width:140px;height:140px;object-fit:cover">{{/if}}
|
||||
<h4>{{this.name}}</h4>
|
||||
<p class="text-muted">{{this.role}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.event_schedule}}
|
||||
<section id="schedules" class="schedule section-padding" data-site-component="event_schedule">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title wow fadeInUp">{{sections.event_schedule.heading}}</h2>
|
||||
{{#if sections.event_schedule.subtitle}}<p class="wow fadeInDown">{{sections.event_schedule.subtitle}}</p>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#each sections.event_schedule.days}}
|
||||
<div class="mb-5">
|
||||
<h4 class="text-center mb-3">{{this.label}}{{#if this.date}} — {{this.date}}{{/if}}</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
{{#each this.sessions}}
|
||||
<tr>
|
||||
<td style="width:28%">{{this.time}}</td>
|
||||
<td style="width:18%">{{this.type}}</td>
|
||||
<td>{{this.title}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.about_crest}}
|
||||
<section id="about-crest" class="section-padding" data-site-component="about_crest">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<h2>{{sections.about_crest.heading}}</h2>
|
||||
<div>{{{sections.about_crest.content}}}</div>
|
||||
{{#if sections.about_crest.cta.text}}
|
||||
<p class="mt-3"><a href="{{sections.about_crest.cta.url}}" class="btn btn-common">{{sections.about_crest.cta.text}}</a></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.sponsors}}
|
||||
<section id="sponsors" class="section-padding" data-site-component="sponsors">
|
||||
<div class="container">
|
||||
{{#unless sections.sponsors.hideHeading}}
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title">{{sections.sponsors.heading}}</h2>
|
||||
{{#if sections.sponsors.subtitle}}<p>{{sections.sponsors.subtitle}}</p>{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="sponsors-groups text-center">
|
||||
{{#each sections.sponsors.groups}}
|
||||
<div class="mb-4">
|
||||
{{#unless ../sections.sponsors.hideGroupTitles}}<h5 class="mb-3">{{this.title}}</h5>{{/unless}}
|
||||
<div class="row justify-content-center align-items-center">
|
||||
{{#each this.items}}
|
||||
<div class="col-6 col-md-3 col-lg-2 mb-3">
|
||||
<img src="{{resolveAsset @root.assetsBaseUrl this.image}}" alt="{{this.alt}}" class="img-fluid">
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.contact}}
|
||||
<section id="contact-map" class="section-padding" data-site-component="contact">
|
||||
<div class="container">
|
||||
<div class="section-title-header text-center">
|
||||
<h2 class="section-title wow fadeInUp">{{sections.contact.heading}}</h2>
|
||||
{{#if sections.contact.subtitle}}<p class="wow fadeInDown">{{sections.contact.subtitle}}</p>{{/if}}
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-4">
|
||||
{{#if sections.contact.phones}}
|
||||
<ul class="list-unstyled">
|
||||
{{#each sections.contact.phones}}
|
||||
<li class="mb-2"><strong>{{this.label}}:</strong> <a href="tel:{{this.tel}}">{{this.number}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{#if sections.contact.address}}<p>{{sections.contact.address}}</p>{{/if}}
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<form role="form" method="post" id="contactForm" name="contact-form" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 form-line">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 form-line">
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control" name="email" placeholder="Email" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 form-line">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="subject" placeholder="Subject">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" rows="4" name="message" placeholder="Message"></textarea>
|
||||
</div>
|
||||
<div class="form-group contact-captcha-group">
|
||||
<label class="contact-captcha-label" for="contactCaptcha">Security check</label>
|
||||
<div class="contact-captcha-row">
|
||||
<span class="contact-captcha-question" data-contact-captcha-question aria-live="polite">Loading…</span>
|
||||
<input type="text" class="form-control contact-captcha-input" id="contactCaptcha" data-contact-captcha-answer inputmode="numeric" autocomplete="off" placeholder="Answer" required>
|
||||
<button type="button" class="btn btn-common contact-captcha-reload" data-contact-captcha-reload aria-label="Reload captcha">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eb-contact-honeypot" aria-hidden="true">
|
||||
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-submit">
|
||||
<button type="submit" class="btn btn-common" id="form-submit">Send Message</button>
|
||||
<div id="msgSubmit" class="h3 text-center hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.map}}
|
||||
<section id="google-map-area" data-site-component="map">
|
||||
<div class="container-fluid">
|
||||
<iframe title="Event location map" style="border:0; height: {{sections.map.height}}px; width: 100%;" src="{{sections.map.embedUrl}}" allowfullscreen loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
{{/if_show}}
|
||||
|
||||
{{#if_show sections.footer}}
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="footer-otherinfo row align-items-start">
|
||||
<div class="col-lg-4 footer-col-brand">
|
||||
{{#if sections.logo.footer.url}}
|
||||
<div class="footer-logo mb-3">
|
||||
<img src="{{resolveAsset assetsBaseUrl sections.logo.footer.url}}" width="220" class="img-fluid" alt="{{sections.logo.footer.alt}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if sections.footer.tagline}}<p class="footer-tagline">{{sections.footer.tagline}}</p>{{/if}}
|
||||
</div>
|
||||
<div class="col-lg-4 footer-col-links">
|
||||
<h6 class="footer-section-heading mb-4">{{sections.footer.linksHeading}}</h6>
|
||||
<ul class="footer-link-list list-unstyled">
|
||||
{{#each sections.footer.links}}
|
||||
<li><a href="{{this.url}}" data-footer-link{{#if (eq this.target "_blank")}} target="_blank" rel="noopener noreferrer"{{/if}}>{{this.text}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-4 footer-col-social">
|
||||
<h6 class="footer-section-heading mb-4">{{sections.footer.social.heading}}</h6>
|
||||
<ul class="footer-social-list list-unstyled d-flex gap-3">
|
||||
{{#each sections.footer.social.links}}
|
||||
<li><a href="{{this.url}}" aria-label="{{this.label}}"><i class="{{this.icon}}"></i></a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-info">
|
||||
<p>Powered by <a href="https://eventbuzz.live" rel="nofollow" target="_blank">EventBuzz.live</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{{/if_show}}
|
||||
|
||||
<a href="#" class="back-to-top"><i class="lni-chevron-up"></i></a>
|
||||
|
||||
<div id="preloader">
|
||||
<div class="sk-circle">
|
||||
<div class="sk-circle1 sk-child"></div><div class="sk-circle2 sk-child"></div><div class="sk-circle3 sk-child"></div>
|
||||
<div class="sk-circle4 sk-child"></div><div class="sk-circle5 sk-child"></div><div class="sk-circle6 sk-child"></div>
|
||||
<div class="sk-circle7 sk-child"></div><div class="sk-circle8 sk-child"></div><div class="sk-circle9 sk-child"></div>
|
||||
<div class="sk-circle10 sk-child"></div><div class="sk-circle11 sk-child"></div><div class="sk-circle12 sk-child"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/jquery-min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/popper.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/bootstrap.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/jquery.countdown.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/waypoints.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/jquery.counterup.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/jquery.nav.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/jquery.easing.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/wow.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/nivo-lightbox.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/video.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/registration-modal.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/feedback-fab.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/contact-form.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/marketing-popup.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/main.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/form-validator.min.js'}}"></script>
|
||||
<script src="{{assetUrl assetsBaseUrl 'assets/js/eventup-ssr-bridge.js'}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
{
|
||||
"version": 1,
|
||||
"meta": {
|
||||
"title": "EventUp Conference",
|
||||
"eventId": ""
|
||||
},
|
||||
"theme": {
|
||||
"fields": [
|
||||
{ "key": "accent", "type": "color", "label": "Accent color", "default": "#189A95" },
|
||||
{ "key": "headingColor", "type": "color", "label": "Heading color", "default": "#015AAA" }
|
||||
]
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"id": "marketing_popup",
|
||||
"label": "Marketing popup",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Introducing" },
|
||||
{ "key": "logo", "type": "image", "label": "Popup logo" },
|
||||
{
|
||||
"key": "autoCloseDuration",
|
||||
"type": "text",
|
||||
"label": "Auto-close after (ms)",
|
||||
"default": "3000"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "logo",
|
||||
"label": "Logos",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "header.url", "type": "image", "label": "Header logo" },
|
||||
{ "key": "header.alt", "type": "text", "label": "Header logo alt", "default": "Event logo" },
|
||||
{ "key": "header.maxHeight", "type": "text", "label": "Header logo max height (px)", "default": "56" },
|
||||
{ "key": "hero.url", "type": "image", "label": "Hero logo" },
|
||||
{ "key": "hero.alt", "type": "text", "label": "Hero logo alt", "default": "" },
|
||||
{ "key": "footer.url", "type": "image", "label": "Footer logo" },
|
||||
{ "key": "footer.alt", "type": "text", "label": "Footer logo alt", "default": "" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hero",
|
||||
"label": "Hero",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "event_name", "type": "text", "label": "Event name", "default": "Your Event Name" },
|
||||
{ "key": "date_venue", "type": "text", "label": "Date & venue", "default": "Date and location" },
|
||||
{ "key": "image.url", "type": "image", "label": "Hero background image" },
|
||||
{ "key": "cta.text", "type": "text", "label": "CTA label", "default": "Register Now" },
|
||||
{ "key": "cta.url", "type": "url", "label": "CTA URL", "default": "#" },
|
||||
{
|
||||
"key": "cta.target",
|
||||
"type": "select",
|
||||
"label": "CTA target",
|
||||
"default": "_blank",
|
||||
"options": [
|
||||
{ "value": "_blank", "label": "New tab" },
|
||||
{ "value": "modal", "label": "Modal (iframe)" },
|
||||
{ "value": "_self", "label": "Same tab" }
|
||||
]
|
||||
},
|
||||
{ "key": "countdown.show", "type": "boolean", "label": "Show countdown", "default": true },
|
||||
{ "key": "countdown.date", "type": "text", "label": "Countdown date (YYYY-MM-DD)", "default": "" },
|
||||
{ "key": "countdown.time", "type": "text", "label": "Countdown time (HH:mm)", "default": "12:00" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "about",
|
||||
"label": "About the event",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "About the Event" },
|
||||
{ "key": "content", "type": "richtext", "label": "Content", "default": "<p>Describe your event here.</p>" },
|
||||
{ "key": "youtubeUrl", "type": "url", "label": "YouTube embed URL (optional)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "video",
|
||||
"label": "Expert videos",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Hear it from experts" },
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Videos",
|
||||
"itemFields": [
|
||||
{ "key": "url", "type": "url", "label": "Video URL" },
|
||||
{ "key": "title", "type": "text", "label": "Title" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "informationBar",
|
||||
"label": "Information bar",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Info items",
|
||||
"itemFields": [
|
||||
{ "key": "icon", "type": "text", "label": "Icon class (LineIcons)", "default": "lni-map-marker" },
|
||||
{ "key": "label", "type": "text", "label": "Label" },
|
||||
{ "key": "value", "type": "text", "label": "Value" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event_highlights",
|
||||
"label": "Why join (highlights)",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Why You Should Join?" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" },
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Highlight cards",
|
||||
"itemFields": [
|
||||
{ "key": "icon", "type": "text", "label": "Icon class", "default": "lni-microphone" },
|
||||
{ "key": "title", "type": "text", "label": "Title" },
|
||||
{ "key": "description", "type": "textarea", "label": "Description" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event_stats",
|
||||
"label": "Stats counters",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Stats",
|
||||
"itemFields": [
|
||||
{ "key": "icon", "type": "text", "label": "Icon class", "default": "lni-mic" },
|
||||
{ "key": "value", "type": "text", "label": "Value" },
|
||||
{ "key": "label", "type": "text", "label": "Label" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "speakers",
|
||||
"label": "Speakers",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Our Speakers" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" },
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Speakers",
|
||||
"itemFields": [
|
||||
{ "key": "name", "type": "text", "label": "Name" },
|
||||
{ "key": "role", "type": "text", "label": "Role" },
|
||||
{ "key": "image", "type": "image", "label": "Photo" },
|
||||
{ "key": "url", "type": "url", "label": "Profile URL", "default": "#" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "event_schedule",
|
||||
"label": "Schedule",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Event Schedule" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" },
|
||||
{
|
||||
"key": "days",
|
||||
"type": "array",
|
||||
"label": "Days",
|
||||
"itemFields": [
|
||||
{ "key": "label", "type": "text", "label": "Day label", "default": "Day 01" },
|
||||
{ "key": "date", "type": "text", "label": "Date line", "default": "" },
|
||||
{
|
||||
"key": "sessions",
|
||||
"type": "array",
|
||||
"label": "Sessions",
|
||||
"itemFields": [
|
||||
{ "key": "time", "type": "text", "label": "Time" },
|
||||
{ "key": "type", "type": "text", "label": "Type", "default": "Main programme" },
|
||||
{ "key": "title", "type": "text", "label": "Title" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "about_crest",
|
||||
"label": "About CREST / secondary about",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "About" },
|
||||
{ "key": "content", "type": "richtext", "label": "Content", "default": "" },
|
||||
{ "key": "cta.text", "type": "text", "label": "CTA text", "default": "" },
|
||||
{ "key": "cta.url", "type": "url", "label": "CTA URL", "default": "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "latest_news",
|
||||
"label": "Latest news",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Latest News" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sponsors",
|
||||
"label": "Sponsors",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading (optional)", "default": "Sponsors" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" },
|
||||
{ "key": "hideHeading", "type": "boolean", "label": "Hide section heading", "default": false },
|
||||
{ "key": "hideGroupTitles", "type": "boolean", "label": "Hide sponsor tier titles", "default": false },
|
||||
{
|
||||
"key": "groups",
|
||||
"type": "array",
|
||||
"label": "Sponsor groups",
|
||||
"itemFields": [
|
||||
{ "key": "title", "type": "text", "label": "Tier title" },
|
||||
{ "key": "tier", "type": "text", "label": "Tier id", "default": "other" },
|
||||
{
|
||||
"key": "items",
|
||||
"type": "array",
|
||||
"label": "Logos",
|
||||
"itemFields": [
|
||||
{ "key": "image", "type": "image", "label": "Logo image" },
|
||||
{ "key": "alt", "type": "text", "label": "Alt text" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "map",
|
||||
"label": "Map",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "embedUrl", "type": "url", "label": "Google Maps embed URL" },
|
||||
{ "key": "height", "type": "text", "label": "Iframe height (px)", "default": "450" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "contact",
|
||||
"label": "Contact",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "heading", "type": "text", "label": "Heading", "default": "Contact Us" },
|
||||
{ "key": "subtitle", "type": "textarea", "label": "Subtitle", "default": "" },
|
||||
{ "key": "formId", "type": "text", "label": "EventBuzz form ID" },
|
||||
{ "key": "apiBaseUrl", "type": "url", "label": "API base URL", "default": "https://api.eventbuzz.live/api" },
|
||||
{ "key": "address", "type": "textarea", "label": "Address", "default": "" },
|
||||
{
|
||||
"key": "phones",
|
||||
"type": "array",
|
||||
"label": "Phone numbers",
|
||||
"itemFields": [
|
||||
{ "key": "label", "type": "text", "label": "Label" },
|
||||
{ "key": "number", "type": "text", "label": "Display number" },
|
||||
{ "key": "tel", "type": "text", "label": "tel: href value" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "feedback",
|
||||
"label": "Feedback FAB",
|
||||
"show": false,
|
||||
"fields": [
|
||||
{ "key": "label", "type": "text", "label": "Button label", "default": "Feedback" },
|
||||
{ "key": "url", "type": "url", "label": "Feedback form URL" },
|
||||
{
|
||||
"key": "target",
|
||||
"type": "select",
|
||||
"label": "Open in",
|
||||
"default": "_blank",
|
||||
"options": [
|
||||
{ "value": "_blank", "label": "New tab" },
|
||||
{ "value": "modal", "label": "Modal" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footer",
|
||||
"label": "Footer",
|
||||
"show": true,
|
||||
"fields": [
|
||||
{ "key": "tagline", "type": "text", "label": "Tagline", "default": "" },
|
||||
{ "key": "linksHeading", "type": "text", "label": "Links heading", "default": "Quick Links" },
|
||||
{ "key": "social.heading", "type": "text", "label": "Social heading", "default": "Follow us" },
|
||||
{
|
||||
"key": "links",
|
||||
"type": "array",
|
||||
"label": "Footer links",
|
||||
"itemFields": [
|
||||
{ "key": "text", "type": "text", "label": "Text" },
|
||||
{ "key": "url", "type": "url", "label": "URL" },
|
||||
{
|
||||
"key": "target",
|
||||
"type": "select",
|
||||
"label": "Target",
|
||||
"default": "_self",
|
||||
"options": [
|
||||
{ "value": "modal", "label": "Modal" },
|
||||
{ "value": "_blank", "label": "New tab" },
|
||||
{ "value": "_self", "label": "Same tab" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "social.links",
|
||||
"type": "array",
|
||||
"label": "Social links",
|
||||
"itemFields": [
|
||||
{ "key": "url", "type": "url", "label": "URL" },
|
||||
{ "key": "icon", "type": "text", "label": "Icon class", "default": "lni-facebook-filled" },
|
||||
{ "key": "label", "type": "text", "label": "Aria label" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,13 +13,15 @@
|
|||
"@aws-sdk/lib-storage": "^3.1012.0",
|
||||
"@hapi/hapi": "^21.3.0",
|
||||
"@napi-rs/canvas": "^0.1.56",
|
||||
"adm-zip": "^0.5.18",
|
||||
"archiver": "^7.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.34.5",
|
||||
"cheerio": "^1.0.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"firebase-admin": "^13.0.0",
|
||||
"google-auth-library": "^9.14.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"hapi-auth-jwt2": "^10.2.0",
|
||||
"ioredis": "^5.4.2",
|
||||
"joi": "^17.13.0",
|
||||
|
|
@ -41,6 +43,9 @@
|
|||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
|
|
@ -3033,26 +3038,6 @@
|
|||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
|
||||
|
|
@ -4600,12 +4585,6 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
|
|
@ -4618,11 +4597,21 @@
|
|||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
|
||||
"integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
|
|
@ -4684,12 +4673,6 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
|
||||
|
|
@ -4860,20 +4843,6 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
|
|
@ -5205,17 +5174,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"node-addon-api": "^5.0.0"
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
|
|
@ -5256,6 +5225,7 @@
|
|||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
|
@ -5578,15 +5548,6 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
|
|
@ -5704,15 +5665,6 @@
|
|||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
|
|
@ -5829,6 +5781,7 @@
|
|||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
|
|
@ -5846,12 +5799,6 @@
|
|||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
|
@ -6101,12 +6048,6 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
|
|
@ -6901,40 +6842,11 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
|
|
@ -6968,27 +6880,6 @@
|
|||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||
|
|
@ -7124,6 +7015,7 @@
|
|||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
|
|
@ -7271,6 +7163,27 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hapi-auth-jwt2": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/hapi-auth-jwt2/-/hapi-auth-jwt2-10.7.0.tgz",
|
||||
|
|
@ -7322,12 +7235,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
|
|
@ -7421,6 +7328,7 @@
|
|||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
|
|
@ -7513,6 +7421,7 @@
|
|||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
|
|
@ -8754,30 +8663,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||
|
|
@ -8914,6 +8799,7 @@
|
|||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
|
|
@ -8922,6 +8808,15 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
|
|
@ -8931,49 +8826,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
|
||||
|
|
@ -9117,6 +8969,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
|
|
@ -9124,10 +8982,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz",
|
||||
"integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "3.0.3",
|
||||
|
|
@ -9227,6 +9088,17 @@
|
|||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
|
|
@ -9356,21 +9228,6 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
|
@ -9393,19 +9250,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
|
|
@ -9418,15 +9262,6 @@
|
|||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
|
|
@ -9471,6 +9306,7 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
|
|
@ -9672,6 +9508,7 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -10312,22 +10149,6 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -10556,6 +10377,7 @@
|
|||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
|
|
@ -10607,7 +10429,6 @@
|
|||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -10916,24 +10737,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
||||
|
|
@ -10946,12 +10749,6 @@
|
|||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/teeny-request": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
|
||||
|
|
@ -11148,6 +10945,19 @@
|
|||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
|
|
@ -11342,15 +11152,6 @@
|
|||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
|
|
@ -11387,6 +11188,12 @@
|
|||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
|
@ -11427,6 +11234,7 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
"version": "1.0.0",
|
||||
"description": "EventBuzz Node.js backend with Hapi and MongoDB",
|
||||
"main": "server.js",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js",
|
||||
|
|
@ -11,6 +14,8 @@
|
|||
"backfill:session-join-codes": "node scripts/backfill-session-join-codes.js",
|
||||
"backfill:join-codes": "node scripts/backfill-join-codes.js",
|
||||
"backfill:tenant-organization": "node scripts/backfill-tenant-organization.js",
|
||||
"backfill:form-submission-files": "node scripts/backfill-form-submission-files.js",
|
||||
"seed:microsite-v2-template": "node scripts/seed-microsite-v2-template.js",
|
||||
"email-worker": "node src/workers/emailQueueWorker.js",
|
||||
"whatsapp-worker": "node src/workers/whatsappQueueWorker.js",
|
||||
"concierge-ingest-worker": "node ../chatbot/workers/ingest.js",
|
||||
|
|
@ -23,13 +28,15 @@
|
|||
"@aws-sdk/lib-storage": "^3.1012.0",
|
||||
"@hapi/hapi": "^21.3.0",
|
||||
"@napi-rs/canvas": "^0.1.56",
|
||||
"adm-zip": "^0.5.18",
|
||||
"archiver": "^7.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.34.5",
|
||||
"cheerio": "^1.0.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"firebase-admin": "^13.0.0",
|
||||
"google-auth-library": "^9.14.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"hapi-auth-jwt2": "^10.2.0",
|
||||
"ioredis": "^5.4.2",
|
||||
"joi": "^17.13.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Backfill: upload base64 data-URL file values in FormSubmission.data to S3
|
||||
* and replace with CloudFront URLs.
|
||||
*
|
||||
* Run:
|
||||
* cd backend && npm run backfill:form-submission-files
|
||||
* cd backend && npm run backfill:form-submission-files -- --dry-run
|
||||
* cd backend && npm run backfill:form-submission-files -- --form-id=<mongoId>
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const Form = require(path.join(__dirname, '..', 'src', 'models', 'Form'));
|
||||
const FormSubmission = require(path.join(__dirname, '..', 'src', 'models', 'FormSubmission'));
|
||||
const {
|
||||
processAllSubmissionDataUrls,
|
||||
hasDataUrlValue,
|
||||
dataMapToObject,
|
||||
} = require(path.join(__dirname, '..', 'src', 'utils', 'formSubmissionFiles'));
|
||||
|
||||
function parseArgs(argv) {
|
||||
const dryRun = argv.includes('--dry-run');
|
||||
const formIdArg = argv.find((a) => a.startsWith('--form-id='));
|
||||
const formId = formIdArg ? formIdArg.split('=')[1]?.trim() : '';
|
||||
const limitArg = argv.find((a) => a.startsWith('--limit='));
|
||||
const limit = limitArg ? Math.max(1, parseInt(limitArg.split('=')[1], 10) || 0) : 0;
|
||||
return { dryRun, formId, limit };
|
||||
}
|
||||
|
||||
function dataChanged(before, after) {
|
||||
return JSON.stringify(before) !== JSON.stringify(after);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const { dryRun, formId, limit } = parseArgs(process.argv.slice(2));
|
||||
const uri = process.env.MONGODB_URI;
|
||||
if (!uri) {
|
||||
console.error('MONGODB_URI is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.AWS_BUCKET || !process.env.AWS_URL) {
|
||||
console.error('AWS_BUCKET and AWS_URL must be set for S3 upload');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await mongoose.connect(uri);
|
||||
console.log(`Connected. dryRun=${dryRun}${formId ? ` formId=${formId}` : ''}${limit ? ` limit=${limit}` : ''}`);
|
||||
|
||||
const query = {};
|
||||
if (formId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(formId)) {
|
||||
console.error('Invalid --form-id');
|
||||
process.exit(1);
|
||||
}
|
||||
query.form = formId;
|
||||
}
|
||||
|
||||
let cursor = FormSubmission.find(query).sort({ submittedAt: 1 }).cursor();
|
||||
const formCache = new Map();
|
||||
|
||||
let scanned = 0;
|
||||
let candidates = 0;
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
|
||||
for await (const submission of cursor) {
|
||||
if (limit && scanned >= limit) break;
|
||||
scanned += 1;
|
||||
|
||||
if (!hasDataUrlValue(submission.data)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates += 1;
|
||||
const formKey = String(submission.form);
|
||||
let form = formCache.get(formKey);
|
||||
if (form === undefined) {
|
||||
form = submission.form ? await Form.findById(submission.form).lean() : null;
|
||||
formCache.set(formKey, form || null);
|
||||
}
|
||||
|
||||
const before = dataMapToObject(submission.data);
|
||||
const orgId = String(submission.tenantOrganization || form?.tenantOrganization || 'org');
|
||||
const eventId = submission.event ? String(submission.event) : '';
|
||||
|
||||
try {
|
||||
const after = await processAllSubmissionDataUrls(before, { orgId, eventId, form });
|
||||
if (!dataChanged(before, after)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
const keys = Object.keys(before).filter((k) => before[k] !== after[k]);
|
||||
console.log(`[dry-run] submission ${submission._id} would update keys: ${keys.join(', ')}`);
|
||||
updated += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
submission.data = after;
|
||||
await submission.save();
|
||||
updated += 1;
|
||||
console.log(`Updated submission ${submission._id}`);
|
||||
} catch (err) {
|
||||
failed += 1;
|
||||
console.error(`Failed submission ${submission._id}:`, err.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Done. scanned=${scanned} candidates=${candidates} updated=${updated} skipped=${skipped} failed=${failed}`,
|
||||
);
|
||||
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Seed the EventUp microsite-v2 template: upload files to S3 and create MicrositeV2Template row.
|
||||
*
|
||||
* Usage: node scripts/seed-microsite-v2-template.js [--user-id=<mongoId>]
|
||||
*
|
||||
* Requires: MONGODB_URI, AWS_* env vars (or will create DB row only with --db-only)
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mongoose = require('mongoose');
|
||||
const AdmZip = require('adm-zip');
|
||||
const { connectDB } = require('../src/config/database');
|
||||
const MicrositeV2Template = require('../src/models/MicrositeV2Template');
|
||||
const User = require('../src/models/User');
|
||||
const {
|
||||
uploadTemplateFile,
|
||||
uploadTemplateAsset,
|
||||
templateAssetsBaseUrl,
|
||||
} = require('../src/utils/s3MicrositeV2');
|
||||
const { parseTemplateSchema } = require('../src/utils/templateSchema');
|
||||
|
||||
const SLUG = 'event-up';
|
||||
const TEMPLATE_DIR = path.join(__dirname, '..', 'microsite-v2-templates', SLUG);
|
||||
const TEMPLATE_ASSETS = path.join(TEMPLATE_DIR, 'assets');
|
||||
const EVENT_UP_ASSETS = path.join(__dirname, '..', '..', 'microsites', 'event-up', 'assets');
|
||||
|
||||
async function findSeedUser(userIdArg) {
|
||||
if (userIdArg) {
|
||||
const u = await User.findById(userIdArg);
|
||||
if (u) return u;
|
||||
}
|
||||
const superAdmin = await User.findOne({ role: 'super_admin' }).sort({ createdAt: 1 });
|
||||
if (superAdmin) return superAdmin;
|
||||
throw new Error('No super_admin user found. Pass --user-id=<mongoId>');
|
||||
}
|
||||
|
||||
function walkDir(dir, base = dir) {
|
||||
const files = [];
|
||||
if (!fs.existsSync(dir)) return files;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...walkDir(full, base));
|
||||
} else {
|
||||
files.push({ full, rel: path.relative(base, full).replace(/\\/g, '/') });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function mimeFor(filePath) {
|
||||
if (filePath.endsWith('.css')) return 'text/css';
|
||||
if (filePath.endsWith('.js')) return 'application/javascript';
|
||||
if (filePath.endsWith('.woff') || filePath.endsWith('.woff2')) return 'font/woff2';
|
||||
if (filePath.endsWith('.png')) return 'image/png';
|
||||
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) return 'image/jpeg';
|
||||
if (filePath.endsWith('.svg')) return 'image/svg+xml';
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dbOnly = process.argv.includes('--db-only');
|
||||
const userIdArg = (process.argv.find((a) => a.startsWith('--user-id=')) || '').split('=')[1];
|
||||
|
||||
await connectDB();
|
||||
|
||||
const user = await findSeedUser(userIdArg);
|
||||
console.log(`Using user: ${user.email || user._id}`);
|
||||
|
||||
const htmlPath = path.join(TEMPLATE_DIR, 'template.html');
|
||||
const jsonPath = path.join(TEMPLATE_DIR, 'template.json');
|
||||
if (!fs.existsSync(htmlPath) || !fs.existsSync(jsonPath)) {
|
||||
throw new Error(`Missing template files in ${TEMPLATE_DIR}`);
|
||||
}
|
||||
|
||||
const htmlBuf = fs.readFileSync(htmlPath);
|
||||
const jsonBuf = fs.readFileSync(jsonPath);
|
||||
const schema = parseTemplateSchema(jsonBuf);
|
||||
|
||||
let htmlS3Key;
|
||||
let schemaS3Key;
|
||||
let thumbnailUrl;
|
||||
let assetsBaseUrl = templateAssetsBaseUrl(SLUG);
|
||||
|
||||
if (!dbOnly && process.env.AWS_BUCKET) {
|
||||
console.log('Uploading template.html...');
|
||||
const htmlUp = await uploadTemplateFile(SLUG, 'template.html', htmlBuf, 'text/html; charset=utf-8');
|
||||
htmlS3Key = htmlUp.s3Key;
|
||||
|
||||
console.log('Uploading template.json...');
|
||||
const jsonUp = await uploadTemplateFile(SLUG, 'template.json', jsonBuf, 'application/json');
|
||||
schemaS3Key = jsonUp.s3Key;
|
||||
|
||||
const thumbCandidates = [
|
||||
path.join(TEMPLATE_DIR, 'thumbnail.jpg'),
|
||||
path.join(TEMPLATE_DIR, 'thumbnail.png'),
|
||||
path.join(EVENT_UP_ASSETS, 'img', 'hero.jpg'),
|
||||
];
|
||||
for (const tp of thumbCandidates) {
|
||||
if (fs.existsSync(tp)) {
|
||||
const ext = path.extname(tp).slice(1) || 'jpg';
|
||||
const thumbUp = await uploadTemplateFile(
|
||||
SLUG,
|
||||
`thumbnail.${ext}`,
|
||||
fs.readFileSync(tp),
|
||||
`image/${ext === 'jpg' ? 'jpeg' : ext}`,
|
||||
);
|
||||
thumbnailUrl = thumbUp.url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!thumbnailUrl) {
|
||||
const placeholder = Buffer.from(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240"><rect fill="#5c6ac4" width="400" height="240"/><text x="50%" y="50%" fill="#fff" text-anchor="middle" dy=".3em" font-family="sans-serif" font-size="24">EventUp</text></svg>',
|
||||
);
|
||||
const thumbUp = await uploadTemplateFile(SLUG, 'thumbnail.svg', placeholder, 'image/svg+xml');
|
||||
thumbnailUrl = thumbUp.url;
|
||||
}
|
||||
|
||||
if (fs.existsSync(EVENT_UP_ASSETS)) {
|
||||
console.log('Uploading assets from event-up...');
|
||||
const assetFiles = walkDir(EVENT_UP_ASSETS);
|
||||
for (const { full, rel } of assetFiles) {
|
||||
await uploadTemplateAsset(SLUG, rel, fs.readFileSync(full), mimeFor(rel));
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(TEMPLATE_ASSETS)) {
|
||||
console.log('Uploading template-specific assets (SSR bridge)...');
|
||||
const overlayFiles = walkDir(TEMPLATE_ASSETS);
|
||||
for (const { full, rel } of overlayFiles) {
|
||||
await uploadTemplateAsset(SLUG, rel, fs.readFileSync(full), mimeFor(rel));
|
||||
}
|
||||
}
|
||||
assetsBaseUrl = templateAssetsBaseUrl(SLUG);
|
||||
console.log('S3 upload complete.');
|
||||
} else {
|
||||
console.log('Skipping S3 upload (--db-only or AWS_BUCKET not set).');
|
||||
}
|
||||
|
||||
const existing = await MicrositeV2Template.findOne({ slug: SLUG });
|
||||
const doc = {
|
||||
slug: SLUG,
|
||||
name: 'EventUp Conference',
|
||||
description: 'Full EventUp conference microsite ported from microsites/event-up — hero, schedule, speakers, sponsors, contact, and more.',
|
||||
thumbnailUrl: thumbnailUrl || '',
|
||||
htmlS3Key: htmlS3Key || '',
|
||||
schemaS3Key: schemaS3Key || '',
|
||||
schema,
|
||||
assetsBaseUrl,
|
||||
assetsS3Prefix: `templates/${SLUG}/assets`,
|
||||
isActive: Boolean(htmlS3Key && schemaS3Key),
|
||||
version: 1,
|
||||
createdBy: user._id,
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
Object.assign(existing, doc);
|
||||
await existing.save();
|
||||
console.log(`Updated MicrositeV2Template: ${existing._id}`);
|
||||
} else {
|
||||
const created = await MicrositeV2Template.create(doc);
|
||||
console.log(`Created MicrositeV2Template: ${created._id}`);
|
||||
}
|
||||
|
||||
await mongoose.disconnect();
|
||||
console.log('Done.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -42,6 +42,7 @@ const eventTicketingManualRoutes = require('./routes/eventTicketingManual.routes
|
|||
const conciergeBotRoutes = require('./routes/conciergeBot.routes');
|
||||
const publicConciergeRoutes = require('./routes/publicConcierge.routes');
|
||||
const publicQuizPollRoutes = require('./routes/publicQuizPoll.routes');
|
||||
const staticMicrositeRoutes = require('./routes/staticMicrosite.routes');
|
||||
|
||||
const createServer = async () => {
|
||||
/** Production: set CORS_ORIGIN (or FRONTEND_URL) to every origin that loads the SPA, e.g. https://console.example.com (no trailing slash). */
|
||||
|
|
@ -160,6 +161,7 @@ const createServer = async () => {
|
|||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.header('Access-Control-Expose-Headers', 'Content-Disposition');
|
||||
res.header('Access-Control-Max-Age', '86400');
|
||||
}
|
||||
return res;
|
||||
|
|
@ -275,6 +277,7 @@ const createServer = async () => {
|
|||
server.route(eventStaffRoutes);
|
||||
server.route(websiteSettingsRoutes);
|
||||
server.route(eventMicrositeRoutes);
|
||||
server.route(staticMicrositeRoutes);
|
||||
server.route(whatsappTemplateRoutes);
|
||||
server.route(badgeBatchRoutes);
|
||||
server.route(eventTicketRoutes);
|
||||
|
|
@ -309,6 +312,7 @@ const createServer = async () => {
|
|||
if (allowOrigin && typeof response.header === 'function') {
|
||||
response.header('Access-Control-Allow-Origin', allowOrigin);
|
||||
response.header('Access-Control-Allow-Credentials', 'true');
|
||||
response.header('Access-Control-Expose-Headers', 'Content-Disposition');
|
||||
}
|
||||
return h.continue;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const PARTICIPANT_STATUS = ['pending', 'confirmed', 'checked_in', 'cancelled'];
|
|||
const SESSION_TYPES = ['keynote', 'workshop', 'panel', 'breakout', 'networking'];
|
||||
const SESSION_STATUS = ['scheduled', 'ongoing', 'completed', 'cancelled'];
|
||||
const FORM_TYPES = ['registration', 'feedback', 'survey', 'custom', 'other', 'web-check-in'];
|
||||
const FIELD_TYPES = ['text', 'textarea', 'email', 'phone', 'select', 'stars', 'radio', 'checkbox', 'checkboxes', 'file'];
|
||||
const FIELD_TYPES = ['text', 'textarea', 'email', 'phone', 'select', 'stars', 'radio', 'checkbox', 'checkboxes', 'file', 'date', 'time', 'section', 'country_state'];
|
||||
/** Registration field → Participant property mapping (or `custom` + customMapsKey). */
|
||||
const FORM_FIELD_MAPS_TO = [
|
||||
'firstName',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ const {
|
|||
} = require('../utils/formPublicHelpers');
|
||||
const { verifyCaptchaChallenge } = require('../utils/publicCaptcha');
|
||||
const { getMembership } = require('../utils/participantMembership');
|
||||
const { publicRegistrationFields } = require('../utils/registrationFormFields');
|
||||
const { processFormSubmissionFileUploads } = require('../utils/formSubmissionFiles');
|
||||
|
||||
function throwRegError(message, code) {
|
||||
const err = new Error(message);
|
||||
|
|
@ -82,6 +84,26 @@ function normalizeSubmitData(raw) {
|
|||
return { ...raw };
|
||||
}
|
||||
|
||||
function fieldDataKey(f) {
|
||||
return String(f?.name || f?.label || '').trim();
|
||||
}
|
||||
|
||||
function csvColumnsFromFormFields(fields) {
|
||||
const cols = [];
|
||||
for (const f of fields || []) {
|
||||
if (f.type === 'section' || !(f.label || f.name)) continue;
|
||||
const base = fieldDataKey(f);
|
||||
if (f.type === 'country_state') {
|
||||
const label = f.label || f.name;
|
||||
cols.push({ header: `${label} - Country`, key: `${base} - Country` });
|
||||
cols.push({ header: `${label} - State`, key: `${base} - State` });
|
||||
} else {
|
||||
cols.push({ header: f.label || f.name, key: base });
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
async function upsertParticipantFromRegistrationForm(form, tenantOrganization, dataObj) {
|
||||
const fields = form.fields || [];
|
||||
const orgOid =
|
||||
|
|
@ -98,7 +120,7 @@ async function upsertParticipantFromRegistrationForm(form, tenantOrganization, d
|
|||
const requireMappedValue = (mapsTo, emptyMessage, missingConfigMessage) => {
|
||||
const fld = fields.find((f) => f.mapsTo === mapsTo);
|
||||
if (!fld?.label) throwRegValidation(missingConfigMessage, 'FORM_FIELD_CONFIG');
|
||||
const raw = dataObj[fld.label.trim()];
|
||||
const raw = dataObj[fieldDataKey(fld)];
|
||||
const val = raw != null ? String(raw).trim() : '';
|
||||
if (!val) throwRegValidation(emptyMessage, 'REGISTRATION_REQUIRED');
|
||||
};
|
||||
|
|
@ -107,7 +129,7 @@ async function upsertParticipantFromRegistrationForm(form, tenantOrganization, d
|
|||
if (!emailField?.label) {
|
||||
throwRegValidation('Form missing email field mapping', 'FORM_FIELD_CONFIG');
|
||||
}
|
||||
const emailRaw = dataObj[emailField.label.trim()];
|
||||
const emailRaw = dataObj[fieldDataKey(emailField)];
|
||||
const email = emailRaw != null ? String(emailRaw).trim().toLowerCase() : '';
|
||||
if (!email) {
|
||||
throwRegValidation('Email is required', 'EMAIL_REQUIRED');
|
||||
|
|
@ -121,8 +143,8 @@ async function upsertParticipantFromRegistrationForm(form, tenantOrganization, d
|
|||
const allowed = ['firstName', 'lastName', 'email', 'phone', 'organization', 'designation', 'notes', 'displayName'];
|
||||
|
||||
for (const f of fields) {
|
||||
if (!f.label) continue;
|
||||
const key = f.label.trim();
|
||||
if (f.type === 'section' || !f.label) continue;
|
||||
const key = fieldDataKey(f);
|
||||
const val = dataObj[key];
|
||||
if (val === undefined || val === null || String(val).trim() === '') continue;
|
||||
const map = f.mapsTo;
|
||||
|
|
@ -372,11 +394,25 @@ const submit = async (request, h) => {
|
|||
}
|
||||
|
||||
const dataObj = normalizeSubmitData(request.payload.data);
|
||||
let processedData = dataObj;
|
||||
|
||||
try {
|
||||
processedData = await processFormSubmissionFileUploads(form, dataObj, {
|
||||
orgId: String(tenantOrganization),
|
||||
eventId: submissionEventId,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'FILE_TOO_LARGE') {
|
||||
return h.response(error('VALIDATION_ERROR', err.message || 'File too large')).code(400);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
let participantId = request.payload.participantId;
|
||||
|
||||
if (form.type === 'registration') {
|
||||
try {
|
||||
participantId = await upsertParticipantFromRegistrationForm(form, tenantOrganization, dataObj);
|
||||
participantId = await upsertParticipantFromRegistrationForm(form, tenantOrganization, processedData);
|
||||
} catch (err) {
|
||||
if (
|
||||
err.code === 'EMAIL_REQUIRED' ||
|
||||
|
|
@ -399,7 +435,7 @@ const submit = async (request, h) => {
|
|||
event: submissionEventId || undefined,
|
||||
user: user?._id,
|
||||
participant: participantId,
|
||||
data: dataObj,
|
||||
data: processedData,
|
||||
});
|
||||
|
||||
if (submissionEventId) {
|
||||
|
|
@ -450,14 +486,48 @@ function escapeCsvCell(val) {
|
|||
return s;
|
||||
}
|
||||
|
||||
function sanitizeExportFilenamePart(value, fallback) {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) return fallback;
|
||||
return (
|
||||
trimmed
|
||||
.replace(/[\\/:*?"<>|]+/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 80) || fallback
|
||||
);
|
||||
}
|
||||
|
||||
function formatExportDateTime(date = new Date()) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
function buildFormSubmissionsExportFilename(formName, eventName) {
|
||||
const eventPart = sanitizeExportFilenamePart(eventName, 'Event');
|
||||
const formPart = sanitizeExportFilenamePart(formName, 'Form');
|
||||
return `${eventPart}-${formPart}-${formatExportDateTime()}.csv`;
|
||||
}
|
||||
|
||||
async function resolveFormEventName(form) {
|
||||
if (form.event && typeof form.event === 'object' && form.event.name) {
|
||||
return String(form.event.name).trim() || 'Event';
|
||||
}
|
||||
const eventId = extractObjectIdString(form.event);
|
||||
if (!eventId) return 'Event';
|
||||
const ev = await Event.findById(eventId).select('name').lean();
|
||||
return ev?.name?.trim() || 'Event';
|
||||
}
|
||||
|
||||
function submissionsToCsv(form, rows) {
|
||||
const labels = (form.fields || []).map((f) => f.label).filter(Boolean);
|
||||
const header = ['Submitted At', 'Participant Email', ...labels];
|
||||
const cols = csvColumnsFromFormFields(form.fields);
|
||||
const header = ['Submitted At', 'Participant Email', ...cols.map((c) => c.header)];
|
||||
const lines = [header.map(escapeCsvCell).join(',')];
|
||||
for (const s of rows) {
|
||||
const o = dataMapToObject(s.data);
|
||||
const email = s.participant && typeof s.participant === 'object' ? s.participant.email || '' : '';
|
||||
const vals = labels.map((lab) => o[lab] ?? '');
|
||||
const vals = cols.map((c) => o[c.key] ?? '');
|
||||
const row = [new Date(s.submittedAt).toISOString(), email, ...vals];
|
||||
lines.push(row.map(escapeCsvCell).join(','));
|
||||
}
|
||||
|
|
@ -492,11 +562,13 @@ const listSubmissions = async (request, h) => {
|
|||
}
|
||||
|
||||
if (exportCsv) {
|
||||
const eventName = await resolveFormEventName(form);
|
||||
const filename = buildFormSubmissionsExportFilename(form.name, eventName);
|
||||
const csv = submissionsToCsv(form, all);
|
||||
return h
|
||||
.response(csv)
|
||||
.type('text/csv; charset=utf-8')
|
||||
.header('Content-Disposition', `attachment; filename="form-${form._id}-submissions.csv"`)
|
||||
.header('Content-Disposition', `attachment; filename="${filename}"`)
|
||||
.code(200);
|
||||
}
|
||||
|
||||
|
|
@ -525,15 +597,7 @@ const getPublicForm = async (request, h) => {
|
|||
description: form.description || '',
|
||||
type: form.type,
|
||||
event: eventPublic,
|
||||
fields: (form.fields || []).map((f) => ({
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: f.required,
|
||||
options: f.options,
|
||||
placeholder: f.placeholder,
|
||||
mapsTo: f.mapsTo,
|
||||
isFixed: f.isFixed,
|
||||
})),
|
||||
fields: publicRegistrationFields(form.fields, form.type),
|
||||
settings: {
|
||||
thankYouMessage: form.settings?.thankYouMessage,
|
||||
showThankYou: form.settings?.showThankYou !== false,
|
||||
|
|
|
|||
|
|
@ -31,14 +31,19 @@ async function loadFormFieldsForPublic(formId) {
|
|||
if (!formId || !mongoose.Types.ObjectId.isValid(String(formId))) return null;
|
||||
const form = await Form.findById(formId).lean();
|
||||
if (!form || !form.settings?.isActive) return null;
|
||||
const fields = (form.fields || []).map((f) => ({
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: Boolean(f.required),
|
||||
options: f.options,
|
||||
placeholder: f.placeholder,
|
||||
mapsTo: f.mapsTo,
|
||||
}));
|
||||
const fields = (form.fields || []).map((f) => {
|
||||
const remarks = f.remarks != null ? String(f.remarks).trim() : '';
|
||||
return {
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: Boolean(f.required),
|
||||
options: f.options,
|
||||
...(f.allowOther ? { allowOther: true, otherLabel: f.otherLabel || 'Other' } : {}),
|
||||
placeholder: f.placeholder,
|
||||
mapsTo: f.mapsTo,
|
||||
...(remarks ? { remarks } : {}),
|
||||
};
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { publicRegistrationFields } = require('../utils/registrationFormFields');
|
||||
const Event = require('../models/Event');
|
||||
const Form = require('../models/Form');
|
||||
const { success, error } = require('../utils/response');
|
||||
|
|
@ -172,15 +173,7 @@ async function getPublicEventForm(request, h) {
|
|||
description: form.description || '',
|
||||
type: form.type,
|
||||
event: eventPublic,
|
||||
fields: (form.fields || []).map((f) => ({
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: f.required,
|
||||
options: f.options,
|
||||
placeholder: f.placeholder,
|
||||
mapsTo: f.mapsTo,
|
||||
isFixed: f.isFixed,
|
||||
})),
|
||||
fields: publicRegistrationFields(form.fields, form.type),
|
||||
settings: {
|
||||
thankYouMessage: form.settings?.thankYouMessage,
|
||||
showThankYou: form.settings?.showThankYou !== false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,410 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Event = require('../models/Event');
|
||||
const EventStaticMicrosite = require('../models/EventStaticMicrosite');
|
||||
const MicrositeV2Template = require('../models/MicrositeV2Template');
|
||||
const { success, error } = require('../utils/response');
|
||||
const { normalizeSubdomain, subdomainAvailability } = require('../utils/subdomainAvailability');
|
||||
const {
|
||||
uploadTemplateFile,
|
||||
uploadTemplateAsset,
|
||||
templateAssetsBaseUrl,
|
||||
getS3ObjectBuffer,
|
||||
} = require('../utils/s3MicrositeV2');
|
||||
const { parseTemplateSchema, buildDefaultConfig } = require('../utils/templateSchema');
|
||||
const {
|
||||
renderStaticMicrositeHtml,
|
||||
publishStaticMicrosite,
|
||||
loadTemplateSchema,
|
||||
} = require('../utils/publishStaticMicrosite');
|
||||
const { renderHandlebarsTemplate } = require('../utils/handlebarsRender');
|
||||
|
||||
function requireSuperAdmin(user) {
|
||||
return user.role === 'super_admin';
|
||||
}
|
||||
|
||||
function orgMismatch(event, user) {
|
||||
if (user.role === 'super_admin') return false;
|
||||
return event.organization?.toString() !== (user.organization?._id || user.organization)?.toString();
|
||||
}
|
||||
|
||||
async function loadEventForUser(eventId, user) {
|
||||
const event = await Event.findById(eventId);
|
||||
if (!event) return { event: null, forbidden: false };
|
||||
if (orgMismatch(event, user)) return { event: null, forbidden: true };
|
||||
return { event, forbidden: false };
|
||||
}
|
||||
|
||||
function extractBufferPart(part, fallbackName) {
|
||||
if (!part) return null;
|
||||
if (Buffer.isBuffer(part)) {
|
||||
return { buffer: part, filename: fallbackName, mimeType: 'application/octet-stream' };
|
||||
}
|
||||
if (typeof part === 'object' && part.payload && Buffer.isBuffer(part.payload)) {
|
||||
return {
|
||||
buffer: part.payload,
|
||||
filename: part.filename || fallbackName,
|
||||
mimeType: part.headers?.['content-type'] || 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
if (typeof part === 'object' && part._data && Buffer.isBuffer(part._data)) {
|
||||
return {
|
||||
buffer: part._data,
|
||||
filename: part.filename || fallbackName,
|
||||
mimeType: part.headers?.['content-type'] || 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Template catalog (read) ---
|
||||
|
||||
const listTemplates = async (request, h) => {
|
||||
const rows = await MicrositeV2Template.find({ isActive: true })
|
||||
.sort({ createdAt: -1 })
|
||||
.select('slug name description thumbnailUrl version isActive createdAt')
|
||||
.lean();
|
||||
return h.response(success(rows));
|
||||
};
|
||||
|
||||
const listAdminTemplates = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can list all templates')).code(403);
|
||||
}
|
||||
const rows = await MicrositeV2Template.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('createdBy', 'name email')
|
||||
.lean();
|
||||
return h.response(success(rows));
|
||||
};
|
||||
|
||||
const getTemplate = async (request, h) => {
|
||||
const template = await MicrositeV2Template.findById(request.params.id).lean();
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found')).code(404);
|
||||
|
||||
let schema = template.schema;
|
||||
if (!schema && template.schemaS3Key) {
|
||||
try {
|
||||
const buf = await getS3ObjectBuffer(template.schemaS3Key);
|
||||
schema = JSON.parse(buf.toString('utf8'));
|
||||
} catch {
|
||||
schema = null;
|
||||
}
|
||||
}
|
||||
|
||||
return h.response(success({ ...template, schema }));
|
||||
};
|
||||
|
||||
// --- Template admin ---
|
||||
|
||||
const createTemplate = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can create templates')).code(403);
|
||||
}
|
||||
|
||||
const { slug, name, description, version } = request.payload;
|
||||
const normalizedSlug = normalizeSubdomain(slug);
|
||||
if (!normalizedSlug) {
|
||||
return h.response(error('VALIDATION_ERROR', 'Valid slug is required')).code(400);
|
||||
}
|
||||
|
||||
const existing = await MicrositeV2Template.findOne({ slug: normalizedSlug }).lean();
|
||||
if (existing) {
|
||||
return h.response(error('CONFLICT', 'Template slug already exists')).code(409);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.create({
|
||||
slug: normalizedSlug,
|
||||
name: name || normalizedSlug,
|
||||
description: description || '',
|
||||
version: version || 1,
|
||||
isActive: false,
|
||||
createdBy: user._id,
|
||||
});
|
||||
|
||||
return h.response(success(template, 'Template created', 201)).code(201);
|
||||
};
|
||||
|
||||
const updateTemplate = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can update templates')).code(403);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.findById(request.params.id);
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found')).code(404);
|
||||
|
||||
const { name, description, isActive, version } = request.payload;
|
||||
if (name !== undefined) template.name = name;
|
||||
if (description !== undefined) template.description = description;
|
||||
if (isActive !== undefined) template.isActive = isActive;
|
||||
if (version !== undefined) template.version = version;
|
||||
|
||||
await template.save();
|
||||
return h.response(success(template));
|
||||
};
|
||||
|
||||
const removeTemplate = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can delete templates')).code(403);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.findById(request.params.id);
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found')).code(404);
|
||||
|
||||
const inUse = await EventStaticMicrosite.countDocuments({ template: template._id });
|
||||
if (inUse > 0) {
|
||||
return h.response(error('CONFLICT', 'Template is in use by event microsites')).code(409);
|
||||
}
|
||||
|
||||
await template.deleteOne();
|
||||
return h.response(success(null, 'Template deleted'));
|
||||
};
|
||||
|
||||
const uploadTemplateFiles = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can upload templates')).code(403);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.findById(request.params.id);
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found')).code(404);
|
||||
|
||||
const payload = request.payload || {};
|
||||
const slug = template.slug;
|
||||
|
||||
const htmlPart = extractBufferPart(payload.templateHtml, 'template.html');
|
||||
const jsonPart = extractBufferPart(payload.templateJson, 'template.json');
|
||||
const thumbPart = extractBufferPart(payload.thumbnail, 'thumbnail.jpg');
|
||||
const zipPart = extractBufferPart(payload.assetsZip, 'assets.zip');
|
||||
|
||||
if (htmlPart) {
|
||||
const up = await uploadTemplateFile(slug, 'template.html', htmlPart.buffer, 'text/html; charset=utf-8');
|
||||
template.htmlS3Key = up.s3Key;
|
||||
}
|
||||
if (jsonPart) {
|
||||
const schema = parseTemplateSchema(jsonPart.buffer);
|
||||
const up = await uploadTemplateFile(slug, 'template.json', jsonPart.buffer, 'application/json');
|
||||
template.schemaS3Key = up.s3Key;
|
||||
template.schema = schema;
|
||||
|
||||
if (htmlPart) {
|
||||
try {
|
||||
const htmlStr = htmlPart.buffer.toString('utf8');
|
||||
const defaultConfig = buildDefaultConfig(schema);
|
||||
renderHandlebarsTemplate(htmlStr, {
|
||||
meta: defaultConfig.meta,
|
||||
theme: defaultConfig.theme,
|
||||
sections: defaultConfig.sections,
|
||||
assetsBaseUrl: template.assetsBaseUrl || templateAssetsBaseUrl(slug),
|
||||
eventId: '',
|
||||
subdomain: '',
|
||||
templateSlug: slug,
|
||||
});
|
||||
} catch (e) {
|
||||
return h.response(error('VALIDATION_ERROR', `Handlebars compile failed: ${e.message}`)).code(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (thumbPart) {
|
||||
const ext = (thumbPart.filename || '').split('.').pop() || 'jpg';
|
||||
const mime = thumbPart.mimeType.startsWith('image/') ? thumbPart.mimeType : `image/${ext}`;
|
||||
const up = await uploadTemplateFile(slug, `thumbnail.${ext}`, thumbPart.buffer, mime);
|
||||
template.thumbnailUrl = up.url;
|
||||
}
|
||||
if (zipPart) {
|
||||
// Lazy require — only needed for super-admin template zip upload.
|
||||
// eslint-disable-next-line global-require
|
||||
const AdmZip = require('adm-zip');
|
||||
const zip = new AdmZip(zipPart.buffer);
|
||||
const entries = zip.getEntries();
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory) continue;
|
||||
const rel = entry.entryName.replace(/^\/+/, '');
|
||||
const data = entry.getData();
|
||||
const mime = rel.endsWith('.css')
|
||||
? 'text/css'
|
||||
: rel.endsWith('.js')
|
||||
? 'application/javascript'
|
||||
: 'application/octet-stream';
|
||||
await uploadTemplateAsset(slug, rel, data, mime);
|
||||
}
|
||||
template.assetsS3Prefix = `templates/${slug}/assets`;
|
||||
}
|
||||
|
||||
template.assetsBaseUrl = templateAssetsBaseUrl(slug);
|
||||
if (template.htmlS3Key && template.schemaS3Key) {
|
||||
template.isActive = true;
|
||||
}
|
||||
|
||||
await template.save();
|
||||
return h.response(success(template, 'Template files uploaded'));
|
||||
};
|
||||
|
||||
const previewTemplateHtml = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
if (!requireSuperAdmin(user)) {
|
||||
return h.response(error('FORBIDDEN', 'Only super_admin can preview templates')).code(403);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.findById(request.params.id).lean();
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found')).code(404);
|
||||
|
||||
try {
|
||||
const schema = await loadTemplateSchema(template);
|
||||
const defaultConfig = buildDefaultConfig(schema);
|
||||
const html = await renderStaticMicrositeHtml({
|
||||
template,
|
||||
config: defaultConfig,
|
||||
});
|
||||
return h.response(html).type('text/html');
|
||||
} catch (e) {
|
||||
return h.response(error('RENDER_ERROR', e.message)).code(400);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Event static microsite ---
|
||||
|
||||
const getForEvent = async (request, h) => {
|
||||
const { event, forbidden } = await loadEventForUser(request.params.eventId, request.auth.credentials.user);
|
||||
if (forbidden) return h.response(error('FORBIDDEN', 'Access denied')).code(403);
|
||||
if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404);
|
||||
|
||||
const site = await EventStaticMicrosite.findOne({ event: event._id })
|
||||
.populate('template', 'slug name description thumbnailUrl version assetsBaseUrl')
|
||||
.lean();
|
||||
|
||||
if (!site) return h.response(error('NOT_FOUND', 'Static microsite not configured')).code(404);
|
||||
return h.response(success(site));
|
||||
};
|
||||
|
||||
const upsertForEvent = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
const { event, forbidden } = await loadEventForUser(request.params.eventId, user);
|
||||
if (forbidden) return h.response(error('FORBIDDEN', 'Access denied')).code(403);
|
||||
if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404);
|
||||
|
||||
const { subdomain, templateId, config } = request.payload;
|
||||
const normalized = normalizeSubdomain(subdomain);
|
||||
if (!normalized) {
|
||||
return h.response(error('VALIDATION_ERROR', 'Valid subdomain is required')).code(400);
|
||||
}
|
||||
if (!templateId || !mongoose.Types.ObjectId.isValid(templateId)) {
|
||||
return h.response(error('VALIDATION_ERROR', 'Valid template id is required')).code(400);
|
||||
}
|
||||
|
||||
const template = await MicrositeV2Template.findOne({ _id: templateId, isActive: true });
|
||||
if (!template) return h.response(error('NOT_FOUND', 'Template not found or inactive')).code(404);
|
||||
|
||||
let site = await EventStaticMicrosite.findOne({ event: event._id });
|
||||
const avail = await subdomainAvailability(normalized, event._id.toString(), site?._id?.toString());
|
||||
if (!avail.available) {
|
||||
return h.response(error('VALIDATION_ERROR', avail.message)).code(400);
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
site = new EventStaticMicrosite({
|
||||
event: event._id,
|
||||
organization: event.organization,
|
||||
subdomain: normalized,
|
||||
template: template._id,
|
||||
config: config || {},
|
||||
updatedBy: user._id,
|
||||
});
|
||||
} else {
|
||||
site.subdomain = normalized;
|
||||
site.template = template._id;
|
||||
if (config !== undefined) site.config = config;
|
||||
site.updatedBy = user._id;
|
||||
}
|
||||
|
||||
const wasNew = site.isNew;
|
||||
await site.save();
|
||||
const populated = await EventStaticMicrosite.findById(site._id)
|
||||
.populate('template', 'slug name description thumbnailUrl version assetsBaseUrl')
|
||||
.lean();
|
||||
|
||||
return h.response(success(populated, wasNew ? 'Created' : 'Updated'));
|
||||
};
|
||||
|
||||
const checkSubdomain = async (request, h) => {
|
||||
const { event, forbidden } = await loadEventForUser(request.params.eventId, request.auth.credentials.user);
|
||||
if (forbidden) return h.response(error('FORBIDDEN', 'Access denied')).code(403);
|
||||
if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404);
|
||||
|
||||
const normalized = normalizeSubdomain(request.query.subdomain);
|
||||
const site = await EventStaticMicrosite.findOne({ event: event._id }).select('_id').lean();
|
||||
const avail = await subdomainAvailability(
|
||||
normalized,
|
||||
event._id.toString(),
|
||||
site?._id?.toString(),
|
||||
);
|
||||
return h.response(success(avail));
|
||||
};
|
||||
|
||||
const publishForEvent = async (request, h) => {
|
||||
const user = request.auth.credentials.user;
|
||||
const { event, forbidden } = await loadEventForUser(request.params.eventId, user);
|
||||
if (forbidden) return h.response(error('FORBIDDEN', 'Access denied')).code(403);
|
||||
if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404);
|
||||
|
||||
const site = await EventStaticMicrosite.findOne({ event: event._id }).populate('template');
|
||||
if (!site) return h.response(error('NOT_FOUND', 'Static microsite not configured')).code(404);
|
||||
|
||||
try {
|
||||
const result = await publishStaticMicrosite({
|
||||
staticMicrosite: site,
|
||||
template: site.template,
|
||||
userId: user._id,
|
||||
});
|
||||
Object.assign(site, result);
|
||||
await site.save();
|
||||
|
||||
const populated = await EventStaticMicrosite.findById(site._id)
|
||||
.populate('template', 'slug name description thumbnailUrl version assetsBaseUrl')
|
||||
.lean();
|
||||
|
||||
return h.response(success({ ...populated, publicUrl: result.publicUrl }, 'Published'));
|
||||
} catch (e) {
|
||||
return h.response(error('PUBLISH_ERROR', e.message)).code(400);
|
||||
}
|
||||
};
|
||||
|
||||
const previewForEvent = async (request, h) => {
|
||||
const { event, forbidden } = await loadEventForUser(request.params.eventId, request.auth.credentials.user);
|
||||
if (forbidden) return h.response(error('FORBIDDEN', 'Access denied')).code(403);
|
||||
if (!event) return h.response(error('NOT_FOUND', 'Event not found')).code(404);
|
||||
|
||||
const site = await EventStaticMicrosite.findOne({ event: event._id }).populate('template');
|
||||
if (!site) return h.response(error('NOT_FOUND', 'Static microsite not configured')).code(404);
|
||||
|
||||
try {
|
||||
const html = await renderStaticMicrositeHtml({
|
||||
template: site.template,
|
||||
config: site.config,
|
||||
eventId: event._id,
|
||||
subdomain: site.subdomain,
|
||||
});
|
||||
return h.response(html).type('text/html');
|
||||
} catch (e) {
|
||||
return h.response(error('RENDER_ERROR', e.message)).code(400);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listTemplates,
|
||||
listAdminTemplates,
|
||||
getTemplate,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
removeTemplate,
|
||||
uploadTemplateFiles,
|
||||
previewTemplateHtml,
|
||||
getForEvent,
|
||||
upsertForEvent,
|
||||
checkSubdomain,
|
||||
publishForEvent,
|
||||
previewForEvent,
|
||||
};
|
||||
|
|
@ -1075,13 +1075,17 @@ async function visitorEventRewardHistory(request, h) {
|
|||
|
||||
/** GET …/visitor/events/{eventId}/feedback-forms/{formId} — public schema for rendering (org must match event). */
|
||||
function visitorFeedbackFormPayload(form) {
|
||||
const fields = (form.fields || []).map((f) => ({
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: !!f.required,
|
||||
options: Array.isArray(f.options) ? f.options.map((o) => String(o)) : [],
|
||||
placeholder: f.placeholder ? String(f.placeholder) : '',
|
||||
}));
|
||||
const fields = (form.fields || []).map((f) => {
|
||||
const remarks = f.remarks != null ? String(f.remarks).trim() : '';
|
||||
return {
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: !!f.required,
|
||||
options: Array.isArray(f.options) ? f.options.map((o) => String(o)) : [],
|
||||
placeholder: f.placeholder ? String(f.placeholder) : '',
|
||||
...(remarks ? { remarks } : {}),
|
||||
};
|
||||
});
|
||||
return {
|
||||
_id: String(form._id),
|
||||
name: form.name,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const eventStaticMicrositeSchema = new mongoose.Schema(
|
||||
{
|
||||
event: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Event',
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
organization: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
subdomain: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
},
|
||||
template: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'MicrositeV2Template',
|
||||
required: true,
|
||||
},
|
||||
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'published'],
|
||||
default: 'draft',
|
||||
},
|
||||
publishedHtmlUrl: String,
|
||||
publishedHtmlS3Key: String,
|
||||
publishedConfigS3Key: String,
|
||||
publishedAt: Date,
|
||||
publishedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
updatedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
module.exports = mongoose.model('EventStaticMicrosite', eventStaticMicrositeSchema);
|
||||
|
|
@ -2,13 +2,22 @@ const mongoose = require('mongoose');
|
|||
|
||||
const formFieldSchema = new mongoose.Schema({
|
||||
label: String,
|
||||
/** Unique submission key; duplicates of the same label use `-1`, `-2`, … suffixes. */
|
||||
name: String,
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'textarea', 'email', 'phone', 'select', 'stars', 'radio', 'checkbox', 'checkboxes', 'file'],
|
||||
enum: ['text', 'textarea', 'email', 'phone', 'select', 'stars', 'radio', 'checkbox', 'checkboxes', 'file', 'date', 'time', 'section', 'country_state'],
|
||||
},
|
||||
required: Boolean,
|
||||
options: [String],
|
||||
/** Dropdown: show an "Other" choice; submission stores the custom text, not the label. */
|
||||
allowOther: { type: Boolean, default: false },
|
||||
otherLabel: { type: String, default: 'Other' },
|
||||
placeholder: String,
|
||||
/** Optional helper text shown below the field on public forms. */
|
||||
remarks: String,
|
||||
/** Builder display type (e.g. Text vs Short Text, Dropdown vs Multiple Choice) for other/custom forms. */
|
||||
uiType: String,
|
||||
/** Registration: which Participant field this collects (`custom` uses customMapsKey → customFields). */
|
||||
mapsTo: String,
|
||||
customMapsKey: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const micrositeV2TemplateSchema = new mongoose.Schema(
|
||||
{
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
},
|
||||
name: { type: String, required: true, trim: true },
|
||||
description: { type: String, trim: true },
|
||||
thumbnailUrl: String,
|
||||
htmlS3Key: String,
|
||||
schemaS3Key: String,
|
||||
/** Inline copy of template.json schema when small enough for fast reads */
|
||||
schema: mongoose.Schema.Types.Mixed,
|
||||
assetsBaseUrl: String,
|
||||
assetsS3Prefix: String,
|
||||
isActive: { type: Boolean, default: true },
|
||||
version: { type: Number, default: 1 },
|
||||
createdBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
micrositeV2TemplateSchema.index({ isActive: 1, createdAt: -1 });
|
||||
|
||||
module.exports = mongoose.model('MicrositeV2Template', micrositeV2TemplateSchema);
|
||||
|
|
@ -5,10 +5,15 @@ const { FORM_TYPES, FIELD_TYPES, FORM_FIELD_MAPS_TO } = require('../config/const
|
|||
/** stripUnknown: Mongo subdocs include _id; duplicates must not fail validation. */
|
||||
const fieldSchema = Joi.object({
|
||||
label: Joi.string(),
|
||||
name: Joi.string().allow('', null),
|
||||
type: Joi.string().valid(...FIELD_TYPES),
|
||||
required: Joi.boolean(),
|
||||
options: Joi.array().items(Joi.string()),
|
||||
placeholder: Joi.string(),
|
||||
allowOther: Joi.boolean(),
|
||||
otherLabel: Joi.string().allow('', null),
|
||||
placeholder: Joi.string().allow('', null),
|
||||
remarks: Joi.string().allow('', null),
|
||||
uiType: Joi.string().allow('', null),
|
||||
mapsTo: Joi.string().valid(...FORM_FIELD_MAPS_TO).allow('', null),
|
||||
customMapsKey: Joi.string().allow('', null),
|
||||
isFixed: Joi.boolean(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
const Joi = require('joi');
|
||||
const controller = require('../controllers/staticMicrosite.controller');
|
||||
|
||||
const mongoId = Joi.string().length(24).hex().required();
|
||||
const optionalMongoId = Joi.string().length(24).hex().allow('', null);
|
||||
|
||||
const multipartPayload = {
|
||||
maxBytes: 100 * 1024 * 1024,
|
||||
output: 'data',
|
||||
parse: true,
|
||||
multipart: true,
|
||||
allow: ['multipart/form-data'],
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/microsite-v2-templates',
|
||||
options: { handler: controller.listTemplates },
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/microsite-v2-templates/admin',
|
||||
options: { handler: controller.listAdminTemplates },
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/microsite-v2-templates/{id}',
|
||||
options: {
|
||||
validate: { params: Joi.object({ id: mongoId }) },
|
||||
handler: controller.getTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/microsite-v2-templates',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
slug: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().allow('', null),
|
||||
version: Joi.number().integer().min(1).optional(),
|
||||
}),
|
||||
},
|
||||
handler: controller.createTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/api/microsite-v2-templates/{id}',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object({ id: mongoId }),
|
||||
payload: Joi.object({
|
||||
name: Joi.string(),
|
||||
description: Joi.string().allow('', null),
|
||||
isActive: Joi.boolean(),
|
||||
version: Joi.number().integer().min(1),
|
||||
}).min(1),
|
||||
},
|
||||
handler: controller.updateTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/api/microsite-v2-templates/{id}',
|
||||
options: {
|
||||
validate: { params: Joi.object({ id: mongoId }) },
|
||||
handler: controller.removeTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/microsite-v2-templates/{id}/upload',
|
||||
options: {
|
||||
payload: multipartPayload,
|
||||
validate: { params: Joi.object({ id: mongoId }) },
|
||||
handler: controller.uploadTemplateFiles,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/microsite-v2-templates/{id}/preview-html',
|
||||
options: {
|
||||
validate: { params: Joi.object({ id: mongoId }) },
|
||||
handler: controller.previewTemplateHtml,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/events/{eventId}/static-microsite',
|
||||
options: {
|
||||
validate: { params: Joi.object({ eventId: mongoId }) },
|
||||
handler: controller.getForEvent,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/api/events/{eventId}/static-microsite',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object({ eventId: mongoId }),
|
||||
payload: Joi.object({
|
||||
subdomain: Joi.string().required(),
|
||||
templateId: mongoId,
|
||||
config: Joi.object().unknown(true),
|
||||
}),
|
||||
},
|
||||
handler: controller.upsertForEvent,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/events/{eventId}/static-microsite/check-subdomain',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object({ eventId: mongoId }),
|
||||
query: Joi.object({ subdomain: Joi.string().required() }),
|
||||
},
|
||||
handler: controller.checkSubdomain,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/events/{eventId}/static-microsite/publish',
|
||||
options: {
|
||||
validate: { params: Joi.object({ eventId: mongoId }) },
|
||||
handler: controller.publishForEvent,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/events/{eventId}/static-microsite/preview-html',
|
||||
options: {
|
||||
validate: { params: Joi.object({ eventId: mongoId }) },
|
||||
handler: controller.previewForEvent,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
const { uploadBufferToS3AndGetCloudFrontUrl } = require('./s3Media');
|
||||
|
||||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
function fieldDataKey(field) {
|
||||
return String(field?.name || field?.label || '').trim();
|
||||
}
|
||||
|
||||
function parseDataUrl(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
const match = /^data:([^;]+);base64,(.+)$/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
return {
|
||||
mimeType: match[1],
|
||||
buffer: Buffer.from(match[2], 'base64'),
|
||||
};
|
||||
}
|
||||
|
||||
function extensionFromMimeType(mimeType) {
|
||||
const mt = String(mimeType || '').toLowerCase();
|
||||
const exact = {
|
||||
'application/pdf': 'pdf',
|
||||
'application/msword': 'doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
||||
'application/vnd.ms-excel': 'xls',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
|
||||
'application/vnd.ms-powerpoint': 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
|
||||
'text/plain': 'txt',
|
||||
'text/csv': 'csv',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
};
|
||||
if (exact[mt]) return exact[mt];
|
||||
if (mt.includes('png')) return 'png';
|
||||
if (mt.includes('jpeg') || mt.includes('jpg')) return 'jpg';
|
||||
if (mt.includes('webp')) return 'webp';
|
||||
if (mt.includes('gif')) return 'gif';
|
||||
if (mt.includes('pdf')) return 'pdf';
|
||||
return 'bin';
|
||||
}
|
||||
|
||||
function maxFormFileBytes() {
|
||||
const n = Number(process.env.FORM_SUBMISSION_MAX_FILE_BYTES);
|
||||
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_BYTES;
|
||||
}
|
||||
|
||||
async function uploadFormFileValue({ value, orgId, eventId, fieldKey }) {
|
||||
if (value == null) return value;
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const parsed = parseDataUrl(trimmed);
|
||||
if (!parsed) return trimmed;
|
||||
|
||||
const maxBytes = maxFormFileBytes();
|
||||
if (!parsed.buffer.length) {
|
||||
return '';
|
||||
}
|
||||
if (parsed.buffer.length > maxBytes) {
|
||||
const err = new Error(
|
||||
`File for "${fieldKey}" exceeds the maximum size (${Math.round(maxBytes / (1024 * 1024))}MB).`,
|
||||
);
|
||||
err.code = 'FILE_TOO_LARGE';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const safeKey = String(fieldKey).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48) || 'file';
|
||||
const filename = `${safeKey}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${extensionFromMimeType(parsed.mimeType)}`;
|
||||
|
||||
const uploaded = await uploadBufferToS3AndGetCloudFrontUrl({
|
||||
buffer: parsed.buffer,
|
||||
contentType: parsed.mimeType,
|
||||
orgId,
|
||||
eventId: eventId || 'no-event',
|
||||
module: 'form-submissions',
|
||||
filename,
|
||||
});
|
||||
|
||||
return uploaded.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload base64 data-URL file field values to S3 and replace with CloudFront URLs.
|
||||
*/
|
||||
async function processFormSubmissionFileUploads(form, data, { orgId, eventId }) {
|
||||
const out = { ...(data || {}) };
|
||||
const fields = form?.fields || [];
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.type !== 'file') continue;
|
||||
const key = fieldDataKey(field);
|
||||
if (!key || !Object.prototype.hasOwnProperty.call(out, key)) continue;
|
||||
const current = out[key];
|
||||
if (current == null || current === '') continue;
|
||||
out[key] = await uploadFormFileValue({
|
||||
value: current,
|
||||
orgId,
|
||||
eventId,
|
||||
fieldKey: key,
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function dataMapToObject(data) {
|
||||
if (!data) return {};
|
||||
if (data instanceof Map) return Object.fromEntries(data);
|
||||
return typeof data === 'object' ? { ...data } : {};
|
||||
}
|
||||
|
||||
function hasDataUrlValue(data) {
|
||||
const obj = dataMapToObject(data);
|
||||
return Object.values(obj).some((val) => typeof val === 'string' && !!parseDataUrl(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file fields from form schema, then any remaining data-URL strings (legacy keys).
|
||||
*/
|
||||
async function processAllSubmissionDataUrls(data, { orgId, eventId, form }) {
|
||||
let out = dataMapToObject(data);
|
||||
if (form) {
|
||||
out = await processFormSubmissionFileUploads(form, out, { orgId, eventId });
|
||||
}
|
||||
for (const [key, val] of Object.entries(out)) {
|
||||
if (typeof val !== 'string' || !parseDataUrl(val)) continue;
|
||||
out[key] = await uploadFormFileValue({
|
||||
value: val,
|
||||
orgId,
|
||||
eventId,
|
||||
fieldKey: key,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processFormSubmissionFileUploads,
|
||||
processAllSubmissionDataUrls,
|
||||
uploadFormFileValue,
|
||||
parseDataUrl,
|
||||
hasDataUrlValue,
|
||||
dataMapToObject,
|
||||
};
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
let HandlebarsLib;
|
||||
|
||||
function getHandlebars() {
|
||||
if (!HandlebarsLib) {
|
||||
// eslint-disable-next-line global-require
|
||||
HandlebarsLib = require('handlebars');
|
||||
}
|
||||
return HandlebarsLib;
|
||||
}
|
||||
|
||||
let helpersRegistered = false;
|
||||
|
||||
function registerHelpers() {
|
||||
if (helpersRegistered) return;
|
||||
helpersRegistered = true;
|
||||
|
||||
const Handlebars = getHandlebars();
|
||||
Handlebars.registerHelper('eq', (a, b) => a === b);
|
||||
Handlebars.registerHelper('neq', (a, b) => a !== b);
|
||||
Handlebars.registerHelper('and', (a, b) => a && b);
|
||||
Handlebars.registerHelper('or', (a, b) => a || b);
|
||||
Handlebars.registerHelper('not', (a) => !a);
|
||||
|
||||
Handlebars.registerHelper('if_show', function ifShow(section, options) {
|
||||
if (section && section.show !== false) {
|
||||
return options.fn(this);
|
||||
}
|
||||
return options.inverse(this);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('assetUrl', (base, path) => {
|
||||
const b = String(base || '').replace(/\/+$/, '');
|
||||
const p = String(path || '').replace(/^\/+/, '');
|
||||
return p ? `${b}/${p}` : b;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('resolveAsset', (base, path) => {
|
||||
const p = String(path || '');
|
||||
if (!p) return '';
|
||||
if (/^https?:\/\//i.test(p)) return p;
|
||||
const b = String(base || '').replace(/\/+$/, '');
|
||||
return `${b}/${p.replace(/^\/+/, '')}`;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('json', (obj) => {
|
||||
const safe = obj && typeof obj === 'object' ? obj : {};
|
||||
return JSON.stringify(safe).replace(/</g, '\\u003c');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} htmlTemplate
|
||||
* @param {object} context
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderHandlebarsTemplate(htmlTemplate, context) {
|
||||
registerHelpers();
|
||||
const Handlebars = getHandlebars();
|
||||
const compile = Handlebars.compile(htmlTemplate, { noEscape: false });
|
||||
return compile(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build render context from template record + user config.
|
||||
*/
|
||||
function buildRenderContext({ template, config, eventId, subdomain }) {
|
||||
const theme = { ...(config.theme || {}) };
|
||||
if (theme.headingColor && !theme.heading) {
|
||||
theme.heading = { color: theme.headingColor };
|
||||
}
|
||||
const sections = config.sections || {};
|
||||
return {
|
||||
meta: {
|
||||
...(config.meta || {}),
|
||||
eventId: config.meta?.eventId || eventId || '',
|
||||
},
|
||||
theme,
|
||||
sections,
|
||||
/** Alias for event-up JS bridge (expects `components`). */
|
||||
components: sections,
|
||||
assetsBaseUrl: template.assetsBaseUrl || '',
|
||||
eventId: eventId ? String(eventId) : '',
|
||||
subdomain: subdomain || '',
|
||||
templateSlug: template.slug || '',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderHandlebarsTemplate,
|
||||
buildRenderContext,
|
||||
registerHelpers,
|
||||
};
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
const MicrositeV2Template = require('../models/MicrositeV2Template');
|
||||
const { getS3ObjectBuffer, uploadSiteFile, publicSiteUrl } = require('./s3MicrositeV2');
|
||||
const { mergeConfigWithDefaults } = require('./templateSchema');
|
||||
const { renderHandlebarsTemplate, buildRenderContext } = require('./handlebarsRender');
|
||||
|
||||
async function loadTemplateHtml(template) {
|
||||
if (!template.htmlS3Key) {
|
||||
throw new Error('Template HTML is not uploaded yet');
|
||||
}
|
||||
const buf = await getS3ObjectBuffer(template.htmlS3Key);
|
||||
return buf.toString('utf8');
|
||||
}
|
||||
|
||||
async function loadTemplateSchema(template) {
|
||||
if (template.schema) return template.schema;
|
||||
if (!template.schemaS3Key) {
|
||||
throw new Error('Template schema is not available');
|
||||
}
|
||||
const buf = await getS3ObjectBuffer(template.schemaS3Key);
|
||||
return JSON.parse(buf.toString('utf8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML for preview or publish.
|
||||
* @param {object} params
|
||||
* @param {import('mongoose').Document|object} params.template
|
||||
* @param {object} params.config
|
||||
* @param {string} [params.eventId]
|
||||
* @param {string} [params.subdomain]
|
||||
*/
|
||||
async function renderStaticMicrositeHtml({ template, config, eventId, subdomain }) {
|
||||
const tpl = template.slug ? template : await MicrositeV2Template.findById(template).lean();
|
||||
if (!tpl) throw new Error('Template not found');
|
||||
|
||||
const schema = await loadTemplateSchema(tpl);
|
||||
const mergedConfig = mergeConfigWithDefaults(schema, config);
|
||||
const htmlSource = await loadTemplateHtml(tpl);
|
||||
const context = buildRenderContext({
|
||||
template: tpl,
|
||||
config: mergedConfig,
|
||||
eventId,
|
||||
subdomain,
|
||||
});
|
||||
return renderHandlebarsTemplate(htmlSource, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish config + rendered HTML to S3.
|
||||
*/
|
||||
async function publishStaticMicrosite({ staticMicrosite, template, userId }) {
|
||||
const tpl = template.slug ? template : await MicrositeV2Template.findById(template).lean();
|
||||
if (!tpl) throw new Error('Template not found');
|
||||
|
||||
const schema = await loadTemplateSchema(tpl);
|
||||
const mergedConfig = mergeConfigWithDefaults(schema, staticMicrosite.config);
|
||||
|
||||
const html = await renderStaticMicrositeHtml({
|
||||
template: tpl,
|
||||
config: mergedConfig,
|
||||
eventId: staticMicrosite.event,
|
||||
subdomain: staticMicrosite.subdomain,
|
||||
});
|
||||
|
||||
const theme = { ...(mergedConfig.theme || {}) };
|
||||
if (theme.headingColor && !theme.heading) {
|
||||
theme.heading = { color: theme.headingColor };
|
||||
}
|
||||
const clientConfig = {
|
||||
meta: {
|
||||
...(mergedConfig.meta || {}),
|
||||
eventId: mergedConfig.meta?.eventId || String(staticMicrosite.event || ''),
|
||||
},
|
||||
theme,
|
||||
components: mergedConfig.sections || {},
|
||||
};
|
||||
|
||||
const configJson = JSON.stringify(clientConfig, null, 2);
|
||||
const htmlUpload = await uploadSiteFile(
|
||||
staticMicrosite.subdomain,
|
||||
'index.html',
|
||||
Buffer.from(html, 'utf8'),
|
||||
'text/html; charset=utf-8',
|
||||
);
|
||||
const configUpload = await uploadSiteFile(
|
||||
staticMicrosite.subdomain,
|
||||
'config.json',
|
||||
Buffer.from(configJson, 'utf8'),
|
||||
'application/json',
|
||||
);
|
||||
|
||||
return {
|
||||
publishedHtmlUrl: htmlUpload.url,
|
||||
publishedHtmlS3Key: htmlUpload.s3Key,
|
||||
publishedConfigS3Key: configUpload.s3Key,
|
||||
publicUrl: publicSiteUrl(staticMicrosite.subdomain),
|
||||
publishedAt: new Date(),
|
||||
publishedBy: userId,
|
||||
status: 'published',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderStaticMicrositeHtml,
|
||||
publishStaticMicrosite,
|
||||
loadTemplateHtml,
|
||||
loadTemplateSchema,
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/** Default registration fields required for participant upsert on submit. */
|
||||
const REGISTRATION_CORE_FIELD_DEFS = [
|
||||
{ label: 'First name', type: 'text', mapsTo: 'firstName', isFixed: true },
|
||||
{ label: 'Last name', type: 'text', mapsTo: 'lastName', isFixed: true },
|
||||
{ label: 'Email', type: 'email', mapsTo: 'email', isFixed: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Prepend missing core registration fields (first/last name, email) so public
|
||||
* forms always collect data required by registration submit handling.
|
||||
*/
|
||||
function ensureRegistrationCoreFields(fields) {
|
||||
const list = Array.isArray(fields) ? [...fields] : [];
|
||||
const missing = REGISTRATION_CORE_FIELD_DEFS.filter(
|
||||
(def) => !list.some((f) => f.mapsTo === def.mapsTo),
|
||||
);
|
||||
if (!missing.length) return list;
|
||||
const injected = missing.map((def) => ({
|
||||
...def,
|
||||
name: def.label,
|
||||
required: true,
|
||||
options: [],
|
||||
}));
|
||||
return [...injected, ...list];
|
||||
}
|
||||
|
||||
function mapFieldForPublicApi(f) {
|
||||
const remarks = f.remarks != null ? String(f.remarks).trim() : '';
|
||||
return {
|
||||
label: f.label,
|
||||
name: f.name || f.label,
|
||||
type: f.type,
|
||||
required: f.required !== false,
|
||||
options: f.options,
|
||||
...(f.allowOther ? { allowOther: true, otherLabel: f.otherLabel || 'Other' } : {}),
|
||||
placeholder: f.placeholder,
|
||||
mapsTo: f.mapsTo,
|
||||
isFixed: f.isFixed,
|
||||
...(remarks ? { remarks } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function publicRegistrationFields(fields, formType) {
|
||||
const raw = formType === 'registration' ? ensureRegistrationCoreFields(fields) : fields || [];
|
||||
return raw.map(mapFieldForPublicApi);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
REGISTRATION_CORE_FIELD_DEFS,
|
||||
ensureRegistrationCoreFields,
|
||||
mapFieldForPublicApi,
|
||||
publicRegistrationFields,
|
||||
};
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
const {
|
||||
uploadBufferToS3AndGetCloudFrontUrl,
|
||||
getBufferFromS3Key,
|
||||
cloudfrontUrlForKey,
|
||||
} = require('./s3Media');
|
||||
|
||||
function getEnv(name, fallback) {
|
||||
const v = process.env[name];
|
||||
if (v == null || String(v).trim() === '') return fallback;
|
||||
return String(v).trim();
|
||||
}
|
||||
|
||||
function sanitizeSegment(segment) {
|
||||
return String(segment || '')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
}
|
||||
|
||||
function getS3Prefix() {
|
||||
return getEnv('MICROSITE_V2_S3_PREFIX', 'eventbuzz/microsite-v2').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function templateKey(slug, filename) {
|
||||
const safeSlug = sanitizeSegment(slug);
|
||||
const safeFile = sanitizeSegment(filename);
|
||||
return `${getS3Prefix()}/templates/${safeSlug}/${safeFile}`;
|
||||
}
|
||||
|
||||
function templateAssetsKey(slug, relativePath) {
|
||||
const safeSlug = sanitizeSegment(slug);
|
||||
const parts = String(relativePath || '')
|
||||
.split('/')
|
||||
.map(sanitizeSegment)
|
||||
.filter(Boolean);
|
||||
return `${getS3Prefix()}/templates/${safeSlug}/assets/${parts.join('/')}`;
|
||||
}
|
||||
|
||||
function siteKey(subdomain, filename) {
|
||||
const safeSub = sanitizeSegment(subdomain);
|
||||
const safeFile = sanitizeSegment(filename);
|
||||
return `${getS3Prefix()}/sites/${safeSub}/${safeFile}`;
|
||||
}
|
||||
|
||||
function templateAssetsBaseUrl(slug) {
|
||||
const prefix = templateAssetsKey(slug, '');
|
||||
return cloudfrontUrlForKey(prefix);
|
||||
}
|
||||
|
||||
function publicSiteUrl(subdomain) {
|
||||
const basePattern = getEnv('MICROSITE_V2_PUBLIC_BASE_URL', 'https://{subdomain}.eventbuzz.live');
|
||||
return basePattern.replace('{subdomain}', subdomain);
|
||||
}
|
||||
|
||||
async function uploadMicrositeV2Object({ key, buffer, contentType }) {
|
||||
const bucket = getEnv('AWS_BUCKET');
|
||||
if (!bucket) throw new Error('AWS_BUCKET is not configured');
|
||||
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const region = getEnv('AWS_DEFAULT_REGION', 'ap-south-1');
|
||||
const accessKeyId = getEnv('AWS_ACCESS_KEY_ID');
|
||||
const secretAccessKey = getEnv('AWS_SECRET_ACCESS_KEY');
|
||||
|
||||
const client = new S3Client({
|
||||
region,
|
||||
credentials: accessKeyId && secretAccessKey ? { accessKeyId, secretAccessKey } : undefined,
|
||||
});
|
||||
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
}),
|
||||
);
|
||||
|
||||
return { s3Key: key, url: cloudfrontUrlForKey(key) };
|
||||
}
|
||||
|
||||
async function uploadTemplateFile(slug, filename, buffer, contentType) {
|
||||
const key = templateKey(slug, filename);
|
||||
return uploadMicrositeV2Object({ key, buffer, contentType });
|
||||
}
|
||||
|
||||
async function uploadTemplateAsset(slug, relativePath, buffer, contentType) {
|
||||
const key = templateAssetsKey(slug, relativePath);
|
||||
return uploadMicrositeV2Object({ key, buffer, contentType });
|
||||
}
|
||||
|
||||
async function uploadSiteFile(subdomain, filename, buffer, contentType) {
|
||||
const key = siteKey(subdomain, filename);
|
||||
return uploadMicrositeV2Object({ key, buffer, contentType });
|
||||
}
|
||||
|
||||
async function getS3ObjectBuffer(s3Key) {
|
||||
return getBufferFromS3Key(s3Key);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getS3Prefix,
|
||||
templateKey,
|
||||
templateAssetsKey,
|
||||
siteKey,
|
||||
templateAssetsBaseUrl,
|
||||
publicSiteUrl,
|
||||
uploadMicrositeV2Object,
|
||||
uploadTemplateFile,
|
||||
uploadTemplateAsset,
|
||||
uploadSiteFile,
|
||||
getS3ObjectBuffer,
|
||||
cloudfrontUrlForKey,
|
||||
uploadBufferToS3AndGetCloudFrontUrl,
|
||||
};
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
const Event = require('../models/Event');
|
||||
const EventMicrosite = require('../models/EventMicrosite');
|
||||
const EventStaticMicrosite = require('../models/EventStaticMicrosite');
|
||||
const { normalizePublishFolder } = require('./publishFolderAvailability');
|
||||
|
||||
/**
|
||||
* Normalize subdomain slug (same rules as legacy publish folder).
|
||||
* @param {string} raw
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeSubdomain(raw) {
|
||||
return normalizePublishFolder(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserved subdomains that cannot be claimed.
|
||||
*/
|
||||
const RESERVED_SUBDOMAINS = new Set([
|
||||
'www',
|
||||
'api',
|
||||
'app',
|
||||
'console',
|
||||
'web',
|
||||
'admin',
|
||||
'mail',
|
||||
'cdn',
|
||||
'static',
|
||||
'assets',
|
||||
'meet',
|
||||
'eventbuzz',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check global subdomain availability across new static microsites and legacy folders.
|
||||
* @param {string} subdomain normalized
|
||||
* @param {string} eventId
|
||||
* @param {string} [excludeStaticMicrositeId]
|
||||
* @returns {Promise<{ available: boolean, message?: string }>}
|
||||
*/
|
||||
async function subdomainAvailability(subdomain, eventId, excludeStaticMicrositeId) {
|
||||
if (!subdomain) {
|
||||
return { available: false, message: 'Subdomain is required' };
|
||||
}
|
||||
if (subdomain.length < 3) {
|
||||
return { available: false, message: 'Subdomain must be at least 3 characters' };
|
||||
}
|
||||
if (RESERVED_SUBDOMAINS.has(subdomain)) {
|
||||
return { available: false, message: 'This subdomain is reserved' };
|
||||
}
|
||||
|
||||
const staticFilter = { subdomain };
|
||||
if (excludeStaticMicrositeId) {
|
||||
staticFilter._id = { $ne: excludeStaticMicrositeId };
|
||||
}
|
||||
const takenStatic = await EventStaticMicrosite.findOne(staticFilter).select('_id').lean();
|
||||
if (takenStatic) {
|
||||
return { available: false, message: 'This subdomain is already in use' };
|
||||
}
|
||||
|
||||
const takenMs = await EventMicrosite.findOne({ publishFolder: subdomain }).select('_id').lean();
|
||||
if (takenMs) {
|
||||
return { available: false, message: 'This subdomain is already in use by another microsite' };
|
||||
}
|
||||
|
||||
const legacyOther = await Event.findOne({
|
||||
'website.folder': subdomain,
|
||||
_id: { $ne: eventId },
|
||||
})
|
||||
.select('_id')
|
||||
.lean();
|
||||
if (legacyOther) {
|
||||
return { available: false, message: 'Another event already uses this subdomain' };
|
||||
}
|
||||
|
||||
return { available: true };
|
||||
}
|
||||
|
||||
module.exports = { normalizeSubdomain, subdomainAvailability, RESERVED_SUBDOMAINS };
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Parse and validate microsite-v2 template.json schema.
|
||||
*/
|
||||
|
||||
function parseTemplateSchema(raw) {
|
||||
let schema;
|
||||
if (Buffer.isBuffer(raw)) {
|
||||
schema = JSON.parse(raw.toString('utf8'));
|
||||
} else if (typeof raw === 'string') {
|
||||
schema = JSON.parse(raw);
|
||||
} else {
|
||||
schema = raw;
|
||||
}
|
||||
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
throw new Error('template.json must be a JSON object');
|
||||
}
|
||||
if (!schema.version) {
|
||||
throw new Error('template.json must include version');
|
||||
}
|
||||
if (!Array.isArray(schema.sections)) {
|
||||
throw new Error('template.json must include sections array');
|
||||
}
|
||||
|
||||
for (const section of schema.sections) {
|
||||
if (!section.id || !section.label) {
|
||||
throw new Error('Each section must have id and label');
|
||||
}
|
||||
if (!Array.isArray(section.fields)) {
|
||||
section.fields = [];
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function defaultForField(field) {
|
||||
if (field.default !== undefined) return field.default;
|
||||
switch (field.type) {
|
||||
case 'boolean':
|
||||
return false;
|
||||
case 'array':
|
||||
return [];
|
||||
case 'color':
|
||||
return '#5c6ac4';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setNested(obj, path, value) {
|
||||
const parts = String(path).split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i];
|
||||
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
||||
cur = cur[p];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function buildDefaultConfig(schema) {
|
||||
const config = {
|
||||
meta: { ...(schema.meta || {}) },
|
||||
theme: {},
|
||||
sections: {},
|
||||
};
|
||||
|
||||
if (schema.theme?.fields) {
|
||||
for (const field of schema.theme.fields) {
|
||||
if (field.key) config.theme[field.key] = defaultForField(field);
|
||||
}
|
||||
}
|
||||
|
||||
for (const section of schema.sections) {
|
||||
const sectionData = { show: section.show !== false };
|
||||
for (const field of section.fields || []) {
|
||||
if (field.type === 'array') {
|
||||
sectionData[field.key] = defaultForField(field);
|
||||
} else if (field.key && field.key.includes('.')) {
|
||||
setNested(sectionData, field.key, defaultForField(field));
|
||||
} else if (field.key) {
|
||||
sectionData[field.key] = defaultForField(field);
|
||||
}
|
||||
}
|
||||
config.sections[section.id] = sectionData;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function mergeConfigWithDefaults(schema, userConfig) {
|
||||
const defaults = buildDefaultConfig(schema);
|
||||
const merged = {
|
||||
meta: { ...defaults.meta, ...(userConfig?.meta || {}) },
|
||||
theme: { ...defaults.theme, ...(userConfig?.theme || {}) },
|
||||
sections: { ...defaults.sections },
|
||||
};
|
||||
|
||||
if (userConfig?.sections && typeof userConfig.sections === 'object') {
|
||||
for (const [id, data] of Object.entries(userConfig.sections)) {
|
||||
merged.sections[id] = { ...(merged.sections[id] || {}), ...data };
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseTemplateSchema,
|
||||
buildDefaultConfig,
|
||||
mergeConfigWithDefaults,
|
||||
};
|
||||
Loading…
Reference in New Issue