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
+6 -79
View File
@@ -25,11 +25,11 @@
{% if req.profile_picture %}
<img src="{{ url_for('uploaded_file', filename=req.profile_picture) }}" alt="{{ req.first_name }}" class="profile-photo">
{% else %}
<div class="profile-photo-placeholder">{{ req.first_name[0] }}{{ req.last_name[0] }}</div>
<div class="profile-photo-placeholder"><span class="first-name">{{ req.first_name[0] }}</span><span class="last-name">{{ req.last_name[0] }}</span></div>
{% endif %}
</div>
<div class="request-info">
<h3>{{ req.first_name }} {{ req.last_name }}</h3>
<h3><span class="first-name">{{ req.first_name }}</span> <span class="last-name">{{ req.last_name }}</span></h3>
<p class="request-email">{{ req.email }}</p>
<p class="request-org">{{ req.organisation if req.organisation else '' }}{% if req.role %} - {{ req.role }}{% endif %}</p>
{% if req.introduction %}
@@ -38,8 +38,8 @@
<p class="request-time">{{ 'requested'|t }} {{ req.created_at|localized_date if req.created_at else 'recently' }}</p>
</div>
<div class="request-actions">
<button class="btn btn-success respond-btn" data-connection-id="{{ req.id }}" data-action="approve">{{ 'approve'|t }}</button>
<button class="btn btn-danger respond-btn" data-connection-id="{{ req.id }}" data-action="reject">{{ 'reject'|t }}</button>
<button class="btn btn-success btn-sm respond-btn" data-connection-id="{{ req.id }}" data-action="approve">{{ 'approve'|t }}</button>
<button class="btn btn-danger btn-sm respond-btn" data-connection-id="{{ req.id }}" data-action="reject">{{ 'reject'|t }}</button>
</div>
</div>
{% endfor %}
@@ -70,85 +70,12 @@ document.querySelectorAll('.respond-btn').forEach(btn => {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Error processing request');
showToast(data.error || 'Error processing request', 'error');
}
} catch (error) {
alert('Error processing request');
showToast('Error processing request', 'error');
}
});
});
</script>
<style>
.request-card {
display: flex;
gap: 20px;
padding: 20px;
border: 1px solid #e2e8f0;
border-radius: 12px;
margin-bottom: 15px;
background: #fff;
}
.request-profile {
flex-shrink: 0;
}
.profile-photo {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.profile-photo-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: #64748b;
}
.request-info {
flex: 1;
}
.request-info h3 {
margin: 0 0 5px 0;
}
.request-email {
color: #64748b;
margin: 0 0 5px 0;
}
.request-org {
color: #475569;
margin: 0 0 10px 0;
}
.request-intro {
font-style: italic;
color: #64748b;
margin: 0 0 10px 0;
}
.request-time {
font-size: 12px;
color: #94a3b8;
margin: 0;
}
.request-actions {
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
}
</style>
{% endblock %}