137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
"""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('<br>'.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) |