from quart import Quart from quart_cors import cors # No longer using Flask-JWT-Extended - replaced with Quart-compatible JWT from dotenv import load_dotenv import os import tempfile import asyncio load_dotenv() import logging as _init_logger _logger = _init_logger.getLogger(__name__) def setup_temp_directories(): """Set up temporary directories for file handling.""" backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) temp_dir = os.path.join(backend_dir, 'temp') upload_dir = os.path.join(backend_dir, 'uploads') for directory in [temp_dir, upload_dir]: try: os.makedirs(directory, exist_ok=True) os.chmod(directory, 0o755) test_file = os.path.join(directory, 'test_write') with open(test_file, 'w') as f: f.write('test') os.remove(test_file) except (OSError, PermissionError) as e: _logger.warning(f"Cannot write to {directory}: {e}") continue if os.path.isdir(temp_dir) and os.access(temp_dir, os.W_OK): os.environ['TMPDIR'] = temp_dir os.environ['TEMP'] = temp_dir os.environ['TMP'] = temp_dir tempfile.tempdir = temp_dir return temp_dir, upload_dir def create_app(): # Set up temp directories BEFORE creating Quart app temp_dir, upload_dir = setup_temp_directories() app = Quart(__name__) # Setup custom logging configuration try: from logging_config import setup_logging, DEFAULT_LOG_LEVEL setup_logging(os.environ.get('LOG_LEVEL', DEFAULT_LOG_LEVEL)) except ImportError: pass # Fallback to default logging if logging_config is not available # Configuration — weak/default secrets are rejected at startup _secret_key = os.environ.get('SECRET_KEY', '') _weak_defaults = {'dev-secret-key', 'your-secret-key-for-sessions-and-tokens', '', 'change-me'} if not _secret_key or _secret_key in _weak_defaults: raise RuntimeError( "SECRET_KEY environment variable is not set or uses a weak default. " "Set a strong random value in backend/.env before starting the server." ) app.config['SECRET_KEY'] = _secret_key _jwt_secret = os.environ.get('JWT_SECRET_KEY', '') if not _jwt_secret or _jwt_secret in _weak_defaults: raise RuntimeError( "JWT_SECRET_KEY environment variable is not set or uses a weak default. " "Set a strong random value in backend/.env before starting the server." ) app.config['JWT_SECRET_KEY'] = _jwt_secret # Fix strict slashes - this prevents 308 redirects for trailing slashes app.url_map.strict_slashes = False app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 86400 # 24 hours # Set longer timeouts for AI operations app.config['TIMEOUT'] = 300 # 5 minutes app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB max upload # Configure Quart/Werkzeug file upload settings app.config['UPLOAD_FOLDER'] = upload_dir app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.jpeg', '.png'] # Configure temp directory for Quart/Werkzeug if temp_dir and os.path.isdir(temp_dir): app.config['TEMP_FOLDER'] = temp_dir _logger.debug(f"Quart configured with temp directory: {temp_dir}") # Additional Werkzeug configuration for multipart form handling app.config['MAX_CONTENT_PATH'] = None # Don't limit content path # Configure Werkzeug to handle uploads without temp files for small files app.config['MAX_FORM_MEMORY_SIZE'] = 16 * 1024 * 1024 # Keep small uploads in memory # Initialize extensions — restrict CORS to known origins _allowed_origins = os.environ.get( 'CORS_ALLOWED_ORIGINS', 'https://optical-dev.oliver.solutions' ) _origins = [o.strip() for o in _allowed_origins.split(',')] app = cors(app, allow_origin=_origins, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) # Security headers middleware @app.after_request async def add_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' response.headers['Content-Security-Policy'] = ( "default-src 'self'; " "script-src 'self'; " "style-src 'self' 'unsafe-inline'; " "img-src 'self' data:; " "connect-src 'self' https://login.microsoftonline.com; " "frame-ancestors 'none';" ) # HSTS — only enable over HTTPS if response.headers.get('Content-Type', '').startswith('text/html'): response.headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains' return response # JWT is now handled by custom Quart-compatible auth system # No longer using JWTManager(app) due to Flask/Quart incompatibility # Initialize AsyncServer WebSocket functionality from .extensions import socketio_server # Store socketio reference on app for backward compatibility app.socketio = socketio_server # Initialize async WebSocket manager from app.websocket_manager_async import init_async_websocket_manager websocket_manager = init_async_websocket_manager() import logging as _app_logging _app_logging.getLogger(__name__).info("Quart app initialized with WebSocket manager") # Initialize AI Runner service for autonomous conversations from app.services.ai_runner_service import init_ai_runner init_ai_runner() # Register blueprints from app.routes.auth import auth_bp from app.routes.personas import personas_bp from app.routes.focus_groups import focus_groups_bp from app.routes.ai_personas import ai_personas_bp from app.routes.focus_group_ai import focus_group_ai_bp from app.routes.folders import folders_bp from app.routes.tasks import tasks_bp from app.routes.admin import admin_bp from app.routes.usage import usage_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(personas_bp, url_prefix='/api/personas') app.register_blueprint(focus_groups_bp, url_prefix='/api/focus-groups') app.register_blueprint(ai_personas_bp, url_prefix='/api/ai-personas') app.register_blueprint(focus_group_ai_bp, url_prefix='/api/focus-group-ai') app.register_blueprint(folders_bp, url_prefix='/api/folders') app.register_blueprint(tasks_bp, url_prefix='/api/tasks') app.register_blueprint(admin_bp, url_prefix='/api/admin') app.register_blueprint(usage_bp, url_prefix='/api/usage') @app.before_serving async def start_task_sweeper(): import asyncio from app.services.task_manager import get_task_manager from app.websocket_manager_async import get_async_websocket_manager asyncio.create_task(get_task_manager().start_sweeper()) # Register the ASGI event loop so cross-thread WebSocket emits (from AI Runner) work ws_mgr = get_async_websocket_manager() if ws_mgr: ws_mgr.set_main_loop(asyncio.get_running_loop()) # Health check endpoint @app.route('/api/health', methods=['GET']) def health_check(): return {'status': 'ok', 'message': 'Backend is running'}, 200 # Create ASGI app with SocketIO integration import socketio as socketio_pkg asgi_app = socketio_pkg.ASGIApp(socketio_server, app) # Store reference to the original Quart app for access in routes asgi_app.quart_app = app return asgi_app