From ce868e1807bcd8cc6e7f19611cb0b5b7ea119c9d Mon Sep 17 00:00:00 2001 From: Matteo Rosati Date: Thu, 15 Jan 2026 12:28:32 +0100 Subject: [PATCH] add endpoint for mongo results (basic auth) --- app.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 147 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 1e4f27f..fa3e2ee 100644 --- a/app.py +++ b/app.py @@ -2,9 +2,12 @@ import os import logging from datetime import datetime from zoneinfo import ZoneInfo -from flask import Flask, request, jsonify +from functools import wraps +from flask import Flask, request, jsonify, Response from ingress import IngressAPI from models import EventType, Plext +from pymongo import MongoClient +from pymongo.errors import PyMongoError # Timezone configuration TIMEZONE = ZoneInfo("Europe/Rome") @@ -18,6 +21,51 @@ logger = logging.getLogger(__name__) app = Flask(__name__) +def check_basic_auth(username: str, password: str) -> bool: + """ + Check if the provided username and password match the configured credentials. + + Args: + username: Username from the request + password: Password from the request + + Returns: + True if credentials match, False otherwise + """ + expected_username = os.getenv("BASIC_AUTH_USER") + expected_password = os.getenv("BASIC_AUTH_PASSWORD") + + if not expected_username or not expected_password: + logger.warning("BASIC_AUTH_USER or BASIC_AUTH_PASSWORD not configured") + return False + + return username == expected_username and password == expected_password + + +def basic_auth_required(f): + """ + Decorator to require basic authentication for an endpoint. + + Returns: + 401 Unauthorized if authentication fails or is missing + """ + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + + if not auth or not check_basic_auth(auth.username, auth.password): + return Response( + "Could not verify your access level for that URL.\n" + "You have to login with proper credentials", + 401, + {"WWW-Authenticate": 'Basic realm="Login Required"'} + ) + + return f(*args, **kwargs) + + return decorated + + def parse_timestamp(value: str) -> int: """ Parse timestamp from either milliseconds (int) or ISO 8601 string. @@ -102,9 +150,93 @@ def plext_to_dict(plext: Plext) -> dict: ], } +@app.route("/plexts/from-db", methods=["GET"]) +@basic_auth_required +def get_plexts_from_db(): + """ + Get plexts from MongoDB with optional filters. -@app.route("/plexts", methods=["GET"]) -def get_plexts(): + Query Parameters: + player_name: Filter by player name (optional) + timestamp_from: Minimum timestamp in milliseconds (optional) + timestamp_to: Maximum timestamp in milliseconds (optional) + + Returns: + JSON response with list of plexts (without _id field) + """ + try: + # Parse query parameters + player_name = request.args.get("player_name") + timestamp_from = request.args.get("timestamp_from") + timestamp_to = request.args.get("timestamp_to") + + # Validate and convert timestamp parameters to integers if provided + if timestamp_from is not None: + try: + timestamp_from = int(timestamp_from) + except ValueError: + return jsonify({"error": "timestamp_from must be an integer"}), 400 + + if timestamp_to is not None: + try: + timestamp_to = int(timestamp_to) + except ValueError: + return jsonify({"error": "timestamp_to must be an integer"}), 400 + + # Build MongoDB filter query + filter_query = {} + + if player_name: + filter_query["player_name"] = player_name + + if timestamp_from is not None: + filter_query["timestamp"] = filter_query.get("timestamp", {}) + filter_query["timestamp"]["$gte"] = timestamp_from + + if timestamp_to is not None: + filter_query["timestamp"] = filter_query.get("timestamp", {}) + filter_query["timestamp"]["$lte"] = timestamp_to + + # Connect to MongoDB + mongo_uri = os.getenv("MONGO_URI") + db_name = os.getenv("DB_NAME") + collection_name = os.getenv("COLLECTION_NAME") + + client = MongoClient(mongo_uri) + db = client[db_name] + collection = db[collection_name] + + try: + # Projection to exclude _id field + projection = {"_id": 0} + + # Execute query with sorting (timestamp DESC - most recent first) + cursor = collection.find( + filter=filter_query, + projection=projection + ).sort("timestamp", -1) + + # Convert cursor to list + plexts = list(cursor) + + return jsonify({ + "count": len(plexts), + "plexts": plexts + }) + + except PyMongoError as e: + logger.error(f"MongoDB error: {e}") + return jsonify({"error": "Database error"}), 500 + finally: + client.close() + + except Exception as e: + logger.exception("Unexpected error in get_plexts_from_db") + return jsonify({"error": "An error occurred"}), 500 + +@app.route("/plexts/from-api", methods=["GET"]) +@basic_auth_required +def get_plexts_from_api(): """ Get plexts from the Ingress API. @@ -199,7 +331,7 @@ def index(): "name": "Ingress Intel API", "version": "1.0.0", "endpoints": { - "/plexts": { + "/plexts/from-api": { "method": "GET", "description": "Get plexts from the Ingress API", "parameters": { @@ -213,7 +345,17 @@ def index(): "max_timestamp": "Maximum timestamp (milliseconds or ISO 8601 format, default: -1)", }, "event_types": [e.name for e in EventType], - } + }, + "/plexts/from-db": { + "method": "GET", + "description": "Get plexts from MongoDB with optional filters", + "parameters": { + "player_name": "Filter by player name (optional)", + "timestamp_from": "Minimum timestamp in milliseconds (optional)", + "timestamp_to": "Maximum timestamp in milliseconds (optional)", + }, + "authentication": "Basic Auth required", + }, }, } )