from flask import Blueprint, request, jsonify, current_app, g from auth.models import User, ApiKey from api.models import Log from extensions import db from functools import wraps from datetime import datetime, timezone, timedelta from api import api_bp from sqlalchemy import and_, text import pytz import logging # Use the existing blueprint from __init__.py instead of creating a new one api = api_bp logger = logging.getLogger(__name__) def require_api_key(f): @wraps(f) def decorated(*args, **kwargs): try: api_key = request.headers.get('X-API-Key') if not api_key: logger.warning('API request without API key', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'endpoint': request.endpoint, 'method': request.method }) return jsonify({"message": "No API key provided"}), 401 key = ApiKey.query.filter_by(key=api_key).first() if not key: logger.warning('Invalid API key used', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'endpoint': request.endpoint, 'method': request.method, 'api_key_prefix': api_key[:8] + '...' if len(api_key) > 8 else api_key }) return jsonify({"message": "Invalid API key"}), 401 # Check if the API key is active if hasattr(key, 'is_active') and not key.is_active: logger.warning('Disabled API key used', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'endpoint': request.endpoint, 'method': request.method, 'api_key_id': key.id, 'company_id': key.company_id }) return jsonify({"message": "API key has been disabled"}), 401 # Update last used timestamp key.last_used = datetime.now(pytz.timezone(current_app.config['TIMEZONE'])) # Save the API key and associated company in g object for use in route functions g.api_key = key g.company_id = key.company_id db.session.commit() return f(*args, **kwargs) except Exception as e: logger.exception('Error in API key authentication', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'endpoint': request.endpoint, 'method': request.method, 'has_api_key': bool(request.headers.get('X-API-Key')), 'error': str(e) }) return jsonify({"message": "Authentication error occurred"}), 500 return decorated @api.route('/log_event', methods=['POST']) @require_api_key def log_event(): try: data = request.get_json() # Parse the timestamp from the request - handle different formats timestamp_str = data['Timestamp'] timestamp_utc = None # Try different timestamp formats formats_to_try = [ '%Y-%m-%dT%H:%M:%S%z', # RFC3339/ISO8601 with timezone offset (from Go app) '%Y-%m-%dT%H:%M:%SZ', # ISO format with Z (UTC) '%Y-%m-%d %H:%M:%S %Z', # Format with timezone name '%Y-%m-%d %H:%M:%S', # Simple format without timezone '%Y-%m-%d %H:%M:%S%z', # Format with numeric timezone ] for fmt in formats_to_try: try: if fmt == '%Y-%m-%d %H:%M:%S %Z': # Special handling for timezone names dt_parts = timestamp_str.rsplit(' ', 1) if len(dt_parts) == 2: dt_str, tz_str = dt_parts # Try to parse the datetime part dt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') # Try to convert timezone abbreviation to a pytz timezone tzinfos = { 'BST': 3600, # British Summer Time (UTC+1) 'GMT': 0, # Greenwich Mean Time 'UTC': 0, # Coordinated Universal Time # Add more timezone abbreviations as needed } if tz_str in tzinfos: # Create aware datetime with the specified timezone offset offset = timedelta(seconds=tzinfos[tz_str]) timestamp_utc = dt.replace(tzinfo=timezone(offset)) break elif fmt == '%Y-%m-%dT%H:%M:%S%z': # Handle RFC3339 format from Go application timestamp_utc = datetime.strptime(timestamp_str, fmt) break else: timestamp_utc = datetime.strptime(timestamp_str, fmt) if fmt == '%Y-%m-%d %H:%M:%S': # No timezone info, assume UTC timestamp_utc = pytz.utc.localize(timestamp_utc) break except ValueError: continue if timestamp_utc is None: return jsonify({'message': f'Could not parse timestamp: {timestamp_str}', 'status': 'error'}), 400 # Convert to the configured timezone from utils.toolbox import get_app_timezone app_timezone = get_app_timezone() if timestamp_utc.tzinfo is None: timestamp_utc = pytz.utc.localize(timestamp_utc) timestamp = timestamp_utc.astimezone(app_timezone) # Check if this is a retry attempt is_retry = data.get('retry', 0) # Check if a record with the same attributes already exists existing_log = Log.query.filter( and_( Log.event_type == data['EventType'], Log.user_name == data['UserName'], Log.computer_name == data['ComputerName'], Log.ip_address == data['IPAddress'], Log.timestamp == timestamp ) ).first() if existing_log: # Record already exists, don't create duplicate return jsonify({'message': 'Event already recorded', 'status': 'success'}), 200 # Create new log entry with company_id and api_key_id from the API key log = Log( event_type=data['EventType'], user_name=data['UserName'], computer_name=data['ComputerName'], ip_address=data['IPAddress'], timestamp=timestamp, retry=is_retry, company_id=g.company_id, # Add the company ID from the API key api_key_id=g.api_key.id # Add the API key ID ) db.session.add(log) db.session.commit() return jsonify({'message': 'Event logged successfully', 'status': 'success'}), 201 except Exception as e: db.session.rollback() logger.exception('Failed to log event in API', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'api_key_id': getattr(g, 'api_key', {}).id if hasattr(g, 'api_key') and g.api_key else None, 'company_id': getattr(g, 'company_id', None), 'request_data': data if 'data' in locals() else None, 'timestamp_str': data.get('Timestamp') if 'data' in locals() else None, 'event_type': data.get('EventType') if 'data' in locals() else None, 'user_name': data.get('UserName') if 'data' in locals() else None, 'computer_name': data.get('ComputerName') if 'data' in locals() else None, 'retry_attempt': data.get('retry', 0) if 'data' in locals() else None, 'error': str(e) }) return jsonify({'message': f'Failed to log event: {str(e)}', 'status': 'error'}), 500 @api.route('/health', methods=['POST']) @require_api_key def health_check(): """ Health check endpoint that verifies: - API key authentication (handled by @require_api_key decorator) - Database connectivity - Application status """ try: # Test database connectivity by performing a simple query # This will raise an exception if the database is not accessible db.session.execute(text('SELECT 1')).fetchone() # Test that we can access the API key from the decorator api_key_id = g.api_key.id if hasattr(g, 'api_key') else None company_id = g.company_id if hasattr(g, 'company_id') else None # Get current timestamp in the configured timezone app_timezone = pytz.timezone(current_app.config['TIMEZONE']) current_time = datetime.now(app_timezone) return jsonify({ 'status': 'ok', 'message': 'Health check passed', 'timestamp': current_time.isoformat(), 'database': 'connected', 'api_key_verified': api_key_id is not None, 'company_id': company_id }), 200 except Exception as e: # Log the error for debugging purposes logger.exception('Health check failed', extra={ 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent'), 'api_key_id': getattr(g, 'api_key', {}).id if hasattr(g, 'api_key') and g.api_key else None, 'company_id': getattr(g, 'company_id', None), 'error': str(e) }) return jsonify({ 'status': 'error', 'message': 'Health check failed', 'error': str(e), 'database': 'disconnected' }), 500