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
+13 -156
View File
@@ -8,12 +8,12 @@
<p class="welcome-subtitle">{{ 'registration_confirmed'|t }}</p>
{% for event in events %}
<section class="event-section">
<section class="event-section card">
<h2>{{ event.name }}</h2>
<div class="event-details">
<p><strong>{{ 'date'|t }}:</strong> {{ event.start_time|localized_date if event.start_time else 'TBD' }}</p>
<p><strong>{{ 'location'|t }}:</strong> {{ event.location }}</p>
<p><strong>{{ 'status'|t }}:</strong> <span class="status-registered">{{ 'registered'|t }}</span></p>
<p><strong>{{ 'status'|t }}:</strong> <span class="status-registered"><i data-lucide="check-circle"></i> {{ 'registered'|t }}</span></p>
</div>
<h3>{{ 'breakout_sessions'|t }}</h3>
@@ -23,179 +23,36 @@
<div class="session-item {% if session.my_rsvp_status == 'registered' %}registered{% endif %}">
<div class="session-info">
<strong>{{ session.name }}</strong>
<span class="session-time">{{ session.start_time|localized_date('%H:%M') if session.start_time else '' }} - {{ session.end_time|localized_date('%H:%M') if session.end_time else '' }}</span>
<span class="session-location">{{ session.location }}</span>
<span class="session-time"><i data-lucide="clock"></i> {{ session.start_time|localized_date('%H:%M') if session.start_time else '' }} - {{ session.end_time|localized_date('%H:%M') if session.end_time else '' }}</span>
<span class="session-location"><i data-lucide="map-pin"></i> {{ session.location }}</span>
{% if session.description %}
<span class="session-description">{{ session.description }}</span>
{% endif %}
</div>
<div class="session-status">
{% if session.my_rsvp_status == 'registered' %}
<span class="badge badge-success">{{ 'registered'|t }}</span>
<span class="badge badge-success"><i data-lucide="check"></i> {{ 'registered'|t }}</span>
{% else %}
<span class="badge badge-secondary">{{ 'not_registered'|t }}</span>
<span class="badge badge-pending">{{ 'not_registered'|t }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="no-sessions">{{ 'no_breakout_sessions'|t }}</p>
<p class="no-sessions"><i data-lucide="calendar"></i> {{ 'no_breakout_sessions'|t }}</p>
{% endif %}
</section>
{% endfor %}
<div class="actions">
<a href="{{ url_for('attendee_dashboard') }}" class="btn btn-primary">{{ 'go_to_dashboard'|t }}</a>
<a href="{{ url_for('attendee_dashboard') }}" class="btn btn-primary"><i data-lucide="layout-dashboard"></i> {{ 'go_to_dashboard'|t }}</a>
</div>
</div>
{% endblock %}
{% block extra_styles %}
<style>
.personal-page {
max-width: 900px;
margin: 0 auto;
padding: 50px 40px;
}
.personal-page h1 {
color: #2c3e50;
margin-bottom: 5px;
}
.welcome-subtitle {
color: #666;
margin-bottom: 30px;
}
.event-section {
background: #fff;
padding: 25px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 25px;
}
.event-section h2 {
color: #2c3e50;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #3498db;
}
.event-details {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.event-details p {
margin: 8px 0;
color: #555;
}
.status-registered {
color: #27ae60;
font-weight: bold;
}
.event-section h3 {
color: #2c3e50;
margin: 20px 0 15px 0;
}
.sessions-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.session-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #95a5a6;
}
.session-item.registered {
border-left-color: #27ae60;
background: #f0f9f4;
}
.session-info {
flex: 1;
}
.session-info strong {
display: block;
color: #2c3e50;
margin-bottom: 5px;
}
.session-info span {
display: block;
font-size: 13px;
color: #666;
}
.session-info .session-time {
color: #3498db;
font-weight: 500;
}
.session-info .session-location {
color: #888;
}
.session-info .session-description {
color: #666;
margin-top: 5px;
font-style: italic;
}
.session-status {
margin-left: 20px;
}
.badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.badge-success {
background: #27ae60;
color: white;
}
.badge-secondary {
background: #95a5a6;
color: white;
}
.no-sessions {
text-align: center;
color: #888;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.actions {
text-align: center;
margin-top: 30px;
}
.actions .btn {
display: inline-block;
padding: 12px 30px;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}