Quatre constats de la liste des gains rapides, tous sur auth.py. SEC-017 -- enumeration de comptes Le formulaire repondait "il vous reste 3 tentative(s)" a un compte connu et "verifiez le nom d utilisateur et le mot de passe" a un inconnu. Le decompte lui-meme etait la fuite : la meme information, etalee sur cinq requetes. S y ajoutait un ecart de temps de reponse, check_password n etant appele que si la ligne existait -- scrypt est cher, l ecart est mesurable. Un seul message pour tous les echecs, et la verification s execute desormais sur les deux branches : contre un hachage aleatoire tire une fois par processus quand l identifiant n existe pas. SEC-018 -- verrou de compte declenchable par un tiers Cinq mauvaises reponses mettaient un compte connu hors service pendant quinze minutes, indefiniment renouvelables. Sur un compte president, c est toute l administration, et aucun ecran ne permettait de defaire. Le compteur et la fenetre restent -- ce sont la trace qu un administrateur lit quand un compte est pilonne, et la fenetre double jusqu a un plafond. Ce qui change : de bons identifiants passent, fenetre ouverte ou non, et remettent le compteur a zero. Le proprietaire du compte ne peut plus etre bloque par un tiers. Ce que cela coute, dit franchement : un verrou dur n arretait de toute facon pas un attaquant ayant trouve le mot de passe -- il lui suffisait d attendre. Le debit de tentatives reste borne par la limite de 10/minute par IP. Une limite par couple (compte, IP) demanderait un stockage dedie ; elle attend Alembic. L evenement account.locked devient account.throttled : "locked" affirmait plus que ce qui se passe. SEC-019 -- deconnexion en GET /auth/logout n avait pas de methods, donc GET, donc hors protection CSRF : n importe quelle page pouvait deconnecter un visiteur avec une balise img. La route passe en POST et l entree de navigation devient un formulaire avec jeton. Le style suit -- les regles .nav-links visaient les liens seuls. SEC-020 -- validation de redirection is_safe_url interrogeait urlparse().netloc. urlparse lit /\evil.com comme un chemin, sans netloc ; plusieurs navigateurs normalisent l antislash en barre oblique avant de resoudre, ce qui en fait //evil.com. La fonction refuse maintenant antislash et caracteres de controle, exige un chemin enracine, et compare l origine explicitement. Le xfail(strict) qui documentait SEC-017 est leve. 40 tests dans test_auth_session.py, dont la table des cibles refusees. Co-Authored-By: Claude Opus 5 <[email protected]>
197 lines
9.2 KiB
HTML
197 lines
9.2 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>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
<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> |