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:
@@ -3,71 +3,13 @@
|
||||
{% block title %}{{ 'staff'|t }} - {{ event.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.staff-table th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.staff-table th.sortable:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.sort-icon::before {
|
||||
content: '\2195';
|
||||
margin-left: 5px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
th.sort-asc .sort-icon::before {
|
||||
content: '\2191';
|
||||
opacity: 1;
|
||||
}
|
||||
th.sort-desc .sort-icon::before {
|
||||
content: '\2193';
|
||||
opacity: 1;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
.modal-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.modal-header h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.modal-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
<div class="event-staff">
|
||||
<div class="staff-header">
|
||||
<h1>{{ 'staff_for'|t }} {{ event.name }}</h1>
|
||||
<a href="{{ url_for('event_detail', code=event.code) }}" class="btn btn-outline">{{ 'back_to_event'|t }}</a>
|
||||
<a href="{{ url_for('event_detail', code=event.code) }}" class="btn btn-outline"><i data-lucide="arrow-left"></i> {{ 'back_to_event'|t }}</a>
|
||||
</div>
|
||||
|
||||
<section class="add-staff-form">
|
||||
<section class="add-staff-form card">
|
||||
<h2>{{ 'add_staff_member'|t }}</h2>
|
||||
<form method="POST" action="{{ url_for('manage_event_staff', event_id=event.id) }}" class="staff-form">
|
||||
<div class="form-row">
|
||||
@@ -85,38 +27,40 @@ th.sort-desc .sort-icon::before {
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'add_staff_member'|t }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i data-lucide="user-plus"></i> {{ 'add_staff_member'|t }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="staff-list">
|
||||
<section class="staff-list card">
|
||||
<h2>{{ 'current_staff'|t }} ({{ staff_members|length }})</h2>
|
||||
{% if staff_members %}
|
||||
<table class="staff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="name" class="sortable">{{ 'name'|t }} <span class="sort-icon"></span></th>
|
||||
<th data-sort="first_name" class="sortable">{{ 'first_name'|t }} <span class="sort-icon"></span></th>
|
||||
<th data-sort="last_name" class="sortable">{{ 'last_name'|t }} <span class="sort-icon"></span></th>
|
||||
<th data-sort="email" class="sortable">{{ 'email'|t }} <span class="sort-icon"></span></th>
|
||||
<th data-sort="status" class="sortable">{{ 'status'|t }} <span class="sort-icon"></span></th>
|
||||
<th data-sort="invite_used" class="sortable">{{ 'status'|t }} <span class="sort-icon"></span></th>
|
||||
<th>{{ 'actions'|t }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="staff-tbody">
|
||||
{% for staff in staff_members %}
|
||||
<tr data-name="{{ staff.first_name }} {{ staff.last_name }}" data-email="{{ staff.email }}" data-status="{{ staff.invite_used }}">
|
||||
<td>{{ staff.first_name }} {{ staff.last_name }}</td>
|
||||
<tr data-first-name="{{ staff.first_name }}" data-last-name="{{ staff.last_name }}" data-email="{{ staff.email }}" data-status="{{ staff.invite_used }}">
|
||||
<td>{{ staff.first_name }}</td>
|
||||
<td>{{ staff.last_name }}</td>
|
||||
<td>{{ staff.email }}</td>
|
||||
<td>
|
||||
{% if staff.invite_used %}
|
||||
<span class="badge badge-success">{{ 'active'|t }}</span>
|
||||
<span class="badge badge-success"><i data-lucide="check"></i> {{ 'active'|t }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-pending">{{ 'invite_pending'|t }}</span>
|
||||
<span class="badge badge-pending"><i data-lucide="clock"></i> {{ 'invite_pending'|t }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('edit_staff', event_id=event.id, staff_id=staff.id) }}" class="btn btn-sm btn-outline">{{ 'edit'|t }}</a>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="showDeleteModal({{ staff.id }}, '{{ staff.first_name }} {{ staff.last_name }}')">{{ 'remove'|t }}</button>
|
||||
<a href="{{ url_for('edit_staff', event_id=event.id, staff_id=staff.id) }}" class="btn btn-sm btn-outline"><i data-lucide="pencil"></i></a>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="showDeleteModal({{ staff.id }}, '{{ staff.first_name }} {{ staff.last_name }}')"><i data-lucide="trash-2"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -128,7 +72,7 @@ th.sort-desc .sort-icon::before {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="deleteModal" class="modal-overlay" style="display: none;">
|
||||
<div id="deleteModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>{{ 'remove_staff_member'|t }}</h2>
|
||||
@@ -136,7 +80,7 @@ th.sort-desc .sort-icon::before {
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeDeleteModal()">{{ 'cancel'|t }}</button>
|
||||
<form id="deleteForm" method="POST" style="display: inline;">
|
||||
<form id="deleteForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger">{{ 'remove'|t }}</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -144,12 +88,17 @@ th.sort-desc .sort-icon::before {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
lucide.createIcons();
|
||||
});
|
||||
|
||||
const sortableHeaders = document.querySelectorAll('.staff-table th.sortable');
|
||||
let currentSort = { column: null, direction: 'asc' };
|
||||
let currentSort = { column: 'first_name', direction: 'asc' };
|
||||
|
||||
sortableHeaders.forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const column = th.dataset.sort;
|
||||
if (!column) return;
|
||||
const direction = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
currentSort = { column, direction };
|
||||
|
||||
@@ -189,4 +138,11 @@ function showDeleteModal(staffId, staffName) {
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user