SEC-WEB-001 / OPS-010, ferme. C'est cette directive qui laissait s'executer
le XSS stocke de SEC-XSS-001 au lieu de le bloquer.
Les cinq derniers gabarits sont migres : match_form 13, calendar 11,
teams 11, evaluate_player 9, view_tryout 8. Total sur le chantier : 82
gestionnaires en ligne retires dans 17 gabarits. Il n'en reste aucun.
Deux motifs generiques de plus dans main.js
data-mirror affichage direct de la valeur d'un curseur.
evaluate_player repetait le meme
oninput="this.nextElementSibling.textContent = ..."
sur ses neuf curseurs de note.
data-submit-on-change remplace onchange="this.form.submit()"
Markup genere dans des chaines JavaScript
match_form construisait sept gestionnaires par concatenation, en y
injectant l'identifiant du joueur. Le markup portait deja data-player-id :
returnToPool et assignToTeam lisent desormais leurs arguments depuis
l'element clique. Cela supprime a la fois l'attribut en ligne et la
concatenation qui l'alimentait. Meme motif que dans coach_availability.
Bascule
CSP_ALLOW_INLINE_SCRIPT passe a false. script-src vaut maintenant
'self' 'nonce-<aleatoire par requete>' https://cdn.jsdelivr.net.
La variable d'environnement reste, comme issue de secours si un
deploiement rencontrait un gestionnaire oublie -- mais la laisser active
revient a renoncer a la protection.
Le cliquet devient une garde
Le budget par gabarit est vide et les tests deviennent absolus : aucun
gestionnaire en ligne, et tout bloc <script> inline doit porter son
nonce. Sans nonce, un bloc n'est simplement pas execute, et rien dans les
journaux ne le signale -- d'ou le test.
Verifications
22 pages parcourues avec les trois roles : toutes rendent en 200, aucune
ne contient de gestionnaire en ligne, et chaque bloc inline porte bien le
nonce de sa propre reponse. Syntaxe JavaScript de chaque gabarit verifiee
par node --check.
193 tests. Le dernier xfail de SEC-WEB-001 reussissait, le marqueur est
retire. Il n'en reste qu'un : SEC-AUTH-006, enumeration de comptes.
style-src conserve 'unsafe-inline' : les attributs style="" sont partout et
ne sont pas un vecteur XSS a eux seuls. Migration distincte, non prioritaire.
Co-Authored-By: Claude Opus 5 <[email protected]>
317 lines
13 KiB
HTML
317 lines
13 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}One on One - TryoutPro{% endblock %}
|
|
{% block page_title %}One on One{% endblock %}
|
|
{% block breadcrumb %}<span class="breadcrumb">Home / One on One</span>{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dashboard-grid">
|
|
<!-- Team Notes Section -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-users"></i> Team Notes</h3>
|
|
{% if org_team %}
|
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if team_notes %}
|
|
{% for note in team_notes %}
|
|
<div class="detail-grid mt-4">
|
|
<div class="detail-item full-width">
|
|
<span class="detail-label"><i class="fas fa-user"></i> Coach: {{ note.coach.username if note.coach else 'Unknown Coach' }}</span>
|
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small mt-2">Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted">No team notes have been added yet. Your coach will post improvement suggestions here.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Personal Notes Section -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-user"></i> Personal Notes</h3>
|
|
{% if coach %}
|
|
<span class="badge badge-coach">From: {{ coach.username }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if personal_notes %}
|
|
{% for note in personal_notes %}
|
|
<div class="detail-grid mt-4">
|
|
<div class="detail-item full-width">
|
|
<span class="detail-label"><i class="fas fa-sticky-note"></i> Note from {{ note.coach.username if note.coach else 'Unknown Coach' }}</span>
|
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small mt-2">Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-muted">No personal notes have been added yet. Your coach may provide individual feedback here.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- My One on One Requests Tracker -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-list-check"></i> My One on One Requests</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if my_requests %}
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Discussion Points</th>
|
|
<th>Status</th>
|
|
<th>Coach Response</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for req in my_requests %}
|
|
<tr>
|
|
<td>{{ req.date.strftime('%b %d, %Y') }}</td>
|
|
<td>{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}</td>
|
|
<td>{{ req.points or 'N/A' }}</td>
|
|
<td>
|
|
{% if req.status == 'pending' %}
|
|
<span class="badge badge-warning"><i class="fas fa-clock"></i> Pending</span>
|
|
{% elif req.status == 'approved' %}
|
|
<span class="badge badge-success"><i class="fas fa-check-circle"></i> Approved</span>
|
|
{% elif req.status == 'rejected' %}
|
|
<span class="badge badge-danger"><i class="fas fa-times-circle"></i> Rejected</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if req.status == 'approved' %}
|
|
<span class="text-success">Session confirmed!</span>
|
|
{% elif req.status == 'rejected' %}
|
|
{% if req.coach_rejection_message %}
|
|
<span class="text-danger small">{{ req.coach_rejection_message }}</span>
|
|
{% else %}
|
|
<span class="text-muted small">Coach is unavailable</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-muted small">Awaiting coach response...</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted">You haven't made any One on One requests yet.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid mt-4">
|
|
<!-- One on One Request Section -->
|
|
<div class="card" style="grid-column: 1 / -1;">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-calendar-check"></i> Request One on One Session</h3>
|
|
{% if coach %}
|
|
<span class="badge badge-info">Coach: {{ coach.username }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if coach %}
|
|
<form method="POST" action="{{ url_for('users.one_on_one') }}" class="form" id="oneOnOneForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="date">Select Date</label>
|
|
<select name="date" id="date" class="form-select" data-change="update-time-slots" required>
|
|
{% for d in dates %}
|
|
<option value="{{ d.value }}" data-day="{{ d.day_of_week }}">{{ d.display }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="start_time">Start Time</label>
|
|
<select name="start_time" id="start_time" class="form-select" data-change="update-end-times" required>
|
|
<option value="">-- Select Date First --</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="end_time">End Time</label>
|
|
<select name="end_time" id="end_time" class="form-select" required>
|
|
<option value="">-- Select Start Time First --</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="points">Discussion Points <span class="text-muted">(What would you like to discuss?)</span></label>
|
|
<textarea name="points" id="points" class="form-textarea" placeholder="Enter topics you'd like to cover in your One on One session..." rows="4"></textarea>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-paper-plane"></i> Send Request
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<p class="text-muted">You need to be assigned to a team with a coach to request a One on One session.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if coach %}
|
|
<!-- Hidden data for JavaScript -->
|
|
<script id="coach-availability-data" type="application/json" nonce="{{ csp_nonce }}">
|
|
{{ coach_availability | tojson }}
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script nonce="{{ csp_nonce }}">
|
|
// Time slots from 8:00 AM to 10:00 PM
|
|
const TIME_SLOTS = [];
|
|
for (let h = 8; h <= 22; h++) {
|
|
for (let m = 0; m < 60; m += 30) {
|
|
const displayHour = h > 12 ? h - 12 : h;
|
|
const displayAmpm = h >= 12 ? 'PM' : 'AM';
|
|
const timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
|
|
const displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
|
|
TIME_SLOTS.push({ time: timeStr, display: displayTime });
|
|
}
|
|
}
|
|
|
|
// Coach availability data
|
|
let coachAvailability = [];
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadCoachAvailability();
|
|
updateTimeSlots();
|
|
});
|
|
|
|
function loadCoachAvailability() {
|
|
const dataEl = document.getElementById('coach-availability-data');
|
|
if (!dataEl) return;
|
|
|
|
try {
|
|
coachAvailability = JSON.parse(dataEl.textContent);
|
|
} catch (e) {
|
|
coachAvailability = [];
|
|
}
|
|
}
|
|
|
|
function updateTimeSlots() {
|
|
const dateSelect = document.getElementById('date');
|
|
const startTimeSelect = document.getElementById('start_time');
|
|
|
|
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
|
|
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
|
|
|
|
// Get available time slots for this day
|
|
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
|
|
|
|
// Build available slots - collect all available minutes then sort
|
|
const availableSlots = [];
|
|
const availableMinutes = [];
|
|
dayAvailability.forEach(av => {
|
|
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
|
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
|
|
|
// Add 30-minute slots
|
|
for (let m = startMinutes; m < endMinutes; m += 30) {
|
|
availableMinutes.push(m);
|
|
}
|
|
});
|
|
|
|
// Sort minutes and convert to time strings
|
|
availableMinutes.sort((a, b) => a - b);
|
|
availableMinutes.forEach(m => {
|
|
const hour = Math.floor(m / 60);
|
|
const minute = m % 60;
|
|
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
|
|
availableSlots.push(timeStr);
|
|
});
|
|
|
|
// Update start time options (sorted)
|
|
startTimeSelect.innerHTML = '<option value="">-- Select Start Time --</option>';
|
|
availableSlots.forEach(slot => {
|
|
const slotData = TIME_SLOTS.find(s => s.time === slot);
|
|
if (slotData) {
|
|
const option = document.createElement('option');
|
|
option.value = slotData.time;
|
|
option.textContent = slotData.display;
|
|
startTimeSelect.appendChild(option);
|
|
}
|
|
});
|
|
|
|
// Reset end time options
|
|
updateEndTimeOptions();
|
|
}
|
|
|
|
function updateEndTimeOptions() {
|
|
const dateSelect = document.getElementById('date');
|
|
const startTimeSelect = document.getElementById('start_time');
|
|
const endTimeSelect = document.getElementById('end_time');
|
|
|
|
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
|
|
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
|
|
const selectedStart = startTimeSelect.value;
|
|
|
|
if (!selectedStart) {
|
|
endTimeSelect.innerHTML = '<option value="">-- Select Start Time First --</option>';
|
|
return;
|
|
}
|
|
|
|
// Get available minutes for this day
|
|
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
|
|
const availableMinutes = [];
|
|
dayAvailability.forEach(av => {
|
|
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
|
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
|
for (let m = startMinutes; m < endMinutes; m += 30) {
|
|
availableMinutes.push(m);
|
|
}
|
|
});
|
|
|
|
// Convert selected start to minutes
|
|
const startMinutesVal = selectedStart.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
|
|
|
// Filter end times that are after start time
|
|
const validEndTimes = availableMinutes.filter(m => m > startMinutesVal);
|
|
validEndTimes.sort((a, b) => a - b);
|
|
|
|
// Update end time options
|
|
endTimeSelect.innerHTML = '<option value="">-- Select End Time --</option>';
|
|
validEndTimes.forEach(m => {
|
|
const hour = Math.floor(m / 60);
|
|
const minute = m % 60;
|
|
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
|
|
const slotData = TIME_SLOTS.find(s => s.time === timeStr);
|
|
if (slotData) {
|
|
const option = document.createElement('option');
|
|
option.value = slotData.time;
|
|
option.textContent = slotData.display;
|
|
endTimeSelect.appendChild(option);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Behaviours are declared in the markup with data-action / data-change and
|
|
// dispatched by the delegated listener in main.js. This replaces inline
|
|
// onclick attributes, which no CSP nonce is able to authorise.
|
|
registerActions({
|
|
'update-time-slots': updateTimeSlots,
|
|
'update-end-times': updateEndTimeOptions,
|
|
});
|
|
</script>
|
|
{% endblock %} |