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) <noreply@anthropic.com>
This commit is contained in:
SamoilenkoVadym 2026-02-09 11:24:49 +00:00
parent 53ab780758
commit 2ea4673bf0
3 changed files with 32 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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"