first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
from . import routes
|
||||
@@ -0,0 +1,52 @@
|
||||
from extensions import db
|
||||
from datetime import datetime
|
||||
from flask import current_app
|
||||
import pytz
|
||||
|
||||
# Import models for direct relationship references
|
||||
import sys
|
||||
import os
|
||||
# Add the parent directory to path for imports to work
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from auth.models import Company, ApiKey
|
||||
from utils.toolbox import get_current_timestamp
|
||||
|
||||
def get_current_time_with_timezone():
|
||||
"""Return current time with the configured timezone"""
|
||||
return get_current_timestamp()
|
||||
|
||||
class Log(db.Model):
|
||||
__tablename__ = 'api_logs'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
event_type = db.Column(db.String(20), nullable=False)
|
||||
user_name = db.Column(db.String(50), nullable=False)
|
||||
computer_name = db.Column(db.String(50), nullable=False)
|
||||
ip_address = db.Column(db.String(15), nullable=False)
|
||||
timestamp = db.Column(db.DateTime, nullable=False, default=get_current_time_with_timezone)
|
||||
retry = db.Column(db.Integer, default=0, nullable=False)
|
||||
company_id = db.Column(db.Integer, db.ForeignKey('app_auth_companies.id'), nullable=True)
|
||||
api_key_id = db.Column(db.Integer, db.ForeignKey('app_auth_api_keys.id'), nullable=True)
|
||||
|
||||
# Relationships
|
||||
company = db.relationship(Company, backref='logs', foreign_keys=[company_id])
|
||||
api_key = db.relationship(ApiKey, backref='logs', foreign_keys=[api_key_id])
|
||||
|
||||
class ErrorLog(db.Model):
|
||||
"""Model for storing application error logs"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
level = db.Column(db.String(20), nullable=False)
|
||||
logger_name = db.Column(db.String(100), nullable=True)
|
||||
message = db.Column(db.Text, nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
pathname = db.Column(db.String(255), nullable=True)
|
||||
lineno = db.Column(db.Integer, nullable=True)
|
||||
request_id = db.Column(db.String(100), nullable=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('app_auth_users.id', ondelete='SET NULL'), nullable=True)
|
||||
remote_addr = db.Column(db.String(50), nullable=True)
|
||||
exception = db.Column(db.Text, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ErrorLog {self.id}: {self.level} - {self.message[:50]}>'
|
||||
|
||||
# API-specific models
|
||||
# Note: ApiKey model is in auth/models.py as it's related to user authentication
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
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
|
||||
Reference in New Issue
Block a user