from __future__ import annotations import logging import os from pathlib import Path from flask import Flask, render_template from app.config import ProductionConfig, config_by_name from app.extensions import csrf, db, login_manager, migrate def create_app(config_name: str | None = None, config_overrides: dict | None = None) -> Flask: try: from dotenv import load_dotenv load_dotenv() except ImportError: pass app = Flask(__name__, instance_relative_config=True) Path(app.instance_path).mkdir(parents=True, exist_ok=True) selected_config = config_name or os.getenv("APP_ENV", "development") config_class = config_by_name.get(selected_config, config_by_name["development"]) app.config.from_object(config_class) if config_overrides: app.config.update(config_overrides) if config_class is ProductionConfig: validate_production_config(app) configure_logging(app) init_extensions(app) register_blueprints(app) register_error_handlers(app) register_security_headers(app) register_template_context(app) from app.commands import register_commands register_commands(app) return app def init_extensions(app: Flask) -> None: from app.models import Account db.init_app(app) migrate.init_app(app, db) csrf.init_app(app) login_manager.login_view = "auth.login" login_manager.login_message_category = "warning" login_manager.init_app(app) @login_manager.user_loader def load_user(account_id: str) -> Account | None: if not account_id.isdigit(): return None return db.session.get(Account, int(account_id)) def register_blueprints(app: Flask) -> None: from app.admin.routes import bp as admin_bp from app.attendance.routes import bp as attendance_bp from app.auth.routes import bp as auth_bp from app.main.routes import bp as main_bp from app.people.routes import bp as people_bp from app.recognition.routes import bp as recognition_bp app.register_blueprint(auth_bp) app.register_blueprint(main_bp) app.register_blueprint(admin_bp) app.register_blueprint(people_bp) app.register_blueprint(attendance_bp) app.register_blueprint(recognition_bp) def register_error_handlers(app: Flask) -> None: @app.errorhandler(403) def forbidden(error): return render_template("errors/403.html"), 403 @app.errorhandler(404) def not_found(error): return render_template("errors/404.html"), 404 @app.errorhandler(500) def internal_error(error): db.session.rollback() app.logger.exception("Unhandled application error: %s", error) return render_template("errors/500.html"), 500 def register_security_headers(app: Flask) -> None: @app.after_request def set_default_security_headers(response): response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Frame-Options", "SAMEORIGIN") response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") response.headers.setdefault("Permissions-Policy", "camera=(self), microphone=()") return response def register_template_context(app: Flask) -> None: from datetime import datetime, timezone from app.models import Role @app.context_processor def inject_globals(): return {"current_year": datetime.now(timezone.utc).year, "Role": Role} def configure_logging(app: Flask) -> None: if app.debug or app.testing: return logging.basicConfig(level=logging.INFO) def validate_production_config(app: Flask) -> None: if app.config["SECRET_KEY"] in {"", "dev-change-me"}: raise RuntimeError("SECRET_KEY must be set for production.")