mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
api layer v0.2.2 - CSV import/export, refactor
This commit is contained in:
@@ -2,9 +2,9 @@ import threading
|
||||
from flask import Flask, request, jsonify, Response
|
||||
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
|
||||
from .devices_endpoint import delete_unknown_devices, delete_all_with_empty_macs, delete_devices
|
||||
from .events_endpoint import delete_device_events, delete_events, delete_events_30, get_events
|
||||
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 delete_unknown_devices, delete_all_with_empty_macs, delete_devices, export_devices, import_csv
|
||||
from .events_endpoint import delete_events, delete_events_30, get_events
|
||||
from .history_endpoint import delete_online_history
|
||||
from .prometheus_endpoint import getMetricStats
|
||||
from .sync_endpoint import handle_sync_post, handle_sync_get
|
||||
@@ -97,6 +97,34 @@ def api_reset_device_props(mac):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return reset_device_props(mac, request.json)
|
||||
|
||||
@app.route("/device/copy", methods=["POST"])
|
||||
def api_copy_device():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
mac_from = data.get("macFrom")
|
||||
mac_to = data.get("macTo")
|
||||
|
||||
if not mac_from or not mac_to:
|
||||
return jsonify({"success": False, "error": "macFrom and macTo are required"}), 400
|
||||
|
||||
return copy_device(mac_from, mac_to)
|
||||
|
||||
@app.route("/device/<mac>/update-column", methods=["POST"])
|
||||
def api_update_device_column(mac):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
column_name = data.get("columnName")
|
||||
column_value = data.get("columnValue")
|
||||
|
||||
if not column_name or not column_value:
|
||||
return jsonify({"success": False, "error": "columnName and columnValue are required"}), 400
|
||||
|
||||
return update_device_column(mac, column_name, column_value)
|
||||
|
||||
# --------------------------
|
||||
# Devices Collections
|
||||
# --------------------------
|
||||
@@ -129,6 +157,21 @@ def api_get_devices_totals():
|
||||
return get_devices_totals()
|
||||
|
||||
|
||||
@app.route("/devices/export", methods=["GET"])
|
||||
@app.route("/devices/export/<format>", methods=["GET"])
|
||||
def api_export_devices(format=None):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
export_format = (format or request.args.get("format", "csv")).lower()
|
||||
return export_devices(export_format)
|
||||
|
||||
@app.route("/devices/import", methods=["POST"])
|
||||
def api_import_csv():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return import_csv(request.files.get("file"))
|
||||
|
||||
# --------------------------
|
||||
# Online history
|
||||
# --------------------------
|
||||
@@ -144,7 +187,7 @@ def api_delete_online_history():
|
||||
# --------------------------
|
||||
|
||||
@app.route("/events/<mac>", methods=["DELETE"])
|
||||
def api_delete_device_events(mac):
|
||||
def api_events_by_mac(mac):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return delete_device_events(mac)
|
||||
@@ -156,7 +199,7 @@ def api_delete_all_events():
|
||||
return delete_events()
|
||||
|
||||
@app.route("/events", methods=["GET"])
|
||||
def api_delete_all_events():
|
||||
def api_get_events():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
@@ -170,22 +213,6 @@ def api_delete_old_events():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return delete_events_30()
|
||||
|
||||
# --------------------------
|
||||
# CSV Import / Export
|
||||
# --------------------------
|
||||
|
||||
@app.route("/devices/export", methods=["GET"])
|
||||
def api_export_csv():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return export_csv()
|
||||
|
||||
@app.route("/devices/import", methods=["POST"])
|
||||
def api_import_csv():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return import_csv(request.files.get("file"))
|
||||
|
||||
# --------------------------
|
||||
# Prometheus metrics endpoint
|
||||
# --------------------------
|
||||
|
||||
@@ -14,8 +14,8 @@ 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 row_to_json, get_date_from_period, is_random_mac, format_date, get_setting_value
|
||||
|
||||
from helper import is_random_mac, format_date, get_setting_value
|
||||
from db.db_helper import row_to_json, get_date_from_period
|
||||
|
||||
# --------------------------
|
||||
# Device Endpoints Functions
|
||||
@@ -272,3 +272,63 @@ def reset_device_props(mac, data=None):
|
||||
conn.close()
|
||||
return jsonify({"success": True})
|
||||
|
||||
def update_device_column(mac, column_name, column_value):
|
||||
"""
|
||||
Update a specific column for a given device.
|
||||
Example: update_device_column("AA:BB:CC:DD:EE:FF", "devParentMAC", "Internet")
|
||||
"""
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Build safe SQL with column name whitelisted
|
||||
sql = f"UPDATE Devices SET {column_name}=? WHERE devMac=?"
|
||||
cur.execute(sql, (column_value, mac))
|
||||
conn.commit()
|
||||
|
||||
if cur.rowcount > 0:
|
||||
return jsonify({"success": True})
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
def copy_device(mac_from, mac_to):
|
||||
"""
|
||||
Copy a device entry from one MAC to another.
|
||||
If a device already exists with mac_to, it will be replaced.
|
||||
"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Drop temporary table if exists
|
||||
cur.execute("DROP TABLE IF EXISTS temp_devices")
|
||||
|
||||
# Create temporary table with source device
|
||||
cur.execute("CREATE TABLE temp_devices AS SELECT * FROM Devices WHERE devMac = ?", (mac_from,))
|
||||
|
||||
# Update temporary table to target MAC
|
||||
cur.execute("UPDATE temp_devices SET devMac = ?", (mac_to,))
|
||||
|
||||
# Delete previous entry with target MAC
|
||||
cur.execute("DELETE FROM Devices WHERE devMac = ?", (mac_to,))
|
||||
|
||||
# Insert new entry from temporary table
|
||||
cur.execute("INSERT INTO Devices SELECT * FROM temp_devices WHERE devMac = ?", (mac_to,))
|
||||
|
||||
# Drop temporary table
|
||||
cur.execute("DROP TABLE temp_devices")
|
||||
|
||||
conn.commit()
|
||||
return jsonify({"success": True, "message": f"Device copied from {mac_from} to {mac_to}"})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@@ -5,16 +5,22 @@ import subprocess
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from flask import jsonify, request
|
||||
from flask import jsonify, request, Response
|
||||
import csv
|
||||
import io
|
||||
from io import StringIO
|
||||
|
||||
# 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 row_to_json, get_date_from_period, is_random_mac, format_date, get_setting_value
|
||||
from helper import is_random_mac, format_date, get_setting_value
|
||||
from db.db_helper import get_table_json
|
||||
|
||||
|
||||
# --------------------------
|
||||
@@ -72,4 +78,118 @@ def delete_unknown_devices():
|
||||
cur.execute("""DELETE FROM Devices WHERE devName='(unknown)' OR devName='(name not found)'""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"success": True, "deleted": cur.rowcount})
|
||||
return jsonify({"success": True, "deleted": cur.rowcount})
|
||||
|
||||
def export_devices(export_format):
|
||||
"""
|
||||
Export devices from the Devices table in teh desired format.
|
||||
- If `macs` is None → delete ALL devices.
|
||||
- If `macs` is a list → delete only matching MACs (supports wildcard '*').
|
||||
"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Fetch all devices
|
||||
devices_json = get_table_json(cur, "SELECT * FROM Devices")
|
||||
conn.close()
|
||||
|
||||
# Ensure columns exist
|
||||
columns = devices_json.columnNames or (
|
||||
list(devices_json["data"][0].keys()) if devices_json["data"] else []
|
||||
)
|
||||
|
||||
|
||||
if export_format == "json":
|
||||
# Convert to standard dict for Flask JSON
|
||||
return jsonify({
|
||||
"data": [row for row in devices_json["data"]],
|
||||
"columns": list(columns)
|
||||
})
|
||||
elif export_format == "csv":
|
||||
|
||||
si = StringIO()
|
||||
writer = csv.DictWriter(si, fieldnames=columns, quoting=csv.QUOTE_ALL)
|
||||
writer.writeheader()
|
||||
for row in devices_json.json["data"]:
|
||||
writer.writerow(row)
|
||||
|
||||
return Response(
|
||||
si.getvalue(),
|
||||
mimetype="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=devices.csv"},
|
||||
)
|
||||
else:
|
||||
return jsonify({"error": f"Unsupported format '{export_format}'"}), 400
|
||||
|
||||
def import_csv(file_storage=None):
|
||||
data = ""
|
||||
skipped = []
|
||||
error = None
|
||||
|
||||
# 1. Try JSON `content` (base64-encoded CSV)
|
||||
if request.is_json and request.json.get("content"):
|
||||
try:
|
||||
data = base64.b64decode(request.json["content"], validate=True).decode("utf-8")
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Base64 decode failed: {e}"}), 400
|
||||
|
||||
# 2. Otherwise, try uploaded file
|
||||
elif file_storage:
|
||||
data = file_storage.read().decode("utf-8")
|
||||
|
||||
# 3. Fallback: try local file (same as PHP `$file = '../../../config/devices.csv';`)
|
||||
else:
|
||||
local_file = "/app/config/devices.csv"
|
||||
try:
|
||||
with open(local_file, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "CSV file missing"}), 404
|
||||
|
||||
if not data:
|
||||
return jsonify({"error": "No CSV data found"}), 400
|
||||
|
||||
# --- Clean up newlines inside quoted fields ---
|
||||
data = re.sub(
|
||||
r'"([^"]*)"',
|
||||
lambda m: m.group(0).replace("\n", " "),
|
||||
data
|
||||
)
|
||||
|
||||
# --- Parse CSV ---
|
||||
lines = data.splitlines()
|
||||
reader = csv.reader(lines)
|
||||
try:
|
||||
header = [h.strip() for h in next(reader)]
|
||||
except StopIteration:
|
||||
return jsonify({"error": "CSV missing header"}), 400
|
||||
|
||||
# --- Wipe Devices table ---
|
||||
conn = get_temp_db_connection()
|
||||
sql = conn.cursor()
|
||||
sql.execute("DELETE FROM Devices")
|
||||
|
||||
# --- Prepare insert ---
|
||||
placeholders = ",".join(["?"] * len(header))
|
||||
insert_sql = f"INSERT INTO Devices ({', '.join(header)}) VALUES ({placeholders})"
|
||||
|
||||
row_count = 0
|
||||
for idx, row in enumerate(reader, start=1):
|
||||
if len(row) != len(header):
|
||||
skipped.append(idx)
|
||||
continue
|
||||
try:
|
||||
sql.execute(insert_sql, [col.strip() for col in row])
|
||||
row_count += 1
|
||||
except sqlite3.Error as e:
|
||||
mylog("error", [f"[ImportCSV] SQL ERROR row {idx}: {e}"])
|
||||
skipped.append(idx)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"inserted": row_count,
|
||||
"skipped_lines": skipped
|
||||
})
|
||||
@@ -14,7 +14,8 @@ 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 row_to_json, get_date_from_period, is_random_mac, format_date, get_setting_value
|
||||
from helper import is_random_mac, format_date, get_setting_value
|
||||
from db.db_helper import row_to_json
|
||||
|
||||
|
||||
# --------------------------
|
||||
@@ -68,16 +69,4 @@ def delete_events():
|
||||
|
||||
return jsonify({"success": True, "message": "Deleted all events"})
|
||||
|
||||
def delete_device_events(mac):
|
||||
"""Delete all events"""
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = "DELETE FROM Events WHERE eve_MAC= ? "
|
||||
cur.execute(sql, (mac,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": "Deleted all events for the device"})
|
||||
|
||||
|
||||
@@ -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 row_to_json, get_date_from_period, is_random_mac, format_date, get_setting_value
|
||||
from helper import is_random_mac, format_date, get_setting_value
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user