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:
+4669
-514
File diff suppressed because it is too large
Load Diff
+13
-9
@@ -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 |
@@ -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
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user