Files
2026-05-07 21:20:25 +00:00

259 lines
8.6 KiB
Python

"""Application factory."""
import os
import logging
from dotenv import load_dotenv
load_dotenv(dotenv_path=os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env'))
# Test debug logging - this should appear when running `flask run --debug`
_root_logger = logging.getLogger(__name__)
_root_logger.debug("[TEST] Debug logging is working!")
from flask import Flask
from config import get_config
from gatehouse_app.extensions import db, migrate, bcrypt, ma, limiter
from gatehouse_app.extensions import session as flask_session
from gatehouse_app.middleware import RequestIDMiddleware, SecurityHeadersMiddleware, setup_cors
from gatehouse_app.exceptions.base import BaseAPIException
from gatehouse_app.utils.response import api_response
from gatehouse_app.services.oidc_jwks_service import OIDCJWKSService
import redis
# Configure SQLAlchemy logging BEFORE any database operations
# This must be done before db.init_app() to prevent verbose logging
_log_level_env = os.getenv("SQLALCHEMY_LOG_LEVEL", "WARNING").upper()
_sqlalchemy_log_level = getattr(logging, _log_level_env, logging.WARNING)
logging.getLogger('sqlalchemy').setLevel(_sqlalchemy_log_level)
logging.getLogger('sqlalchemy.engine').setLevel(_sqlalchemy_log_level)
logging.getLogger('sqlalchemy.dialects').setLevel(_sqlalchemy_log_level)
logging.getLogger('sqlalchemy.pool').setLevel(_sqlalchemy_log_level)
def create_app(config_name=None):
"""
Create and configure the Flask application.
Args:
config_name: Configuration name (development, testing, production)
Returns:
Flask application instance
"""
flask_app = Flask(__name__)
# Load configuration
config = get_config(config_name)
flask_app.config.from_object(config)
# Initialize extensions
initialize_extensions(flask_app)
# Setup middleware
setup_middleware(flask_app)
# Register blueprints
register_blueprints(flask_app)
# Register error handlers
register_error_handlers(flask_app)
# Setup logging
setup_logging(flask_app)
# Initialize OIDC JWKS service with a signing key
initialize_oidc_jwks(flask_app)
return flask_app
def initialize_extensions(app):
"""Initialize Flask extensions."""
# Database
db.init_app(app)
migrate.init_app(app, db)
# Security
bcrypt.init_app(app)
# CORS - using custom middleware only (see app/middleware/cors.py)
# Flask-CORS disabled to avoid conflicts
# cors.init_app(app)
# Marshmallow
ma.init_app(app)
# Rate limiting
if app.config.get("RATELIMIT_ENABLED"):
limiter.init_app(app)
# Redis for sessions and Flask-Session
try:
redis_url = app.config.get("REDIS_URL")
if redis_url:
import gatehouse_app.extensions
gatehouse_app.extensions.redis_client = redis.from_url(redis_url)
app.config["SESSION_REDIS"] = gatehouse_app.extensions.redis_client
logging.info(f"Redis connected successfully for sessions")
except Exception as e:
logging.warning(f"Redis connection failed: {e}")
# Flask-Session - configure with Redis if available, otherwise filesystem
flask_session.init_app(app)
def setup_middleware(app):
"""Setup application middleware."""
RequestIDMiddleware(app)
SecurityHeadersMiddleware(app)
setup_cors(app)
def register_blueprints(app):
"""Register application blueprints."""
from gatehouse_app.api import register_api_blueprints
register_api_blueprints(app)
# Register OIDC discovery endpoint at root (OIDC spec requirement)
from gatehouse_app.api.v1.oidc import get_oidc_config
from flask import jsonify
@app.route("/.well-known/openid-configuration", methods=["GET"])
def oidc_discovery():
"""OpenID Connect Discovery endpoint at root level (OIDC spec requirement)."""
config = get_oidc_config()
response = jsonify(config)
response.headers["Cache-Control"] = "max-age=86400"
return response, 200
def register_error_handlers(app):
"""Register error handlers."""
@app.errorhandler(BaseAPIException)
def handle_api_exception(error):
"""Handle custom API exceptions."""
return api_response(
success=False,
message=error.message,
status=error.status_code,
error_type=error.error_type,
error_details=error.error_details,
)
@app.errorhandler(404)
def handle_not_found(error):
"""Handle 404 errors."""
return api_response(
success=False,
message="Resource not found",
status=404,
error_type="NOT_FOUND",
)
@app.errorhandler(405)
def handle_method_not_allowed(error):
"""Handle 405 errors."""
return api_response(
success=False,
message="Method not allowed",
status=405,
error_type="METHOD_NOT_ALLOWED",
)
@app.errorhandler(500)
def handle_internal_error(error):
"""Handle 500 errors."""
app.logger.error(f"Internal server error: {error}")
return api_response(
success=False,
message="Internal server error",
status=500,
error_type="INTERNAL_ERROR",
)
@app.errorhandler(Exception)
def handle_unexpected_error(error):
"""Handle unexpected errors."""
app.logger.error(f"Unexpected error: {error}", exc_info=True)
return api_response(
success=False,
message="An unexpected error occurred",
status=500,
error_type="INTERNAL_ERROR",
)
def setup_logging(app):
"""Setup application logging."""
log_level = getattr(logging, app.config.get("LOG_LEVEL", "INFO"))
# Create formatter
formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] %(name)s: %(message)s"
)
# Configure root logger - this ensures all module loggers (like app.services.oidc_service)
# will output DEBUG level logs when in development mode
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
if app.config.get("LOG_TO_STDOUT"):
# Clear existing handlers on root logger to avoid duplicates
root_logger.handlers.clear()
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
stream_handler.setLevel(log_level)
root_logger.addHandler(stream_handler)
# Disable Werkzeug's default logger to avoid log duplication and interference
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.setLevel(logging.INFO)
# Ensure child loggers propagate to root logger
# This is the key fix - explicitly enable propagation for common app loggers
for logger_name in ['app', 'app.api', 'app.api.oidc', 'app.services', 'app.models']:
child_logger = logging.getLogger(logger_name)
child_logger.propagate = True
child_logger.setLevel(log_level)
# Configure Flask app logger - clear handlers so it only propagates to root
app.logger.handlers.clear()
app.logger.setLevel(log_level)
# Configure SQLAlchemy logging level (also set at module level before DB init)
sqlalchemy_log_level = getattr(logging, app.config.get("SQLALCHEMY_LOG_LEVEL", "WARNING"), logging.WARNING)
logging.getLogger('sqlalchemy').setLevel(sqlalchemy_log_level)
logging.getLogger('sqlalchemy.engine').setLevel(sqlalchemy_log_level)
logging.getLogger('sqlalchemy.dialects').setLevel(sqlalchemy_log_level)
logging.getLogger('sqlalchemy.pool').setLevel(sqlalchemy_log_level)
# Suppress watchdog debug logging
logging.getLogger('watchdog.observers.inotify_buffer').setLevel(logging.INFO)
app.logger.info("Application startup")
# Test debug log after logging is configured
app.logger.debug("[TEST] Debug logging is working!")
def initialize_oidc_jwks(app):
"""Initialize OIDC JWKS service with a signing key.
This ensures that signing keys are available for token generation.
Keys are loaded from the database if available, otherwise a new key
is generated and persisted to the database.
Args:
app: Flask application instance
"""
with app.app_context():
try:
jwks_service = OIDCJWKSService()
# Use initialize_with_key which handles loading from DB
# or generating a new key if none exists
signing_key = jwks_service.initialize_with_key()
app.logger.info(f"[OIDC] Signing key initialized: kid={signing_key.kid}")
except Exception as e:
app.logger.error(f"[OIDC] Failed to initialize JWKS: {e}")