first commit

This commit is contained in:
ghostersk
2025-05-25 20:26:18 +01:00
commit 5375ef6121
77 changed files with 9073 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
from flask import Blueprint
api_bp = Blueprint('api', __name__)
from . import routes
+52
View File
@@ -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
View File
@@ -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