Security hardening: env-based config, CSRF protection, secure code generation, plus presenter management and email communications system

- Move all credentials from hardcoded to .env with python-dotenv (add .env.example template and .gitignore)
- Add CSRF token generation and validation on all state-changing requests
- Replace random.choices with secrets.choice for all secure code generation
- Add session cookie security headers (HttpOnly, Secure, SameSite)
- Add presenter management: assign/remove presenters to breakout sessions, presenter QR scanning
- Add email communications system: templates per event, custom email sending, sent email tracking
- Add staff/organizer profile pages with staff code display
- New DB tables: event_email_templates, sent_emails, user_communications
- New DB columns: staff.reminder_count/reminder_dates, breakout_session_organizers.presenter_code
- Expand English translation strings across all new features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:34:48 +00:00
parent 934848b7f5
commit a3546b4c01
45 changed files with 8296 additions and 3207 deletions
+4669 -514
View File
File diff suppressed because it is too large Load Diff
+13 -9
View File
@@ -1,9 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#1a1a2e"/>
<text x="16" y="22" font-family="Arial, sans-serif" font-size="14" font-weight="700" fill="#ffffff" text-anchor="middle">NE</text>
<circle cx="6" cy="10" r="2" fill="#3b82f6"/>
<circle cx="26" cy="10" r="2" fill="#3b82f6"/>
<circle cx="16" cy="6" r="2" fill="#3b82f6"/>
<line x1="6" y1="10" x2="16" y2="6" stroke="#3b82f6" stroke-width="1.5"/>
<line x1="26" y1="10" x2="16" y2="6" stroke="#3b82f6" stroke-width="1.5"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<!-- Hexagon background -->
<polygon points="16,1 29,8 29,23 16,30 3,23 3,8" fill="#1e3a5f"/>
<!-- 4 dots in a square -->
<circle cx="10" cy="10" r="2.5" fill="#ffffff" opacity="0.9"/>
<circle cx="22" cy="10" r="2.5" fill="#ffffff" opacity="0.9"/>
<circle cx="22" cy="21" r="2.5" fill="#ffffff" opacity="0.9"/>
<circle cx="10" cy="21" r="2.5" fill="#ffffff" opacity="0.9"/>
<!-- N-pattern: BL->TL, TL->BR, BR->TR -->
<line x1="10" y1="21" x2="10" y2="10" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
<line x1="10" y1="10" x2="22" y2="21" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
<line x1="22" y1="21" x2="22" y2="10" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 789 B

+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 40" fill="none">
<!-- Icon Mark -->
<g>
<!-- Hexagon background -->
<polygon points="20,2 36,10 36,26 20,34 4,26 4,10" fill="#1e3a5f"/>
<!-- 4 dots in a square: TL, TR, BR, BL -->
<circle cx="12" cy="12" r="3" fill="#ffffff" opacity="0.9"/>
<circle cx="28" cy="12" r="3" fill="#ffffff" opacity="0.9"/>
<circle cx="28" cy="24" r="3" fill="#ffffff" opacity="0.9"/>
<circle cx="12" cy="24" r="3" fill="#ffffff" opacity="0.9"/>
<!-- N-pattern connections: BL->TL, TL->BR, BR->TR -->
<line x1="12" y1="24" x2="12" y2="12" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
<line x1="12" y1="12" x2="28" y2="24" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
<line x1="28" y1="24" x2="28" y2="12" stroke="#ffffff" stroke-width="1.5" opacity="0.7"/>
</g>
<!-- Wordmark -->
<text x="46" y="27" font-family="'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="18" font-weight="800" fill="#1e3a5f" letter-spacing="-0.02em">NetEvents</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+326 -19
View File
@@ -1,45 +1,186 @@
// NetEvent - Main JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Auto-hide flash messages after 5 seconds
// ----- CSRF Token Injection -----
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
if (csrfToken) {
document.querySelectorAll('form[method="POST"], form[method="post"]').forEach(function(form) {
if (!form.querySelector('input[name="_csrf_token"]')) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = '_csrf_token';
input.value = csrfToken;
form.appendChild(input);
}
});
}
// ----- Toast Notification System -----
const toastContainer = document.getElementById('toast-container');
function showToast(message, type = 'info', duration = 5000) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const iconMap = {
success: 'check-circle',
error: 'x-circle',
warning: 'alert-triangle',
info: 'info'
};
toast.innerHTML = `
<div class="toast-icon">
<i data-lucide="${iconMap[type] || 'info'}"></i>
</div>
<div class="toast-content">
<p class="toast-message">${message}</p>
</div>
<button class="toast-close" aria-label="Close">
<i data-lucide="x"></i>
</button>
`;
toastContainer.appendChild(toast);
lucide.createIcons({ icons: { 'check-circle': lucide.icons['check-circle'], 'x-circle': lucide.icons['x-circle'], 'alert-triangle': lucide.icons['alert-triangle'], 'info': lucide.icons['info'], 'x': lucide.icons['x'] } });
// Close button handler
toast.querySelector('.toast-close').addEventListener('click', () => {
dismissToast(toast);
});
// Auto dismiss
if (duration > 0) {
setTimeout(() => {
dismissToast(toast);
}, duration);
}
return toast;
}
function dismissToast(toast) {
toast.classList.add('toast-out');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
// Convert existing flash messages to toasts
const flashMessages = document.querySelectorAll('.flash-message');
flashMessages.forEach(msg => {
setTimeout(() => {
msg.style.transition = 'opacity 0.5s';
msg.style.opacity = '0';
setTimeout(() => msg.remove(), 500);
}, 5000);
const message = msg.textContent.trim();
const typeMatch = message.toLowerCase().match(/\b(success|error|warning|info)\b/);
const type = typeMatch ? typeMatch[1] : 'info';
// Create toast (no auto-dismiss for flash messages, they persist)
showToast(message, type, 0);
// Remove original flash message
msg.remove();
});
// Form validation
// Remove flash-messages container if empty
const flashContainer = document.querySelector('.flash-messages');
if (flashContainer && flashContainer.children.length === 0) {
flashContainer.remove();
}
// ----- Hamburger Menu Toggle -----
const hamburger = document.querySelector('.hamburger');
const navLinks = document.querySelector('.nav-links');
if (hamburger && navLinks) {
hamburger.addEventListener('click', function() {
navLinks.classList.toggle('active');
const isExpanded = navLinks.classList.contains('active');
hamburger.setAttribute('aria-expanded', isExpanded);
// Toggle icon visibility using CSS classes
const menuIcon = hamburger.querySelector('.hamburger-icon');
const closeIcon = hamburger.querySelector('.hamburger-close-icon');
if (isExpanded) {
menuIcon.classList.add('d-none');
closeIcon.classList.remove('d-none');
} else {
menuIcon.classList.remove('d-none');
closeIcon.classList.add('d-none');
}
});
// Close menu when clicking a link (for mobile)
navLinks.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
navLinks.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
hamburger.querySelector('.hamburger-icon').classList.remove('d-none');
hamburger.querySelector('.hamburger-close-icon').classList.add('d-none');
});
});
// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!hamburger.contains(e.target) && !navLinks.contains(e.target)) {
navLinks.classList.remove('active');
hamburger.setAttribute('aria-expanded', 'false');
const menuIcon = hamburger.querySelector('.hamburger-icon');
const closeIcon = hamburger.querySelector('.hamburger-close-icon');
if (menuIcon) menuIcon.classList.remove('d-none');
if (closeIcon) closeIcon.classList.add('d-none');
}
});
}
// ----- Form Validation with Toast -----
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
// Skip if already handled or has data-no-validate
if (e.target.dataset.noValidate) return;
const requiredInputs = form.querySelectorAll('[required]');
let isValid = true;
let firstInvalid = null;
requiredInputs.forEach(input => {
if (!input.value.trim()) {
isValid = false;
input.style.borderColor = '#ef4444';
input.style.borderColor = 'var(--danger-color)';
input.style.boxShadow = '0 0 0 3px var(--danger-light)';
if (!firstInvalid) firstInvalid = input;
} else {
input.style.borderColor = '';
input.style.boxShadow = '';
}
});
if (!isValid && !e.target.dataset.noValidate) {
if (!isValid) {
e.preventDefault();
alert('Please fill in all required fields.');
showToast('Please fill in all required fields.', 'error');
if (firstInvalid) firstInvalid.focus();
}
});
});
// Photo upload form
// ----- Photo Upload Form -----
const photoForm = document.querySelector('.photo-upload-form');
if (photoForm) {
const submitBtn = photoForm.querySelector('button[type="submit"]');
photoForm.addEventListener('submit', async function(e) {
e.preventDefault();
// Add loading state
if (submitBtn) {
submitBtn.classList.add('btn-loading');
submitBtn.disabled = true;
}
const formData = new FormData(this);
try {
@@ -51,23 +192,38 @@ document.addEventListener('DOMContentLoaded', function() {
const data = await response.json();
if (data.success) {
showToast('Photo updated successfully!', 'success');
// Reload page to show new photo
location.reload();
setTimeout(() => location.reload(), 1000);
} else {
alert(data.error || 'Error uploading photo');
showToast(data.error || 'Error uploading photo', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
} catch (error) {
alert('Error uploading photo');
showToast('Error uploading photo', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
});
}
// Connection request buttons
// ----- Connection Request Buttons -----
const connectForms = document.querySelectorAll('.connect-form');
connectForms.forEach(form => {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = this.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.classList.add('btn-loading');
submitBtn.disabled = true;
}
const formData = new FormData(this);
try {
@@ -81,13 +237,164 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) {
const card = this.closest('.attendee-card');
const actionsDiv = card.querySelector('.attendee-actions');
actionsDiv.innerHTML = '<span class="badge badge-pending">Request Pending</span>';
actionsDiv.innerHTML = '<span class="badge badge-pending"><i data-lucide="clock"></i> Request Pending</span>';
lucide.createIcons();
showToast('Connection request sent!', 'success');
} else {
alert(data.error || 'Error sending connection request');
showToast(data.error || 'Error sending connection request', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
} catch (error) {
alert('Error sending connection request');
showToast('Error sending connection request', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
});
});
});
// ----- Button Loading States on Form Submit -----
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(e) {
// Skip if already loading or novalidate
if (this.dataset.noValidate) return;
const submitBtn = this.querySelector('button[type="submit"]:not([data-no-loading])');
if (submitBtn && !submitBtn.classList.contains('btn-loading')) {
submitBtn.classList.add('btn-loading');
submitBtn.disabled = true;
}
});
});
// ----- Toggle Switch Initialization -----
const toggles = document.querySelectorAll('.toggle input');
toggles.forEach(toggle => {
toggle.addEventListener('change', function() {
const form = this.closest('form');
if (form) {
form.submit();
}
});
});
// ----- Modal Close on Escape -----
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(modal => {
const closeBtn = modal.querySelector('.modal-close');
if (closeBtn) closeBtn.click();
});
}
});
// ----- Copy Link Utility (shared across templates) -----
window.copyLink = function(url) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(function() {
showToast('Link copied!', 'success');
}).catch(function() {
fallbackCopy(url);
});
} else {
fallbackCopy(url);
}
};
window.fallbackCopy = function(url) {
var textArea = document.createElement('textarea');
textArea.value = url;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showToast('Link copied!', 'success');
} catch (err) {
showToast('Failed to copy link.', 'error');
}
document.body.removeChild(textArea);
};
// ----- Smooth scroll for anchor links -----
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const target = document.querySelector(targetId);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// ----- Custom Confirmation Modal (replaces native confirm()) -----
const confirmModal = document.getElementById('confirm-modal');
const confirmTitle = document.getElementById('confirm-title');
const confirmMessage = document.getElementById('confirm-message');
const confirmOk = document.getElementById('confirm-ok');
const confirmCancel = document.getElementById('confirm-cancel');
let confirmCallback = null;
function showConfirm(message, callback, title) {
confirmMessage.textContent = message;
confirmTitle.textContent = title || 'Confirm';
confirmModal.style.display = 'flex';
confirmCallback = callback;
confirmOk.focus();
}
function hideConfirm() {
confirmModal.style.display = 'none';
confirmCallback = null;
}
if (confirmOk) {
confirmOk.addEventListener('click', function() {
if (confirmCallback) confirmCallback();
hideConfirm();
});
}
if (confirmCancel) {
confirmCancel.addEventListener('click', hideConfirm);
}
if (confirmModal) {
confirmModal.addEventListener('click', function(e) {
if (e.target === confirmModal) hideConfirm();
});
}
// Intercept data-confirm on forms and buttons
document.addEventListener('click', function(e) {
const target = e.target.closest('[data-confirm]');
if (!target) return;
const message = target.getAttribute('data-confirm');
if (!message) return;
e.preventDefault();
showConfirm(message, function() {
if (target.tagName === 'FORM') {
target.submit();
} else if (target.tagName === 'A') {
window.location.href = target.getAttribute('href');
} else {
// For buttons, remove data-confirm and re-click
target.removeAttribute('data-confirm');
target.click();
target.setAttribute('data-confirm', message);
}
});
}, true);
});