cohorta/backend/app/__init__.py
Vadym Samoilenko 3e1865edbd Apply Jintech security audit remediation (sprint 3) — 87/92 findings fixed
- 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>
2026-03-20 12:51:18 +00:00

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