From 2ea4673bf0997db46dc30233ea6af217adb89c24 Mon Sep 17 00:00:00 2001 From: SamoilenkoVadym Date: Mon, 9 Feb 2026 11:24:49 +0000 Subject: [PATCH] Fix session persistence and improve AJAX detection for file uploads Fixed three critical issues: 1. Session persistence - Cookies not saved after page refresh - Replaced APPLICATION_ROOT with SESSION_COOKIE_PATH - Added proper cookie settings for reverse proxy (HttpOnly, SameSite) - Set correct cookie path matching URL_PREFIX 2. AJAX detection for FormData uploads (JPG, etc.) - Enhanced @login_required to detect POST/PUT/DELETE as AJAX - Added Content-Type check for JSON requests - Added path prefix check for API endpoints 3. JavaScript AJAX identification - Updated fetchWithAuth() to add X-Requested-With header - Properly handles both JSON and FormData requests using Headers API - Ensures all fetch calls are identified as AJAX by server Changes: - web_app.py: Fixed Flask session cookie configuration - src/auth.py: Improved AJAX detection logic in login_required decorator - templates/index.html: Enhanced fetchWithAuth() with proper headers This fixes: - Users having to re-login on every page refresh - "Unexpected token '<'" errors when uploading JPG files - Session cookies not persisting through reverse proxy Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/auth.py | 9 +++++++-- templates/index.html | 18 ++++++++++++++++++ web_app.py | 9 +++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/auth.py b/src/auth.py index 9ab3135..4d27d54 100644 --- a/src/auth.py +++ b/src/auth.py @@ -30,8 +30,13 @@ def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): # Check if request is AJAX/API call - is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \ - 'application/json' in request.headers.get('Accept', '') + # Also check for POST/PUT/DELETE methods which are typically API calls + is_ajax = ( + request.headers.get('X-Requested-With') == 'XMLHttpRequest' or + 'application/json' in request.headers.get('Accept', '') or + request.headers.get('Content-Type', '').startswith('application/json') or + (request.method in ['POST', 'PUT', 'DELETE'] and request.path.startswith(URL_PREFIX)) + ) if 'user_id' not in session: # Return JSON for AJAX requests, redirect for normal requests diff --git a/templates/index.html b/templates/index.html index 5254a11..6222bfc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -996,6 +996,24 @@ // Helper function to handle fetch with authentication check async function fetchWithAuth(url, options = {}) { try { + // Add X-Requested-With header to identify AJAX requests + // Use Headers object to properly handle both JSON and FormData requests + if (!options.headers) { + options.headers = {}; + } + + // Convert to Headers object if it's a plain object + if (!(options.headers instanceof Headers)) { + const headers = new Headers(); + for (const [key, value] of Object.entries(options.headers)) { + headers.append(key, value); + } + headers.append('X-Requested-With', 'XMLHttpRequest'); + options.headers = headers; + } else { + options.headers.append('X-Requested-With', 'XMLHttpRequest'); + } + const response = await fetch(url, options); // Check for authentication error diff --git a/web_app.py b/web_app.py index 843d382..8062ab6 100644 --- a/web_app.py +++ b/web_app.py @@ -59,8 +59,6 @@ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max file size app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) # URL prefix for reverse proxy redirects URL_PREFIX = os.getenv('URL_PREFIX', '/solventum-image-metadata') -# APPLICATION_ROOT sets cookie path for reverse proxy setups -app.config['APPLICATION_ROOT'] = os.getenv('APPLICATION_ROOT', '/solventum-image-metadata') # Docker mode detection DOCKER_MODE = os.getenv('DOCKER_MODE', 'false').lower() == 'true' @@ -75,7 +73,14 @@ else: # Use temp directory for local development app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp() +# Session configuration app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', secrets.token_hex(32)) +# Cookie settings for reverse proxy +app.config['SESSION_COOKIE_PATH'] = URL_PREFIX +app.config['SESSION_COOKIE_HTTPONLY'] = True +app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' +# Set Secure flag only for HTTPS (production) +app.config['SESSION_COOKIE_SECURE'] = os.getenv('FLASK_ENV') == 'production' # Excel file path for metadata lookup EXCEL_PATH = Path(__file__).parent / "Celum ID to Adobe Asset Path Mapping Spreadsheet (1).xlsx"