from __future__ import annotations import logging from datetime import timedelta from logging.handlers import RotatingFileHandler from pathlib import Path from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy import event from sqlalchemy.engine import Engine from sqlalchemy.engine.url import make_url from .services import Office365Service from .settings import Settings, load_settings db = SQLAlchemy() def _ensure_sqlite_directory(database_url: str) -> None: url = make_url(database_url) if url.drivername != "sqlite" or not url.database or url.database == ":memory:": return Path(url.database).parent.mkdir(parents=True, exist_ok=True) def _configure_logging(app: Flask) -> None: log_dir = Path(app.root_path).parent / "logs" log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / "office365_self_service.log" root_logger = logging.getLogger() formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s - %(message)s") if not any(isinstance(handler, RotatingFileHandler) for handler in root_logger.handlers): file_handler = RotatingFileHandler( log_path, maxBytes=2 * 1024 * 1024, backupCount=5, encoding="utf-8", ) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) root_logger.setLevel(logging.INFO) app.logger.info("Logging initialized at %s", log_path) @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_conn, connection_record): if "sqlite" in str(type(dbapi_conn)): cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() def create_app( settings_override: Settings | None = None, service_factory=None, ) -> Flask: settings = settings_override or load_settings() app = Flask(__name__, template_folder="templates", static_folder="static") app.config["SETTINGS"] = settings app.config["JSON_AS_ASCII"] = False app.config["SECRET_KEY"] = settings.session_secret app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=8) app.config["SESSION_COOKIE_SAMESITE"] = "Lax" app.config["SQLALCHEMY_DATABASE_URI"] = settings.database_url app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False _configure_logging(app) _ensure_sqlite_directory(settings.database_url) db.init_app(app) with app.app_context(): from .models import AuditEvent, RedemptionCode db.create_all() if service_factory is None: service = Office365Service(settings) elif callable(service_factory): service = service_factory(settings) else: service = service_factory app.extensions["office365_service"] = service from .routes import bp_admin, bp_user app.register_blueprint(bp_admin) app.register_blueprint(bp_user) return app