mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
api layer v0.2.5 - /sessions + graphql tests
This commit is contained in:
@@ -4,10 +4,11 @@ from flask_cors import CORS
|
||||
from .graphql_endpoint import devicesSchema
|
||||
from .device_endpoint import get_device_data, set_device_data, delete_device, delete_device_events, reset_device_props, copy_device, update_device_column
|
||||
from .devices_endpoint import get_all_devices, delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv, devices_totals, devices_by_status
|
||||
from .events_endpoint import delete_events, delete_events_30, get_events
|
||||
from .events_endpoint import delete_events, delete_events_30, get_events, create_event
|
||||
from .history_endpoint import delete_online_history
|
||||
from .prometheus_endpoint import getMetricStats
|
||||
from .nettools_endpoint import wakeonlan, traceroute, speedtest
|
||||
from .prometheus_endpoint import get_metric_stats
|
||||
from .sessions_endpoint import get_sessions, delete_session, create_session, get_sessions_calendar
|
||||
from .nettools_endpoint import wakeonlan, traceroute, speedtest, nslookup, nmap_scan, internet_info
|
||||
from .sync_endpoint import handle_sync_post, handle_sync_get
|
||||
import sys
|
||||
|
||||
@@ -30,6 +31,7 @@ CORS(
|
||||
r"/devices/*": {"origins": "*"},
|
||||
r"/history/*": {"origins": "*"},
|
||||
r"/nettools/*": {"origins": "*"},
|
||||
r"/sessions/*": {"origins": "*"},
|
||||
r"/events/*": {"origins": "*"}
|
||||
},
|
||||
supports_credentials=True,
|
||||
@@ -212,7 +214,47 @@ def api_speedtest():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return speedtest()
|
||||
|
||||
@app.route("/nettools/nslookup", methods=["POST"])
|
||||
def api_nslookup():
|
||||
"""
|
||||
API endpoint to handle nslookup requests.
|
||||
Expects JSON with 'devLastIP'.
|
||||
"""
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "devLastIP" not in data:
|
||||
return jsonify({"success": False, "error": "Missing 'devLastIP'"}), 400
|
||||
|
||||
ip = data["devLastIP"]
|
||||
return nslookup(ip)
|
||||
|
||||
@app.route("/nettools/nmap", methods=["POST"])
|
||||
def api_nmap():
|
||||
"""
|
||||
API endpoint to handle nmap scan requests.
|
||||
Expects JSON with 'scan' (IP address) and 'mode' (scan mode).
|
||||
"""
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "scan" not in data or "mode" not in data:
|
||||
return jsonify({"success": False, "error": "Missing 'scan' or 'mode'"}), 400
|
||||
|
||||
ip = data["scan"]
|
||||
mode = data["mode"]
|
||||
return nmap_scan(ip, mode)
|
||||
|
||||
|
||||
@app.route("/nettools/internetinfo", methods=["GET"])
|
||||
def api_internet_info():
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
return internet_info()
|
||||
|
||||
# --------------------------
|
||||
# Online history
|
||||
# --------------------------
|
||||
@@ -227,6 +269,25 @@ def api_delete_online_history():
|
||||
# Device Events
|
||||
# --------------------------
|
||||
|
||||
@app.route("/events/create/<mac>", methods=["POST"])
|
||||
def api_create_event(mac):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.json or {}
|
||||
ip = data.get("ip", "0.0.0.0")
|
||||
event_type = data.get("event_type", "Device Down")
|
||||
additional_info = data.get("additional_info", "")
|
||||
pending_alert = data.get("pending_alert", 1)
|
||||
event_time = data.get("event_time", None)
|
||||
|
||||
# Call the helper to insert into DB
|
||||
create_event(mac, ip, event_type, additional_info, pending_alert, event_time)
|
||||
|
||||
# Return consistent JSON response
|
||||
return jsonify({"success": True, "message": f"Event created for {mac}"})
|
||||
|
||||
|
||||
@app.route("/events/<mac>", methods=["DELETE"])
|
||||
def api_events_by_mac(mac):
|
||||
if not is_authorized():
|
||||
@@ -244,8 +305,7 @@ def api_get_events():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
mac = request.json.get("mac") if request.is_json else None
|
||||
|
||||
mac = request.args.get("mac")
|
||||
return get_events(mac)
|
||||
|
||||
@app.route("/events/30days", methods=["DELETE"])
|
||||
@@ -254,19 +314,74 @@ def api_delete_old_events():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return delete_events_30()
|
||||
|
||||
# --------------------------
|
||||
# Sessions
|
||||
# --------------------------
|
||||
|
||||
@app.route("/sessions/create", methods=["POST"])
|
||||
def api_create_session():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.json
|
||||
mac = data.get("mac")
|
||||
ip = data.get("ip")
|
||||
start_time = data.get("start_time")
|
||||
end_time = data.get("end_time")
|
||||
event_type_conn = data.get("event_type_conn", "Connected")
|
||||
event_type_disc = data.get("event_type_disc", "Disconnected")
|
||||
|
||||
if not mac or not ip or not start_time:
|
||||
return jsonify({"success": False, "error": "Missing required parameters"}), 400
|
||||
|
||||
return create_session(mac, ip, start_time, end_time, event_type_conn, event_type_disc)
|
||||
|
||||
|
||||
@app.route("/sessions/delete", methods=["DELETE"])
|
||||
def api_delete_session():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
mac = request.json.get("mac") if request.is_json else None
|
||||
if not mac:
|
||||
return jsonify({"success": False, "error": "Missing MAC parameter"}), 400
|
||||
|
||||
return delete_session(mac)
|
||||
|
||||
|
||||
@app.route("/sessions/list", methods=["GET"])
|
||||
def api_get_sessions():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
mac = request.args.get("mac")
|
||||
start_date = request.args.get("start_date")
|
||||
end_date = request.args.get("end_date")
|
||||
|
||||
return get_sessions(mac, start_date, end_date)
|
||||
|
||||
@app.route("/sessions/calendar", methods=["GET"])
|
||||
def api_get_sessions_calendar():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
# Query params: /sessions/calendar?start=2025-08-01&end=2025-08-21
|
||||
start_date = request.args.get("start")
|
||||
end_date = request.args.get("end")
|
||||
|
||||
return get_sessions_calendar(start_date, end_date)
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Prometheus metrics endpoint
|
||||
# --------------------------
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
|
||||
# Check for API token in headers
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
|
||||
# Return Prometheus metrics as plain text
|
||||
return Response(getMetricStats(), mimetype="text/plain")
|
||||
return Response(get_metric_stats(), mimetype="text/plain")
|
||||
|
||||
# --------------------------
|
||||
# SYNC endpoint
|
||||
|
||||
@@ -14,7 +14,7 @@ INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from database import get_temp_db_connection
|
||||
from helper import is_random_mac, format_date, get_setting_value
|
||||
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, timeNowTZ, mylog, ensure_datetime
|
||||
from db.db_helper import row_to_json
|
||||
|
||||
|
||||
@@ -22,6 +22,38 @@ from db.db_helper import row_to_json
|
||||
# Events Endpoints Functions
|
||||
# --------------------------
|
||||
|
||||
|
||||
def create_event(
|
||||
mac: str,
|
||||
ip: str,
|
||||
event_type: str = "Device Down",
|
||||
additional_info: str = "",
|
||||
pending_alert: int = 1,
|
||||
event_time: datetime | None = None
|
||||
):
|
||||
"""
|
||||
Insert a single event into the Events table and return a standardized JSON response.
|
||||
Exceptions will propagate to the caller.
|
||||
"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
if isinstance(event_time, str):
|
||||
start_time = ensure_datetime(event_time)
|
||||
|
||||
start_time = ensure_datetime(event_time)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType, eve_AdditionalInfo, eve_PendingAlertEmail)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (mac, ip, start_time, event_type, additional_info, pending_alert))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
mylog("debug", f"[Events] Created event for {mac} ({event_type})")
|
||||
return jsonify({"success": True, "message": f"Created event for {mac}"})
|
||||
|
||||
|
||||
def get_events(mac=None):
|
||||
"""
|
||||
Fetch all events, or events for a specific MAC if provided.
|
||||
@@ -49,7 +81,7 @@ def delete_events_30():
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', '-30 day')"
|
||||
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', '-30 days')"
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -70,3 +102,4 @@ def delete_events():
|
||||
return jsonify({"success": True, "message": "Deleted all events"})
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ class Query(ObjectType):
|
||||
try:
|
||||
with open(folder + 'table_devices.json', 'r') as f:
|
||||
devices_data = json.load(f)["data"]
|
||||
total_count = len(devices_data)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[graphql_schema] Error loading devices data: {e}')
|
||||
return DeviceResult(devices=[], count=0)
|
||||
|
||||
@@ -95,4 +95,128 @@ def speedtest():
|
||||
"success": False,
|
||||
"error": "Speedtest failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
}), 500
|
||||
|
||||
|
||||
def nslookup(ip):
|
||||
"""
|
||||
Run an nslookup on the given IP address.
|
||||
Returns JSON with the lookup output or error.
|
||||
"""
|
||||
# Validate IP
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid IP address"
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Run nslookup command
|
||||
result = subprocess.run(
|
||||
["nslookup", ip],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output_lines = result.stdout.strip().split("\n")
|
||||
return jsonify({"success": True, "output": output_lines})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "nslookup failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def nmap_scan(ip, mode):
|
||||
"""
|
||||
Run an nmap scan on the given IP address with the requested mode.
|
||||
Modes supported:
|
||||
- "fast" → nmap -F <ip>
|
||||
- "normal" → nmap <ip>
|
||||
- "detail" → nmap -A <ip>
|
||||
- "skipdiscovery" → nmap -Pn <ip>
|
||||
Returns JSON with the scan output or error.
|
||||
"""
|
||||
# Validate IP
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid IP address"
|
||||
}), 400
|
||||
|
||||
# Map scan modes to nmap arguments
|
||||
mode_args = {
|
||||
"fast": ["-F"],
|
||||
"normal": [],
|
||||
"detail": ["-A"],
|
||||
"skipdiscovery": ["-Pn"]
|
||||
}
|
||||
|
||||
if mode not in mode_args:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Invalid scan mode '{mode}'"
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Build and run nmap command
|
||||
cmd = ["nmap"] + mode_args[mode] + [ip]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output_lines = result.stdout.strip().split("\n")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
"ip": ip,
|
||||
"output": output_lines
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "nmap scan failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def internet_info():
|
||||
"""
|
||||
API endpoint to fetch internet info using ipinfo.io.
|
||||
Returns JSON with the info or error.
|
||||
"""
|
||||
try:
|
||||
# Perform the request via curl
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "https://ipinfo.io"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output = result.stdout.strip()
|
||||
if not output:
|
||||
raise ValueError("Empty response from ipinfo.io")
|
||||
|
||||
# Clean up the JSON-like string by removing { } , and "
|
||||
cleaned_output = output.replace("{", "").replace("}", "").replace(",", "").replace('"', "")
|
||||
|
||||
return jsonify({"success": True, "output": cleaned_output})
|
||||
|
||||
except (subprocess.CalledProcessError, ValueError) as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Failed to fetch internet info",
|
||||
"details": str(e)
|
||||
}), 500
|
||||
|
||||
@@ -18,7 +18,7 @@ def escape_label_value(val):
|
||||
# Define a base URL with the user's home directory
|
||||
folder = apiPath
|
||||
|
||||
def getMetricStats():
|
||||
def get_metric_stats():
|
||||
output = []
|
||||
|
||||
# 1. Dashboard totals
|
||||
|
||||
172
server/api_server/sessions_endpoint.py
Executable file
172
server/api_server/sessions_endpoint.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from flask import jsonify, request
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from database import get_temp_db_connection
|
||||
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, timeNowTZ
|
||||
from db.db_helper import row_to_json
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Sessions Endpoints Functions
|
||||
# --------------------------
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def create_session(mac, ip, start_time, end_time=None, event_type_conn="Connected", event_type_disc="Disconnected"):
|
||||
"""Insert a new session into Sessions table"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO Sessions (ses_MAC, ses_IP, ses_DateTimeConnection, ses_DateTimeDisconnection,
|
||||
ses_EventTypeConnection, ses_EventTypeDisconnection)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (mac, ip, start_time, end_time, event_type_conn, event_type_disc))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": f"Session created for MAC {mac}"})
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def delete_session(mac):
|
||||
"""Delete all sessions for a given MAC"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("DELETE FROM Sessions WHERE ses_MAC = ?", (mac,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": f"Deleted sessions for MAC {mac}"})
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def get_sessions(mac=None, start_date=None, end_date=None):
|
||||
"""Retrieve sessions optionally filtered by MAC and date range"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = "SELECT * FROM Sessions WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if mac:
|
||||
sql += " AND ses_MAC = ?"
|
||||
params.append(mac)
|
||||
if start_date:
|
||||
sql += " AND ses_DateTimeConnection >= ?"
|
||||
params.append(start_date)
|
||||
if end_date:
|
||||
sql += " AND ses_DateTimeDisconnection <= ?"
|
||||
params.append(end_date)
|
||||
|
||||
cur.execute(sql, tuple(params))
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Convert rows to list of dicts
|
||||
table_data = [dict(r) for r in rows]
|
||||
|
||||
return jsonify({"success": True, "sessions": table_data})
|
||||
|
||||
|
||||
|
||||
def get_sessions_calendar(start_date, end_date):
|
||||
"""
|
||||
Fetch sessions between a start and end date for calendar display.
|
||||
Returns JSON list of calendar sessions.
|
||||
"""
|
||||
|
||||
if not start_date or not end_date:
|
||||
return jsonify({"success": False, "error": "Missing start or end date"}), 400
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = """
|
||||
-- Correct missing connection/disconnection sessions:
|
||||
-- If ses_EventTypeConnection is missing, backfill from last disconnection
|
||||
-- If ses_EventTypeDisconnection is missing, forward-fill from next connection
|
||||
|
||||
SELECT
|
||||
SES1.ses_MAC, SES1.ses_EventTypeConnection, SES1.ses_DateTimeConnection,
|
||||
SES1.ses_EventTypeDisconnection, SES1.ses_DateTimeDisconnection, SES1.ses_IP,
|
||||
SES1.ses_AdditionalInfo, SES1.ses_StillConnected,
|
||||
|
||||
CASE
|
||||
WHEN SES1.ses_EventTypeConnection = '<missing event>' THEN
|
||||
IFNULL(
|
||||
(SELECT MAX(SES2.ses_DateTimeDisconnection)
|
||||
FROM Sessions AS SES2
|
||||
WHERE SES2.ses_MAC = SES1.ses_MAC
|
||||
AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection
|
||||
AND SES2.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?)
|
||||
),
|
||||
DATETIME(SES1.ses_DateTimeDisconnection, '-1 hour')
|
||||
)
|
||||
ELSE SES1.ses_DateTimeConnection
|
||||
END AS ses_DateTimeConnectionCorrected,
|
||||
|
||||
CASE
|
||||
WHEN SES1.ses_EventTypeDisconnection = '<missing event>' THEN
|
||||
(SELECT MIN(SES2.ses_DateTimeConnection)
|
||||
FROM Sessions AS SES2
|
||||
WHERE SES2.ses_MAC = SES1.ses_MAC
|
||||
AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection
|
||||
AND SES2.ses_DateTimeConnection BETWEEN Date(?) AND Date(?)
|
||||
)
|
||||
ELSE SES1.ses_DateTimeDisconnection
|
||||
END AS ses_DateTimeDisconnectionCorrected
|
||||
|
||||
FROM Sessions AS SES1
|
||||
WHERE (SES1.ses_DateTimeConnection BETWEEN Date(?) AND Date(?))
|
||||
OR (SES1.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?))
|
||||
OR SES1.ses_StillConnected = 1
|
||||
"""
|
||||
|
||||
cur.execute(sql, (start_date, end_date, start_date, end_date, start_date, end_date, start_date, end_date))
|
||||
rows = cur.fetchall()
|
||||
|
||||
table_data = []
|
||||
for r in rows:
|
||||
row = dict(r)
|
||||
|
||||
# Determine color
|
||||
if row["ses_EventTypeConnection"] == "<missing event>" or row["ses_EventTypeDisconnection"] == "<missing event>":
|
||||
color = "#f39c12"
|
||||
elif row["ses_StillConnected"] == 1:
|
||||
color = "#00a659"
|
||||
else:
|
||||
color = "#0073b7"
|
||||
|
||||
# Tooltip
|
||||
tooltip = (
|
||||
f"Connection: {format_event_date(row['ses_DateTimeConnection'], row['ses_EventTypeConnection'])}\n"
|
||||
f"Disconnection: {format_event_date(row['ses_DateTimeDisconnection'], row['ses_EventTypeDisconnection'])}\n"
|
||||
f"IP: {row['ses_IP']}"
|
||||
)
|
||||
|
||||
# Append calendar entry
|
||||
table_data.append({
|
||||
"resourceId": row["ses_MAC"],
|
||||
"title": "",
|
||||
"start": format_date_iso(row["ses_DateTimeConnectionCorrected"]),
|
||||
"end": format_date_iso(row["ses_DateTimeDisconnectionCorrected"]),
|
||||
"color": color,
|
||||
"tooltip": tooltip,
|
||||
"className": "no-border"
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return jsonify({"success": True, "sessions": table_data})
|
||||
Reference in New Issue
Block a user