a3546b4c01
- 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>
130 lines
4.3 KiB
HTML
130 lines
4.3 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ 'scan_qr'|t }} - NetEvents{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="scan-page">
|
|
<div class="scan-header">
|
|
<h1>{{ 'scan_attendee'|t }}</h1>
|
|
<p>{{ 'scan_qr_description'|t }}</p>
|
|
<a href="{{ url_for('attendee_dashboard') }}" class="btn btn-outline"><i data-lucide="arrow-left"></i> {{ 'back_to_dashboard'|t }}</a>
|
|
</div>
|
|
|
|
<div class="scanner-container">
|
|
<div id="qr-reader" class="qr-reader"></div>
|
|
<div id="scan-result" class="scan-result hidden">
|
|
<div class="result-icon" id="result-icon"></div>
|
|
<h2 id="result-title"></h2>
|
|
<p id="result-message"></p>
|
|
<button id="scan-again" class="btn btn-primary"><i data-lucide="scan-line"></i> {{ 'scan_again'|t }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scan-info">
|
|
<p>{{ 'scan_info_text'|t }}</p>
|
|
<p><strong>{{ 'scan_info_warning'|t }}</strong></p>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
|
<script>
|
|
const myId = {{ session.user_id }};
|
|
const myEventId = {{ session.event_id }};
|
|
let html5QrCode;
|
|
let scanning = true;
|
|
|
|
async function sendConnectionRequest(scannedId) {
|
|
try {
|
|
const response = await fetch('/attendee/scan-request', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ scanned_id: scannedId })
|
|
});
|
|
|
|
const data = await response.json();
|
|
const resultDiv = document.getElementById('scan-result');
|
|
const icon = document.getElementById('result-icon');
|
|
const title = document.getElementById('result-title');
|
|
const message = document.getElementById('result-message');
|
|
|
|
icon.className = 'result-icon';
|
|
icon.innerHTML = '<i data-lucide="clock"></i>';
|
|
|
|
if (data.success) {
|
|
document.getElementById('qr-reader').style.display = 'none';
|
|
resultDiv.classList.remove('hidden');
|
|
icon.classList.add('pending');
|
|
icon.innerHTML = '<i data-lucide="clock"></i>';
|
|
title.textContent = '{{ "request_sent"|t }}';
|
|
message.textContent = '{{ "request_sent_message"|t }}';
|
|
} else {
|
|
document.getElementById('qr-reader').style.display = 'none';
|
|
resultDiv.classList.remove('hidden');
|
|
icon.classList.add('error');
|
|
icon.innerHTML = '<i data-lucide="x-circle"></i>';
|
|
title.textContent = '{{ "error"|t }}';
|
|
message.textContent = data.error || '{{ "failed_send_request"|t }}';
|
|
}
|
|
|
|
lucide.createIcons();
|
|
scanning = false;
|
|
} catch (error) {
|
|
console.error('Connection error:', error);
|
|
showToast('Error sending connection request', 'error');
|
|
}
|
|
}
|
|
|
|
function onScanSuccess(decodedText) {
|
|
if (!scanning) return;
|
|
|
|
// Expected format: "NETEVENT:{event_id}:{attendee_id}"
|
|
const parts = decodedText.split(':');
|
|
if (parts.length === 3 && parts[0] === 'NETEVENT') {
|
|
const eventId = parseInt(parts[1]);
|
|
const attendeeId = parseInt(parts[2]);
|
|
|
|
if (eventId === myEventId) {
|
|
if (attendeeId === myId) {
|
|
showToast('{{ "cannot_scan_own_qr"|t }}', 'warning');
|
|
return;
|
|
}
|
|
sendConnectionRequest(attendeeId);
|
|
} else {
|
|
showToast('{{ "qr_different_event"|t }}', 'error');
|
|
}
|
|
} else {
|
|
// QR format not recognized
|
|
showToast('{{ "unrecognized_qr"|t }}', 'error');
|
|
}
|
|
}
|
|
|
|
function startScanner() {
|
|
html5QrCode = new Html5Qrcode("qr-reader");
|
|
|
|
html5QrCode.start(
|
|
{ facingMode: "environment" },
|
|
{
|
|
fps: 10,
|
|
qrbox: { width: 250, height: 250 }
|
|
},
|
|
onScanSuccess,
|
|
(errorMessage) => {
|
|
// Ignore scan errors
|
|
}
|
|
).catch(err => {
|
|
console.error('Camera error:', err);
|
|
showToast('{{ "camera_permission_error"|t }}', 'error');
|
|
});
|
|
}
|
|
|
|
function resetScanner() {
|
|
document.getElementById('qr-reader').style.display = 'block';
|
|
document.getElementById('scan-result').classList.add('hidden');
|
|
scanning = true;
|
|
}
|
|
|
|
document.getElementById('scan-again').addEventListener('click', resetScanner);
|
|
|
|
window.addEventListener('DOMContentLoaded', startScanner);
|
|
</script>
|
|
{% endblock %} |