Jarian Cottingham f4b84cc412 fix: security hardening, code fixes, and infrastructure improvements
- Replace eval() with json.loads() in database.py (RCE fix)
- Use json.dumps() for safe storage of list fields
- Add API key authentication middleware
- Remove hardcoded credentials, require env vars
- Disable Flask debug mode
- Restrict FTP homedir to /app/data with read-only perms
- Fix threading: Lock -> RLock, add WAL mode
- Fix API calls to use correct DatabaseManager methods
- Fix main.py FTP method names
- Fix click.click.echo typo
- Implement scheduler _run_all_jobs
- Add __main__.py for module execution
- Pin dependency versions
- Use .env vars in docker-compose, read-only DB for FTP
- Implement AI text chunking with overlap windows
- Add schema validation for AI responses
- Skip unsupported file types instead of fallback
2026-07-05 13:05:08 +00:00

210 lines
6.9 KiB
Python

"""
REST API for FactsDB service
Provides endpoints for querying facts and managing the system
"""
from flask import Flask, jsonify, request, Response
from functools import wraps
from typing import Dict, Any, List
import json
import os
from .config import Config
from .database import DatabaseManager
from .monitoring import get_metrics, get_metrics_json, start_uptime_monitor, increment_fact_extraction, increment_file_processing, increment_error
# Start uptime monitoring
uptime_thread = start_uptime_monitor()
def require_api_key(f):
"""Decorator to require API key authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = os.getenv('API_KEY', '')
if api_key:
provided_key = request.headers.get('X-API-Key', '')
if provided_key != api_key:
return jsonify({'error': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated_function
def create_app() -> Flask:
"""Create and configure the Flask application"""
app = Flask(__name__)
config = Config()
db_manager = DatabaseManager(config.database)
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'service': 'FactsDB'
})
@app.route('/tables', methods=['GET'])
@require_api_key
def get_tables():
"""Get all available tables with record counts"""
try:
tables = db_manager.get_all_tables()
return jsonify({
'tables': tables,
'total_tables': len(tables)
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/tables/<table_name>', methods=['GET'])
@require_api_key
def get_table_data(table_name: str):
"""Get all facts from a specific table"""
try:
facts = db_manager.query_table(table_name)
return jsonify({
'table_name': table_name,
'facts': facts,
'count': len(facts)
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/tables/<table_name>/query', methods=['POST'])
@require_api_key
def query_table(table_name: str):
"""Query facts from a specific table with custom query"""
try:
data = request.get_json()
query = data.get('query', '') if data else ''
facts = db_manager.query_table(table_name, query)
return jsonify({
'table_name': table_name,
'facts': facts,
'count': len(facts)
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/tables/<table_name>/count', methods=['GET'])
@require_api_key
def get_table_count(table_name: str):
"""Get record count for a specific table"""
try:
count = db_manager.get_table_record_count(table_name)
return jsonify({
'table_name': table_name,
'count': count
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/fact/<int:fact_id>', methods=['GET'])
@require_api_key
def get_fact(fact_id: int):
"""Get a specific fact by ID"""
try:
with db_manager.get_connection() as conn:
cursor = conn.execute('''
SELECT * FROM facts WHERE id = ?
''', (fact_id,))
fact = cursor.fetchone()
if fact:
return jsonify(dict(fact))
else:
return jsonify({'error': 'Fact not found'}), 404
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/search', methods=['GET'])
@require_api_key
def search():
"""Search across all tables"""
try:
query = request.args.get('q', '')
if not query:
return jsonify({'error': 'Search query required'}), 400
# Simple search implementation - in a real system this would be more sophisticated
results = []
# Get all tables
tables = db_manager.get_all_tables()
for table in tables:
table_name = table['table_name']
facts = db_manager.query_table(table_name)
for fact in facts:
# Search in key fields
search_content = f"{fact.get('title', '')} {fact.get('summary', '')} {fact.get('main_topic', '')}"
if query.lower() in search_content.lower():
results.append({
'table_name': table_name,
'fact': fact
})
return jsonify({
'query': query,
'results': results,
'count': len(results)
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/version', methods=['GET'])
def version():
"""Get service version"""
return jsonify({
'version': '1.0.0',
'service': 'FactsDB'
})
@app.route('/metrics', methods=['GET'])
def metrics():
"""Prometheus metrics endpoint"""
try:
metrics_text = get_metrics()
return Response(metrics_text, mimetype='text/plain')
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/metrics/json', methods=['GET'])
def metrics_json():
"""JSON metrics endpoint"""
try:
metrics_data = get_metrics_json()
return jsonify(metrics_data)
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
@app.route('/stats', methods=['GET'])
@require_api_key
def stats():
"""Get detailed service statistics"""
try:
db_stats = db_manager.get_database_stats()
metrics_data = get_metrics_json()
return jsonify({
'database_stats': db_stats,
'metrics': metrics_data['metrics'],
'timestamp': metrics_data['timestamp']
})
except Exception as e:
increment_error()
return jsonify({'error': str(e)}), 500
return app
# Create a default app instance
app = create_app()
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5000)