SEC-WEB-001 / OPS-010. script-src porte toujours 'unsafe-inline' : c'est
pour cela que le XSS stocke de SEC-XSS-001 s'executait au lieu d'etre
bloque. Le retirer n'est pas un changement d'une ligne.
Ce qui bloque reellement
Un nonce autorise des elements <script> ; il ne peut rien pour un
attribut onclick="...". Mesure faite : 76 gestionnaires en ligne repartis
dans 15 gabarits. Tant qu'il en reste un, la politique ne peut pas etre
durcie.
Piege supplementaire, documente dans build_csp() : en CSP niveau 3, un
navigateur ignore 'unsafe-inline' des qu'un nonce est present. Emettre
les deux ne serait donc pas une transition douce -- ce serait couper
d'un coup tous les scripts en ligne et tous les onclick, et uniquement
sur les navigateurs recents. La bascule doit etre atomique, d'ou un
drapeau unique : CSP_ALLOW_INLINE_SCRIPT.
Infrastructure posee
build_csp() assemble l'en-tete selon le drapeau. Un nonce est genere par
requete et n'est emis que lorsque l'inline est interdit. Les 15 blocs
<script> portent deja nonce="{{ csp_nonce }}", inerte aujourd'hui : la
bascule finale sera un changement de configuration, pas de gabarits.
Couche partagee migree en premier
base.html et macros.html sont rendus sur absolument toutes les pages. Six
gestionnaires retires, remplaces par des attributs data-action et un
ecouteur delegue unique dans main.js. La delegation plutot qu'un
ecouteur par widget : le contenu injecte dynamiquement herite du
comportement sans re-attachement.
Un cliquet plutot qu'une promesse
tests/test_csp.py fixe un budget par gabarit qui ne peut que baisser.
Ajouter un gestionnaire en ligne fait echouer la suite ; en retirer sans
mettre le budget a jour aussi, ce qui force a enregistrer la progression
dans le diff. A zero, il ne reste qu'a basculer le drapeau.
Le cliquet a d'ailleurs corrige mon propre relevé : mon grep initial
comptait 83 gestionnaires, la mesure exacte en donne 76 -- le motif ne
verifiait pas l'espace avant l'attribut.
style-src conserve 'unsafe-inline' : les attributs style="" sont partout et
ne constituent pas un vecteur XSS a eux seuls. Migration distincte.
192 tests.
Co-Authored-By: Claude Opus 5 <[email protected]>
276 lines
13 KiB
HTML
276 lines
13 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}Notes - TryoutPro{% endblock %}
|
|
{% block page_title %}Notes{% endblock %}
|
|
{% block breadcrumb %}<span class="breadcrumb">Home / Notes</span>{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dashboard-grid">
|
|
<!-- Add Team Notes Section -->
|
|
{% if org_team %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-users"></i> Team Notes</h3>
|
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('users.manage_team_notes') }}" class="form" id="teamNotesForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
|
|
<div class="form-group">
|
|
<label for="team_notes_content">Team Notes Content</label>
|
|
<textarea name="content" id="team_notes_content" class="form-textarea" rows="4" placeholder="Enter improvement suggestions and notes for your team...">{{ latest_team_note.content if latest_team_note else '' }}</textarea>
|
|
<p class="form-text">These notes will be visible to all players on your team.</p>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i> {% if latest_team_note %}Update{% else %}Add{% endif %} Team Notes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Add Personal Note Section -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-user-friends"></i> Add Personal Note</h3>
|
|
{% if org_team %}
|
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('users.add_personal_note') }}" class="form" id="personalNoteForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
|
|
<div class="form-group">
|
|
<label for="player_id">Select Player</label>
|
|
<select name="player_id" id="player_id" class="form-select" required>
|
|
<option value="">-- Select a Player --</option>
|
|
{% for player in players %}
|
|
<option value="{{ player.id }}">{{ player.username }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="note_content">Note Content</label>
|
|
<textarea name="content" id="note_content" class="form-textarea" rows="3" placeholder="Enter personal feedback or coaching tips for this player..." required></textarea>
|
|
<p class="form-text">These notes will only be visible to the selected player.</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="context">Context (Optional)</label>
|
|
<p class="form-text text-muted">Link this note to a specific match, tryout, or team for better organization.</p>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="note_match_id">Match</label>
|
|
<select name="match_id" id="note_match_id" class="form-select">
|
|
<option value="">-- Select Match --</option>
|
|
{% for match in matches %}
|
|
<option value="{{ match.id }}">{{ match.title }} - {{ match.date.strftime('%m/%d/%Y') }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="note_tryout_id">Tryout</label>
|
|
<select name="tryout_id" id="note_tryout_id" class="form-select">
|
|
<option value="">-- Select Tryout --</option>
|
|
{% for tryout in tryouts %}
|
|
<option value="{{ tryout.id }}">{{ tryout.title }} - {{ tryout.date.strftime('%m/%d/%Y') }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="note_team_id">Team</label>
|
|
<select name="team_id" id="note_team_id" class="form-select">
|
|
<option value="">-- Select Team --</option>
|
|
{% for team in teams %}
|
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i> Add Note
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- One on One Requests Section -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-calendar-check"></i> One on One Requests</h3>
|
|
{% if org_team %}
|
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if one_on_one_requests %}
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Player</th>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Discussion Points</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for req in one_on_one_requests %}
|
|
<tr>
|
|
<td>{{ req.player.username if req.player else 'Unknown' }}</td>
|
|
<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">Pending</span>
|
|
{% elif req.status == 'approved' %}
|
|
<span class="badge badge-success">Approved</span>
|
|
{% elif req.status == 'rejected' %}
|
|
<span class="badge badge-danger">Rejected</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if req.status == 'pending' %}
|
|
<form method="POST" action="{{ url_for('users.accept_one_on_one', request_id=req.id) }}" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="submit" class="btn btn-sm btn-success" title="Accept">
|
|
<i class="fas fa-check"></i> Accept
|
|
</button>
|
|
</form>
|
|
<button type="button" class="btn btn-sm btn-danger" onclick="showRejectModal({{ req.id }})" title="Refuse">
|
|
<i class="fas fa-times"></i> Refuse
|
|
</button>
|
|
{% elif req.status == 'rejected' and req.coach_rejection_message %}
|
|
<span class="text-muted small" title="{{ req.coach_rejection_message }}">Reason: {{ req.coach_rejection_message[:50] }}{% if req.coach_rejection_message|length > 50 %}...{% endif %}</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted">No One on One requests from your players yet.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reject Modal -->
|
|
<div id="rejectModal" class="modal" style="display:none;">
|
|
<div class="modal-overlay" onclick="hideRejectModal()"></div>
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4><i class="fas fa-times-circle"></i> Reject One on One Request</h4>
|
|
<button type="button" class="modal-close" onclick="hideRejectModal()">×</button>
|
|
</div>
|
|
<form id="rejectForm" method="POST" action="">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label for="rejection_reason">Reason for rejection (optional):</label>
|
|
<textarea name="rejection_reason" id="rejection_reason" class="form-textarea" rows="3" placeholder="Let the player know why this time doesn't work..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="hideRejectModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-danger">Reject Request</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid mt-4">
|
|
<!-- Team Notes History -->
|
|
{% if org_team and team_notes %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-history"></i> Team Notes History</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Last Updated</th>
|
|
<th>Content Preview</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for note in team_notes %}
|
|
<tr>
|
|
<td>{{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') if note.updated_at else 'Unknown date' }}</td>
|
|
<td>{{ note.content[:100] if note.content else '' }}{% if note.content and note.content|length > 100 %}...{% endif %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Personal Notes History -->
|
|
{% if personal_notes %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-sticky-note"></i> Recent Personal Notes</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="detail-grid">
|
|
{% for note in personal_notes %}
|
|
<div class="detail-item full-width mb-4">
|
|
<span class="detail-label">
|
|
<i class="fas fa-user"></i> {{ note.player.username if note.player else 'Unknown Player' }} -
|
|
<span class="text-muted">{{ note.created_at.strftime('%B %d, %Y') if note.created_at else 'Unknown date' }}</span>
|
|
</span>
|
|
<span class="detail-value">{{ note.content | nl2br if note.content else '' }}</span>
|
|
<span class="detail-label text-muted small">From: {{ note.coach.username if note.coach else 'Unknown Coach' }}</span>
|
|
{% if note.match_id or note.team_id or note.tryout_id %}
|
|
<div class="mt-2">
|
|
{% if note.match_id and note.match %}
|
|
<span class="badge badge-info" title="From match"><i class="fas fa-futbol"></i> {{ note.match.title }}</span>
|
|
{% endif %}
|
|
{% if note.team_id and note.team %}
|
|
<span class="badge badge-warning" title="From team"><i class="fas fa-users"></i> {{ note.team.name }}</span>
|
|
{% endif %}
|
|
{% if note.tryout_id and note.tryout %}
|
|
<span class="badge badge-success" title="From tryout"><i class="fas fa-calendar-alt"></i> {{ note.tryout.title }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script nonce="{{ csp_nonce }}">
|
|
function showRejectModal(requestId) {
|
|
const modal = document.getElementById('rejectModal');
|
|
const form = document.getElementById('rejectForm');
|
|
form.action = "{{ url_for('users.reject_one_on_one', request_id=0) }}".replace('0', requestId);
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function hideRejectModal() {
|
|
document.getElementById('rejectModal').style.display = 'none';
|
|
document.getElementById('rejection_reason').value = '';
|
|
}
|
|
</script>
|
|
{% endblock %} |