Files

258 lines
8.4 KiB
Python
Raw Permalink Normal View History

"""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)
2025-10-17 15:09:38 +02:00
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