api layer v0.3.1 - /dbquery
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled

Signed-off-by: jokob-sk <jokob-sk@gmail.com>
This commit is contained in:
jokob-sk
2025-08-28 08:12:23 +10:00
parent ae12195439
commit b1b67c268f
3 changed files with 265 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ from .history_endpoint import delete_online_history
from .prometheus_endpoint import get_metric_stats from .prometheus_endpoint import get_metric_stats
from .sessions_endpoint import get_sessions, delete_session, create_session, get_sessions_calendar, get_device_sessions, get_session_events from .sessions_endpoint import get_sessions, delete_session, create_session, get_sessions_calendar, get_device_sessions, get_session_events
from .nettools_endpoint import wakeonlan, traceroute, speedtest, nslookup, nmap_scan, internet_info from .nettools_endpoint import wakeonlan, traceroute, speedtest, nslookup, nmap_scan, internet_info
from .dbquery_endpoint import read_query, write_query, update_query, delete_query
from .sync_endpoint import handle_sync_post, handle_sync_get from .sync_endpoint import handle_sync_post, handle_sync_get
import sys import sys
@@ -33,6 +34,7 @@ CORS(
r"/history/*": {"origins": "*"}, r"/history/*": {"origins": "*"},
r"/nettools/*": {"origins": "*"}, r"/nettools/*": {"origins": "*"},
r"/sessions/*": {"origins": "*"}, r"/sessions/*": {"origins": "*"},
r"/dbquery/*": {"origins": "*"},
r"/events/*": {"origins": "*"} r"/events/*": {"origins": "*"}
}, },
supports_credentials=True, supports_credentials=True,
@@ -263,6 +265,73 @@ def api_internet_info():
return jsonify({"success": False, "error": "Forbidden"}), 403 return jsonify({"success": False, "error": "Forbidden"}), 403
return internet_info() return internet_info()
# --------------------------
# DB query
# --------------------------
@app.route("/dbquery/read", methods=["POST"])
def dbquery_read():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
raw_sql_b64 = data.get("rawSql")
if not raw_sql_b64:
return jsonify({"error": "rawSql is required"}), 400
return read_query(raw_sql_b64)
@app.route("/dbquery/write", methods=["POST"])
def dbquery_write():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
raw_sql_b64 = data.get("rawSql")
if not raw_sql_b64:
return jsonify({"error": "rawSql is required"}), 400
return write_query(raw_sql_b64)
@app.route("/dbquery/update", methods=["POST"])
def dbquery_update():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
required = ["columnName", "id", "dbtable", "columns", "values"]
if not all(data.get(k) for k in required):
return jsonify({"error": "Missing required parameters"}), 400
return update_query(
column_name=data["columnName"],
ids=data["id"],
dbtable=data["dbtable"],
columns=data["columns"],
values=data["values"],
)
@app.route("/dbquery/delete", methods=["POST"])
def dbquery_delete():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
data = request.get_json() or {}
required = ["columnName", "id", "dbtable"]
if not all(data.get(k) for k in required):
return jsonify({"error": "Missing required parameters"}), 400
return delete_query(
column_name=data["columnName"],
ids=data["id"],
dbtable=data["dbtable"],
)
# -------------------------- # --------------------------
# Online history # Online history
# -------------------------- # --------------------------

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python
import json
import argparse
import os
import pathlib
import base64
import re
import sys
from datetime import datetime
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
def read_query(raw_sql_b64):
"""Execute a read-only query (SELECT)."""
try:
raw_sql = base64.b64decode(raw_sql_b64).decode("utf-8")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(raw_sql)
rows = cur.fetchall()
# Convert rows → dict list
columns = [col[0] for col in cur.description] if cur.description else []
results = [dict(zip(columns, row)) for row in rows]
conn.close()
return jsonify({"success": True, "results": results})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def write_query(raw_sql_b64):
"""Execute a write query (INSERT/UPDATE/DELETE)."""
try:
raw_sql = base64.b64decode(raw_sql_b64).decode("utf-8")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(raw_sql)
conn.commit()
affected = cur.rowcount
conn.close()
return jsonify({"success": True, "affected_rows": affected})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def update_query(column_name, ids, dbtable, columns, values):
"""Update rows in dbtable based on column_name + ids."""
try:
conn = get_temp_db_connection()
cur = conn.cursor()
if not isinstance(ids, list):
ids = [ids]
updated_count = 0
for i in range(len(ids)):
set_clause = ", ".join([f"{col} = ?" for col in columns])
sql = f"UPDATE {dbtable} SET {set_clause} WHERE {column_name} = ?"
params = list(values) + [ids[i]]
cur.execute(sql, params)
updated_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "updated_count": updated_count})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400
def delete_query(column_name, ids, dbtable):
"""Delete rows in dbtable based on column_name + ids."""
try:
conn = get_temp_db_connection()
cur = conn.cursor()
if not isinstance(ids, list):
ids = [ids]
deleted_count = 0
for id_val in ids:
sql = f"DELETE FROM {dbtable} WHERE {column_name} = ?"
cur.execute(sql, (id_val,))
deleted_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted_count": deleted_count})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 400

93
test/test_dbquery_endpoints.py Executable file
View File

@@ -0,0 +1,93 @@
import sys
import base64
import random
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import get_setting_value, timeNowTZ
from api_server.api_server_start import app
@pytest.fixture(scope="session")
def api_token():
return get_setting_value("API_TOKEN")
@pytest.fixture
def client():
with app.test_client() as client:
yield client
@pytest.fixture(scope="session")
def test_mac():
# Generate a unique MAC for each test run
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
def auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def b64(sql: str) -> str:
"""Helper to base64 encode SQL"""
return base64.b64encode(sql.encode("utf-8")).decode("utf-8")
# -----------------------------
# Device lifecycle via dbquery endpoints
# -----------------------------
def test_dbquery_create_device(client, api_token, test_mac):
sql = f"""
INSERT INTO Devices (devMac, devName, devVendor, devOwner, devFirstConnection, devLastConnection, devLastIP)
VALUES ('{test_mac}', 'UnitTestDevice', 'TestVendor', 'UnitTest', '{timeNowTZ()}', '{timeNowTZ()}', '192.168.100.22' )
"""
resp = client.post("/dbquery/write", json={"rawSql": b64(sql)}, headers=auth_headers(api_token))
print(resp.json)
print(resp)
assert resp.status_code == 200
assert resp.json.get("success") is True
assert resp.json.get("affected_rows") == 1
def test_dbquery_read_device(client, api_token, test_mac):
sql = f"SELECT * FROM Devices WHERE devMac = '{test_mac}'"
resp = client.post("/dbquery/read", json={"rawSql": b64(sql)}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
results = resp.json.get("results")
assert any(row["devMac"] == test_mac for row in results)
def test_dbquery_update_device(client, api_token, test_mac):
sql = f"""
UPDATE Devices
SET devName = 'UnitTestDeviceRenamed'
WHERE devMac = '{test_mac}'
"""
resp = client.post("/dbquery/write", json={"rawSql": b64(sql)}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
assert resp.json.get("affected_rows") == 1
# Verify update
sql_check = f"SELECT devName FROM Devices WHERE devMac = '{test_mac}'"
resp2 = client.post("/dbquery/read", json={"rawSql": b64(sql_check)}, headers=auth_headers(api_token))
assert resp2.status_code == 200
assert resp2.json.get("results")[0]["devName"] == "UnitTestDeviceRenamed"
def test_dbquery_delete_device(client, api_token, test_mac):
sql = f"DELETE FROM Devices WHERE devMac = '{test_mac}'"
resp = client.post("/dbquery/write", json={"rawSql": b64(sql)}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
assert resp.json.get("affected_rows") == 1
# Verify deletion
sql_check = f"SELECT * FROM Devices WHERE devMac = '{test_mac}'"
resp2 = client.post("/dbquery/read", json={"rawSql": b64(sql_check)}, headers=auth_headers(api_token))
assert resp2.status_code == 200
assert resp2.json.get("results") == []