"""Flask application factory for Sneaky Klaus. This module provides the application factory function that creates and configures the Flask application instance. """ from pathlib import Path from flask import Flask from flask_bcrypt import Bcrypt from flask_session import Session from flask_sqlalchemy import SQLAlchemy from flask_wtf.csrf import CSRFProtect # Initialize Flask extensions (without app instance) db = SQLAlchemy() bcrypt = Bcrypt() csrf = CSRFProtect() session = Session() def create_app(config_name: str | None = None) -> Flask: """Create and configure the Flask application. Args: config_name: Configuration environment name (development, production, testing). If None, uses FLASK_ENV environment variable. Returns: Configured Flask application instance. """ # Create Flask app instance app = Flask(__name__) # Load configuration from src.config import ProductionConfig, get_config config_class = get_config(config_name) app.config.from_object(config_class) # Validate production config if needed if config_name == "production": ProductionConfig.validate() # Ensure data directory exists data_dir = Path(app.config["DATA_DIR"]) data_dir.mkdir(parents=True, exist_ok=True) # Initialize extensions db.init_app(app) bcrypt.init_app(app) csrf.init_app(app) # Initialize session with SQLAlchemy backend app.config["SESSION_SQLALCHEMY"] = db session.init_app(app) # Register blueprints (will be created later) # from src.routes import admin, auth, participant, public # app.register_blueprint(admin.bp) # app.register_blueprint(auth.bp) # app.register_blueprint(participant.bp) # app.register_blueprint(public.bp) # Register error handlers register_error_handlers(app) # First-run setup check register_setup_check(app) # Import models to ensure they're registered with SQLAlchemy with app.app_context(): from src.models import Admin, Exchange # noqa: F401 db.create_all() return app def register_error_handlers(app: Flask) -> None: """Register custom error handlers. Args: app: Flask application instance. """ from flask import render_template @app.errorhandler(404) def not_found_error(_error): """Handle 404 Not Found errors.""" return render_template("errors/404.html"), 404 @app.errorhandler(403) def forbidden_error(_error): """Handle 403 Forbidden errors.""" return render_template("errors/403.html"), 403 @app.errorhandler(500) def internal_error(error): """Handle 500 Internal Server Error.""" db.session.rollback() # Rollback any failed transactions app.logger.error(f"Internal server error: {error}") return render_template("errors/500.html"), 500 @app.errorhandler(429) def rate_limit_error(_error): """Handle 429 Too Many Requests errors.""" from flask import flash, redirect, request flash("Too many attempts. Please try again later.", "error") return redirect(request.referrer or "/"), 429 def register_setup_check(app: Flask) -> None: """Register before_request handler for first-run setup detection. Args: app: Flask application instance. """ from flask import redirect, request, url_for @app.before_request def check_setup_required(): """Redirect to setup page if no admin exists. This runs before every request to ensure the application has been set up with an admin account. """ # Skip check for certain endpoints if request.endpoint in ["setup", "static", "health"]: return # Check if we've already determined setup is required if not hasattr(app, "_setup_checked"): from src.models.admin import Admin admin_count = db.session.query(Admin).count() app.config["REQUIRES_SETUP"] = admin_count == 0 app._setup_checked = True # Redirect to setup if needed if app.config.get("REQUIRES_SETUP") and request.endpoint != "setup": return redirect(url_for("setup"))