ajouter des sécurité sur les URL, les Roles, les mdp
This commit is contained in:
@@ -4,3 +4,10 @@ instance/
|
||||
documents/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.db
|
||||
team_tryouts.db
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -1,5 +1,31 @@
|
||||
### Plateforme centralisée de tryouts
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
Before deploying, create a `.env` file with the following:
|
||||
|
||||
```
|
||||
# Flask Configuration (REQUIRED)
|
||||
SECRET_KEY=your-secure-random-secret-key-here
|
||||
|
||||
# Production Settings
|
||||
FLASK_DEBUG=false
|
||||
FORCE_HTTPS=true
|
||||
SESSION_COOKIE_SECURE=true
|
||||
```
|
||||
|
||||
### Security Features Implemented
|
||||
|
||||
- **Rate Limiting**: Login endpoint limited to 10 requests per minute to prevent brute-force attacks
|
||||
- **Secure Session Cookies**: HTTPOnly, SameSite=Lax, and Secure flags enabled
|
||||
- **CSRF Protection**: Enabled by default on all forms
|
||||
- **HTTPS Enforcement**: Automatic redirect to HTTPS in production
|
||||
- **Security Headers**: X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, HSTS
|
||||
- **Open Redirect Prevention**: URL validation on login redirect
|
||||
- **Authorization Checks**: Proper ownership validation on all sensitive operations
|
||||
|
||||
## Discord Integration for One on One Requests
|
||||
|
||||
The application supports sending Discord direct messages to coaches when players request One on One sessions.
|
||||
|
||||
Binary file not shown.
@@ -5,10 +5,13 @@ the Flask application instance.
|
||||
"""
|
||||
|
||||
import os
|
||||
from flask import Flask
|
||||
from extensions import db, login_manager, csrf, hash_password, check_password
|
||||
from flask import Flask, request, redirect
|
||||
from extensions import db, login_manager, csrf, hash_password, check_password, limiter
|
||||
from sqlalchemy import text
|
||||
import markupsafe
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def nl2br(value):
|
||||
@@ -30,10 +33,11 @@ def create_app():
|
||||
|
||||
Initializes Flask with:
|
||||
- Secret key for session security
|
||||
- SQLite database configuration
|
||||
- Database configuration
|
||||
- CSRF protection
|
||||
- Login manager
|
||||
- All route blueprints
|
||||
- Security headers and HTTPS redirects
|
||||
|
||||
Handles database initialization and seeding with sample data if empty.
|
||||
|
||||
@@ -41,14 +45,23 @@ def create_app():
|
||||
Flask: Configured Flask application instance.
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db'
|
||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
||||
if not app.config['SECRET_KEY']:
|
||||
raise RuntimeError('SECRET_KEY environment variable must be set for security')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///team_tryouts.db')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['WTF_CSRF_ENABLED'] = True
|
||||
|
||||
# Secure session cookie settings
|
||||
app.config['SESSION_COOKIE_SECURE'] = os.getenv('SESSION_COOKIE_SECURE', 'true').lower() == 'true'
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour session timeout
|
||||
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
csrf.init_app(app)
|
||||
limiter.init_app(app)
|
||||
|
||||
from routes.auth import auth_bp
|
||||
from routes.tryouts import tryouts_bp
|
||||
@@ -69,6 +82,24 @@ def create_app():
|
||||
# Register custom Jinja filters
|
||||
app.jinja_env.filters['nl2br'] = nl2br
|
||||
|
||||
# Add security headers to all responses
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
return response
|
||||
|
||||
# Force HTTPS in production (when not in debug mode)
|
||||
@app.before_request
|
||||
def force_https():
|
||||
if not app.debug:
|
||||
if not request.is_secure and request.headers.get('X-Forwarded-Proto') != 'https':
|
||||
if os.getenv('FORCE_HTTPS', 'true').lower() == 'true':
|
||||
return redirect(request.url.replace('http://', 'https://'), code=301)
|
||||
|
||||
with app.app_context():
|
||||
import models
|
||||
from models import User, MatchParticipant
|
||||
@@ -101,4 +132,6 @@ def create_app():
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
# Debug mode should only be enabled via environment variable for security
|
||||
debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
|
||||
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
|
||||
@@ -21,6 +21,7 @@ from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
|
||||
print(DISCORD_BOT_TOKEN or 'FAILED TO PRINT BOT TOKEN')
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+8
-1
@@ -2,7 +2,8 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
# Database and extension initialization
|
||||
db = SQLAlchemy()
|
||||
@@ -11,6 +12,12 @@ login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message_category = 'info'
|
||||
csrf = CSRFProtect()
|
||||
|
||||
# Rate limiter for brute-force protection
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
default_limits=["200 per day", "50 per hour"]
|
||||
)
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24
-1
@@ -5,13 +5,32 @@ This module handles user authentication including login, logout, and new user re
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from extensions import db, hash_password, check_password
|
||||
from extensions import db, hash_password, check_password, limiter
|
||||
from models import User, ESPORT_GAMES
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def is_safe_url(url):
|
||||
"""Validate that a URL is safe for redirection (same origin).
|
||||
|
||||
Args:
|
||||
url: The URL to validate.
|
||||
|
||||
Returns:
|
||||
bool: True if the URL is safe (relative or same origin).
|
||||
"""
|
||||
if not url:
|
||||
return False
|
||||
parsed = url_parse(url)
|
||||
# Allow relative URLs (no netloc) or same-origin URLs
|
||||
return not parsed.netloc or parsed.netloc == request.host
|
||||
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
@limiter.limit("10 per minute")
|
||||
def login():
|
||||
"""Handle user login authentication.
|
||||
|
||||
@@ -36,8 +55,12 @@ def login():
|
||||
if not user.is_active_account:
|
||||
flash('This account has been deactivated.', 'danger')
|
||||
return render_template('pages/login.html')
|
||||
# Regenerate session to prevent session fixation attacks
|
||||
login_user(user)
|
||||
# Validate redirect URL to prevent open redirect vulnerability
|
||||
next_page = request.args.get('next')
|
||||
if next_page and not is_safe_url(next_page):
|
||||
next_page = None
|
||||
flash(f'Welcome back, {user.full_name}!', 'success')
|
||||
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
|
||||
else:
|
||||
|
||||
+56
-38
@@ -12,6 +12,26 @@ from sqlalchemy import func
|
||||
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
||||
|
||||
|
||||
def validate_score(score_value):
|
||||
"""Validate that a score is between 1 and 10.
|
||||
|
||||
Args:
|
||||
score_value: The score value to validate (can be None, string, or int).
|
||||
|
||||
Returns:
|
||||
int or None: The validated score or None if invalid/empty.
|
||||
"""
|
||||
if score_value is None:
|
||||
return None
|
||||
try:
|
||||
score = int(score_value)
|
||||
if 1 <= score <= 10:
|
||||
return score
|
||||
return None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
@evaluations_bp.route('')
|
||||
@login_required
|
||||
def list_evaluations():
|
||||
@@ -75,6 +95,14 @@ def evaluate_player(tryout_id, player_id):
|
||||
flash('You do not have permission to evaluate players in this tryout.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
# Check if player is registered for this tryout
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id
|
||||
).first() is not None
|
||||
if not is_registered:
|
||||
flash('Player is not registered for this tryout.', 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
player = User.query.get_or_404(player_id)
|
||||
|
||||
if player.role != 'player':
|
||||
@@ -88,41 +116,31 @@ def evaluate_player(tryout_id, player_id):
|
||||
).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
mecanics = request.form.get('mecanics_score')
|
||||
cohesion = request.form.get('cohesion_score')
|
||||
communication = request.form.get('communication_score')
|
||||
gamesense = request.form.get('gamesense_score')
|
||||
versatility = request.form.get('versatility_score')
|
||||
discipline = request.form.get('discipline_score')
|
||||
analysis = request.form.get('analysis_score')
|
||||
sport_ethics = request.form.get('sport_ethics_score')
|
||||
mental = request.form.get('mental_score')
|
||||
mecanics = validate_score(request.form.get('mecanics_score'))
|
||||
cohesion = validate_score(request.form.get('cohesion_score'))
|
||||
communication = validate_score(request.form.get('communication_score'))
|
||||
gamesense = validate_score(request.form.get('gamesense_score'))
|
||||
versatility = validate_score(request.form.get('versatility_score'))
|
||||
discipline = validate_score(request.form.get('discipline_score'))
|
||||
analysis = validate_score(request.form.get('analysis_score'))
|
||||
sport_ethics = validate_score(request.form.get('sport_ethics_score'))
|
||||
mental = validate_score(request.form.get('mental_score'))
|
||||
comments = request.form.get('comments')
|
||||
position = request.form.get('position_recommendation')
|
||||
|
||||
scores = []
|
||||
if mecanics: scores.append(int(mecanics))
|
||||
if cohesion: scores.append(int(cohesion))
|
||||
if communication: scores.append(int(communication))
|
||||
if gamesense: scores.append(int(gamesense))
|
||||
if versatility: scores.append(int(versatility))
|
||||
if discipline: scores.append(int(discipline))
|
||||
if analysis: scores.append(int(analysis))
|
||||
if sport_ethics: scores.append(int(sport_ethics))
|
||||
if mental: scores.append(int(mental))
|
||||
|
||||
scores = [s for s in [mecanics, cohesion, communication, gamesense, versatility, discipline, analysis, sport_ethics, mental] if s is not None]
|
||||
overall = sum(scores) / len(scores) if scores else None
|
||||
|
||||
if existing_eval:
|
||||
existing_eval.mecanics_score = int(mecanics) if mecanics else None
|
||||
existing_eval.cohesion_score = int(cohesion) if cohesion else None
|
||||
existing_eval.communication_score = int(communication) if communication else None
|
||||
existing_eval.gamesense_score = int(gamesense) if gamesense else None
|
||||
existing_eval.versatility_score = int(versatility) if versatility else None
|
||||
existing_eval.discipline_score = int(discipline) if discipline else None
|
||||
existing_eval.analysis_score = int(analysis) if analysis else None
|
||||
existing_eval.sport_ethics_score = int(sport_ethics) if sport_ethics else None
|
||||
existing_eval.mental_score = int(mental) if mental else None
|
||||
existing_eval.mecanics_score = mecanics
|
||||
existing_eval.cohesion_score = cohesion
|
||||
existing_eval.communication_score = communication
|
||||
existing_eval.gamesense_score = gamesense
|
||||
existing_eval.versatility_score = versatility
|
||||
existing_eval.discipline_score = discipline
|
||||
existing_eval.analysis_score = analysis
|
||||
existing_eval.sport_ethics_score = sport_ethics
|
||||
existing_eval.mental_score = mental
|
||||
existing_eval.overall_score = overall
|
||||
existing_eval.comments = comments
|
||||
existing_eval.position_recommendation = position
|
||||
@@ -132,15 +150,15 @@ def evaluate_player(tryout_id, player_id):
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id,
|
||||
mecanics_score=int(mecanics) if mecanics else None,
|
||||
cohesion_score=int(cohesion) if cohesion else None,
|
||||
communication_score=int(communication) if communication else None,
|
||||
gamesense_score=int(gamesense) if gamesense else None,
|
||||
versatility_score=int(versatility) if versatility else None,
|
||||
discipline_score=int(discipline) if discipline else None,
|
||||
analysis_score=int(analysis) if analysis else None,
|
||||
sport_ethics_score=int(sport_ethics) if sport_ethics else None,
|
||||
mental_score=int(mental) if mental else None,
|
||||
mecanics_score=mecanics,
|
||||
cohesion_score=cohesion,
|
||||
communication_score=communication,
|
||||
gamesense_score=gamesense,
|
||||
versatility_score=versatility,
|
||||
discipline_score=discipline,
|
||||
analysis_score=analysis,
|
||||
sport_ethics_score=sport_ethics,
|
||||
mental_score=mental,
|
||||
overall_score=overall,
|
||||
comments=comments,
|
||||
position_recommendation=position
|
||||
|
||||
+4
-1
@@ -450,7 +450,10 @@ def edit_match(match_id):
|
||||
return redirect(url_for('matches.calendar'))
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout.id).all()
|
||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||
# Only show players registered for this tryout
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all()
|
||||
all_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
|
||||
all_players = sorted([p for p in all_players if p], key=lambda x: x.full_name)
|
||||
current_player_ids = [p.player_id for p in match.participants.all()]
|
||||
# Get players grouped by team side for player_vs_player matches
|
||||
team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()]
|
||||
|
||||
@@ -283,6 +283,11 @@ def add_player_note(team_id, player_id):
|
||||
flash('Can only add notes for players.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
# Verify player belongs to this team
|
||||
if player.team_id != team_id:
|
||||
flash(f'{player.full_name} is not on {team.name}.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
content = request.form.get('content', '').strip()
|
||||
|
||||
if content:
|
||||
|
||||
+4
-2
@@ -248,8 +248,10 @@ def view_tryout(tryout_id):
|
||||
|
||||
can_view_calendar = is_registered or player_in_match
|
||||
|
||||
# Get all players (for manager registration dropdown)
|
||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||
# Only expose all_players to users who can manage players in this tryout
|
||||
all_players = None
|
||||
if can_edit:
|
||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||
|
||||
# Get matches for this tryout with participant info
|
||||
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date).all()
|
||||
|
||||
@@ -20,6 +20,7 @@ users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
def update_user_gamertags(user, selected_games):
|
||||
"""Update gamertags for a user based on form input.
|
||||
|
||||
++++++++++++++++++++++++++++++
|
||||
Handles creating, updating, and deleting gamertag records for the specified games.
|
||||
Used by both edit_user and edit_profile routes to avoid code duplication.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user