"""Team Tryouts Application - Flask Application Factory. This module provides the application factory for creating and configuring the Flask application instance. """ import os 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): """Convert newlines to HTML line breaks. Args: value: String value to convert. Returns: Markup: HTML-safe string with line breaks. """ if value: return markupsafe.Markup('
'.join(str(value).splitlines())) return '' def create_app(): """Create and configure the Flask application. Initializes Flask with: - Secret key for session security - Database configuration - CSRF protection - Login manager - All route blueprints - Security headers and HTTPS redirects Handles database initialization and seeding with sample data if empty. Returns: Flask: Configured Flask application instance. """ app = Flask(__name__) 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 from routes.evaluations import evaluations_bp from routes.users import users_bp from routes.main import main_bp from routes.teams import teams_bp from routes.matches import matches_bp app.register_blueprint(auth_bp) app.register_blueprint(tryouts_bp) app.register_blueprint(evaluations_bp) app.register_blueprint(users_bp) app.register_blueprint(main_bp) app.register_blueprint(teams_bp) app.register_blueprint(matches_bp) # 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 try: # Check if the database schema is up to date by testing a query # that uses all model columns including team_side for match participants db.session.execute(text('SELECT games, team_side FROM match_participants LIMIT 1')) db.create_all() except Exception: # If there's a schema mismatch, drop and recreate all tables db.session.rollback() db.drop_all() db.create_all() # Seed database if empty if User.query.count() == 0: from seed import seed_database seed_database() # Start the Discord bot for notifications try: from discord_bot import start_bot start_bot() except Exception as e: import logging logging.getLogger(__name__).warning(f"Could not start Discord bot: {e}") return app if __name__ == '__main__': app = create_app() # 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)