MERGE: resolve conflicts

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-11-10 10:11:34 +11:00
77 changed files with 1670 additions and 811 deletions

View File

@@ -14,61 +14,20 @@ from helper import get_setting_value
from db.db_helper import get_date_from_period
from app_state import updateState
from api_server.graphql_endpoint import devicesSchema
from api_server.device_endpoint import (
get_device_data,
set_device_data,
delete_device,
delete_device_events,
reset_device_props,
copy_device,
update_device_column,
)
from api_server.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 api_server.events_endpoint import (
delete_events,
delete_events_older_than,
get_events,
create_event,
get_events_totals,
)
from api_server.history_endpoint import delete_online_history
from api_server.prometheus_endpoint import get_metric_stats
from api_server.sessions_endpoint import (
get_sessions,
delete_session,
create_session,
get_sessions_calendar,
get_device_sessions,
get_session_events,
)
from api_server.nettools_endpoint import (
wakeonlan,
traceroute,
speedtest,
nslookup,
nmap_scan,
internet_info,
)
from api_server.dbquery_endpoint import read_query, write_query, update_query, delete_query
from api_server.sync_endpoint import handle_sync_post, handle_sync_get
from messaging.in_app import (
write_notification,
mark_all_notifications_read,
delete_notifications,
get_unread_notifications,
delete_notification,
mark_notification_as_read,
)
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_older_than, get_events, create_event, get_events_totals
from .history_endpoint import delete_online_history
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 .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 .logs_endpoint import clean_log
from models.user_events_queue_instance import UserEventsQueueInstance
from messaging.in_app import write_notification, mark_all_notifications_read, delete_notifications, get_unread_notifications, delete_notification, mark_notification_as_read
# Flask application
app = Flask(__name__)
@@ -85,11 +44,24 @@ CORS(
r"/dbquery/*": {"origins": "*"},
r"/messaging/*": {"origins": "*"},
r"/events/*": {"origins": "*"},
r"/logs/*": {"origins": "*"}
},
supports_credentials=True,
allow_headers=["Authorization", "Content-Type"],
)
# -------------------------------------------------------------------
# Custom handler for 404 - Route not found
# -------------------------------------------------------------------
@app.errorhandler(404)
def not_found(error):
response = {
"success": False,
"error": "API route not found",
"message": f"The requested URL {error.description if hasattr(error, 'description') else ''} was not found on the server.",
}
return jsonify(response), 404
# --------------------------
# GraphQL Endpoints
# --------------------------
@@ -107,9 +79,9 @@ def graphql_debug():
def graphql_endpoint():
# Check for API token in headers
if not is_authorized():
msg = "[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct."
mylog("verbose", [msg])
return jsonify({"error": msg}), 401
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
mylog('verbose', [msg])
return jsonify({"success": False, "message": msg}), 401
# Retrieve and log request data
data = request.get_json()
@@ -137,7 +109,7 @@ def graphql_endpoint():
@app.route("/settings/<setKey>", methods=["GET"])
def api_get_setting(setKey):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
value = get_setting_value(setKey)
return jsonify({"success": True, "value": value})
@@ -150,51 +122,49 @@ def api_get_setting(setKey):
@app.route("/device/<mac>", methods=["GET"])
def api_get_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return get_device_data(mac)
@app.route("/device/<mac>", methods=["POST"])
def api_set_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return set_device_data(mac, request.json)
@app.route("/device/<mac>/delete", methods=["DELETE"])
def api_delete_device(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_device(mac)
@app.route("/device/<mac>/events/delete", methods=["DELETE"])
def api_delete_device_events(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_device_events(mac)
@app.route("/device/<mac>/reset-props", methods=["POST"])
def api_reset_device_props(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "macFrom and macTo are required"}), 400
return copy_device(mac_from, mac_to)
@@ -202,16 +172,14 @@ def api_copy_device():
@app.route("/device/<mac>/update-column", methods=["POST"])
def api_update_device_column(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "columnName and columnValue are required"}), 400
return update_device_column(mac, column_name, column_value)
@@ -224,15 +192,15 @@ def api_update_device_column(mac):
@app.route("/devices", methods=["GET"])
def api_get_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return get_all_devices()
@app.route("/devices", methods=["DELETE"])
def api_delete_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
macs = request.json.get("macs") if request.is_json else None
return delete_devices(macs)
@@ -241,14 +209,14 @@ def api_delete_devices():
@app.route("/devices/empty-macs", methods=["DELETE"])
def api_delete_all_empty_macs():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_all_with_empty_macs()
@app.route("/devices/unknown", methods=["DELETE"])
def api_delete_unknown_devices():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_unknown_devices()
@@ -256,7 +224,7 @@ def api_delete_unknown_devices():
@app.route("/devices/export/<format>", methods=["GET"])
def api_export_devices(format=None):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
export_format = (format or request.args.get("format", "csv")).lower()
return export_devices(export_format)
@@ -265,21 +233,21 @@ def api_export_devices(format=None):
@app.route("/devices/import", methods=["POST"])
def api_import_csv():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return import_csv(request.files.get("file"))
@app.route("/devices/totals", methods=["GET"])
def api_devices_totals():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return devices_totals()
@app.route("/devices/by-status", methods=["GET"])
def api_devices_by_status():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
status = request.args.get("status", "") if request.args else None
@@ -292,7 +260,7 @@ def api_devices_by_status():
@app.route("/nettools/wakeonlan", methods=["POST"])
def api_wakeonlan():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
mac = request.json.get("devMac")
return wakeonlan(mac)
@@ -301,7 +269,7 @@ def api_wakeonlan():
@app.route("/nettools/traceroute", methods=["POST"])
def api_traceroute():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
ip = request.json.get("devLastIP")
return traceroute(ip)
@@ -309,7 +277,7 @@ def api_traceroute():
@app.route("/nettools/speedtest", methods=["GET"])
def api_speedtest():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return speedtest()
@@ -320,11 +288,11 @@ def api_nslookup():
Expects JSON with 'devLastIP'.
"""
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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
return jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing 'devLastIP'"}), 400
ip = data["devLastIP"]
return nslookup(ip)
@@ -337,11 +305,11 @@ def api_nmap():
Expects JSON with 'scan' (IP address) and 'mode' (scan mode).
"""
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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
return jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing 'scan' or 'mode'"}), 400
ip = data["scan"]
mode = data["mode"]
@@ -351,7 +319,7 @@ def api_nmap():
@app.route("/nettools/internetinfo", methods=["GET"])
def api_internet_info():
if not is_authorized():
return jsonify({"success": False, "error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return internet_info()
@@ -363,26 +331,26 @@ def api_internet_info():
@app.route("/dbquery/read", methods=["POST"])
def dbquery_read():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "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
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "rawSql is required"}), 400
return write_query(raw_sql_b64)
@@ -390,12 +358,12 @@ def dbquery_write():
@app.route("/dbquery/update", methods=["POST"])
def dbquery_update():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing required 'columnName', 'id', 'dbtable', 'columns', or 'values' query parameter"}), 400
return update_query(
column_name=data["columnName"],
@@ -409,12 +377,12 @@ def dbquery_update():
@app.route("/dbquery/delete", methods=["POST"])
def dbquery_delete():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing required 'columnName', 'id', or 'dbtable' query parameter"}), 400
return delete_query(
column_name=data["columnName"],
@@ -431,10 +399,51 @@ def dbquery_delete():
@app.route("/history", methods=["DELETE"])
def api_delete_online_history():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_online_history()
# --------------------------
# Logs
# --------------------------
@app.route("/logs", methods=["DELETE"])
def api_clean_log():
if not is_authorized():
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
file = request.args.get("file")
if not file:
return jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing 'file' query parameter"}), 400
return clean_log(file)
@app.route("/logs/add-to-execution-queue", methods=["POST"])
def api_add_to_execution_queue():
if not is_authorized():
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
queue = UserEventsQueueInstance()
# Get JSON payload safely
data = request.get_json(silent=True) or {}
action = data.get("action")
if not action:
return jsonify({
"success": False, "message": "ERROR: Missing parameters", "error": "Missing required 'action' field in JSON body"}), 400
success, message = queue.add_event(action)
status_code = 200 if success else 400
response = {"success": success, "message": message}
if not success:
response["error"] = "ERROR"
return jsonify(response), status_code
# --------------------------
# Device Events
# --------------------------
@@ -443,7 +452,7 @@ def api_delete_online_history():
@app.route("/events/create/<mac>", methods=["POST"])
def api_create_event(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
data = request.json or {}
ip = data.get("ip", "0.0.0.0")
@@ -462,21 +471,21 @@ def api_create_event(mac):
@app.route("/events/<mac>", methods=["DELETE"])
def api_events_by_mac(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_device_events(mac)
@app.route("/events", methods=["DELETE"])
def api_delete_all_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_events()
@app.route("/events", methods=["GET"])
def api_get_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
mac = request.args.get("mac")
return get_events(mac)
@@ -489,15 +498,15 @@ def api_delete_old_events(days: int):
Example: DELETE /events/30
"""
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_events_older_than(days)
@app.route("/sessions/totals", methods=["GET"])
def api_get_events_totals():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
period = get_date_from_period(request.args.get("period", "7 days"))
return get_events_totals(period)
@@ -511,7 +520,7 @@ def api_get_events_totals():
@app.route("/sessions/create", methods=["POST"])
def api_create_session():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
data = request.json
mac = data.get("mac")
@@ -522,7 +531,7 @@ def api_create_session():
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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing required 'mac', 'ip', or 'start_time' query parameter"}), 400
return create_session(
mac, ip, start_time, end_time, event_type_conn, event_type_disc
@@ -532,11 +541,11 @@ def api_create_session():
@app.route("/sessions/delete", methods=["DELETE"])
def api_delete_session():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "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 jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing 'mac' query parameter"}), 400
return delete_session(mac)
@@ -544,7 +553,7 @@ def api_delete_session():
@app.route("/sessions/list", methods=["GET"])
def api_get_sessions():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
mac = request.args.get("mac")
start_date = request.args.get("start_date")
@@ -556,7 +565,7 @@ def api_get_sessions():
@app.route("/sessions/calendar", methods=["GET"])
def api_get_sessions_calendar():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
# Query params: /sessions/calendar?start=2025-08-01&end=2025-08-21
start_date = request.args.get("start")
@@ -568,7 +577,7 @@ def api_get_sessions_calendar():
@app.route("/sessions/<mac>", methods=["GET"])
def api_device_sessions(mac):
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
period = request.args.get("period", "1 day")
return get_device_sessions(mac, period)
@@ -577,7 +586,7 @@ def api_device_sessions(mac):
@app.route("/sessions/session-events", methods=["GET"])
def api_get_session_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
session_event_type = request.args.get("type", "all")
period = get_date_from_period(request.args.get("period", "7 days"))
@@ -590,7 +599,7 @@ def api_get_session_events():
@app.route("/metrics")
def metrics():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
# Return Prometheus metrics as plain text
return Response(get_metric_stats(), mimetype="text/plain")
@@ -602,15 +611,15 @@ def metrics():
@app.route("/messaging/in-app/write", methods=["POST"])
def api_write_notification():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
data = request.json or {}
content = data.get("content")
level = data.get("level", "alert")
if not content:
return jsonify({"success": False, "error": "Missing content"}), 400
return jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing content"}), 400
write_notification(content, level)
return jsonify({"success": True})
@@ -618,7 +627,7 @@ def api_write_notification():
@app.route("/messaging/in-app/unread", methods=["GET"])
def api_get_unread_notifications():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return get_unread_notifications()
@@ -626,7 +635,7 @@ def api_get_unread_notifications():
@app.route("/messaging/in-app/read/all", methods=["POST"])
def api_mark_all_notifications_read():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return jsonify(mark_all_notifications_read())
@@ -634,7 +643,7 @@ def api_mark_all_notifications_read():
@app.route("/messaging/in-app/delete", methods=["DELETE"])
def api_delete_all_notifications():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
return delete_notifications()
@@ -643,35 +652,34 @@ def api_delete_all_notifications():
def api_delete_notification(guid):
"""Delete a single notification by GUID."""
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
result = delete_notification(guid)
if result.get("success"):
return jsonify({"success": True})
else:
return jsonify({"success": False, "error": result.get("error")}), 500
return jsonify({"success": False, "message": "ERROR", "error": result.get("error")}), 500
@app.route("/messaging/in-app/read/<guid>", methods=["POST"])
def api_mark_notification_read(guid):
"""Mark a single notification as read by GUID."""
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
result = mark_notification_as_read(guid)
if result.get("success"):
return jsonify({"success": True})
else:
return jsonify({"success": False, "error": result.get("error")}), 500
return jsonify({"success": False, "message": "ERROR", "error": result.get("error")}), 500
# --------------------------
# SYNC endpoint
# --------------------------
@app.route("/sync", methods=["GET", "POST"])
def sync_endpoint():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return jsonify({"success": False, "message": "ERROR: Not authorized", "error": "Forbidden"}), 403
if request.method == "GET":
return handle_sync_get()
@@ -681,7 +689,7 @@ def sync_endpoint():
msg = "[sync endpoint] Method Not Allowed"
write_notification(msg, "alert")
mylog("verbose", [msg])
return jsonify({"error": "Method Not Allowed"}), 405
return jsonify({"success": False, "message": "ERROR: No allowed", "error": "Method Not Allowed"}), 405
# --------------------------

View File

@@ -10,7 +10,8 @@ INSTALL_PATH = os.getenv("NETALERTX_APP", "/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, get_setting_value
from utils.datetime_utils import timeNowDB, format_date
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
@@ -25,9 +26,11 @@ def get_device_data(mac):
conn = get_temp_db_connection()
cur = conn.cursor()
now = timeNowDB()
# Special case for new device
if mac.lower() == "new":
now = datetime.now().strftime("%Y-%m-%d %H:%M")
device_data = {
"devMac": "",
"devName": "",
@@ -75,7 +78,6 @@ def get_device_data(mac):
# Compute period date for sessions/events
period = request.args.get("period", "") # e.g., '7 days', '1 month', etc.
period_date_sql = get_date_from_period(period)
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Fetch device info + computed fields
sql = f"""
@@ -103,7 +105,7 @@ def get_device_data(mac):
AND eve_EventType = 'Device Down') AS devDownAlerts,
(SELECT CAST(MAX(0, SUM(
julianday(IFNULL(ses_DateTimeDisconnection,'{current_date}')) -
julianday(IFNULL(ses_DateTimeDisconnection,'{now}')) -
julianday(CASE WHEN ses_DateTimeConnection < {period_date_sql}
THEN {period_date_sql} ELSE ses_DateTimeConnection END)
) * 24) AS INT)
@@ -186,10 +188,8 @@ def set_device_data(mac, data):
data.get("devSkipRepeated", 0),
data.get("devIsNew", 0),
data.get("devIsArchived", 0),
data.get("devLastConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
data.get(
"devFirstConnection", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
),
data.get("devLastConnection", timeNowDB()),
data.get("devFirstConnection", timeNowDB()),
data.get("devLastIP", ""),
data.get("devGUID", ""),
data.get("devCustomProps", ""),

View File

@@ -16,6 +16,7 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from db.db_helper import get_table_json, get_device_condition_by_status
from utils.datetime_utils import format_date
# --------------------------

View File

@@ -10,11 +10,9 @@ INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import (
mylog,
ensure_datetime,
)
from helper import is_random_mac, mylog
from db.db_helper import row_to_json, get_date_from_period
from utils.datetime_utils import format_date, format_date_iso, format_event_date, ensure_datetime
# --------------------------

View File

@@ -1,5 +1,5 @@
import graphene
from graphene import ObjectType, String, Int, Boolean, List, Field, InputObjectType
from graphene import ObjectType, String, Int, Boolean, List, Field, InputObjectType, Argument
import json
import sys
import os
@@ -111,6 +111,22 @@ class SettingResult(ObjectType):
settings = List(Setting)
count = Int()
# --- LANGSTRINGS ---
# In-memory cache for lang strings
_langstrings_cache = {} # caches lists per file (core JSON or plugin)
_langstrings_cache_mtime = {} # tracks last modified times
# LangString ObjectType
class LangString(ObjectType):
langCode = String()
langStringKey = String()
langStringText = String()
class LangStringResult(ObjectType):
langStrings = List(LangString)
count = Int()
# Define Query Type with Pagination Support
class Query(ObjectType):
@@ -324,6 +340,107 @@ class Query(ObjectType):
return SettingResult(settings=settings, count=len(settings))
# --- LANGSTRINGS ---
langStrings = Field(
LangStringResult,
langCode=Argument(String, required=False),
langStringKey=Argument(String, required=False)
)
def resolve_langStrings(self, info, langCode=None, langStringKey=None, fallback_to_en=True):
"""
Collect language strings, optionally filtered by language code and/or string key.
Caches in memory for performance. Can fallback to 'en_us' if a string is missing.
"""
global _langstrings_cache, _langstrings_cache_mtime
langStrings = []
# --- CORE JSON FILES ---
language_folder = '/app/front/php/templates/language/'
if os.path.exists(language_folder):
for filename in os.listdir(language_folder):
if filename.endswith('.json'):
file_lang_code = filename.replace('.json', '')
# Filter by langCode if provided
if langCode and file_lang_code != langCode:
continue
file_path = os.path.join(language_folder, filename)
file_mtime = os.path.getmtime(file_path)
cache_key = f'core_{file_lang_code}'
# Use cached data if available and not modified
if cache_key in _langstrings_cache_mtime and _langstrings_cache_mtime[cache_key] == file_mtime:
lang_list = _langstrings_cache[cache_key]
else:
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
lang_list = [
LangString(
langCode=file_lang_code,
langStringKey=key,
langStringText=value
) for key, value in data.items()
]
_langstrings_cache[cache_key] = lang_list
_langstrings_cache_mtime[cache_key] = file_mtime
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog('none', f'[graphql_schema] Error loading core language strings from {filename}: {e}')
lang_list = []
langStrings.extend(lang_list)
# --- PLUGIN STRINGS ---
plugin_file = folder + 'table_plugins_language_strings.json'
try:
file_mtime = os.path.getmtime(plugin_file)
cache_key = 'plugin'
if cache_key in _langstrings_cache_mtime and _langstrings_cache_mtime[cache_key] == file_mtime:
plugin_list = _langstrings_cache[cache_key]
else:
with open(plugin_file, 'r', encoding='utf-8') as f:
plugin_data = json.load(f).get("data", [])
plugin_list = [
LangString(
langCode=entry.get("Language_Code"),
langStringKey=entry.get("String_Key"),
langStringText=entry.get("String_Value")
) for entry in plugin_data
]
_langstrings_cache[cache_key] = plugin_list
_langstrings_cache_mtime[cache_key] = file_mtime
except (FileNotFoundError, json.JSONDecodeError) as e:
mylog('none', f'[graphql_schema] Error loading plugin language strings from {plugin_file}: {e}')
plugin_list = []
# Filter plugin strings by langCode if provided
if langCode:
plugin_list = [p for p in plugin_list if p.langCode == langCode]
langStrings.extend(plugin_list)
# --- Filter by string key if requested ---
if langStringKey:
langStrings = [ls for ls in langStrings if ls.langStringKey == langStringKey]
# --- Fallback to en_us if enabled and requested lang is missing ---
if fallback_to_en and langCode and langCode != "en_us":
for i, ls in enumerate(langStrings):
if not ls.langStringText: # empty string triggers fallback
# try to get en_us version
en_list = _langstrings_cache.get("core_en_us", [])
en_list += [p for p in _langstrings_cache.get("plugin", []) if p.langCode == "en_us"]
en_fallback = [e for e in en_list if e.langStringKey == ls.langStringKey]
if en_fallback:
langStrings[i] = en_fallback[0]
mylog('trace', f'[graphql_schema] Collected {len(langStrings)} language strings '
f'(langCode={langCode}, key={langStringKey}, fallback_to_en={fallback_to_en})')
return LangStringResult(langStrings=langStrings, count=len(langStrings))
# helps sorting inconsistent dataset mixed integers and strings
def mixed_type_sort_key(value):

View File

@@ -0,0 +1,58 @@
import os
import sys
from flask import jsonify
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from const import logPath
from logger import mylog, Logger
from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from messaging.in_app import write_notification
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
def clean_log(log_file):
"""
Purge the content of an allowed log file within the /app/log/ directory.
Args:
log_file (str): Name of the log file to purge.
Returns:
flask.Response: JSON response with success and message keys
"""
allowed_files = [
'app.log', 'app_front.log', 'IP_changes.log', 'stdout.log', 'stderr.log',
'app.php_errors.log', 'execution_queue.log', 'db_is_locked.log'
]
# Validate filename if purging allowed
if log_file not in allowed_files:
msg = f"[clean_log] File {log_file} is not allowed to be purged"
mylog('none', [msg])
write_notification(msg, 'interrupt')
return jsonify({"success": False, "message": msg}), 400
log_path = os.path.join(logPath, log_file)
try:
# Purge content
with open(log_path, "w") as f:
f.write("File manually purged\n")
msg = f"[clean_log] File {log_file} purged successfully"
mylog('minimal', [msg])
write_notification(msg, 'interrupt')
return jsonify({"success": True, "message": msg}), 200
except Exception as e:
msg = f"[clean_log] ERROR Failed to purge {log_file}: {e}"
mylog('none', [msg])
write_notification(msg, 'interrupt')
return jsonify({"success": False, "message": msg}), 500

View File

@@ -10,14 +10,9 @@ INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import (
format_date,
format_date_iso,
format_event_date,
format_date_diff,
format_ip_long,
)
from db.db_helper import get_date_from_period
from helper import is_random_mac, get_setting_value, mylog, format_ip_long
from db.db_helper import row_to_json, get_date_from_period
from utils.datetime_utils import format_date_iso, format_event_date, format_date_diff, parse_datetime, format_date
# --------------------------
@@ -231,6 +226,7 @@ def get_device_sessions(mac, period):
cur.execute(sql, (mac,))
rows = cur.fetchall()
conn.close()
tz_name = get_setting_value("TIMEZONE") or "UTC"
table_data = {"data": []}
@@ -255,11 +251,9 @@ def get_device_sessions(mac, period):
] in ("<missing event>", None):
dur = "..."
elif row["ses_StillConnected"]:
dur = format_date_diff(row["ses_DateTimeConnection"], None)["text"]
dur = format_date_diff(row["ses_DateTimeConnection"], None, tz_name)["text"]
else:
dur = format_date_diff(
row["ses_DateTimeConnection"], row["ses_DateTimeDisconnection"]
)["text"]
dur = format_date_diff(row["ses_DateTimeConnection"], row["ses_DateTimeDisconnection"], tz_name)["text"]
# Additional Info
info = row["ses_AdditionalInfo"]
@@ -295,6 +289,7 @@ def get_session_events(event_type, period_date):
conn = get_temp_db_connection()
conn.row_factory = sqlite3.Row
cur = conn.cursor()
tz_name = get_setting_value("TIMEZONE") or "UTC"
# Base SQLs
sql_events = f"""
@@ -382,11 +377,11 @@ def get_session_events(event_type, period_date):
if event_type in ("sessions", "missing"):
# Duration
if row[5] and row[6]:
delta = format_date_diff(row[5], row[6])
delta = format_date_diff(row[5], row[6], tz_name)
row[7] = delta["text"]
row[8] = int(delta["total_minutes"] * 60) # seconds
elif row[12] == 1:
delta = format_date_diff(row[5], None)
delta = format_date_diff(row[5], None, tz_name)
row[7] = delta["text"]
row[8] = int(delta["total_minutes"] * 60) # seconds
else:

View File

@@ -2,7 +2,8 @@ import os
import base64
from flask import jsonify, request
from logger import mylog
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from messaging.in_app import write_notification
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
@@ -17,22 +18,20 @@ def handle_sync_get():
raw_data = f.read()
except FileNotFoundError:
msg = f"[Plugin: SYNC] Data file not found: {file_path}"
write_notification(msg, "alert", timeNowTZ())
write_notification(msg, "alert", timeNowDB())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
response_data = base64.b64encode(raw_data).decode("utf-8")
write_notification("[Plugin: SYNC] Data sent", "info", timeNowTZ())
return jsonify(
{
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,
"message": "OK",
"data_base64": response_data,
"timestamp": timeNowTZ(),
}
), 200
write_notification("[Plugin: SYNC] Data sent", "info", timeNowDB())
return jsonify({
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,
"message": "OK",
"data_base64": response_data,
"timestamp": timeNowDB()
}), 200
def handle_sync_post():
@@ -65,11 +64,11 @@ def handle_sync_post():
f.write(data)
except Exception as e:
msg = f"[Plugin: SYNC] Failed to store data: {e}"
write_notification(msg, "alert", timeNowTZ())
write_notification(msg, "alert", timeNowDB())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowTZ())
write_notification(msg, "info", timeNowDB())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200