Quatre taches de la matrice du rapport, toutes sans dependance, qu aucune liste de « ce qui reste » ne reprenait. OPS-003 — app/.env.exemple disait « copiez ce fichier et remplissez les valeurs pour la production », puis posait FLASK_DEBUG=true, SESSION_COOKIE_SECURE=false et FORCE_HTTPS=false. Le debogueur Werkzeug execute du code soumis par le navigateur : cette ligne transformait un copier-coller en shell distant. Chaque valeur est desormais sure a la copie, et le fichier refuse de demarrer tant que les deux secrets obligatoires ne sont pas remplis plutot que de demarrer grand ouvert. Renomme en .env.example : l orthographe francaise ne correspondait pas a l exception !.env.example du .gitignore, donc le fichier n etait suivi que par accident de l ordre des regles. Les deux points de la decision ouverte du §8 tombent d un seul git mv. OPS-002 — trusted_proxy='*' et HOST ne sont plus soudes dans wsgi.py. Les defauts sont **inchanges**, deliberement : choisir sans connaitre la topologie coupe la prod si nginx est ailleurs, ou casse la limitation de debit pour tout le monde si on cesse de croire X-Forwarded-For alors que c etait la seule source d adresses. Ce sont maintenant des variables, les valeurs sures sont dans .env.example pour un nouveau deploiement, et docs/deployment.md donne les quatre topologies avec la valeur de chacune. wsgi.py avertit au demarrage tant que les deux defauts sont en place. Le commentaire de HOST annoncait « bind to localhost by default » a cote d un defaut a 0.0.0.0 : il decrivait l intention pendant que le code faisait l inverse. Il dit maintenant ce qu il fait. QUA-004 — Font Awesome et FullCalendar etaient charges sans empreinte, depuis des hotes que la CSP autorise nommement. Qui controle ces CDN controlait ce qui s execute sur chaque page. Empreintes posees, avec ce que SRI promet et ce qu il ne promet pas ecrit a cote : ca fige le fichier, ca ne prouve pas qu il etait honnete au moment du calcul. **Le CSS de FullCalendar n existait pas.** La v6 embarque ses styles dans le JS et ce fichier n est pas publie : le <link> repondait 404 a chaque ouverture du calendrier depuis la montee de version. Une feuille de style en echec est silencieuse dans le navigateur, c est ce qui l a fait durer. CI-003 — actions epinglees sur un commit, version en commentaire, dans les deux forges. Un tag est un pointeur mobile : deplacer v4 fait executer du code arbitraire dans le job qui detient la cle SSH de production. Ce job recoit aussi enfin un bloc permissions. 517 tests.
212 lines
10 KiB
HTML
212 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ current_locale }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Team Tryout Management{% endblock %}</title>
|
|
{# Subresource integrity (QUA-004). Without it, whoever controls the CDN
|
|
controls what runs on every page of this site — and the CSP names these
|
|
hosts as allowed, so it would not object.
|
|
|
|
What SRI does and does not do: it pins this exact file, so the browser
|
|
refuses a version that has been altered since. It does not prove the
|
|
file was honest when the hash was taken. This hash is the one cdnjs
|
|
publishes for the release, not one derived from the copy we downloaded.
|
|
|
|
integrity requires crossorigin. Changing the version means changing
|
|
the hash, or the asset silently stops loading. #}
|
|
<link rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
|
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
|
crossorigin="anonymous"
|
|
referrerpolicy="no-referrer">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏆</text></svg>">
|
|
</head>
|
|
<body>
|
|
{% if current_user.is_authenticated %}
|
|
<nav class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">
|
|
<i class="fas fa-trophy"></i>
|
|
<span>TryoutPro</span>
|
|
</div>
|
|
<div class="user-badge">
|
|
<div class="user-avatar">
|
|
{{ current_user.username[:2] | upper }}
|
|
</div>
|
|
<div class="user-info">
|
|
<span class="user-name">{{ current_user.username }}</span>
|
|
<span class="user-role role-{{ current_user.role }}">{{ current_user.role | capitalize }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul class="nav-links">
|
|
<li>
|
|
<a href="{{ url_for('main.dashboard') }}" class="{% if request.endpoint and 'dashboard' in request.endpoint %}active{% endif %}">
|
|
<i class="fas fa-th-large"></i>
|
|
<span>{{ _('Dashboard') }}</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url_for('tryouts.list_tryouts') }}" class="{% if request.endpoint and 'tryouts' in request.endpoint and request.endpoint != 'tryouts.create_tryout' %}active{% endif %}">
|
|
<i class="fas fa-calendar-alt"></i>
|
|
<span>{{ _('Tryouts') }}</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url_for('matches.calendar') }}" class="{% if request.endpoint and 'calendar' in request.endpoint %}active{% endif %}">
|
|
<i class="fas fa-calendar"></i>
|
|
<span>{{ _('Calendar') }}</span>
|
|
</a>
|
|
</li>
|
|
{% if current_user.can_evaluate() %}
|
|
<li>
|
|
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
|
|
<i class="fas fa-clipboard-check"></i>
|
|
<span>{{ _('Evaluations') }}</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% if current_user.role == 'player' %}
|
|
<li>
|
|
<a href="{{ url_for('teams.my_teams') }}" class="{% if request.endpoint == 'teams.my_teams' %}active{% endif %}">
|
|
<i class="fas fa-users"></i>
|
|
<span>{{ _('My Team(s)') }}</span>
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li>
|
|
<a href="{{ url_for('teams.list_teams') }}" class="{% if request.endpoint and 'teams' in request.endpoint and request.endpoint != 'teams.my_teams' %}active{% endif %}">
|
|
<i class="fas fa-users-cog"></i>
|
|
<span>{{ _('Manage Teams') }}</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% if current_user.can_manage_users() %}
|
|
<li>
|
|
<a href="{{ url_for('users.list_users') }}" class="{% if request.endpoint and 'users' in request.endpoint and request.endpoint != 'users.profile' %}active{% endif %}">
|
|
<i class="fas fa-users-cog"></i>
|
|
<span>{{ _('Manage Users') }}</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% if current_user.role == 'player' %}
|
|
<li>
|
|
<a href="{{ url_for('users.my_notes') }}" class="{% if request.endpoint == 'users.my_notes' %}active{% endif %}">
|
|
<i class="fas fa-sticky-note"></i>
|
|
<span>{{ _('My Notes') }}</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
{% if current_user.role == 'coach' %}
|
|
<li>
|
|
<a href="{{ url_for('users.manage_coach_availability') }}" class="{% if request.endpoint == 'users.manage_coach_availability' %}active{% endif %}">
|
|
<i class="fas fa-clock"></i>
|
|
<span>{{ _('Availability') }}</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url_for('users.notes_dashboard') }}" class="{% if request.endpoint == 'users.notes_dashboard' or request.endpoint == 'users.manage_team_notes' or request.endpoint == 'users.manage_personal_notes' %}active{% endif %}">
|
|
<i class="fas fa-sticky-note"></i>
|
|
<span>{{ _('Notes & One on One') }}</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
<li>
|
|
<a href="{{ url_for('users.list_contracts') }}" class="{% if request.endpoint and 'contracts' in request.endpoint %}active{% endif %}">
|
|
<i class="fas fa-file-contract"></i>
|
|
<span>{{ _('Contracts') }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-divider"></li>
|
|
<li>
|
|
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
|
|
<i class="fas fa-user"></i>
|
|
<span>{{ _('My Profile') }}</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
{# A form, not a link: logging out is a state change, and a
|
|
GET route carries no CSRF token — any site could sign the
|
|
user out with an <img> tag. Styled as a nav entry. #}
|
|
<form method="POST" action="{{ url_for('auth.logout') }}" class="nav-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<button type="submit" class="logout-link">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
<span>{{ _('Logout') }}</span>
|
|
</button>
|
|
</form>
|
|
</li>
|
|
<li class="nav-divider"></li>
|
|
<li class="nav-language">
|
|
{% include "layouts/_language_switcher.html" %}
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<div class="main-content" id="mainContent">
|
|
<header class="top-bar">
|
|
<button class="sidebar-toggle" id="sidebarToggle" data-action="toggle-sidebar">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
<div class="page-header">
|
|
<h1>{% block page_title %}{{ _('Dashboard') }}{% endblock %}</h1>
|
|
{% block breadcrumb %}{% endblock %}
|
|
</div>
|
|
<button class="dark-mode-toggle" id="darkModeToggle" data-action="toggle-dark-mode"
|
|
title="{{ _('Toggle dark mode') }}">
|
|
<i class="fas fa-moon"></i>
|
|
</button>
|
|
{% block header_actions %}{% endblock %}
|
|
</header>
|
|
<div class="flash-messages">
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ category }} alert-dismissible">
|
|
<span>{{ message }}</span>
|
|
<button type="button" class="alert-close" data-action="dismiss-alert"
|
|
aria-label="{{ _('Dismiss') }}">×</button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
</div>
|
|
<div class="content">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="auth-wrapper">
|
|
<div class="flash-messages">
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ category }} alert-dismissible">
|
|
<span>{{ message }}</span>
|
|
<button type="button" class="alert-close" data-action="dismiss-alert"
|
|
aria-label="{{ _('Dismiss') }}">×</button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
</div>
|
|
<div class="auth-container">
|
|
<div class="auth-header">
|
|
<i class="fas fa-trophy"></i>
|
|
<h2>TryoutPro</h2>
|
|
<p>{{ _('Team Tryout Management System') }}</p>
|
|
</div>
|
|
<div class="auth-language">
|
|
{% include "layouts/_language_switcher.html" %}
|
|
</div>
|
|
{% block auth_content %}{% endblock %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html> |