"""Flask application factory""" import os import logging import json from datetime import datetime from flask import Flask, request from flask_login import LoginManager from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_wtf.csrf import CSRFProtect class JSONFormatter(logging.Formatter): """Custom JSON formatter for structured logging""" def format(self, record): log_data = { 'timestamp': datetime.utcnow().isoformat() + 'Z', 'level': record.levelname, 'logger': record.name, 'message': record.getMessage(), 'module': record.module, 'function': record.funcName, 'line': record.lineno, } # Add exception info if present if record.exc_info: log_data['exception'] = self.formatException(record.exc_info) # Add extra fields if present if hasattr(record, 'extra_data'): log_data.update(record.extra_data) return json.dumps(log_data) def configure_logging(app): """Configure structured logging for the application Args: app: Flask application instance """ # Remove default Flask handlers app.logger.handlers.clear() # Create console handler console_handler = logging.StreamHandler() if app.config.get('DEBUG'): # Human-readable format for development console_handler.setFormatter(logging.Formatter( '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' )) else: # JSON format for production console_handler.setFormatter(JSONFormatter()) console_handler.setLevel(logging.INFO) app.logger.addHandler(console_handler) app.logger.setLevel(logging.INFO) # Log all requests @app.before_request def log_request(): app.logger.info( f'Request: {request.method} {request.path}', extra={'extra_data': { 'method': request.method, 'path': request.path, 'remote_addr': request.remote_addr, 'user_agent': str(request.user_agent) }} ) # Log all responses and apply security headers @app.after_request def log_response(response): app.logger.info( f'Response: {response.status_code} for {request.method} {request.path}', extra={'extra_data': { 'status_code': response.status_code, 'method': request.method, 'path': request.path }} ) # Apply security headers in production (T196) if not app.config.get('DEBUG'): if app.config.get('STRICT_TRANSPORT_SECURITY'): response.headers['Strict-Transport-Security'] = app.config['STRICT_TRANSPORT_SECURITY'] if app.config.get('X_CONTENT_TYPE_OPTIONS'): response.headers['X-Content-Type-Options'] = app.config['X_CONTENT_TYPE_OPTIONS'] if app.config.get('X_FRAME_OPTIONS'): response.headers['X-Frame-Options'] = app.config['X_FRAME_OPTIONS'] if app.config.get('X_XSS_PROTECTION'): response.headers['X-XSS-Protection'] = app.config['X_XSS_PROTECTION'] return response def validate_environment(app): """Validate required environment variables on startup Args: app: Flask application instance Raises: ValueError: If required environment variables are missing """ required_vars = [] if not app.config.get('DEBUG'): # Production requirements if not app.config.get('SECRET_KEY'): required_vars.append('SECRET_KEY') if not app.config.get('ANTHROPIC_API_KEY'): required_vars.append('ANTHROPIC_API_KEY') if required_vars: raise ValueError(f"Missing required environment variables: {', '.join(required_vars)}") app.logger.info("Environment validation passed") def create_app(config_name='development'): """Create and configure the Flask application Args: config_name: Configuration environment (development, production, testing) Returns: Flask application instance """ app = Flask(__name__) # Load configuration if config_name == 'production': from config.production import ProductionConfig app.config.from_object(ProductionConfig) elif config_name == 'testing': from config.testing import TestingConfig app.config.from_object(TestingConfig) else: from config.development import DevelopmentConfig app.config.from_object(DevelopmentConfig) # Configure structured logging (T194) configure_logging(app) # Validate environment variables (T209) try: validate_environment(app) except ValueError as e: app.logger.error(f"Environment validation failed: {e}") raise # Ensure data directory exists os.makedirs(app.config['DATA_DIR'], exist_ok=True) # Initialize Flask-WTF CSRF Protection csrf = CSRFProtect() csrf.init_app(app) # Initialize Flask-Login login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'auth.login' login_manager.login_message = 'Please log in to access this page.' @login_manager.user_loader def load_user(user_id): """Load user by ID for Flask-Login""" from app.models.user import User return User.get_by_id(user_id) # Initialize Flask-Limiter limiter = Limiter( app=app, key_func=get_remote_address, storage_uri=app.config['RATELIMIT_STORAGE_URL'], default_limits=[f"{app.config['RATELIMIT_PER_HOUR']}/hour"] if app.config.get('RATELIMIT_ENABLED') else [] ) # Register Jinja2 filters from app.utils.markdown_utils import markdown_filter app.jinja_env.filters['markdown'] = markdown_filter # Register blueprints from app.routes import submission, dashboard, admin, auth, landing app.register_blueprint(submission.bp) app.register_blueprint(dashboard.bp) app.register_blueprint(admin.bp) app.register_blueprint(auth.bp) app.register_blueprint(landing.bp) # Landing page (product selection) # Health check endpoint (T208) @app.route('/health') def health_check(): """Health check endpoint for monitoring Returns: JSON response with application status """ from flask import jsonify health_status = { 'status': 'healthy', 'timestamp': datetime.utcnow().isoformat() + 'Z', 'environment': 'production' if not app.config.get('DEBUG') else 'development' } # Check critical dependencies try: # Check data directory is writable data_dir = app.config.get('DATA_DIR') if not os.path.exists(data_dir): health_status['status'] = 'unhealthy' health_status['error'] = f'Data directory {data_dir} does not exist' return jsonify(health_status), 503 # Check AI API key is configured if not app.config.get('ANTHROPIC_API_KEY'): health_status['status'] = 'degraded' health_status['warning'] = 'AI analysis unavailable: ANTHROPIC_API_KEY not configured' return jsonify(health_status), 200 except Exception as e: health_status['status'] = 'unhealthy' health_status['error'] = str(e) app.logger.error(f'Health check failed: {e}') return jsonify(health_status), 503 # Register error handlers @app.errorhandler(403) def forbidden(e): """Handle 403 Forbidden errors""" from flask import render_template app.logger.warning(f'403 Forbidden: {request.path} - {e.description}') return render_template('error_403.html'), 403 @app.errorhandler(404) def not_found(e): """Handle 404 Not Found errors""" from flask import render_template app.logger.warning(f'404 Not Found: {request.path} - {e.description}') return render_template('error_404.html'), 404 @app.errorhandler(500) def internal_error(e): """Handle 500 Internal Server errors""" from flask import render_template app.logger.error(f'500 Internal Server Error: {request.path}', exc_info=True) return render_template('error_500.html' if os.path.exists( os.path.join(app.template_folder, 'error_500.html') ) else 'error_404.html'), 500 return app