- Fix missing await on FocusGroup.get_messages() (N-L1) - Replace time.sleep with asyncio.sleep in key_theme_service and focus_group_service (N-P10) - Replace flask import with quart in focus_groups.py (N-S3) - Add logger.error before all 500 returns in focus_groups.py (N-P6) - Add logging to silent except blocks across routes (N-M10, N-M11) - Add @rate_limit to 6 remaining AI endpoints (N-H4) - Add --confirm flag to populate scripts before delete_many (S-H2) - Remove hardcoded Azure ID fallbacks from msal_service.py and msalConfig.ts (A-M2, F-H4) - Centralize make_serializable() in utils.py, remove duplicates from 3 route files (N-P7) - Replace all datetime.utcnow() with datetime.now(timezone.utc) across entire backend (M-L2) - AuthContext.tsx: only mark token validated on 200 success, not on non-401 errors (F-H2) - Rename authType → auth_type in auth.py (N-S4) - Add security_report.md and security_report.pdf with full 92-finding status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
171 lines
No EOL
6.9 KiB
Python
Executable file
171 lines
No EOL
6.9 KiB
Python
Executable file
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://ai-sandbox.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
|
|
|
|
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')
|
|
|
|
# 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 |