234 lines
10 KiB
Python
234 lines
10 KiB
Python
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 |