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:
+60
-138
@@ -2,61 +2,30 @@
|
||||
|
||||
{% block title %}{{ 'profile'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.profile-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 50px 40px;
|
||||
}
|
||||
.profile-container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.photo-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.photo-box h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.profile-photo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile-page">
|
||||
<h1>{{ 'my_profile'|t }}</h1>
|
||||
|
||||
<div class="profile-container">
|
||||
<div class="photo-box">
|
||||
<div class="profile-photo-box">
|
||||
<h3>{{ 'profile_photo'|t }}</h3>
|
||||
<div class="profile-photo-section">
|
||||
<div class="current-photo" id="photo-container">
|
||||
{% if attendee.profile_picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + attendee.profile_picture) }}" alt="{{ 'profile_photo'|t }}" class="profile-img" id="profile-preview">
|
||||
{% else %}
|
||||
<img src="" alt="{{ 'profile_preview'|t }}" class="profile-img" id="profile-preview" style="display: none;">
|
||||
<div class="no-photo" id="no-photo">{{ 'no_photo'|t }}</div>
|
||||
<img src="" alt="{{ 'profile_preview'|t }}" class="profile-img d-none" id="profile-preview">
|
||||
<div class="no-photo" id="no-photo"><i data-lucide="user"></i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<canvas id="crop-canvas" style="display: none;"></canvas>
|
||||
<canvas id="crop-canvas" class="d-none"></canvas>
|
||||
|
||||
<div class="zoom-controls">
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(-0.25)">-</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(-0.25)"><i data-lucide="minus"></i></button>
|
||||
<span id="zoom-level">100%</span>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(0.25)">+</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="handleFormSubmit()">{{ 'save'|t }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(0.25)"><i data-lucide="plus"></i></button>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="handleFormSubmit()">{{ 'save'|t }}</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -67,14 +36,21 @@
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('attendee_profile') }}" class="profile-form">
|
||||
<div class="form-group">
|
||||
<label for="first_name">{{ 'first_name'|t }}</label>
|
||||
<input type="text" id="first_name" name="first_name" value="{{ attendee.first_name }}" required>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="first_name">{{ 'first_name'|t }}</label>
|
||||
<input type="text" id="first_name" name="first_name" value="{{ attendee.first_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last_name">{{ 'last_name'|t }}</label>
|
||||
<input type="text" id="last_name" name="last_name" value="{{ attendee.last_name }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last_name">{{ 'last_name'|t }}</label>
|
||||
<input type="text" id="last_name" name="last_name" value="{{ attendee.last_name }}" required>
|
||||
<label for="email">{{ 'email'|t }}</label>
|
||||
<input type="email" id="email" name="email" value="{{ attendee.email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -87,14 +63,16 @@
|
||||
<input type="text" id="role" name="role" value="{{ attendee.role or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">{{ 'phone'|t }}</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ attendee.phone or '' }}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="phone">{{ 'phone'|t }}</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ attendee.phone or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="linkedin">{{ 'linkedin'|t }}</label>
|
||||
<input type="url" id="linkedin" name="linkedin" value="{{ attendee.linkedin or '' }}">
|
||||
<div class="form-group">
|
||||
<label for="linkedin">{{ 'linkedin'|t }}</label>
|
||||
<input type="url" id="linkedin" name="linkedin" value="{{ attendee.linkedin or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -102,14 +80,35 @@
|
||||
<textarea id="introduction" name="introduction" rows="4">{{ attendee.introduction or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="preferred_language">{{ 'language'|t }}</label>
|
||||
<select id="preferred_language" name="preferred_language">
|
||||
{% for lang in ['en', 'nl', 'de', 'fr', 'es', 'it', 'pl'] %}
|
||||
<option value="{{ lang }}" {% if attendee.preferred_language == lang %}selected{% endif %}>{{ lang }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group switch-group">
|
||||
<label for="visible_to_others">{{ 'visible_to_attendees'|t }}</label>
|
||||
<label class="switch">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="visible_to_others" name="visible_to_others" value="1" {% if attendee.visible_to_others %}checked{% endif %}>
|
||||
<span class="slider"></span>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>{{ 'attendee_code'|t }}</label>
|
||||
<input type="text" value="{{ attendee.attendee_code or '' }}" readonly class="input-readonly">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ 'member_since'|t }}</label>
|
||||
<input type="text" value="{{ attendee.created_at|localized_date if attendee.created_at else '' }}" readonly class="input-readonly">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'update_profile'|t }}</button>
|
||||
</div>
|
||||
@@ -117,87 +116,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.switch-group label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 26px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
.current-photo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
position: relative;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.profile-img {
|
||||
position: absolute;
|
||||
border-radius: 0;
|
||||
cursor: move;
|
||||
}
|
||||
.no-photo, #no-photo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var zoomLevel = 1.0;
|
||||
var translateX = 0;
|
||||
@@ -334,7 +252,7 @@ function handleFormSubmit(e) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
if (!originalImage) {
|
||||
alert('Please select an image first');
|
||||
showToast('Please select an image first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -393,16 +311,20 @@ function handleFormSubmit(e) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
showToast('Profile photo updated!', 'success');
|
||||
setTimeout(function() { location.reload(); }, 1000);
|
||||
} else {
|
||||
alert(data.error || 'Upload failed');
|
||||
showToast(data.error || 'Upload failed', 'error');
|
||||
}
|
||||
}).catch(function(error) {
|
||||
alert('Error uploading photo');
|
||||
showToast('Error uploading photo', 'error');
|
||||
});
|
||||
}, 'image/jpeg', 0.9);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
init();
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user