Compare commits

...

20 Commits

Author SHA1 Message Date
jokob-sk
5fd30fe3c8 FE
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-20 08:41:38 +10:00
jokob-sk
2fa181ffbc api layer v0.2.4 - /nettools endpoint 2025-08-20 08:40:14 +10:00
jokob-sk
a2bccdfb8e FE 2025-08-20 08:11:56 +10:00
jokob-sk
f3b159116f Merge branch 'main' of https://github.com/jokob-sk/NetAlertX 2025-08-20 08:11:05 +10:00
jokob-sk
03b9a9cf0d api layer v0.2.3 - /device(s) endpoints work 2025-08-20 08:10:55 +10:00
Jokob @NetAlertX
bf2fae6e1a Merge pull request #1140 from cvc90/Fix-Relative-URL-in-report.php
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
Changing absolute path URL to relative path URL in report.php
2025-08-19 22:19:53 +10:00
Carlos V.
086fa54035 Update report.php
Change static route to relative route in URL for proper proxy operation
2025-08-19 13:07:58 +02:00
jokob-sk
962bbaa5a1 api layer v0.2.2 - CSV import/export, refactor
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
2025-08-19 07:56:54 +10:00
jokob-sk
9c71a8ecab api layer v0.2.1 - /events /history 2025-08-16 17:19:14 +10:00
jokob-sk
deff5a4ed0 api layer v0.2 - /devices
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
2025-08-16 16:43:15 +10:00
jokob-sk
e10c1c9c8d Added pt_pt language file
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
2025-08-16 08:18:23 +10:00
jokob-sk
b155fe2b06 api layer v0.1
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
2025-08-15 08:04:02 +10:00
jokob-sk
840bfe32d2 sync plugin endpoint refactor 2025-08-14 14:28:10 +10:00
jokob-sk
f33ef9861b css fixes, CurrentScan removed mac uniqueness check
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-13 08:22:30 +10:00
jokob-sk
cbe71cc203 UNIFIAPI v0.5, css fixes
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
2025-08-13 06:48:36 +10:00
jokob-sk
beaf8131ae UNIFIAPI v0.4
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
2025-08-11 21:39:51 +10:00
jokob-sk
99bfbb56de better check for available device #1132 2025-08-11 19:58:24 +10:00
jokob-sk
e73c8e830a better check for available device #1132 2025-08-11 19:52:16 +10:00
jokob-sk
1c4e6c7e38 UNIFIAPI v0.3 FE setings done 2025-08-11 15:00:22 +10:00
jokob-sk
1319c3380d UNIFIAPI v0.3
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
2025-08-10 21:24:17 +10:00
53 changed files with 3436 additions and 956 deletions

View File

@@ -11,7 +11,7 @@ on:
- rewrite
jobs:
docker_dev:
docker_rewrite:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:

View File

@@ -79,10 +79,11 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
| `SETPWD` | [set_password](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/set_password/) | ⚙ | Set password | | Yes |
| `SMTP` | [_publisher_email](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_email/) | ▶️ | Email notifications | | |
| `SNMPDSC` | [snmp_discovery](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/snmp_discovery/) | 🔍/📥 | SNMP device import & sync | | |
| `SYNC` | [sync](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/sync/) | 🔍/⚙/📥 | Sync & import from NetAlertX instances | 🖧 🔄 | Yes |
| `SYNC` | [sync](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/sync/) | 🔍/⚙/📥 | Sync & import from NetAlertX instances | 🖧 🔄 | Yes |
| `TELEGRAM` | [_publisher_telegram](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_telegram/) | ▶️ | Telegram notifications | | |
| `UI` | [ui_settings](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/ui_settings/) | ♻ | UI specific settings | | Yes |
| `UNFIMP` | [unifi_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/unifi_import/) | 🔍/📥/🆎 | UniFi device import & sync | 🖧 | |
| `UNIFIAPI` | [unifi_api_import](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/unifi_api_import/) | 🔍/📥/🆎 | UniFi device import (SM API, multi-site) | | |
| `VNDRPDT` | [vendor_update](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/vendor_update/) | ⚙ | Vendor database update | | |
| `WEBHOOK` | [_publisher_webhook](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/_publisher_webhook/) | ▶️ | Webhook notifications | | |
| `WEBMON` | [website_monitor](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/website_monitor/) | ♻ | Website down monitoring | | |

View File

@@ -18,6 +18,7 @@
--color-yellow: #f39c12;
--color-red: #dd4b39;
--color-gray: #8c8c8c;
--color-black: #000;
}
.input-group .checkbox
@@ -604,6 +605,20 @@ body
cursor: default;
}
.btn-outline:hover
{
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-black);
}
.btn-outline
{
border: 1px solid var(--color-gray);
background: transparent;
color: var(--color-gray);
}
/* -----------------------------------------------------------------------------
Customized Full Calendar
----------------------------------------------------------------------------- */

View File

@@ -419,6 +419,12 @@ td.highlight {
border: 1px solid #353c42;
}
.btn-outline {
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-white);
}
/* Used in debug log page */
.log-red {
color: #ff4038;

View File

@@ -422,6 +422,12 @@
border: 1px solid #353c42;
}
.btn-outline {
border: 1px solid var(--color-black);
background: transparent;
color: var(--color-white);
}
/* Used in debug log page */
.log-red {
color: #ff4038;

View File

@@ -497,7 +497,7 @@ function updateDevicePageName(mac) {
let owner = getDevDataByMac(mac, "devOwner");
// If data is missing, re-cache and retry once
if (mac != 'new' && (name === "Unknown" || owner === "Unknown")) {
if (mac != 'new' && (name === null|| owner === null)) {
console.warn("Device not found in cache, retrying after re-cache:", mac);
showSpinner();
cacheDevices().then(() => {

View File

@@ -53,14 +53,6 @@
var deviceData = JSON.parse(data);
// // Deactivate next previous buttons
// if (readAllData) {
// $('#btnPrevious').attr ('disabled','');
// $('#btnPrevious').addClass ('text-gray50');
// $('#btnNext').attr ('disabled','');
// $('#btnNext').addClass ('text-gray50');
// }
// some race condition, need to implement delay
setTimeout(() => {
$.get('php/server/query_json.php', {
@@ -256,25 +248,27 @@
// update readonly fields
handleReadOnly(settingsData, disabledFields);
};
};
// console.log(relevantSettings)
// console.log(relevantSettings)
generateSimpleForm(relevantSettings);
generateSimpleForm(relevantSettings);
toggleNetworkConfiguration(mac == 'Internet')
toggleNetworkConfiguration(mac == 'Internet')
initSelect2();
initHoverNodeInfo();
initSelect2();
initHoverNodeInfo();
hideSpinner();
hideSpinner();
})
})
}, 100);
});
}, 100);
});
}
}
// ----------------------------------------

View File

@@ -12,7 +12,7 @@ var timerRefreshData = ''
var emptyArr = ['undefined', "", undefined, null, 'null'];
var UI_LANG = "English";
const allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"]; // needs to be same as in lang.php
const allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "pt_pt", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"]; // needs to be same as in lang.php
var settingsJSON = {}
@@ -328,6 +328,9 @@ function getLangCode() {
case 'Portuguese (pt_br)':
lang_code = 'pt_br';
break;
case 'Portuguese (pt_pt)':
lang_code = 'pt_pt';
break;
case 'Turkish (tr_tr)':
lang_code = 'tr_tr';
break;
@@ -609,7 +612,7 @@ function createDeviceLink(input)
{
if(checkMacOrInternet(input))
{
return `<span class="anonymizeMac"><a href="/deviceDetails.php?mac=${input}" target="_blank">${getNameByMacAddress(input)}</a><span>`
return `<span class="anonymizeMac"><a href="/deviceDetails.php?mac=${input}" target="_blank">${getDevDataByMac(input, "devName")}</a><span>`
}
return input;
@@ -813,7 +816,6 @@ function forceLoadUrl(relativeUrl) {
}
// -----------------------------------------------------------------------------
function navigateToDeviceWithIp (ip) {
@@ -836,11 +838,6 @@ function navigateToDeviceWithIp (ip) {
});
}
// -----------------------------------------------------------------------------
function getNameByMacAddress(macAddress) {
return getDevDataByMac(macAddress, "devName")
}
// -----------------------------------------------------------------------------
// Check if MAC or Internet
function checkMacOrInternet(inputStr) {
@@ -1013,7 +1010,7 @@ function getDevDataByMac(macAddress, dbColumn) {
if (!devicesCache || devicesCache == "") {
console.error(`Session variable "${sessionDataKey}" not found.`);
return "Unknown";
return null;
}
const devices = JSON.parse(devicesCache);
@@ -1033,7 +1030,7 @@ function getDevDataByMac(macAddress, dbColumn) {
}
console.error("⚠ Device with MAC not found:" + macAddress)
return "Unknown"; // Return a default value if MAC address is not found
return null; // Return a default value if MAC address is not found
}
// -----------------------------------------------------------------------------

View File

@@ -167,11 +167,10 @@ function showModalPopupForm(
message,
btnCancel = getString("Gen_Cancel"),
btnOK = getString("Gen_Okay"),
curValue = "",
callbackFunction = null,
triggeredBy = null,
curValue = null,
popupFormJson = null,
parentSettingKey = null
parentSettingKey = null,
triggeredBy = null
) {
// set captions
prefix = "modal-form";
@@ -182,12 +181,11 @@ function showModalPopupForm(
$(`#${prefix}-cancel`).html(btnCancel);
$(`#${prefix}-OK`).html(btnOK);
if (callbackFunction != null) {
modalCallbackFunction = callbackFunction;
}
// if curValue not null
if (triggeredBy != null) {
$('#'+prefix).attr("data-myparam-triggered-by", triggeredBy)
if (curValue)
{
initialValues = JSON.parse(atob(curValue));
}
outputHtml = "";
@@ -195,17 +193,24 @@ function showModalPopupForm(
if (Array.isArray(popupFormJson)) {
popupFormJson.forEach((field, index) => {
// You'll need to define these or map them from `field`
const setName = field.name?.find(n => n.language_code === "en_us")?.string || setKey;
const setKey = field.function || `field_${index}`;
const setName = getString(`${parentSettingKey}_popupform_${setKey}_name`);
const labelClasses = "col-sm-2"; // example, or from your obj.labelClasses
const inputClasses = "col-sm-10"; // example, or from your obj.inputClasses
const fieldData = field.default_value ?? "";
const fieldOptionsOverride = field.type?.elements[0]?.elementOptions || [];
let initialValue = '';
if (curValue && Array.isArray(initialValues)) {
const match = initialValues.find(
v => v[1] == setKey
);
if (match) {
initialValue = match[3];
}
}
const setKey = field.function || `field_${index}`;
const setValue = field.default_value ?? "";
const fieldOptionsOverride = field.type?.elements[0]?.elementOptions || [];
const setValue = initialValue;
const setType = JSON.stringify(field.type);
const setEvents = field.events || []; // default to empty array if missing
const setObj = { setKey, setValue, setType, setEvents };
// Generate the input field HTML
@@ -222,7 +227,7 @@ function showModalPopupForm(
${generateFormHtml(
null, // settingsData only required for datatables
setObj,
fieldData.toString(),
null,
fieldOptionsOverride,
null
)}
@@ -237,10 +242,53 @@ function showModalPopupForm(
$(`#modal-form-plc`).html(outputHtml);
// $(`#${prefix}-field`).val(curValue);
// setTimeout(function () {
// $(`#${prefix}-field`).focus();
// }, 500);
// Bind OK button click event
$(`#${prefix}-OK`).off("click").on("click", function() {
let settingsArray = [];
if (Array.isArray(popupFormJson)) {
popupFormJson.forEach(field => {
collectSetting(
`${parentSettingKey}_popupform`, // prefix
field.function, // setCodeName
field.type, // setType (object)
settingsArray
);
});
}
// Encode settings
const jsonData = JSON.stringify(settingsArray);
const encodedValue = btoa(jsonData);
// Get label from the FIRST field (value in 4th column)
const label = settingsArray[0][3]
// Add new option to target select
const selectId = parentSettingKey;
// If triggered by an option, update it; otherwise append new
if (triggeredBy && $(triggeredBy).is("option")) {
// Update existing option
$(triggeredBy)
.attr("value", encodedValue)
.text(label);
} else {
const newOption = $("<option class='interactable-option'></option>")
.attr("value", encodedValue)
.text(label);
$("#" + selectId).append(newOption);
initListInteractionOptions(newOption);
}
console.log("Collected popup form settings:", settingsArray);
if (typeof modalCallbackFunction === "function") {
modalCallbackFunction(settingsArray);
}
$(`#${prefix}`).modal("hide");
});
// Show modal
$(`#${prefix}`).modal("show");

View File

@@ -246,13 +246,6 @@ function settingsCollectedCorrectly(settingsArray, settingsJSON_DB) {
// Manipulating Editable List options
// -------------------------------------------------------------------
// ---------------------------------------------------------
// Add row to datatable
function addDataTableRow(el)
{
alert("a")
}
// ---------------------------------------------------------
// Clone datatable row
function cloneDataTableRow(el){
@@ -311,42 +304,30 @@ function removeDataTableRow(el) {
// ---------------------------------------------------------
// Add item via pop up form dialog
function addViaPopupForm(element) {
console.log(element)
console.log(element);
const fromId = $(element).attr("my-input-from");
const toId = $(element).attr("my-input-to");
const curValue = $(`#${fromId}`).val();
const triggeredBy = $(element).attr("id");
const parsed = JSON.parse(atob($(element).data("elementoptionsbase64")));
const curValue = $(`#${toId}`).val();
const parsed = JSON.parse(atob($(`#${toId}`).data("elementoptionsbase64")));
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
console.log(`fromId | toId | triggeredBy | curValue: ${fromId} | ${toId} | ${triggeredBy} | ${curValue}`);
console.log(`toId | curValue: ${toId} | ${curValue}`);
showModalPopupForm(
`<i class="fa fa-pen-to-square"></i> ${getString(
"Gen_Update_Value"
)}`, // title
getString("settings_update_item_warning"), // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Add"), // btnOK
curValue, // curValue
null, // callbackFunction
triggeredBy, // triggeredBy
popupFormJson, // popupform
toId // parentSettingKey
`<i class="fa-solid fa-square-plus"></i> ${getString("Gen_Add")}`, // title
"", // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Add"), // btnOK
null, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
element // triggeredBy
);
}
// ---------------------------------------------------------
// Add item to list via popup form
function addViaPopupFormToList(element, clearInput = true) {
// flag something changes to prevent navigating from page
settingsChanged();
}
// ---------------------------------------------------------
// Add item to list
function addList(element, clearInput = true) {
@@ -475,18 +456,41 @@ function initListInteractionOptions(element) {
// Perform action based on click count
if (clickCounter === 1) {
// Single-click action
showModalFieldInput(
`<i class="fa fa-pen-to-square"></i> ${getString(
"Gen_Update_Value"
)}`,
getString("settings_update_item_warning"),
getString("Gen_Cancel"),
getString("Gen_Update"),
$option.html(),
function () {
updateOptionItem($option, $(`#modal-field-input-field`).val());
}
);
const $parent = $option.parent();
const transformers = $parent.attr("my-transformers");
if (transformers && transformers === "name|base64") {
// Parent has my-transformers="name|base64"
const toId = $parent.attr("id");
const curValue = $option.val();
const parsed = JSON.parse(atob($parent.data("elementoptionsbase64")));
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
showModalPopupForm(
`<i class="fa fa-pen-to-square"></i> ${getString("Gen_Update_Value")}`, // title
"", // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Update"), // btnOK
curValue, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
this // triggeredBy
);
} else {
// Fallback to normal field input
showModalFieldInput(
`<i class="fa fa-pen-to-square"></i> ${getString("Gen_Update_Value")}`,
getString("settings_update_item_warning"),
getString("Gen_Cancel"),
getString("Gen_Update"),
$option.html(),
function () {
updateOptionItem($option, $(`#modal-field-input-field`).val());
}
);
}
} else if (clickCounter === 2) {
// Double-click action
removeOptionItem($option);
@@ -718,7 +722,7 @@ function applyTransformers(val, transformers) {
}
// ------------------------------------------------------------
// Function to reverse transformers applied to a value
// Function to reverse transformers applied to a value - returns the LABEL
function reverseTransformers(val, transformers) {
transformers.reverse().forEach((transformer) => {
switch (transformer) {
@@ -733,10 +737,10 @@ function reverseTransformers(val, transformers) {
}
break;
case "name|base64":
// // Implement base64 decoding logic
// if (isBase64(val)) {
// val = atob(val);
// }
// Implement base64 decoding logic
if (isBase64(val)) {
val = JSON.parse(atob(val))[0][3];
}
val = val; // probably TODO ⚠
break;
case "getString":
@@ -993,13 +997,102 @@ function genListWithInputSet(options, valuesArray, targetField, transformers, pl
$("#" + placeholder).replaceWith(listHtml);
}
// -----------------------------------------------------------------
// Collects a setting based on code name
function collectSetting(prefix, setCodeName, setType, settingsArray) {
// Parse setType if it's a JSON string
const setTypeObject = (typeof setType === "string")
? JSON.parse(processQuotes(setType))
: setType;
const dataType = setTypeObject.dataType;
// Pick element with input value
let elements = setTypeObject.elements.filter(el => el.elementHasInputValue === 1);
let elementWithInputValue = elements.length === 0
? setTypeObject.elements[setTypeObject.elements.length - 1]
: elements[0];
const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
const opts = handleElementOptions('none', elementOptions, transformers, val = "");
// Map of handlers
const handlers = {
datatableString: () => {
const value = collectTableData(`#${setCodeName}_table`);
return btoa(JSON.stringify(value));
},
simpleValue: () => {
let value = $(`#${setCodeName}`).val();
return applyTransformers(value, transformers);
},
checkbox: () => {
let value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
if (dataType === "boolean") {
value = value === 1 ? "True" : "False";
}
return applyTransformers(value, transformers);
},
array: () => {
let temps = [];
if (opts.isOrdeable) {
temps = $(`#${setCodeName}`).val();
} else {
const sel = $(`#${setCodeName}`).attr("my-editable") === "true" ? "" : ":selected";
$(`#${setCodeName} option${sel}`).each(function() {
const vl = $(this).val();
if (vl !== '') {
temps.push(applyTransformers(vl, transformers));
}
});
}
return JSON.stringify(temps);
},
none: () => "",
json: () => {
let value = $(`#${setCodeName}`).val();
value = applyTransformers(value, transformers);
return JSON.stringify(value, null, 2);
},
fallback: () => {
console.error(`[collectSetting] Couldn't determine how to handle (${setCodeName}|${dataType}|${opts.inputType})`);
let value = $(`#${setCodeName}`).val();
return applyTransformers(value, transformers);
}
};
// Select handler key
let handlerKey;
if (dataType === "string" && elementType === "datatable") {
handlerKey = "datatableString";
} else if (dataType === "string" ||
(dataType === "integer" && (opts.inputType === "number" || opts.inputType === "text"))) {
handlerKey = "simpleValue";
} else if (opts.inputType === "checkbox") {
handlerKey = "checkbox";
} else if (dataType === "array") {
handlerKey = "array";
} else if (dataType === "none") {
handlerKey = "none";
} else if (dataType === "json") {
handlerKey = "json";
} else {
handlerKey = "fallback";
}
const value = handlers[handlerKey]();
settingsArray.push([prefix, setCodeName, dataType, value]);
return settingsArray;
}
// ------------------------------------------------------------------------------
// Generate the form control for setting
function generateFormHtml(settingsData, set, overrideValue, overrideOptions, originalSetKey) {
let inputHtml = '';
isEmpty(overrideValue) ? inVal = set['setValue'] : inVal = overrideValue;
const setKey = set['setKey'];
const setType = set['setType'];
@@ -1014,7 +1107,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
// }
// Parse the setType JSON string
console.log(processQuotes(setType));
// console.log(processQuotes(setType));
const setTypeObject = JSON.parse(processQuotes(setType))
const dataType = setTypeObject.dataType;
@@ -1076,6 +1169,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
my-customparams="${customParams}"
my-customid="${customId}"
my-originalSetKey="${originalSetKey}"
data-elementoptionsbase64="${elementOptionsBase64}"
${multi}
${readOnly ? "disabled" : ""}>
<option value="" id="${setKey + "_temp_"}"></option>

View File

@@ -782,10 +782,17 @@ function initSelect2() {
// ------------------------------------------
// Render a device link with hover-over functionality
function renderDeviceLink(data, container, useName = false) {
if (!data.id || !isValidMac(data.id)) return data.text; // default placeholder etc.
// If no valid MAC, return placeholder text
if (!data.id || !isValidMac(data.id)) {
return `<span>${data.text}<span/>`;
}
const device = getDevDataByMac(data.id);
if (!device) {
return data.text;
}
// Build and return badge parts
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,

View File

@@ -197,7 +197,7 @@
<div class="col-sm-9">
${isRootNode ? '' : `<a class="anonymize" href="#">`}
<span my-data-mac="${node.parent_mac}" data-mac="${node.parent_mac}" data-devIsNetworkNodeDynamic="1" onclick="handleNodeClick(this)">
${isRootNode ? getString('Network_Root') : getNameByMacAddress(node.parent_mac)}
${isRootNode ? getString('Network_Root') : getDevDataByMac(node.parent_mac, "devName")}
</span>
${isRootNode ? '' : `</a>`}
</div>

View File

@@ -26,35 +26,31 @@
if (isset ($_REQUEST['action']) && !empty ($_REQUEST['action'])) {
$action = $_REQUEST['action'];
switch ($action) {
case 'getServerDeviceData': getServerDeviceData(); break;
case 'setDeviceData': setDeviceData(); break;
case 'deleteDevice': deleteDevice(); break;
case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break;
// check server/api_server/api_server_start.py for equivalents
case 'getServerDeviceData': getServerDeviceData(); break; // equivalent: get_device_data
case 'setDeviceData': setDeviceData(); break; // equivalent: set_device_data
case 'deleteDevice': deleteDevice(); break; // equivalent: delete_device(mac)
case 'deleteAllWithEmptyMACs': deleteAllWithEmptyMACs(); break; // equivalent: delete_all_with_empty_macs
case 'deleteAllDevices': deleteAllDevices(); break;
case 'deleteUnknownDevices': deleteUnknownDevices(); break;
case 'deleteEvents': deleteEvents(); break;
case 'deleteEvents30': deleteEvents30(); break;
case 'deleteActHistory': deleteActHistory(); break;
case 'deleteDeviceEvents': deleteDeviceEvents(); break;
case 'resetDeviceProps': resetDeviceProps(); break;
case 'PiaBackupDBtoArchive': PiaBackupDBtoArchive(); break;
case 'PiaRestoreDBfromArchive': PiaRestoreDBfromArchive(); break;
case 'PiaPurgeDBBackups': PiaPurgeDBBackups(); break;
case 'ExportCSV': ExportCSV(); break;
case 'ImportCSV': ImportCSV(); break;
case 'deleteAllDevices': deleteAllDevices(); break; // equivalent: delete_devices(macs)
case 'deleteUnknownDevices': deleteUnknownDevices(); break; // equivalent: delete_unknown_devices
case 'deleteEvents': deleteEvents(); break; // equivalent: delete_events
case 'deleteEvents30': deleteEvents30(); break; // equivalent: delete_events_30
case 'deleteActHistory': deleteActHistory(); break; // equivalent: delete_online_history
case 'deleteDeviceEvents': deleteDeviceEvents(); break; // equivalent: delete_device_events(mac)
case 'resetDeviceProps': resetDeviceProps(); break; // equivalent: reset_device_props
case 'ExportCSV': ExportCSV(); break; // equivalent: export_devices
case 'ImportCSV': ImportCSV(); break; // equivalent: import_csv
case 'getDevicesTotals': getDevicesTotals(); break;
case 'getDevicesListCalendar': getDevicesListCalendar(); break; //todo: slowly deprecate this
case 'getDevicesTotals': getDevicesTotals(); break; // equivalent: devices_totals
case 'getDevicesListCalendar': getDevicesListCalendar(); break; // equivalent: devices_by_status
case 'updateNetworkLeaf': updateNetworkLeaf(); break;
case 'getIcons': getIcons(); break;
case 'getActions': getActions(); break;
case 'getDevices': getDevices(); break;
case 'copyFromDevice': copyFromDevice(); break;
case 'wakeonlan': wakeonlan(); break;
case 'updateNetworkLeaf': updateNetworkLeaf(); break; // equivalent: update_device_column(mac, column_name, column_value)
default: logServerConsole ('Action: '. $action); break;
case 'copyFromDevice': copyFromDevice(); break; // equivalent: copy_device(mac_from, mac_to)
case 'wakeonlan': wakeonlan(); break; // equivalent: wakeonlan
default: logServerConsole ('Action: '. $action); break; // equivalent:
}
}
@@ -517,92 +513,6 @@ function deleteActHistory() {
}
}
//------------------------------------------------------------------------------
// Backup DB to Archiv
//------------------------------------------------------------------------------
function PiaBackupDBtoArchive() {
// prepare fast Backup
$dbfilename = 'app.db';
$file = '../../../db/'.$dbfilename;
$newfile = '../../../db/'.$dbfilename.'.latestbackup';
// copy files as a fast Backup
if (!copy($file, $newfile)) {
echo lang('BackDevices_Backup_CopError');
} else {
// Create archive with actual date
$Pia_Archive_Name = 'appdb_'.date("Ymd_His").'.zip';
$Pia_Archive_Path = '../../../db/';
exec('zip -j '.$Pia_Archive_Path.$Pia_Archive_Name.' ../../../db/'.$dbfilename, $output);
// chheck if archive exists
if (file_exists($Pia_Archive_Path.$Pia_Archive_Name) && filesize($Pia_Archive_Path.$Pia_Archive_Name) > 0) {
echo lang('BackDevices_Backup_okay').': ('.$Pia_Archive_Name.')';
unlink($newfile);
echo("<meta http-equiv='refresh' content='1'>");
} else {
echo lang('BackDevices_Backup_Failed').' ('.$dbfilename.'.latestbackup)';
}
}
}
//------------------------------------------------------------------------------
// Restore DB from Archiv
//------------------------------------------------------------------------------
function PiaRestoreDBfromArchive() {
// prepare fast Backup
$file = '../../../db/'.$dbfilename;
$oldfile = '../../../db/'.$dbfilename.'.prerestore';
// copy files as a fast Backup
if (!copy($file, $oldfile)) {
echo lang('BackDevices_Restore_CopError');
} else {
// extract latest archive and overwrite the actual .db
$Pia_Archive_Path = '../../../db/';
exec('/bin/ls -Art '.$Pia_Archive_Path.'*.zip | /bin/tail -n 1 | /usr/bin/xargs -n1 /bin/unzip -o -d ../../../db/', $output);
// check if the .db exists
if (file_exists($file)) {
echo lang('BackDevices_Restore_okay');
unlink($oldfile);
echo("<meta http-equiv='refresh' content='1'>");
} else {
echo lang('BackDevices_Restore_Failed');
}
}
}
//------------------------------------------------------------------------------
// Purge Backups
//------------------------------------------------------------------------------
function PiaPurgeDBBackups() {
$Pia_Archive_Path = '../../../db';
$Pia_Backupfiles = array();
$files = array_diff(scandir($Pia_Archive_Path, SCANDIR_SORT_DESCENDING), array('.', '..', $dbfilename, 'netalertxdb-reset.zip'));
foreach ($files as &$item)
{
$item = $Pia_Archive_Path.'/'.$item;
if (stristr($item, 'setting_') == '') {array_push($Pia_Backupfiles, $item);}
}
if (sizeof($Pia_Backupfiles) > 3)
{
rsort($Pia_Backupfiles);
unset($Pia_Backupfiles[0], $Pia_Backupfiles[1], $Pia_Backupfiles[2]);
$Pia_Backupfiles_Purge = array_values($Pia_Backupfiles);
for ($i = 0; $i < sizeof($Pia_Backupfiles_Purge); $i++)
{
unlink($Pia_Backupfiles_Purge[$i]);
}
}
echo lang('BackDevices_DBTools_Purge');
echo("<meta http-equiv='refresh' content='1'>");
}
//------------------------------------------------------------------------------
// Export CSV of devices
//------------------------------------------------------------------------------
@@ -827,75 +737,6 @@ function getDevicesListCalendar() {
// Query Device Data
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
function getIcons() {
global $db;
// Device Data
$sql = 'select devIcon from Devices group by devIcon';
$result = $db->query($sql);
// arrays of rows
$tableData = array();
while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
$icon = handleNull($row['devIcon'], "<i class='fa fa-laptop'></i>");
// Push row data
$tableData[] = array('id' => $icon,
'name' => $icon );
}
// Control no rows
if (empty($tableData)) {
$tableData = [];
}
// Return json
echo (json_encode ($tableData));
}
//------------------------------------------------------------------------------
function getActions() {
$tableData = array(
array('id' => 'wake-on-lan',
'name' => lang('DevDetail_WOL_Title'))
);
// Return json
echo (json_encode ($tableData));
}
//------------------------------------------------------------------------------
function getDevices() {
global $db;
// Device Data
$sql = 'select devMac, devName from Devices';
$result = $db->query($sql);
// arrays of rows
$tableData = array();
while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
$name = handleNull($row['devName'], "(unknown)");
$mac = handleNull($row['devMac'], "(unknown)");
// Push row data
$tableData[] = array('id' => $mac,
'name' => $name );
}
// Control no rows
if (empty($tableData)) {
$tableData = [];
}
// Return json
echo (json_encode ($tableData));
}
// ----------------------------------------------------------------------------------------
function updateNetworkLeaf()
{

View File

@@ -833,4 +833,4 @@
"settings_system_label": "System",
"settings_update_item_warning": "",
"test_event_tooltip": "Speichere die Änderungen, bevor Sie die Einstellungen testen."
}
}

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Système",
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
}
}

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
}
}

View File

@@ -5,7 +5,7 @@
// ###################################
$defaultLang = "en_us";
$allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"];
$allLanguages = ["en_us", "es_es", "de_de", "fr_fr", "it_it", "ru_ru", "nb_no", "pl_pl", "pt_br", "pt_pt", "tr_tr", "zh_cn", "cs_cz", "ar_ar", "ca_ca", "uk_ua"];
global $db;
@@ -19,6 +19,7 @@ switch($result){
case 'Norwegian': $pia_lang_selected = 'nb_no'; break;
case 'Polish (pl_pl)': $pia_lang_selected = 'pl_pl'; break;
case 'Portuguese (pt_br)': $pia_lang_selected = 'pt_br'; break;
case 'Portuguese (pt_pt)': $pia_lang_selected = 'pt_pt'; break;
case 'Italian (it_it)': $pia_lang_selected = 'it_it'; break;
case 'Russian': $pia_lang_selected = 'ru_ru'; break;
case 'Turkish (tr_tr)': $pia_lang_selected = 'tr_tr'; break;

View File

@@ -33,6 +33,6 @@ def merge_translations(main_file, other_files):
if __name__ == "__main__":
current_path = os.path.dirname(os.path.abspath(__file__))
# language codes can be found here: http://www.lingoes.net/en/translator/langcode.htm
json_files = ["en_us.json", "de_de.json", "es_es.json", "fr_fr.json", "nb_no.json", "ru_ru.json", "it_it.json", "pt_br.json", "pl_pl.json", "zh_cn.json", "tr_tr.json", "cs_cz.json", "ar_ar.json", "ca_ca.json", "uk_ua.json"]
json_files = ["en_us.json", "de_de.json", "es_es.json", "fr_fr.json", "nb_no.json", "ru_ru.json", "it_it.json", "pt_br.json", "pt_pt.json", "pl_pl.json", "zh_cn.json", "tr_tr.json", "cs_cz.json", "ar_ar.json", "ca_ca.json", "uk_ua.json"]
file_paths = [os.path.join(current_path, file) for file in json_files]
merge_translations(file_paths[0], file_paths[1:])

View File

@@ -0,0 +1,763 @@
{
"API_CUSTOM_SQL_description": "",
"API_CUSTOM_SQL_name": "",
"API_TOKEN_description": "",
"API_TOKEN_name": "",
"API_display_name": "",
"API_icon": "",
"About_Design": "",
"About_Exit": "",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "",
"AppEvents_GUID": "",
"AppEvents_Helper1": "",
"AppEvents_Helper2": "",
"AppEvents_Helper3": "",
"AppEvents_ObjectForeignKey": "",
"AppEvents_ObjectIndex": "",
"AppEvents_ObjectIsArchived": "",
"AppEvents_ObjectIsNew": "",
"AppEvents_ObjectPlugin": "",
"AppEvents_ObjectPrimaryID": "",
"AppEvents_ObjectSecondaryID": "",
"AppEvents_ObjectStatus": "",
"AppEvents_ObjectStatusColumn": "",
"AppEvents_ObjectType": "",
"AppEvents_Plugin": "",
"AppEvents_Type": "",
"BackDevDetail_Actions_Ask_Run": "",
"BackDevDetail_Actions_Not_Registered": "",
"BackDevDetail_Actions_Title_Run": "",
"BackDevDetail_Copy_Ask": "",
"BackDevDetail_Copy_Title": "",
"BackDevDetail_Tools_WOL_error": "",
"BackDevDetail_Tools_WOL_okay": "",
"BackDevices_Arpscan_disabled": "",
"BackDevices_Arpscan_enabled": "",
"BackDevices_Backup_CopError": "",
"BackDevices_Backup_Failed": "",
"BackDevices_Backup_okay": "",
"BackDevices_DBTools_DelDevError_a": "",
"BackDevices_DBTools_DelDevError_b": "",
"BackDevices_DBTools_DelDev_a": "",
"BackDevices_DBTools_DelDev_b": "",
"BackDevices_DBTools_DelEvents": "",
"BackDevices_DBTools_DelEventsError": "",
"BackDevices_DBTools_ImportCSV": "",
"BackDevices_DBTools_ImportCSVError": "",
"BackDevices_DBTools_ImportCSVMissing": "",
"BackDevices_DBTools_Purge": "",
"BackDevices_DBTools_UpdDev": "",
"BackDevices_DBTools_UpdDevError": "",
"BackDevices_DBTools_Upgrade": "",
"BackDevices_DBTools_UpgradeError": "",
"BackDevices_Device_UpdDevError": "",
"BackDevices_Restore_CopError": "",
"BackDevices_Restore_Failed": "",
"BackDevices_Restore_okay": "",
"BackDevices_darkmode_disabled": "",
"BackDevices_darkmode_enabled": "",
"CLEAR_NEW_FLAG_description": "",
"CLEAR_NEW_FLAG_name": "",
"CustProps_cant_remove": "",
"DAYS_TO_KEEP_EVENTS_description": "",
"DAYS_TO_KEEP_EVENTS_name": "",
"DISCOVER_PLUGINS_description": "",
"DISCOVER_PLUGINS_name": "",
"DevDetail_Children_Title": "",
"DevDetail_Copy_Device_Title": "",
"DevDetail_Copy_Device_Tooltip": "",
"DevDetail_CustomProperties_Title": "",
"DevDetail_CustomProps_reset_info": "",
"DevDetail_DisplayFields_Title": "",
"DevDetail_EveandAl_AlertAllEvents": "",
"DevDetail_EveandAl_AlertDown": "",
"DevDetail_EveandAl_Archived": "",
"DevDetail_EveandAl_NewDevice": "",
"DevDetail_EveandAl_NewDevice_Tooltip": "",
"DevDetail_EveandAl_RandomMAC": "",
"DevDetail_EveandAl_ScanCycle": "",
"DevDetail_EveandAl_ScanCycle_a": "",
"DevDetail_EveandAl_ScanCycle_z": "",
"DevDetail_EveandAl_Skip": "",
"DevDetail_EveandAl_Title": "",
"DevDetail_Events_CheckBox": "",
"DevDetail_GoToNetworkNode": "",
"DevDetail_Icon": "",
"DevDetail_Icon_Descr": "",
"DevDetail_Loading": "",
"DevDetail_MainInfo_Comments": "",
"DevDetail_MainInfo_Favorite": "",
"DevDetail_MainInfo_Group": "",
"DevDetail_MainInfo_Location": "",
"DevDetail_MainInfo_Name": "",
"DevDetail_MainInfo_Network": "",
"DevDetail_MainInfo_Network_Port": "",
"DevDetail_MainInfo_Network_Site": "",
"DevDetail_MainInfo_Network_Title": "",
"DevDetail_MainInfo_Owner": "",
"DevDetail_MainInfo_SSID": "",
"DevDetail_MainInfo_Title": "",
"DevDetail_MainInfo_Type": "",
"DevDetail_MainInfo_Vendor": "",
"DevDetail_MainInfo_mac": "",
"DevDetail_NavToChildNode": "",
"DevDetail_Network_Node_hover": "",
"DevDetail_Network_Port_hover": "",
"DevDetail_Nmap_Scans": "",
"DevDetail_Nmap_Scans_desc": "",
"DevDetail_Nmap_buttonDefault": "",
"DevDetail_Nmap_buttonDefault_text": "",
"DevDetail_Nmap_buttonDetail": "",
"DevDetail_Nmap_buttonDetail_text": "",
"DevDetail_Nmap_buttonFast": "",
"DevDetail_Nmap_buttonFast_text": "",
"DevDetail_Nmap_buttonSkipDiscovery": "",
"DevDetail_Nmap_buttonSkipDiscovery_text": "",
"DevDetail_Nmap_resultsLink": "",
"DevDetail_Owner_hover": "",
"DevDetail_Periodselect_All": "",
"DevDetail_Periodselect_LastMonth": "",
"DevDetail_Periodselect_LastWeek": "",
"DevDetail_Periodselect_LastYear": "",
"DevDetail_Periodselect_today": "",
"DevDetail_Run_Actions_Title": "",
"DevDetail_Run_Actions_Tooltip": "",
"DevDetail_SessionInfo_FirstSession": "",
"DevDetail_SessionInfo_LastIP": "",
"DevDetail_SessionInfo_LastSession": "",
"DevDetail_SessionInfo_StaticIP": "",
"DevDetail_SessionInfo_Status": "",
"DevDetail_SessionInfo_Title": "",
"DevDetail_SessionTable_Additionalinfo": "",
"DevDetail_SessionTable_Connection": "",
"DevDetail_SessionTable_Disconnection": "",
"DevDetail_SessionTable_Duration": "",
"DevDetail_SessionTable_IP": "",
"DevDetail_SessionTable_Order": "",
"DevDetail_Shortcut_CurrentStatus": "",
"DevDetail_Shortcut_DownAlerts": "",
"DevDetail_Shortcut_Presence": "",
"DevDetail_Shortcut_Sessions": "",
"DevDetail_Tab_Details": "",
"DevDetail_Tab_Events": "",
"DevDetail_Tab_EventsTableDate": "",
"DevDetail_Tab_EventsTableEvent": "",
"DevDetail_Tab_EventsTableIP": "",
"DevDetail_Tab_EventsTableInfo": "",
"DevDetail_Tab_Nmap": "",
"DevDetail_Tab_NmapEmpty": "",
"DevDetail_Tab_NmapTableExtra": "",
"DevDetail_Tab_NmapTableHeader": "",
"DevDetail_Tab_NmapTableIndex": "",
"DevDetail_Tab_NmapTablePort": "",
"DevDetail_Tab_NmapTableService": "",
"DevDetail_Tab_NmapTableState": "",
"DevDetail_Tab_NmapTableText": "",
"DevDetail_Tab_NmapTableTime": "",
"DevDetail_Tab_Plugins": "",
"DevDetail_Tab_Presence": "",
"DevDetail_Tab_Sessions": "",
"DevDetail_Tab_Tools": "",
"DevDetail_Tab_Tools_Internet_Info_Description": "",
"DevDetail_Tab_Tools_Internet_Info_Error": "",
"DevDetail_Tab_Tools_Internet_Info_Start": "",
"DevDetail_Tab_Tools_Internet_Info_Title": "",
"DevDetail_Tab_Tools_Nslookup_Description": "",
"DevDetail_Tab_Tools_Nslookup_Error": "",
"DevDetail_Tab_Tools_Nslookup_Start": "",
"DevDetail_Tab_Tools_Nslookup_Title": "",
"DevDetail_Tab_Tools_Speedtest_Description": "",
"DevDetail_Tab_Tools_Speedtest_Start": "",
"DevDetail_Tab_Tools_Speedtest_Title": "",
"DevDetail_Tab_Tools_Traceroute_Description": "",
"DevDetail_Tab_Tools_Traceroute_Error": "",
"DevDetail_Tab_Tools_Traceroute_Start": "",
"DevDetail_Tab_Tools_Traceroute_Title": "",
"DevDetail_Tools_WOL": "",
"DevDetail_Tools_WOL_noti": "",
"DevDetail_Tools_WOL_noti_text": "",
"DevDetail_Type_hover": "",
"DevDetail_Vendor_hover": "",
"DevDetail_WOL_Title": "",
"DevDetail_button_AddIcon": "",
"DevDetail_button_AddIcon_Help": "",
"DevDetail_button_AddIcon_Tooltip": "",
"DevDetail_button_Delete": "",
"DevDetail_button_DeleteEvents": "",
"DevDetail_button_DeleteEvents_Warning": "",
"DevDetail_button_Delete_ask": "",
"DevDetail_button_OverwriteIcons": "",
"DevDetail_button_OverwriteIcons_Tooltip": "",
"DevDetail_button_OverwriteIcons_Warning": "",
"DevDetail_button_Reset": "",
"DevDetail_button_Save": "",
"DeviceEdit_ValidMacIp": "",
"Device_MultiEdit": "",
"Device_MultiEdit_Backup": "",
"Device_MultiEdit_Fields": "",
"Device_MultiEdit_MassActions": "",
"Device_MultiEdit_Tooltip": "",
"Device_Searchbox": "",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
"Device_Shortcut_Archived": "",
"Device_Shortcut_Connected": "",
"Device_Shortcut_Devices": "",
"Device_Shortcut_DownAlerts": "",
"Device_Shortcut_DownOnly": "",
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",
"Device_TableHead_FQDN": "",
"Device_TableHead_Favorite": "",
"Device_TableHead_FirstSession": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
"Device_TableHead_LastSession": "",
"Device_TableHead_Location": "",
"Device_TableHead_MAC": "",
"Device_TableHead_MAC_full": "",
"Device_TableHead_Name": "",
"Device_TableHead_NetworkSite": "",
"Device_TableHead_Owner": "",
"Device_TableHead_ParentRelType": "",
"Device_TableHead_Parent_MAC": "",
"Device_TableHead_Port": "",
"Device_TableHead_PresentLastScan": "",
"Device_TableHead_ReqNicsOnline": "",
"Device_TableHead_RowID": "",
"Device_TableHead_Rowid": "",
"Device_TableHead_SSID": "",
"Device_TableHead_SourcePlugin": "",
"Device_TableHead_Status": "",
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
"Device_Table_nav_prev": "",
"Device_Tablelenght": "",
"Device_Tablelenght_all": "",
"Device_Title": "",
"Devices_Filters": "",
"ENABLE_PLUGINS_description": "",
"ENABLE_PLUGINS_name": "",
"ENCRYPTION_KEY_description": "",
"ENCRYPTION_KEY_name": "",
"Email_display_name": "",
"Email_icon": "",
"Events_Loading": "",
"Events_Periodselect_All": "",
"Events_Periodselect_LastMonth": "",
"Events_Periodselect_LastWeek": "",
"Events_Periodselect_LastYear": "",
"Events_Periodselect_today": "",
"Events_Searchbox": "",
"Events_Shortcut_AllEvents": "",
"Events_Shortcut_DownAlerts": "",
"Events_Shortcut_Events": "",
"Events_Shortcut_MissSessions": "",
"Events_Shortcut_NewDevices": "",
"Events_Shortcut_Sessions": "",
"Events_Shortcut_VoidSessions": "",
"Events_TableHead_AdditionalInfo": "",
"Events_TableHead_Connection": "",
"Events_TableHead_Date": "",
"Events_TableHead_Device": "",
"Events_TableHead_Disconnection": "",
"Events_TableHead_Duration": "",
"Events_TableHead_DurationOrder": "",
"Events_TableHead_EventType": "",
"Events_TableHead_IP": "",
"Events_TableHead_IPOrder": "",
"Events_TableHead_Order": "",
"Events_TableHead_Owner": "",
"Events_TableHead_PendingAlert": "",
"Events_Table_info": "",
"Events_Table_nav_next": "",
"Events_Table_nav_prev": "",
"Events_Tablelenght": "",
"Events_Tablelenght_all": "",
"Events_Title": "",
"GRAPHQL_PORT_description": "",
"GRAPHQL_PORT_name": "",
"Gen_Action": "",
"Gen_Add": "",
"Gen_AddDevice": "",
"Gen_Add_All": "",
"Gen_All_Devices": "",
"Gen_AreYouSure": "",
"Gen_Backup": "",
"Gen_Cancel": "",
"Gen_Change": "",
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
"Gen_Error": "",
"Gen_Filter": "",
"Gen_Generate": "",
"Gen_InvalidMac": "",
"Gen_LockedDB": "",
"Gen_NetworkMask": "",
"Gen_Offline": "",
"Gen_Okay": "",
"Gen_Online": "",
"Gen_Purge": "",
"Gen_ReadDocs": "",
"Gen_Remove_All": "",
"Gen_Remove_Last": "",
"Gen_Reset": "",
"Gen_Restore": "",
"Gen_Run": "",
"Gen_Save": "",
"Gen_Saved": "",
"Gen_Search": "",
"Gen_Select": "",
"Gen_SelectIcon": "",
"Gen_SelectToPreview": "",
"Gen_Selected_Devices": "",
"Gen_Subnet": "",
"Gen_Switch": "",
"Gen_Upd": "",
"Gen_Upd_Fail": "",
"Gen_Update": "",
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
"General_display_name": "",
"General_icon": "",
"HRS_TO_KEEP_NEWDEV_description": "",
"HRS_TO_KEEP_NEWDEV_name": "",
"HRS_TO_KEEP_OFFDEV_description": "",
"HRS_TO_KEEP_OFFDEV_name": "",
"LOADED_PLUGINS_description": "",
"LOADED_PLUGINS_name": "",
"LOG_LEVEL_description": "",
"LOG_LEVEL_name": "",
"Loading": "",
"Login_Box": "",
"Login_Default_PWD": "",
"Login_Info": "",
"Login_Psw-box": "",
"Login_Psw_alert": "",
"Login_Psw_folder": "",
"Login_Psw_new": "",
"Login_Psw_run": "",
"Login_Remember": "",
"Login_Remember_small": "",
"Login_Submit": "",
"Login_Toggle_Alert_headline": "",
"Login_Toggle_Info": "",
"Login_Toggle_Info_headline": "",
"Maint_PurgeLog": "",
"Maint_RestartServer": "",
"Maint_Restart_Server_noti_text": "",
"Maintenance_InitCheck": "",
"Maintenance_InitCheck_Checking": "",
"Maintenance_InitCheck_QuickSetupGuide": "",
"Maintenance_InitCheck_Success": "",
"Maintenance_ReCheck": "",
"Maintenance_Running_Version": "",
"Maintenance_Status": "",
"Maintenance_Title": "",
"Maintenance_Tool_DownloadConfig": "",
"Maintenance_Tool_DownloadConfig_text": "",
"Maintenance_Tool_DownloadWorkflows": "",
"Maintenance_Tool_DownloadWorkflows_text": "",
"Maintenance_Tool_ExportCSV": "",
"Maintenance_Tool_ExportCSV_noti": "",
"Maintenance_Tool_ExportCSV_noti_text": "",
"Maintenance_Tool_ExportCSV_text": "",
"Maintenance_Tool_ImportCSV": "",
"Maintenance_Tool_ImportCSV_noti": "",
"Maintenance_Tool_ImportCSV_noti_text": "",
"Maintenance_Tool_ImportCSV_text": "",
"Maintenance_Tool_ImportConfig_noti": "",
"Maintenance_Tool_ImportPastedCSV": "",
"Maintenance_Tool_ImportPastedCSV_noti_text": "",
"Maintenance_Tool_ImportPastedCSV_text": "",
"Maintenance_Tool_ImportPastedConfig": "",
"Maintenance_Tool_ImportPastedConfig_noti_text": "",
"Maintenance_Tool_ImportPastedConfig_text": "",
"Maintenance_Tool_arpscansw": "",
"Maintenance_Tool_arpscansw_noti": "",
"Maintenance_Tool_arpscansw_noti_text": "",
"Maintenance_Tool_arpscansw_text": "",
"Maintenance_Tool_backup": "",
"Maintenance_Tool_backup_noti": "",
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",
"Maintenance_Tool_darkmode_text": "",
"Maintenance_Tool_del_ActHistory": "",
"Maintenance_Tool_del_ActHistory_noti": "",
"Maintenance_Tool_del_ActHistory_noti_text": "",
"Maintenance_Tool_del_ActHistory_text": "",
"Maintenance_Tool_del_alldev": "",
"Maintenance_Tool_del_alldev_noti": "",
"Maintenance_Tool_del_alldev_noti_text": "",
"Maintenance_Tool_del_alldev_text": "",
"Maintenance_Tool_del_allevents": "",
"Maintenance_Tool_del_allevents30": "",
"Maintenance_Tool_del_allevents30_noti": "",
"Maintenance_Tool_del_allevents30_noti_text": "",
"Maintenance_Tool_del_allevents30_text": "",
"Maintenance_Tool_del_allevents_noti": "",
"Maintenance_Tool_del_allevents_noti_text": "",
"Maintenance_Tool_del_allevents_text": "",
"Maintenance_Tool_del_empty_macs": "",
"Maintenance_Tool_del_empty_macs_noti": "",
"Maintenance_Tool_del_empty_macs_noti_text": "",
"Maintenance_Tool_del_empty_macs_text": "",
"Maintenance_Tool_del_selecteddev": "",
"Maintenance_Tool_del_selecteddev_text": "",
"Maintenance_Tool_del_unknowndev": "",
"Maintenance_Tool_del_unknowndev_noti": "",
"Maintenance_Tool_del_unknowndev_noti_text": "",
"Maintenance_Tool_del_unknowndev_text": "",
"Maintenance_Tool_displayed_columns_text": "",
"Maintenance_Tool_drag_me": "",
"Maintenance_Tool_order_columns_text": "",
"Maintenance_Tool_purgebackup": "",
"Maintenance_Tool_purgebackup_noti": "",
"Maintenance_Tool_purgebackup_noti_text": "",
"Maintenance_Tool_purgebackup_text": "",
"Maintenance_Tool_restore": "",
"Maintenance_Tool_restore_noti": "",
"Maintenance_Tool_restore_noti_text": "",
"Maintenance_Tool_restore_text": "",
"Maintenance_Tool_upgrade_database_noti": "",
"Maintenance_Tool_upgrade_database_noti_text": "",
"Maintenance_Tool_upgrade_database_text": "",
"Maintenance_Tools_Tab_BackupRestore": "",
"Maintenance_Tools_Tab_Logging": "",
"Maintenance_Tools_Tab_Settings": "",
"Maintenance_Tools_Tab_Tools": "",
"Maintenance_Tools_Tab_UISettings": "",
"Maintenance_arp_status": "",
"Maintenance_arp_status_off": "",
"Maintenance_arp_status_on": "",
"Maintenance_built_on": "",
"Maintenance_current_version": "",
"Maintenance_database_backup": "",
"Maintenance_database_backup_found": "",
"Maintenance_database_backup_total": "",
"Maintenance_database_lastmod": "",
"Maintenance_database_path": "",
"Maintenance_database_rows": "",
"Maintenance_database_size": "",
"Maintenance_lang_selector_apply": "",
"Maintenance_lang_selector_empty": "",
"Maintenance_lang_selector_lable": "",
"Maintenance_lang_selector_text": "",
"Maintenance_new_version": "",
"Maintenance_themeselector_apply": "",
"Maintenance_themeselector_empty": "",
"Maintenance_themeselector_lable": "",
"Maintenance_themeselector_text": "",
"Maintenance_version": "",
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",
"Navigation_Integrations": "",
"Navigation_Maintenance": "",
"Navigation_Monitoring": "",
"Navigation_Network": "",
"Navigation_Notifications": "",
"Navigation_Plugins": "",
"Navigation_Presence": "",
"Navigation_Report": "",
"Navigation_Settings": "",
"Navigation_SystemInfo": "",
"Navigation_Workflows": "",
"Network_Assign": "",
"Network_Cant_Assign": "",
"Network_Cant_Assign_No_Node_Selected": "",
"Network_Configuration_Error": "",
"Network_Connected": "",
"Network_Devices": "",
"Network_ManageAdd": "",
"Network_ManageAdd_Name": "",
"Network_ManageAdd_Name_text": "",
"Network_ManageAdd_Port": "",
"Network_ManageAdd_Port_text": "",
"Network_ManageAdd_Submit": "",
"Network_ManageAdd_Type": "",
"Network_ManageAdd_Type_text": "",
"Network_ManageAssign": "",
"Network_ManageDel": "",
"Network_ManageDel_Name": "",
"Network_ManageDel_Name_text": "",
"Network_ManageDel_Submit": "",
"Network_ManageDevices": "",
"Network_ManageEdit": "",
"Network_ManageEdit_ID": "",
"Network_ManageEdit_ID_text": "",
"Network_ManageEdit_Name": "",
"Network_ManageEdit_Name_text": "",
"Network_ManageEdit_Port": "",
"Network_ManageEdit_Port_text": "",
"Network_ManageEdit_Submit": "",
"Network_ManageEdit_Type": "",
"Network_ManageEdit_Type_text": "",
"Network_ManageLeaf": "",
"Network_ManageUnassign": "",
"Network_NoAssignedDevices": "",
"Network_NoDevices": "",
"Network_Node": "",
"Network_Node_Name": "",
"Network_Parent": "",
"Network_Root": "",
"Network_Root_Not_Configured": "",
"Network_Root_Unconfigurable": "",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_Table_Hostname": "",
"Network_Table_IP": "",
"Network_Table_State": "",
"Network_Title": "",
"Network_UnassignedDevices": "",
"Notifications_All": "",
"Notifications_Mark_All_Read": "",
"PIALERT_WEB_PASSWORD_description": "",
"PIALERT_WEB_PASSWORD_name": "",
"PIALERT_WEB_PROTECTION_description": "",
"PIALERT_WEB_PROTECTION_name": "",
"PLUGINS_KEEP_HIST_description": "",
"PLUGINS_KEEP_HIST_name": "",
"Plugins_DeleteAll": "",
"Plugins_Filters_Mac": "",
"Plugins_History": "",
"Plugins_Obj_DeleteListed": "",
"Plugins_Objects": "",
"Plugins_Out_of": "",
"Plugins_Unprocessed_Events": "",
"Plugins_no_control": "",
"Presence_CalHead_day": "",
"Presence_CalHead_lang": "",
"Presence_CalHead_month": "",
"Presence_CalHead_quarter": "",
"Presence_CalHead_week": "",
"Presence_CalHead_year": "",
"Presence_CallHead_Devices": "",
"Presence_Key_OnlineNow": "",
"Presence_Key_OnlineNow_desc": "",
"Presence_Key_OnlinePast": "",
"Presence_Key_OnlinePastMiss": "",
"Presence_Key_OnlinePastMiss_desc": "",
"Presence_Key_OnlinePast_desc": "",
"Presence_Loading": "",
"Presence_Shortcut_AllDevices": "",
"Presence_Shortcut_Archived": "",
"Presence_Shortcut_Connected": "",
"Presence_Shortcut_Devices": "",
"Presence_Shortcut_DownAlerts": "",
"Presence_Shortcut_Favorites": "",
"Presence_Shortcut_NewDevices": "",
"Presence_Title": "",
"REFRESH_FQDN_description": "",
"REFRESH_FQDN_name": "",
"REPORT_DASHBOARD_URL_description": "",
"REPORT_DASHBOARD_URL_name": "",
"REPORT_ERROR": "",
"REPORT_MAIL_description": "",
"REPORT_MAIL_name": "",
"REPORT_TITLE": "",
"RandomMAC_hover": "",
"Reports_Sent_Log": "",
"SCAN_SUBNETS_description": "",
"SCAN_SUBNETS_name": "",
"SYSTEM_TITLE": "",
"Setting_Override": "",
"Setting_Override_Description": "",
"Settings_Metadata_Toggle": "",
"Settings_Show_Description": "",
"Settings_device_Scanners_desync": "",
"Settings_device_Scanners_desync_popup": "",
"Speedtest_Results": "",
"Systeminfo_AvailableIps": "",
"Systeminfo_CPU": "",
"Systeminfo_CPU_Cores": "",
"Systeminfo_CPU_Name": "",
"Systeminfo_CPU_Speed": "",
"Systeminfo_CPU_Temp": "",
"Systeminfo_CPU_Vendor": "",
"Systeminfo_Client_Resolution": "",
"Systeminfo_Client_User_Agent": "",
"Systeminfo_General": "",
"Systeminfo_General_Date": "",
"Systeminfo_General_Date2": "",
"Systeminfo_General_Full_Date": "",
"Systeminfo_General_TimeZone": "",
"Systeminfo_Memory": "",
"Systeminfo_Memory_Total_Memory": "",
"Systeminfo_Memory_Usage": "",
"Systeminfo_Memory_Usage_Percent": "",
"Systeminfo_Motherboard": "",
"Systeminfo_Motherboard_BIOS": "",
"Systeminfo_Motherboard_BIOS_Date": "",
"Systeminfo_Motherboard_BIOS_Vendor": "",
"Systeminfo_Motherboard_Manufactured": "",
"Systeminfo_Motherboard_Name": "",
"Systeminfo_Motherboard_Revision": "",
"Systeminfo_Network": "",
"Systeminfo_Network_Accept_Encoding": "",
"Systeminfo_Network_Accept_Language": "",
"Systeminfo_Network_Connection_Port": "",
"Systeminfo_Network_HTTP_Host": "",
"Systeminfo_Network_HTTP_Referer": "",
"Systeminfo_Network_HTTP_Referer_String": "",
"Systeminfo_Network_Hardware": "",
"Systeminfo_Network_Hardware_Interface_Mask": "",
"Systeminfo_Network_Hardware_Interface_Name": "",
"Systeminfo_Network_Hardware_Interface_RX": "",
"Systeminfo_Network_Hardware_Interface_TX": "",
"Systeminfo_Network_IP": "",
"Systeminfo_Network_IP_Connection": "",
"Systeminfo_Network_IP_Server": "",
"Systeminfo_Network_MIME": "",
"Systeminfo_Network_Request_Method": "",
"Systeminfo_Network_Request_Time": "",
"Systeminfo_Network_Request_URI": "",
"Systeminfo_Network_Secure_Connection": "",
"Systeminfo_Network_Secure_Connection_String": "",
"Systeminfo_Network_Server_Name": "",
"Systeminfo_Network_Server_Name_String": "",
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
"Systeminfo_Storage": "",
"Systeminfo_Storage_Device": "",
"Systeminfo_Storage_Mount": "",
"Systeminfo_Storage_Size": "",
"Systeminfo_Storage_Type": "",
"Systeminfo_Storage_Usage": "",
"Systeminfo_Storage_Usage_Free": "",
"Systeminfo_Storage_Usage_Mount": "",
"Systeminfo_Storage_Usage_Total": "",
"Systeminfo_Storage_Usage_Used": "",
"Systeminfo_System": "",
"Systeminfo_System_AVG": "",
"Systeminfo_System_Architecture": "",
"Systeminfo_System_Kernel": "",
"Systeminfo_System_OSVersion": "",
"Systeminfo_System_Running_Processes": "",
"Systeminfo_System_System": "",
"Systeminfo_System_Uname": "",
"Systeminfo_System_Uptime": "",
"Systeminfo_This_Client": "",
"Systeminfo_USB_Devices": "",
"TICKER_MIGRATE_TO_NETALERTX": "",
"TIMEZONE_description": "",
"TIMEZONE_name": "",
"UI_DEV_SECTIONS_description": "",
"UI_DEV_SECTIONS_name": "",
"UI_ICONS_description": "",
"UI_ICONS_name": "",
"UI_LANG_description": "",
"UI_LANG_name": "",
"UI_MY_DEVICES_description": "",
"UI_MY_DEVICES_name": "",
"UI_NOT_RANDOM_MAC_description": "",
"UI_NOT_RANDOM_MAC_name": "",
"UI_PRESENCE_description": "",
"UI_PRESENCE_name": "",
"UI_REFRESH_description": "",
"UI_REFRESH_name": "",
"VERSION_description": "",
"VERSION_name": "",
"WF_Action_Add": "",
"WF_Action_field": "",
"WF_Action_type": "",
"WF_Action_value": "",
"WF_Actions": "",
"WF_Add": "",
"WF_Add_Condition": "",
"WF_Add_Group": "",
"WF_Condition_field": "",
"WF_Condition_operator": "",
"WF_Condition_value": "",
"WF_Conditions": "",
"WF_Conditions_logic_rules": "",
"WF_Duplicate": "",
"WF_Enabled": "",
"WF_Export": "",
"WF_Export_Copy": "",
"WF_Import": "",
"WF_Import_Copy": "",
"WF_Name": "",
"WF_Remove": "",
"WF_Remove_Copy": "",
"WF_Save": "",
"WF_Trigger": "",
"WF_Trigger_event_type": "",
"WF_Trigger_type": "",
"add_icon_event_tooltip": "",
"add_option_event_tooltip": "",
"copy_icons_event_tooltip": "",
"devices_old": "",
"general_event_description": "",
"general_event_title": "",
"go_to_device_event_tooltip": "",
"go_to_node_event_tooltip": "",
"new_version_available": "",
"report_guid": "",
"report_guid_missing": "",
"report_select_format": "",
"report_time": "",
"run_event_tooltip": "",
"select_icon_event_tooltip": "",
"settings_core_icon": "",
"settings_core_label": "",
"settings_device_scanners": "",
"settings_device_scanners_icon": "",
"settings_device_scanners_info": "",
"settings_device_scanners_label": "",
"settings_enabled": "",
"settings_enabled_icon": "",
"settings_expand_all": "",
"settings_imported": "",
"settings_imported_label": "",
"settings_missing": "",
"settings_missing_block": "",
"settings_old": "",
"settings_other_scanners": "",
"settings_other_scanners_icon": "",
"settings_other_scanners_label": "",
"settings_publishers": "",
"settings_publishers_icon": "",
"settings_publishers_info": "",
"settings_publishers_label": "",
"settings_readonly": "",
"settings_saved": "",
"settings_system_icon": "",
"settings_system_label": "",
"settings_update_item_warning": "",
"test_event_tooltip": ""
}

View File

@@ -760,4 +760,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
}
}

View File

@@ -133,7 +133,7 @@
<div class="modal-footer">
<button id="modal-form-cancel" type="button" class="btn btn-outline pull-left" style="min-width: 80px;" data-dismiss="modal"> Cancel </button>
<button id="modal-form-OK" type="button" class="btn btn-outline btn-modal-submit" style="min-width: 80px;" onclick="modalDefaultForm()"> OK </button>
<button id="modal-form-OK" type="button" class="btn btn-outline btn-modal-submit" style="min-width: 80px;" > OK </button>
</div>
</div>

View File

@@ -4,6 +4,7 @@ from pytz import timezone, all_timezones, UnknownTimeZoneError
import sys
import re
import base64
import json
from datetime import datetime
INSTALL_PATH = "/app"
@@ -116,6 +117,51 @@ def decodeBase64(inputParamBase64):
return result
# -------------------------------------------------------------------
def decode_settings_base64(encoded_str, convert_types=True):
"""
Decodes a base64-encoded JSON list of settings into a dict.
Each setting entry format:
[group, key, type, value]
Example:
[
["group", "name", "string", "Home - local"],
["group", "base_url", "string", "https://..."],
["group", "api_version", "integer", "2"],
["group", "verify_ssl", "boolean", "False"]
]
Returns:
{
"name": "Home - local",
"base_url": "https://...",
"api_version": 2,
"verify_ssl": False
}
"""
decoded_json = base64.b64decode(encoded_str).decode("utf-8")
settings_list = json.loads(decoded_json)
settings_dict = {}
for _, key, _type, value in settings_list:
if convert_types:
_type_lower = _type.lower()
if _type_lower == "boolean":
settings_dict[key] = value.lower() == "true"
elif _type_lower == "integer":
settings_dict[key] = int(value)
elif _type_lower == "float":
settings_dict[key] = float(value)
else:
settings_dict[key] = value
else:
settings_dict[key] = value
return settings_dict
# -------------------------------------------------------------------
def normalize_mac(mac):
# Split the MAC address by colon (:) or hyphen (-) and convert each part to uppercase

View File

@@ -115,7 +115,7 @@ Initially, I had one virtual machine (VM) with 6 network cards, one for each VLA
2. Set the schedule (5 minutes works for me).
3. **API Token**: Use any string, but it must match the clients (e.g., `abc123`).
4. **Encryption Key**: Use any string, but it must match the clients (e.g., `abc123`).
5. Under **Nodes**, add the full URL for each client, e.g., `http://192.168.1.20.20211/`.
5. Under **Nodes**, add the full URL for each client, e.g., `http://192.168.1.20.20212/`, where the port `20212` is the value of the `GRAPHQL_PORT` setting of the given node (client)
6. **Node Name**: Leave blank.
7. Check **Sync Devices**.

View File

@@ -245,7 +245,7 @@
"description": [
{
"language_code": "en_us",
"string": "If specified, the hub will pull Devices data from the listed nodes. The <code>API_TOKEN</code> and <code>SYNC_encryption_key</code> must be set to the same value across the hub and all the nodes to ensure proper authentication and communication."
"string": "If specified, the hub will pull Devices data from the listed nodes. The <code>API_TOKEN</code> and <code>SYNC_encryption_key</code> must be set to the same value across the hub and all the nodes to ensure proper authentication and communication. Add full host URL and use the value of the <code>GRAPHQL_PORT</code> setting of the target, as the port."
}
]
},
@@ -271,7 +271,7 @@
"description": [
{
"language_code": "en_us",
"string": "The URL of the hub (target instance). Set on the Node. Without a trailig slash, for example <code>http://192.168.1.82:20211</code>"
"string": "The URL of the hub (target instance) with the targets <code>GRAPHQL_PORT</code> set as port. Set on the Node. Without a trailig slash, for example <code>http://192.168.1.82:20212</code>"
}
]
},

View File

@@ -265,66 +265,81 @@ def main():
return 0
# ------------------------------------------------------------------
# Data retrieval methods
api_endpoints = [
f"/sync", # New Python-based endpoint
f"/plugins/sync/hub.php" # Legacy PHP endpoint
]
# send data to the HUB
def send_data(api_token, file_content, encryption_key, file_path, node_name, pref, hub_url):
# Encrypt the log data using the encryption_key
"""Send encrypted data to HUB, preferring /sync endpoint and falling back to PHP version."""
encrypted_data = encrypt_data(file_content, encryption_key)
mylog('verbose', [f'[{pluginName}] Sending encrypted_data: "{encrypted_data}"'])
# Prepare the data payload for the POST request
data = {
'data': encrypted_data,
'file_path': file_path,
'plugin': pref,
'node_name': node_name
}
# Set the authorization header with the API token
headers = {'Authorization': f'Bearer {api_token}'}
api_endpoint = f"{hub_url}/plugins/sync/hub.php"
response = requests.post(api_endpoint, data=data, headers=headers)
mylog('verbose', [f'[{pluginName}] response: "{response}"'])
for endpoint in api_endpoints:
final_endpoint = hub_url + endpoint
try:
response = requests.post(final_endpoint, data=data, headers=headers, timeout=5)
mylog('verbose', [f'[{pluginName}] Tried endpoint: {final_endpoint}, status: {response.status_code}'])
if response.status_code == 200:
message = f'[{pluginName}] Data for "{file_path}" sent successfully via {final_endpoint}'
mylog('verbose', [message])
write_notification(message, 'info', timeNowTZ())
return True
except requests.RequestException as e:
mylog('verbose', [f'[{pluginName}] Error calling {final_endpoint}: {e}'])
# If all endpoints fail
message = f'[{pluginName}] Failed to send data for "{file_path}" via all endpoints'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return False
if response.status_code == 200:
message = f'[{pluginName}] Data for "{file_path}" sent successfully'
mylog('verbose', [message])
write_notification(message, 'info', timeNowTZ())
else:
message = f'[{pluginName}] Failed to send data for "{file_path}" (Status code: {response.status_code})'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
# get data from the nodes to the HUB
def get_data(api_token, node_url):
"""Get data from NODE, preferring /sync endpoint and falling back to PHP version."""
mylog('verbose', [f'[{pluginName}] Getting data from node: "{node_url}"'])
# Set the authorization header with the API token
headers = {'Authorization': f'Bearer {api_token}'}
api_endpoint = f"{node_url}/plugins/sync/hub.php"
response = requests.get(api_endpoint, headers=headers)
# mylog('verbose', [f'[{pluginName}] response: "{response.text}"'])
for endpoint in api_endpoints:
final_endpoint = node_url + endpoint
if response.status_code == 200:
try:
# Parse JSON response
response_json = response.json()
return response_json
response = requests.get(final_endpoint, headers=headers, timeout=5)
mylog('verbose', [f'[{pluginName}] Tried endpoint: {final_endpoint}, status: {response.status_code}'])
except json.JSONDecodeError:
message = f'[{pluginName}] Failed to parse JSON response from "{node_url}"'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
if response.status_code == 200:
try:
return response.json()
except json.JSONDecodeError:
message = f'[{pluginName}] Failed to parse JSON from {final_endpoint}'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
except requests.RequestException as e:
mylog('verbose', [f'[{pluginName}] Error calling {final_endpoint}: {e}'])
else:
message = f'[{pluginName}] Failed to send data for "{node_url}" (Status code: {response.status_code})'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""
# If all endpoints fail
message = f'[{pluginName}] Failed to get data from "{node_url}" via all endpoints'
mylog('verbose', [message])
write_notification(message, 'alert', timeNowTZ())
return ""

View File

@@ -7,13 +7,12 @@ Unifi import plugin using the Site Manager API.
### Quick setup guide
Navigate to your UniFi Site Manager _⚙️ Settings -> Control Plane -> Integrations_.
- `api_key` : You can generate your API key under the _Your API Keys_ section.
- `base_url` : You can find your base url in the _API Request Format_ section, e.g. `https://192.168.100.1/proxy/network/integration/`
- `version` : You can find your version as part of the url in the _API Request Format_ section, e.g. `v1`
- `skip_ssl` : To skip SSL with you don't have an SSL certificate
Navigate to your UniFi Site Manager _Settings -> Control Plane -> Integrations_.
- `UNIFIAPI_api_key` : You can generate your API key under the _Your API Keys_ section.
- `UNIFIAPI_base_url` : You can find your base url in the _API Request Format_ section, e.g. `https://192.168.100.1/proxy/network/integration/`
- `UNIFIAPI_api_version` : You can find your version as part of the url in the _API Request Format_ section, e.g. `v1`
- `UNIFIAPI_verify_ssl` : To skip SSL with you don't have an SSL certificate
### Usage

View File

@@ -208,9 +208,7 @@
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": [
"_in"
]
"sourceSuffixes": []
},
{
"separator": ""
@@ -223,11 +221,27 @@
},
{
"getStringKey": "Gen_Add"
}
],
"transformers": []
},
{
"elementType": "select",
"elementHasInputValue": 1,
"elementOptions": [
{
"multiple": "true"
},
{
"readonly": "true"
},
{
"editable": "true"
},
{
"popupForm": [
{
"function": "name",
"function": "UNIFIAPI_site_name",
"type": {
"dataType": "string",
"elements": [
@@ -237,14 +251,8 @@
{
"placeholder": "Enter value"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
@@ -271,7 +279,7 @@
]
},
{
"function": "base_url",
"function": "UNIFIAPI_base_url",
"type": {
"dataType": "string",
"elements": [
@@ -281,14 +289,8 @@
{
"placeholder": "https://host_ip/proxy/network/integration/"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
@@ -315,7 +317,7 @@
]
},
{
"function": "version",
"function": "UNIFIAPI_api_version",
"type": {
"dataType": "string",
"elements": [
@@ -325,14 +327,8 @@
{
"placeholder": "v1"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
@@ -359,7 +355,7 @@
]
},
{
"function": "api_key",
"function": "UNIFIAPI_api_key",
"type": {
"dataType": "string",
"elements": [
@@ -369,14 +365,8 @@
{
"placeholder": "Enter value"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
@@ -403,7 +393,7 @@
]
},
{
"function": "hide.site.verify_ssl",
"function": "UNIFIAPI_verify_ssl",
"type": {
"dataType": "boolean",
"elements": [
@@ -441,22 +431,6 @@
"transformers": []
}
],
"transformers": []
},
{
"elementType": "select",
"elementHasInputValue": 1,
"elementOptions": [
{
"multiple": "true"
},
{
"readonly": "true"
},
{
"editable": "true"
}
],
"transformers": [
"name|base64"
]
@@ -520,7 +494,7 @@
"description": [
{
"language_code": "en_us",
"string": "UniFi sites"
"string": "UniFi site configurations. Use a unique name for each site. You can find necessary details to configure this in your controller under <i>Settings -> Control Plane -> Integrations</i>."
}
]
}
@@ -599,24 +573,6 @@
},
{
"column": "Watched_Value2",
"mapped_to_column": "cur_Vendor",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Vendor"
}
]
},
{
"column": "Watched_Value3",
"mapped_to_column": "cur_Type",
"css_classes": "col-sm-2",
"show": true,
@@ -634,9 +590,9 @@
]
},
{
"column": "Watched_Value4",
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"show": true,
"type": "label",
"default_value": "",
"options": [],
@@ -646,7 +602,25 @@
"name": [
{
"language_code": "en_us",
"string": "N/A"
"string": "Connected"
}
]
},
{
"column": "Watched_Value4",
"mapped_to_column": "cur_NetworkNodeMAC",
"css_classes": "col-sm-2",
"show": true,
"type": "device_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Parent"
}
]
},
@@ -654,7 +628,7 @@
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "Example Plugin"
"value": "UNIFIAPI"
},
"css_classes": "col-sm-2",
"show": false,

View File

@@ -6,12 +6,13 @@ import sys
import json
import sqlite3
from pytz import timezone
from unifi_sm_api.api import SiteManagerAPI
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64, decode_settings_base64
from plugin_utils import get_plugins_configs
from logger import mylog, Logger
from const import pluginsPath, fullDbPath, logPath
@@ -26,7 +27,7 @@ conf.tz = timezone(get_setting_value('TIMEZONE'))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
pluginName = '<unique_prefix>'
pluginName = 'UNIFIAPI'
# Define the current path and log file paths
LOG_PATH = logPath + '/plugins'
@@ -37,88 +38,145 @@ RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
plugin_objects = Plugin_Objects(RESULT_FILE)
def main():
mylog('verbose', [f'[{pluginName}] In script'])
# Retrieve configuration settings
some_setting = get_setting_value('SYNC_plugins')
unifi_sites_configs = get_setting_value('UNIFIAPI_sites')
mylog('verbose', [f'[{pluginName}] some_setting value {some_setting}'])
mylog('verbose', [f'[{pluginName}] number of unifi_sites_configs: {len(unifi_sites_configs)}'])
for site_config in unifi_sites_configs:
# retrieve data
device_data = get_device_data(some_setting)
siteDict = decode_settings_base64(site_config)
# Process the data into native application tables
if len(device_data) > 0:
mylog('verbose', [f'[{pluginName}] siteDict: {json.dumps(siteDict)}'])
mylog('none', [f'[{pluginName}] Connecting to: {siteDict["UNIFIAPI_site_name"]}'])
# insert devices into the lats_result.log
# make sure the below mapping is mapped in config.json, for example:
#"database_column_definitions": [
# {
# "column": "Object_PrimaryID", <--------- the value I save into primaryId
# "mapped_to_column": "cur_MAC", <--------- gets inserted into the CurrentScan DB table column cur_MAC
#
for device in device_data:
plugin_objects.add_object(
primaryId = device['mac_address'],
secondaryId = device['ip_address'],
watched1 = device['hostname'],
watched2 = device['vendor'],
watched3 = device['device_type'],
watched4 = device['last_seen'],
extra = '',
foreignKey = device['mac_address']
# helpVal1 = "Something1", # Optional Helper values to be passed for mapping into the app
# helpVal2 = "Something1", # If you need to use even only 1, add the remaining ones too
# helpVal3 = "Something1", # and set them to 'null'. Check the the docs for details:
# helpVal4 = "Something1", # https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS_DEV.md
)
api = SiteManagerAPI(
api_key=siteDict["UNIFIAPI_api_key"],
version=siteDict["UNIFIAPI_api_version"],
base_url=siteDict["UNIFIAPI_base_url"],
verify_ssl=siteDict["UNIFIAPI_verify_ssl"]
)
mylog('verbose', [f'[{pluginName}] New entries: "{len(device_data)}"'])
sites_resp = api.get_sites()
sites = sites_resp.get("data", [])
# log result
plugin_objects.write_result_file()
for site in sites:
# retrieve data
device_data = get_device_data(site, api)
# Process the data into native application tables
if len(device_data) > 0:
# insert devices into the lats_result.log
for device in device_data:
plugin_objects.add_object(
primaryId = device['dev_mac'], # mac
secondaryId = device['dev_ip'], # IP
watched1 = device['dev_name'], # name
watched2 = device['dev_type'], # device_type (AP/Switch etc)
watched3 = device['dev_connected'], # connectedAt or empty
watched4 = device['dev_parent_mac'],# parent_mac or "Internet"
extra = '',
foreignKey = device['dev_mac']
)
mylog('verbose', [f'[{pluginName}] New entries: "{len(device_data)}"'])
# log result
plugin_objects.write_result_file()
return 0
# retrieve data
def get_device_data(some_setting):
def get_device_data(site, api):
device_data = []
# do some processing, call exteranl APIs, and return a device_data list
# ...
#
# Sample data for testing purposes, you can adjust the processing in main() as needed
# ... before adding it to the plugin_objects.add_object(...)
device_data = [
{
'device_id': 'device1',
'mac_address': '00:11:22:33:44:55',
'ip_address': '192.168.1.2',
'hostname': 'iPhone 12',
'vendor': 'Apple Inc.',
'device_type': 'Smartphone',
'last_seen': '2024-06-27 10:00:00',
'port': '1',
'network_id': 'network1'
},
{
'device_id': 'device2',
'mac_address': '00:11:22:33:44:66',
'ip_address': '192.168.1.3',
'hostname': 'Moto G82',
'vendor': 'Motorola Inc.',
'device_type': 'Laptop',
'last_seen': '2024-06-27 10:05:00',
'port': '',
'network_id': 'network1'
}
]
mylog('verbose', [f'[{pluginName}] Site: {site} '])
site_id = site["id"]
site_name = site.get("name", "Unnamed Site")
mylog('verbose', [f'[{pluginName}] Site: {site_name} ({site_id})'])
# --- Devices ---
unifi_devices_resp = api.get_unifi_devices(site_id)
unifi_devices = unifi_devices_resp.get("data", [])
mylog('verbose', [f'[{pluginName}] Site: {site_name} unifi devices: {json.dumps(unifi_devices_resp, indent=2)}'])
# --- Clients ---
clients_resp = api.get_clients(site_id)
clients = clients_resp.get("data", [])
mylog('verbose', [f'[{pluginName}] Site: {site_name} clients: {json.dumps(clients_resp, indent=2)}'])
# Build a lookup for devices by their 'id' to find parent MAC easily
device_id_to_mac = {dev['id']: dev.get('macAddress', '') for dev in unifi_devices}
# Helper to resolve uplinkDeviceId to parent MAC, or "Internet" if no uplink
def resolve_parent_mac(uplink_id):
if not uplink_id:
return "Internet"
return device_id_to_mac.get(uplink_id, "Unknown")
# Process Unifi devices
for device in unifi_devices:
dev_mac = device.get('macAddress', '')
dev_ip = device.get('ipAddress', '')
dev_name = device.get('name', '')
# Determine device_type based on features and type
# If device has "accessPoint" feature => type "AP"
# Else if "switching" feature => type "Switch"
# fallback to "Unknown"
features = device.get('features', [])
if 'accessPoint' in features:
device_type = 'AP'
elif 'switching' in features:
device_type = 'Switch'
else:
device_type = 'Unknown'
dev_type = device_type
# No connectedAt for devices, so empty
dev_connected = ''
uplinkDeviceId = device.get('uplinkDeviceId', '')
dev_parent_mac = resolve_parent_mac(uplinkDeviceId)
device_data.append({
"dev_mac": dev_mac,
"dev_ip": dev_ip,
"dev_name": dev_name,
"dev_type": dev_type,
"dev_connected": dev_connected,
"dev_parent_mac": dev_parent_mac
})
# Process Clients (child devices connected to APs or switches)
for client in clients:
dev_mac = client.get('macAddress', '')
dev_ip = client.get('ipAddress', '')
dev_name = client.get('name', '')
device_type = ""
dev_type = device_type
dev_connected = client.get('connectedAt', '')
uplinkDeviceId = client.get('uplinkDeviceId', '')
dev_parent_mac = resolve_parent_mac(uplinkDeviceId)
device_data.append({
"dev_mac": dev_mac,
"dev_ip": dev_ip,
"dev_name": dev_name,
"dev_type": dev_type,
"dev_connected": dev_connected,
"dev_parent_mac": dev_parent_mac
})
# Return the data to be detected by the main application
return device_data
if __name__ == '__main__':
main()

View File

@@ -86,7 +86,7 @@
// Function to update the displayed data and timestamp based on the selected format and index
function updateData(format, index) {
// Fetch data from the API endpoint
fetch(`/php/server/query_json.php?file=table_notifications.json&nocache=${Date.now()}`)
fetch(`php/server/query_json.php?file=table_notifications.json&nocache=${Date.now()}`)
.then(response => response.json())
.then(data => {
if (index < 0) {

View File

@@ -553,7 +553,7 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
}, 1500);
} else {
var settingsArray = [];
let settingsArray = [];
// collect values for each of the different input form controls
// get settings to determine setting type to store values appropriately
@@ -565,121 +565,123 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
setType = set["setType"]
setCodeName = set["setKey"]
// console.log(prefix);
settingsArray = collectSetting(prefix, setCodeName, setType, settingsArray)
const setTypeObject = JSON.parse(processQuotes(setType))
// console.log(setTypeObject);
// // console.log(prefix);
const dataType = setTypeObject.dataType;
// const setTypeObject = JSON.parse(processQuotes(setType))
// // console.log(setTypeObject);
// get the element with the input value(s)
let elements = setTypeObject.elements.filter(element => element.elementHasInputValue === 1);
// const dataType = setTypeObject.dataType;
// if none found, take last
if(elements.length == 0)
{
elementWithInputValue = setTypeObject.elements[setTypeObject.elements.length - 1]
} else
{
elementWithInputValue = elements[0]
}
// // get the element with the input value(s)
// let elements = setTypeObject.elements.filter(element => element.elementHasInputValue === 1);
const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
const {
inputType,
readOnly,
isMultiSelect,
isOrdeable,
cssClasses,
placeholder,
suffix,
sourceIds,
separator,
editable,
valRes,
getStringKey,
onClick,
onChange,
customParams,
customId,
columns,
base64Regex,
elementOptionsBase64
} = handleElementOptions('none', elementOptions, transformers, val = "");
// // if none found, take last
// if(elements.length == 0)
// {
// elementWithInputValue = setTypeObject.elements[setTypeObject.elements.length - 1]
// } else
// {
// elementWithInputValue = elements[0]
// }
let value;
// const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
// const {
// inputType,
// readOnly,
// isMultiSelect,
// isOrdeable,
// cssClasses,
// placeholder,
// suffix,
// sourceIds,
// separator,
// editable,
// valRes,
// getStringKey,
// onClick,
// onChange,
// customParams,
// customId,
// columns,
// base64Regex,
// elementOptionsBase64
// } = handleElementOptions('none', elementOptions, transformers, val = "");
if (dataType === "string" && elementWithInputValue.elementType === "datatable" ) {
// let value;
value = collectTableData(`#${setCodeName}_table`)
settingsArray.push([prefix, setCodeName, dataType, btoa(JSON.stringify(value))]);
// if (dataType === "string" && elementWithInputValue.elementType === "datatable" ) {
} else if (dataType === "string" ||
(dataType === "integer" && (inputType === "number" || inputType === "text"))) {
// value = collectTableData(`#${setCodeName}_table`)
// settingsArray.push([prefix, setCodeName, dataType, btoa(JSON.stringify(value))]);
// } else if (dataType === "string" ||
// (dataType === "integer" && (inputType === "number" || inputType === "text"))) {
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
settingsArray.push([prefix, setCodeName, dataType, value]);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (inputType === 'checkbox') {
// } else if (inputType === 'checkbox') {
value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
// value = $(`#${setCodeName}`).is(':checked') ? 1 : 0;
if(dataType === "boolean")
{
value = value == 1 ? "True" : "False";
}
// if(dataType === "boolean")
// {
// value = value == 1 ? "True" : "False";
// }
value = applyTransformers(value, transformers);
settingsArray.push([prefix, setCodeName, dataType, value]);
// value = applyTransformers(value, transformers);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "array" ) {
// } else if (dataType === "array" ) {
let temps = [];
// let temps = [];
if(isOrdeable)
{
temps = $(`#${setCodeName}`).val()
} else
{
// make sure to collect all if set as "editable" or selected only otherwise
$(`#${setCodeName}`).attr("my-editable") == "true" ? additionalSelector = "" : additionalSelector = ":selected";
// if(isOrdeable)
// {
// temps = $(`#${setCodeName}`).val()
// } else
// {
// // make sure to collect all if set as "editable" or selected only otherwise
// $(`#${setCodeName}`).attr("my-editable") == "true" ? additionalSelector = "" : additionalSelector = ":selected";
$(`#${setCodeName} option${additionalSelector}`).each(function() {
const vl = $(this).val();
if (vl !== '') {
temps.push(applyTransformers(vl, transformers));
}
});
}
// $(`#${setCodeName} option${additionalSelector}`).each(function() {
// const vl = $(this).val();
// if (vl !== '') {
// temps.push(applyTransformers(vl, transformers));
// }
// });
// }
value = JSON.stringify(temps);
// value = JSON.stringify(temps);
settingsArray.push([prefix, setCodeName, dataType, value]);
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "none") {
// no value to save
value = ""
settingsArray.push([prefix, setCodeName, dataType, value]);
// } else if (dataType === "none") {
// // no value to save
// value = ""
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else if (dataType === "json") {
// } else if (dataType === "json") {
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
value = JSON.stringify(value, null, 2)
settingsArray.push([prefix, setCodeName, dataType, value]);
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
// value = JSON.stringify(value, null, 2)
// settingsArray.push([prefix, setCodeName, dataType, value]);
} else {
// } else {
console.error(`[saveSettings] Couldn't determine how to handle (setCodeName|dataType|inputType):(${setCodeName}|${dataType}|${inputType})`);
// console.error(`[saveSettings] Couldn't determine how to handle (setCodeName|dataType|inputType):(${setCodeName}|${dataType}|${inputType})`);
value = $('#' + setCodeName).val();
value = applyTransformers(value, transformers);
console.error(`[saveSettings] Saving value "${value}"`);
settingsArray.push([prefix, setCodeName, dataType, value]);
}
// value = $('#' + setCodeName).val();
// value = applyTransformers(value, transformers);
// console.error(`[saveSettings] Saving value "${value}"`);
// settingsArray.push([prefix, setCodeName, dataType, value]);
// }
});
// sanity check to make sure settings were loaded & collected correctly

View File

@@ -1,9 +1,14 @@
import threading
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
from .graphql_schema import devicesSchema
from .prometheus_metrics import getMetricStats
from graphene import Schema
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 .history_endpoint import delete_online_history
from .prometheus_endpoint import getMetricStats
from .nettools_endpoint import wakeonlan
from .sync_endpoint import handle_sync_post, handle_sync_get
import sys
# Register NetAlertX directories
@@ -17,7 +22,19 @@ from messaging.in_app import write_notification
# Flask application
app = Flask(__name__)
CORS(app, resources={r"/metrics": {"origins": "*"}}, supports_credentials=True, allow_headers=["Authorization"])
CORS(
app,
resources={
r"/metrics": {"origins": "*"},
r"/device/*": {"origins": "*"},
r"/devices/*": {"origins": "*"},
r"/history/*": {"origins": "*"},
r"/nettools/*": {"origins": "*"},
r"/events/*": {"origins": "*"}
},
supports_credentials=True,
allow_headers=["Authorization", "Content-Type"]
)
# --------------------------
# GraphQL Endpoints
@@ -49,29 +66,226 @@ def graphql_endpoint():
return jsonify(result.data)
# --------------------------
# Prometheus /metrics Endpoint
# Device Endpoints
# --------------------------
@app.route("/device/<mac>", methods=["GET"])
def api_get_device(mac):
if not is_authorized():
return jsonify({"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 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 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 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 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
# --------------------------
@app.route("/devices", methods=["GET"])
def api_get_devices():
if not is_authorized():
return jsonify({"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
macs = request.json.get("macs") if request.is_json else None
return delete_devices(macs)
@app.route("/devices/empty-macs", methods=["DELETE"])
def api_delete_all_empty_macs():
if not is_authorized():
return jsonify({"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 delete_unknown_devices()
@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"))
@app.route("/devices/totals", methods=["GET"])
def api_devices_totals():
if not is_authorized():
return jsonify({"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
status = request.args.get("status", "") if request.args else None
return devices_by_status(status)
# --------------------------
# Net tools
# --------------------------
@app.route("/nettools/wakeonlan", methods=["POST"])
def api_wakeonlan():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.json.get("devMac")
return wakeonlan(mac)
# --------------------------
# Online history
# --------------------------
@app.route("/history", methods=["DELETE"])
def api_delete_online_history():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_online_history()
# --------------------------
# Device Events
# --------------------------
@app.route("/events/<mac>", methods=["DELETE"])
def api_events_by_mac(mac):
if not is_authorized():
return jsonify({"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 delete_events()
@app.route("/events", methods=["GET"])
def api_get_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
mac = request.json.get("mac") if request.is_json else None
return get_events(mac)
@app.route("/events/30days", methods=["DELETE"])
def api_delete_old_events():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
return delete_events_30()
# --------------------------
# Prometheus metrics endpoint
# --------------------------
@app.route("/metrics")
def metrics():
# Check for API token in headers
if not is_authorized():
msg = '[metrics] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
mylog('verbose', [msg])
return jsonify({"error": msg}), 401
return jsonify({"error": "Forbidden"}), 403
# Return Prometheus metrics as plain text
return Response(getMetricStats(), mimetype="text/plain")
# --------------------------
# SYNC endpoint
# --------------------------
@app.route("/sync", methods=["GET", "POST"])
def sync_endpoint():
if not is_authorized():
return jsonify({"error": "Forbidden"}), 403
if request.method == "GET":
return handle_sync_get()
elif request.method == "POST":
return handle_sync_post()
else:
msg = "[sync endpoint] Method Not Allowed"
write_notification(msg, "alert")
mylog("verbose", [msg])
return jsonify({"error": "Method Not Allowed"}), 405
# --------------------------
# Background Server Start
# --------------------------
def is_authorized():
token = request.headers.get("Authorization")
return token == f"Bearer {get_setting_value('API_TOKEN')}"
is_authorized = token == f"Bearer {get_setting_value('API_TOKEN')}"
if not is_authorized:
msg = f"[api] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct."
write_notification(msg, "alert")
mylog("verbose", [msg])
return is_authorized
def start_server(graphql_port, app_state):
@@ -79,7 +293,7 @@ def start_server(graphql_port, app_state):
if app_state.graphQLServerStarted == 0:
mylog('verbose', [f'[graphql_server] Starting on port: {graphql_port}'])
mylog('verbose', [f'[graphql endpoint] Starting on port: {graphql_port}'])
# Start Flask app in a separate thread
thread = threading.Thread(

View File

@@ -0,0 +1,336 @@
#!/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
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
# Device Endpoints Functions
# --------------------------
def get_device_data(mac):
"""Fetch device info with children, event stats, and presence calculation."""
# Open temporary connection for this request
conn = get_temp_db_connection()
cur = conn.cursor()
# Special case for new device
if mac.lower() == "new":
now = datetime.now().strftime("%Y-%m-%d %H:%M")
device_data = {
"devMac": "",
"devName": "",
"devOwner": "",
"devType": "",
"devVendor": "",
"devFavorite": 0,
"devGroup": "",
"devComments": "",
"devFirstConnection": now,
"devLastConnection": now,
"devLastIP": "",
"devStaticIP": 0,
"devScan": 0,
"devLogEvents": 0,
"devAlertEvents": 0,
"devAlertDown": 0,
"devParentRelType": "default",
"devReqNicsOnline": 0,
"devSkipRepeated": 0,
"devLastNotification": "",
"devPresentLastScan": 0,
"devIsNew": 1,
"devLocation": "",
"devIsArchived": 0,
"devParentMAC": "",
"devParentPort": "",
"devIcon": "",
"devGUID": "",
"devSite": "",
"devSSID": "",
"devSyncHubNode": "",
"devSourcePlugin": "",
"devCustomProps": "",
"devStatus": "Unknown",
"devIsRandomMAC": False,
"devSessions": 0,
"devEvents": 0,
"devDownAlerts": 0,
"devPresenceHours": 0,
"devFQDN": ""
}
return jsonify(device_data)
# 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"""
SELECT
d.*,
CASE
WHEN d.devAlertDown != 0 AND d.devPresentLastScan = 0 THEN 'Down'
WHEN d.devPresentLastScan = 1 THEN 'On-line'
ELSE 'Off-line'
END AS devStatus,
(SELECT COUNT(*) FROM Sessions
WHERE ses_MAC = d.devMac AND (
ses_DateTimeConnection >= {period_date_sql} OR
ses_DateTimeDisconnection >= {period_date_sql} OR
ses_StillConnected = 1
)) AS devSessions,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
AND eve_EventType NOT IN ('Connected','Disconnected')) AS devEvents,
(SELECT COUNT(*) FROM Events
WHERE eve_MAC = d.devMac AND eve_DateTime >= {period_date_sql}
AND eve_EventType = 'Device Down') AS devDownAlerts,
(SELECT CAST(MAX(0, SUM(
julianday(IFNULL(ses_DateTimeDisconnection,'{current_date}')) -
julianday(CASE WHEN ses_DateTimeConnection < {period_date_sql}
THEN {period_date_sql} ELSE ses_DateTimeConnection END)
) * 24) AS INT)
FROM Sessions
WHERE ses_MAC = d.devMac
AND ses_DateTimeConnection IS NOT NULL
AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1)
AND (ses_DateTimeConnection >= {period_date_sql}
OR ses_DateTimeDisconnection >= {period_date_sql} OR ses_StillConnected = 1)
) AS devPresenceHours
FROM Devices d
WHERE d.devMac = ? OR CAST(d.rowid AS TEXT) = ?
"""
# Fetch device
cur.execute(sql, (mac, mac))
row = cur.fetchone()
if not row:
return jsonify({"error": "Device not found"}), 404
device_data = row_to_json(list(row.keys()), row)
device_data['devFirstConnection'] = format_date(device_data['devFirstConnection'])
device_data['devLastConnection'] = format_date(device_data['devLastConnection'])
device_data['devIsRandomMAC'] = is_random_mac(device_data['devMac'])
# Fetch children
cur.execute("SELECT * FROM Devices WHERE devParentMAC = ? ORDER BY devPresentLastScan DESC", ( device_data['devMac'],))
children_rows = cur.fetchall()
children = [row_to_json(list(r.keys()), r) for r in children_rows]
children_nics = [c for c in children if c.get("devParentRelType") == "nic"]
device_data['devChildrenDynamic'] = children
device_data['devChildrenNicsDynamic'] = children_nics
conn.close()
return jsonify(device_data)
def set_device_data(mac, data):
"""Update or create a device."""
if data.get("createNew", False):
sql = """
INSERT INTO Devices (
devMac, devName, devOwner, devType, devVendor, devIcon,
devFavorite, devGroup, devLocation, devComments,
devParentMAC, devParentPort, devSSID, devSite,
devStaticIP, devScan, devAlertEvents, devAlertDown,
devParentRelType, devReqNicsOnline, devSkipRepeated,
devIsNew, devIsArchived, devLastConnection,
devFirstConnection, devLastIP, devGUID, devCustomProps,
devSourcePlugin
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
values = (
mac,
data.get("devName", ""),
data.get("devOwner", ""),
data.get("devType", ""),
data.get("devVendor", ""),
data.get("devIcon", ""),
data.get("devFavorite", 0),
data.get("devGroup", ""),
data.get("devLocation", ""),
data.get("devComments", ""),
data.get("devParentMAC", ""),
data.get("devParentPort", ""),
data.get("devSSID", ""),
data.get("devSite", ""),
data.get("devStaticIP", 0),
data.get("devScan", 0),
data.get("devAlertEvents", 0),
data.get("devAlertDown", 0),
data.get("devParentRelType", "default"),
data.get("devReqNicsOnline", 0),
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("devLastIP", ""),
data.get("devGUID", ""),
data.get("devCustomProps", ""),
data.get("devSourcePlugin", "DUMMY"),
)
else:
sql = """
UPDATE Devices SET
devName=?, devOwner=?, devType=?, devVendor=?, devIcon=?,
devFavorite=?, devGroup=?, devLocation=?, devComments=?,
devParentMAC=?, devParentPort=?, devSSID=?, devSite=?,
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?,
devParentRelType=?, devReqNicsOnline=?, devSkipRepeated=?,
devIsNew=?, devIsArchived=?, devCustomProps=?
WHERE devMac=?
"""
values = (
data.get("devName", ""),
data.get("devOwner", ""),
data.get("devType", ""),
data.get("devVendor", ""),
data.get("devIcon", ""),
data.get("devFavorite", 0),
data.get("devGroup", ""),
data.get("devLocation", ""),
data.get("devComments", ""),
data.get("devParentMAC", ""),
data.get("devParentPort", ""),
data.get("devSSID", ""),
data.get("devSite", ""),
data.get("devStaticIP", 0),
data.get("devScan", 0),
data.get("devAlertEvents", 0),
data.get("devAlertDown", 0),
data.get("devParentRelType", "default"),
data.get("devReqNicsOnline", 0),
data.get("devSkipRepeated", 0),
data.get("devIsNew", 0),
data.get("devIsArchived", 0),
data.get("devCustomProps", ""),
mac
)
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(sql, values)
conn.commit()
conn.close()
return jsonify({"success": True})
def delete_device(mac):
"""Delete a device by MAC."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Devices WHERE devMac=?", (mac,))
conn.commit()
conn.close()
return jsonify({"success": True})
def delete_device_events(mac):
"""Delete all events for a device."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Events WHERE eve_MAC=?", (mac,))
conn.commit()
conn.close()
return jsonify({"success": True})
def reset_device_props(mac, data=None):
"""Reset device custom properties to default."""
default_props = get_setting_value("NEWDEV_devCustomProps")
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(
"UPDATE Devices SET devCustomProps=? WHERE devMac=?",
(default_props, mac),
)
conn.commit()
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()

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python
import json
import subprocess
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
from helper import is_random_mac, format_date, get_setting_value
from db.db_helper import get_table_json, get_device_condition_by_status
# --------------------------
# Device Endpoints Functions
# --------------------------
def get_all_devices():
"""Retrieve all devices from the database."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("SELECT * FROM Devices")
rows = cur.fetchall()
# Convert rows to list of dicts using column names
columns = [col[0] for col in cur.description]
devices = [dict(zip(columns, row)) for row in rows]
conn.close()
return jsonify({"success": True, "devices": devices})
def delete_devices(macs):
"""
Delete devices from the Devices table.
- 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()
if not macs:
# No MACs provided → delete all
cur.execute("DELETE FROM Devices")
conn.commit()
conn.close()
return jsonify({"success": True, "deleted": "all"})
deleted_count = 0
for mac in macs:
if "*" in mac:
# Wildcard matching
sql_pattern = mac.replace("*", "%")
cur.execute("DELETE FROM Devices WHERE devMAC LIKE ?", (sql_pattern,))
else:
# Exact match
cur.execute("DELETE FROM Devices WHERE devMAC = ?", (mac,))
deleted_count += cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted_count": deleted_count})
def delete_all_with_empty_macs():
"""Delete devices with empty MAC addresses."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM Devices WHERE devMAC IS NULL OR devMAC = ''")
deleted = cur.rowcount
conn.commit()
conn.close()
return jsonify({"success": True, "deleted": deleted})
def delete_unknown_devices():
"""Delete devices marked as unknown."""
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute("""DELETE FROM Devices WHERE devName='(unknown)' OR devName='(name not found)'""")
conn.commit()
conn.close()
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
})
def devices_totals():
conn = get_temp_db_connection()
sql = conn.cursor()
# Build a combined query with sub-selects for each status
query = f"""
SELECT
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('my')}) AS devices,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('connected')}) AS connected,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('favorites')}) AS favorites,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('new')}) AS new,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('down')}) AS down,
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status('archived')}) AS archived
"""
sql.execute(query)
row = sql.fetchone() # returns a tuple like (devices, connected, favorites, new, down, archived)
conn.close()
# Return counts as JSON array
return jsonify(list(row))
def devices_by_status(status=None):
"""
Return devices filtered by status.
"""
conn = get_temp_db_connection()
sql = conn.cursor()
# Build condition for SQL
condition = get_device_condition_by_status(status) if status else ""
query = f"SELECT * FROM Devices {condition}"
sql.execute(query)
table_data = []
for row in sql.fetchall():
r = dict(row) # Convert sqlite3.Row to dict for .get()
dev_name = r.get("devName", "")
if r.get("devFavorite") == 1:
dev_name = f'<span class="text-yellow">&#9733</span>&nbsp;{dev_name}'
table_data.append({
"id": r.get("devMac", ""),
"title": dev_name,
"favorite": r.get("devFavorite", 0)
})
conn.close()
return jsonify(table_data)

View File

@@ -0,0 +1,72 @@
#!/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
from db.db_helper import row_to_json
# --------------------------
# Events Endpoints Functions
# --------------------------
def get_events(mac=None):
"""
Fetch all events, or events for a specific MAC if provided.
Returns JSON list of events.
"""
conn = get_temp_db_connection()
cur = conn.cursor()
if mac:
sql = "SELECT * FROM Events WHERE eve_MAC=? ORDER BY eve_DateTime DESC"
cur.execute(sql, (mac,))
else:
sql = "SELECT * FROM Events ORDER BY eve_DateTime DESC"
cur.execute(sql)
rows = cur.fetchall()
events = [row_to_json(list(r.keys()), r) for r in rows]
conn.close()
return jsonify({"success": True, "events": events})
def delete_events_30():
"""Delete all events older than 30 days"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', '-30 day')"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted events older than 30 days"})
def delete_events():
"""Delete all events"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Events"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted all events"})

View File

@@ -0,0 +1,35 @@
#!/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
# --------------------------------------------------
# Online History Activity Endpoints Functions
# --------------------------------------------------
def delete_online_history():
"""Delete all online history activity"""
conn = get_temp_db_connection()
cur = conn.cursor()
sql = "DELETE FROM Online_History"
cur.execute(sql)
conn.commit()
conn.close()
return jsonify({"success": True, "message": "Deleted online history"})

View File

@@ -0,0 +1,21 @@
import subprocess
import re
from flask import jsonify
def wakeonlan(mac):
# Validate MAC
if not re.match(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', mac):
return jsonify({"success": False, "error": f"Invalid MAC: {mac}"}), 400
try:
result = subprocess.run(
["wakeonlan", mac],
capture_output=True,
text=True,
check=True
)
return jsonify({"success": True, "message": "WOL packet sent", "output": result.stdout.strip()})
except subprocess.CalledProcessError as e:
return jsonify({"success": False, "error": "Failed to send WOL packet", "details": e.stderr.strip()}), 500

View File

@@ -0,0 +1,71 @@
import os
import base64
from flask import jsonify, request
from logger import mylog
from helper import get_setting_value, timeNowTZ
from messaging.in_app import write_notification
INSTALL_PATH = "/app"
def handle_sync_get():
"""Handle GET requests for SYNC (NODE → HUB)."""
file_path = INSTALL_PATH + "/api/table_devices.json"
try:
with open(file_path, "rb") as f:
raw_data = f.read()
except FileNotFoundError:
msg = f"[Plugin: SYNC] Data file not found: {file_path}"
write_notification(msg, "alert", timeNowTZ())
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
def handle_sync_post():
"""Handle POST requests for SYNC (HUB receiving from NODE)."""
data = request.form.get("data", "")
node_name = request.form.get("node_name", "")
plugin = request.form.get("plugin", "")
storage_path = INSTALL_PATH + "/log/plugins"
os.makedirs(storage_path, exist_ok=True)
encoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.encoded.{node_name}")
]
decoded_files = [
f for f in os.listdir(storage_path)
if f.startswith(f"last_result.{plugin}.decoded.{node_name}")
]
file_count = len(encoded_files + decoded_files) + 1
file_path_new = os.path.join(
storage_path,
f"last_result.{plugin}.encoded.{node_name}.{file_count}.log"
)
try:
with open(file_path_new, "w") as f:
f.write(data)
except Exception as e:
msg = f"[Plugin: SYNC] Failed to store data: {e}"
write_notification(msg, "alert", timeNowTZ())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowTZ())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200

View File

@@ -8,7 +8,8 @@ import json
from const import fullDbPath, sql_devices_stats, sql_devices_all, sql_generateGuid
from logger import mylog
from helper import json_obj, initOrSetParam, row_to_json, timeNowTZ
from helper import timeNowTZ
from db.db_helper import row_to_json, get_table_json, json_obj
from workflows.app_events import AppEvent_obj
from db.db_upgrade import ensure_column, ensure_views, ensure_CurrentScan, ensure_plugins_tables, ensure_Parameters, ensure_Settings, ensure_Indexes
@@ -121,26 +122,41 @@ class DB():
AppEvent_obj(self)
#-------------------------------------------------------------------------------
# #-------------------------------------------------------------------------------
# def get_table_as_json(self, sqlQuery):
# # mylog('debug',[ '[Database] - get_table_as_json - Query: ', sqlQuery])
# try:
# self.sql.execute(sqlQuery)
# columnNames = list(map(lambda x: x[0], self.sql.description))
# rows = self.sql.fetchall()
# except sqlite3.Error as e:
# mylog('verbose',[ '[Database] - SQL ERROR: ', e])
# return json_obj({}, []) # return empty object
# result = {"data":[]}
# for row in rows:
# tmp = row_to_json(columnNames, row)
# result["data"].append(tmp)
# # mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames])
# # mylog('debug',[ '[Database] - get_table_as_json - returning json ', json.dumps(result) ])
# return json_obj(result, columnNames)
def get_table_as_json(self, sqlQuery):
# mylog('debug',[ '[Database] - get_table_as_json - Query: ', sqlQuery])
"""
Wrapper to use the central get_table_as_json helper.
"""
try:
self.sql.execute(sqlQuery)
columnNames = list(map(lambda x: x[0], self.sql.description))
rows = self.sql.fetchall()
except sqlite3.Error as e:
mylog('verbose',[ '[Database] - SQL ERROR: ', e])
return json_obj({}, []) # return empty object
result = {"data":[]}
for row in rows:
tmp = row_to_json(columnNames, row)
result["data"].append(tmp)
result = get_table_json(self.sql, sqlQuery)
except Exception as e:
mylog('verbose', ['[Database] - get_table_as_json ERROR:', e])
return json_obj({}, []) # return empty object on failure
# mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames])
# mylog('debug',[ '[Database] - get_table_as_json - returning json ', json.dumps(result) ])
return json_obj(result, columnNames)
return result
#-------------------------------------------------------------------------------
# referece from here: https://codereview.stackexchange.com/questions/241043/interface-class-for-sqlite-databases
@@ -204,3 +220,13 @@ def get_array_from_sql_rows(rows):
#-------------------------------------------------------------------------------
def get_temp_db_connection():
"""
Returns a new SQLite connection with Row factory.
Should be used per-thread/request to avoid cross-thread issues.
"""
conn = sqlite3.connect(fullDbPath, timeout=5, isolation_level=None)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA busy_timeout=5000;") # 5s wait before giving up
conn.row_factory = sqlite3.Row
return conn

269
server/db/db_helper.py Executable file
View File

@@ -0,0 +1,269 @@
import sys
import sqlite3
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import if_byte_then_to_str
from logger import mylog
#-------------------------------------------------------------------------------
# Return the SQL WHERE clause for filtering devices based on their status.
def get_device_condition_by_status(device_status):
"""
Return the SQL WHERE clause for filtering devices based on their status.
Parameters:
device_status (str): The status of the device. Possible values:
- 'all' : All active devices
- 'my' : Same as 'all' (active devices)
- 'connected' : Devices that are active and present in the last scan
- 'favorites' : Devices marked as favorite
- 'new' : Devices marked as new
- 'down' : Devices not present in the last scan but with alerts
- 'archived' : Devices that are archived
Returns:
str: SQL WHERE clause corresponding to the device status.
Defaults to 'WHERE 1=0' for unrecognized statuses.
"""
conditions = {
'all': 'WHERE devIsArchived=0',
'my': 'WHERE devIsArchived=0',
'connected': 'WHERE devIsArchived=0 AND devPresentLastScan=1',
'favorites': 'WHERE devIsArchived=0 AND devFavorite=1',
'new': 'WHERE devIsArchived=0 AND devIsNew=1',
'down': 'WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0',
'archived': 'WHERE devIsArchived=1'
}
return conditions.get(device_status, 'WHERE 1=0')
#-------------------------------------------------------------------------------
# Creates a JSON-like dictionary from a database row
def row_to_json(names, row):
"""
Convert a database row into a JSON-like dictionary.
Parameters:
names (list of str): List of column names corresponding to the row fields.
row (dict or sequence): A database row, typically a dictionary or list-like object,
where each column can be accessed by index or key.
Returns:
dict: A dictionary where keys are column names and values are the corresponding
row values. Byte values are automatically converted to strings using
`if_byte_then_to_str`.
Example:
names = ['id', 'name', 'data']
row = {0: 1, 1: b'Example', 2: b'\x01\x02'}
row_to_json(names, row)
# Returns: {'id': 1, 'name': 'Example', 'data': '\\x01\\x02'}
"""
rowEntry = {}
for index, name in enumerate(names):
rowEntry[name] = if_byte_then_to_str(row[name])
return rowEntry
#-------------------------------------------------------------------------------
def sanitize_SQL_input(val):
"""
Sanitize a value for use in SQL queries by replacing single quotes in strings.
Parameters:
val (any): The value to sanitize.
Returns:
str or any:
- Returns an empty string if val is None.
- Returns a string with single quotes replaced by underscores if val is a string.
- Returns val unchanged if it is any other type.
"""
if val is None:
return ''
if isinstance(val, str):
return val.replace("'", "_")
return val # Return non-string values as they are
# -------------------------------------------------------------------------------------------
def get_date_from_period(period):
"""
Convert a period string into an SQLite date expression.
Parameters:
period (str): The requested period (e.g., '7 days', '1 month', '1 year', '100 years').
Returns:
str: An SQLite date expression like "date('now', '-7 day')" corresponding to the period.
"""
days_map = {
'7 days': 7,
'1 month': 30,
'1 year': 365,
'100 years': 3650, # actually 10 years in original PHP
}
days = days_map.get(period, 1) # default 1 day
period_sql = f"date('now', '-{days} day')"
return period_sql
#-------------------------------------------------------------------------------
def print_table_schema(db, table):
"""
Print the schema of a database table to the log.
Parameters:
db: A database connection object with a `sql` cursor.
table (str): The name of the table whose schema is to be printed.
Returns:
None: Logs the column information including cid, name, type, notnull, default value, and primary key.
"""
sql = db.sql
sql.execute(f"PRAGMA table_info({table})")
result = sql.fetchall()
if not result:
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
return
mylog('debug', f'[Schema] Structure for table: {table}')
header = f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
mylog('debug', header)
mylog('debug', '-' * len(header))
for row in result:
# row = (cid, name, type, notnull, dflt_value, pk)
line = f"{row[0]:<4} {row[1]:<20} {row[2]:<10} {row[3]:<8} {str(row[4]):<10} {row[5]:<2}"
mylog('debug', line)
#-------------------------------------------------------------------------------
# Generate a WHERE condition for SQLite based on a list of values.
def list_to_where(logical_operator, column_name, condition_operator, values_list):
"""
Generate a WHERE condition for SQLite based on a list of values.
Parameters:
- logical_operator: The logical operator ('AND' or 'OR') to combine conditions.
- column_name: The name of the column to filter on.
- condition_operator: The condition operator ('LIKE', 'NOT LIKE', '=', '!=', etc.).
- values_list: A list of values to be included in the condition.
Returns:
- A string representing the WHERE condition.
"""
# If the list is empty, return an empty string
if not values_list:
return ""
# Replace {s-quote} with single quote in values_list
values_list = [value.replace("{s-quote}", "'") for value in values_list]
# Build the WHERE condition for the first value
condition = f"{column_name} {condition_operator} '{values_list[0]}'"
# Add the rest of the values using the logical operator
for value in values_list[1:]:
condition += f" {logical_operator} {column_name} {condition_operator} '{value}'"
return f'({condition})'
#-------------------------------------------------------------------------------
def get_table_json(sql, sql_query):
"""
Execute a SQL query and return the results as JSON-like dict.
Args:
sql: SQLite cursor or connection wrapper supporting execute(), description, and fetchall().
sql_query (str): The SQL query to execute.
Returns:
dict: JSON-style object with data and column names.
"""
try:
sql.execute(sql_query)
column_names = [col[0] for col in sql.description]
rows = sql.fetchall()
except sqlite3.Error as e:
mylog('verbose', ['[Database] - SQL ERROR: ', e])
return json_obj({}, []) # return empty object
result = {"data": [row_to_json(column_names, row) for row in rows]}
return json_obj(result, column_names)
#-------------------------------------------------------------------------------
class json_obj:
"""
A wrapper class for JSON-style objects returned from database queries.
Provides dict-like access to the JSON data while storing column metadata.
Attributes:
json (dict): The actual JSON-style data returned from the database.
columnNames (list): List of column names corresponding to the data.
"""
def __init__(self, jsn, columnNames):
"""
Initialize the json_obj with JSON data and column names.
Args:
jsn (dict): JSON-style dictionary containing the data.
columnNames (list): List of column names for the data.
"""
self.json = jsn
self.columnNames = columnNames
def get(self, key, default=None):
"""
Dict-like .get() access to the JSON data.
Args:
key (str): Key to retrieve from the JSON data.
default: Value to return if key is not found (default: None).
Returns:
Value corresponding to key in the JSON data, or default if not present.
"""
return self.json.get(key, default)
def keys(self):
"""
Return the keys of the JSON data.
Returns:
Iterable of keys in the JSON dictionary.
"""
return self.json.keys()
def items(self):
"""
Return the items of the JSON data.
Returns:
Iterable of (key, value) pairs in the JSON dictionary.
"""
return self.json.items()
def __getitem__(self, key):
"""
Allow bracket-access (obj[key]) to the JSON data.
Args:
key (str): Key to retrieve from the JSON data.
Returns:
Value corresponding to the key.
"""
return self.json[key]

View File

@@ -230,8 +230,7 @@ def ensure_CurrentScan(sql) -> bool:
cur_SSID STRING(250),
cur_NetworkNodeMAC STRING(250),
cur_PORT STRING(250),
cur_Type STRING(250),
UNIQUE(cur_MAC)
cur_Type STRING(250)
);
""")

View File

@@ -18,7 +18,6 @@ import hashlib
import random
import string
import ipaddress
import dns.resolver
import conf
from const import *
@@ -53,22 +52,6 @@ def get_timezone_offset():
return offset_formatted
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
subnets = []
# multiple interfaces
if type(scan_subnets) is list:
for interface in scan_subnets :
subnets.append(interface)
# one interface only
else:
subnets.append(scan_subnets)
return subnets
#-------------------------------------------------------------------------------
# File system permission handling
#-------------------------------------------------------------------------------
@@ -217,12 +200,6 @@ def get_setting(key):
return None
#-------------------------------------------------------------------------------
# Settings
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Return setting value
def get_setting_value(key):
@@ -248,8 +225,6 @@ def get_setting_value(key):
#-------------------------------------------------------------------------------
# Convert the setting value to the corresponding python type
def setting_value_to_python_type(set_type, set_value):
value = '----not processed----'
@@ -341,6 +316,30 @@ def setting_value_to_python_type(set_type, set_value):
return value
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
"""
Normalize scan subnet input into a list of subnets.
Parameters:
scan_subnets (str or list): A single subnet string or a list of subnet strings.
Returns:
list: A list containing all subnets. If a single subnet is provided, it is returned as a single-element list.
"""
subnets = []
# multiple interfaces
if isinstance(scan_subnets, list):
for interface in scan_subnets:
subnets.append(interface)
# one interface only
else:
subnets.append(scan_subnets)
return subnets
#-------------------------------------------------------------------------------
# Reverse transformed values if needed
def reverseTransformers(val, transformers):
@@ -360,41 +359,6 @@ def reverseTransformers(val, transformers):
else:
return reverse_transformers(val, transformers)
#-------------------------------------------------------------------------------
# Generate a WHERE condition for SQLite based on a list of values.
def list_to_where(logical_operator, column_name, condition_operator, values_list):
"""
Generate a WHERE condition for SQLite based on a list of values.
Parameters:
- logical_operator: The logical operator ('AND' or 'OR') to combine conditions.
- column_name: The name of the column to filter on.
- condition_operator: The condition operator ('LIKE', 'NOT LIKE', '=', '!=', etc.).
- values_list: A list of values to be included in the condition.
Returns:
- A string representing the WHERE condition.
"""
# If the list is empty, return an empty string
if not values_list:
return ""
# Replace {s-quote} with single quote in values_list
values_list = [value.replace("{s-quote}", "'") for value in values_list]
# Build the WHERE condition for the first value
condition = f"{column_name} {condition_operator} '{values_list[0]}'"
# Add the rest of the values using the logical operator
for value in values_list[1:]:
condition += f" {logical_operator} {column_name} {condition_operator} '{value}'"
return f'({condition})'
#-------------------------------------------------------------------------------
# IP validation methods
@@ -432,6 +396,19 @@ def check_IP_format (pIP):
# String manipulation methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def generate_random_string(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length))
#-------------------------------------------------------------------------------
def extract_between_strings(text, start, end):
start_index = text.find(start)
end_index = text.find(end, start_index + len(start))
if start_index != -1 and end_index != -1:
return text[start_index + len(start):end_index]
else:
return ""
#-------------------------------------------------------------------------------
@@ -474,7 +451,6 @@ def removeDuplicateNewLines(text):
return text
#-------------------------------------------------------------------------------
def sanitize_string(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
@@ -482,15 +458,6 @@ def sanitize_string(input):
return input
#-------------------------------------------------------------------------------
def sanitize_SQL_input(val):
if val is None:
return ''
if isinstance(val, str):
return val.replace("'", "_")
return val # Return non-string values as they are
#-------------------------------------------------------------------------------
# Function to normalize the string and remove diacritics
def normalize_string(text):
@@ -501,8 +468,29 @@ def normalize_string(text):
# Filter out diacritics and unwanted characters
return ''.join(c for c in normalized_text if unicodedata.category(c) != 'Mn')
# ------------------------------------------------------------------------------
# MAC and IP helper methods
#-------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------
def is_random_mac(mac: str) -> bool:
"""Determine if a MAC address is random, respecting user-defined prefixes not to mark as random."""
is_random = mac[1].upper() in ["2", "6", "A", "E"]
# Get prefixes from settings
prefixes = get_setting_value("UI_NOT_RANDOM_MAC")
# If detected as random, make sure it doesn't start with a prefix the user wants to exclude
if is_random:
for prefix in prefixes:
if mac.upper().startswith(prefix.upper()):
is_random = False
break
return is_random
# -------------------------------------------------------------------------------------------
def generate_mac_links (html, deviceUrl):
p = re.compile(r'(?:[0-9a-fA-F]:?){12}')
@@ -514,15 +502,6 @@ def generate_mac_links (html, deviceUrl):
return html
#-------------------------------------------------------------------------------
def extract_between_strings(text, start, end):
start_index = text.find(start)
end_index = text.find(end, start_index + len(start))
if start_index != -1 and end_index != -1:
return text[start_index + len(start):end_index]
else:
return ""
#-------------------------------------------------------------------------------
def extract_mac_addresses(text):
mac_pattern = r"([0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2})"
@@ -536,11 +515,6 @@ def extract_ip_addresses(text):
return ip_addresses
#-------------------------------------------------------------------------------
def generate_random_string(length):
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(length))
# Helper function to determine if a MAC address is random
def is_random_mac(mac):
# Check if second character matches "2", "6", "A", "E" (case insensitive)
@@ -555,13 +529,14 @@ def is_random_mac(mac):
break
return is_random
#-------------------------------------------------------------------------------
# Helper function to calculate number of children
def get_number_of_children(mac, devices):
# Count children by checking devParentMAC for each device
return sum(1 for dev in devices if dev.get("devParentMAC", "").strip() == mac.strip())
#-------------------------------------------------------------------------------
# Function to convert IP to a long integer
def format_ip_long(ip_address):
try:
@@ -596,8 +571,6 @@ def add_json_list (row, list):
return list
#-------------------------------------------------------------------------------
# Checks if the object has a __dict__ attribute. If it does, it assumes that it's an instance of a class and serializes its attributes dynamically.
class NotiStrucEncoder(json.JSONEncoder):
@@ -607,19 +580,6 @@ class NotiStrucEncoder(json.JSONEncoder):
return obj.__dict__
return super().default(obj)
#-------------------------------------------------------------------------------
# Creates a JSON object from a DB row
def row_to_json(names, row):
rowEntry = {}
index = 0
for name in names:
rowEntry[name]= if_byte_then_to_str(row[name])
index += 1
return rowEntry
#-------------------------------------------------------------------------------
# Get language strings from plugin JSON
def collect_lang_strings(json, pref, stringSqlParams):
@@ -633,29 +593,33 @@ def collect_lang_strings(json, pref, stringSqlParams):
return stringSqlParams
#-------------------------------------------------------------------------------
# Misc
# Date and time methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def print_table_schema(db, table):
sql = db.sql
sql.execute(f"PRAGMA table_info({table})")
result = sql.fetchall()
# -------------------------------------------------------------------------------------------
def format_date(date_str: str) -> str:
"""Format a date string as 'YYYY-MM-DD HH:MM'"""
dt = datetime.datetime.fromisoformat(date_str) if isinstance(date_str, str) else date_str
return dt.strftime('%Y-%m-%d %H:%M')
if not result:
mylog('none', f'[Schema] Table "{table}" not found or has no columns.')
return
# -------------------------------------------------------------------------------------------
def format_date_diff(date1: str, date2: str) -> str:
"""Return difference between two dates formatted as 'Xd HH:MM'"""
dt1 = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
dt2 = datetime.datetime.fromisoformat(date2) if isinstance(date2, str) else date2
delta = dt2 - dt1
mylog('debug', f'[Schema] Structure for table: {table}')
header = f"{'cid':<4} {'name':<20} {'type':<10} {'notnull':<8} {'default':<10} {'pk':<2}"
mylog('debug', header)
mylog('debug', '-' * len(header))
days = delta.days
hours, remainder = divmod(delta.seconds, 3600)
minutes = remainder // 60
for row in result:
# row = (cid, name, type, notnull, dflt_value, pk)
line = f"{row[0]:<4} {row[1]:<20} {row[2]:<10} {row[3]:<8} {str(row[4]):<10} {row[5]:<2}"
mylog('debug', line)
return f"{days}d {hours:02}:{minutes:02}"
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> str:
"""Return ISO 8601 string for a date"""
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
#-------------------------------------------------------------------------------
def checkNewVersion():
@@ -698,22 +662,6 @@ def checkNewVersion():
return newVersion
#-------------------------------------------------------------------------------
def initOrSetParam(db, parID, parValue):
sql = db.sql
sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'")
db.commitDB()
#-------------------------------------------------------------------------------
class json_obj:
def __init__(self, jsn, columnNames):
self.json = jsn
self.columnNames = columnNames
#-------------------------------------------------------------------------------
class noti_obj:
def __init__(self, json, text, html):

View File

@@ -12,7 +12,7 @@ import re
# Register NetAlertX libraries
import conf
from const import fullConfPath, applicationPath, fullConfFolder, default_tz
from helper import fixPermissions, collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string
from helper import fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string
from app_state import updateState
from logger import mylog
from api import update_api
@@ -176,7 +176,7 @@ def importConfigs (db, all_plugins):
conf.API_TOKEN = ccd('API_TOKEN', 't_' + generate_random_string(20) , c_d, 'API token', '{"dataType": "string","elements": [{"elementType": "input","elementHasInputValue": 1,"elementOptions": [{ "cssClasses": "col-xs-12" }],"transformers": []},{"elementType": "button","elementOptions": [{ "getStringKey": "Gen_Generate" },{ "customParams": "API_TOKEN" },{ "onClick": "generateApiToken(this, 20)" },{ "cssClasses": "col-xs-12" }],"transformers": []}]}', '[]', 'General')
# UI
conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'German', 'Spanish', 'French', 'Norwegian', 'Russian', 'Italian (it_it)', 'Portuguese (pt_br)', 'Polish (pl_pl)', 'Chinese (zh_cn)', 'Turkish (tr_tr)', 'Czech (cs_cz)', 'Arabic (ar_ar)', 'Catalan (ca_ca)', 'Ukrainian (uk_ua)' ]", 'UI')
conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['English', 'German', 'Spanish', 'French', 'Norwegian', 'Russian', 'Italian (it_it)', 'Portuguese (pt_br)', 'Portuguese (pt_pt)', 'Polish (pl_pl)', 'Chinese (zh_cn)', 'Turkish (tr_tr)', 'Czech (cs_cz)', 'Arabic (ar_ar)', 'Catalan (ca_ca)', 'Ukrainian (uk_ua)' ]", 'UI')
# Init timezone in case it changed and handle invalid values
try:

View File

@@ -24,43 +24,46 @@ from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_f
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'
# Show Frontend User Notification
def write_notification(content, level, timestamp):
def write_notification(content, level='alert', timestamp=None):
# Generate GUID
guid = str(uuid.uuid4())
if timestamp is None:
timestamp = timeNowTZ()
# Prepare notification dictionary
notification = {
'timestamp': str(timestamp),
'guid': guid,
'read': 0,
'level': level,
'content': content
}
# Generate GUID
guid = str(uuid.uuid4())
# If file exists, load existing data, otherwise initialize as empty list
if os.path.exists(NOTIFICATION_API_FILE):
with open(NOTIFICATION_API_FILE, 'r') as file:
# Check if the file object is of type _io.TextIOWrapper
if isinstance(file, _io.TextIOWrapper):
file_contents = file.read() # Read file contents
if file_contents == '':
file_contents = '[]' # If file is empty, initialize as empty list
# Prepare notification dictionary
notification = {
'timestamp': str(timestamp),
'guid': guid,
'read': 0,
'level': level,
'content': content
}
# mylog('debug', ['[Notification] User Notifications file: ', file_contents])
notifications = json.loads(file_contents) # Parse JSON data
else:
mylog('none', '[Notification] File is not of type _io.TextIOWrapper')
notifications = []
else:
notifications = []
# If file exists, load existing data, otherwise initialize as empty list
if os.path.exists(NOTIFICATION_API_FILE):
with open(NOTIFICATION_API_FILE, 'r') as file:
# Check if the file object is of type _io.TextIOWrapper
if isinstance(file, _io.TextIOWrapper):
file_contents = file.read() # Read file contents
if file_contents == '':
file_contents = '[]' # If file is empty, initialize as empty list
# Append new notification
notifications.append(notification)
# mylog('debug', ['[Notification] User Notifications file: ', file_contents])
notifications = json.loads(file_contents) # Parse JSON data
else:
mylog('none', '[Notification] File is not of type _io.TextIOWrapper')
notifications = []
else:
notifications = []
# Write updated data back to file
with open(NOTIFICATION_API_FILE, 'w') as file:
json.dump(notifications, file, indent=4)
# Append new notification
notifications.append(notification)
# Write updated data back to file
with open(NOTIFICATION_API_FILE, 'w') as file:
json.dump(notifications, file, indent=4)
# Trim notifications
def remove_old(keepNumberOfEntries):

View File

@@ -8,12 +8,13 @@ import re
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value, list_to_where, check_IP_format, sanitize_SQL_input
from helper import timeNowTZ, get_setting_value, check_IP_format
from logger import mylog
from const import vendorsPath, vendorsPathNewest, sql_generateGuid
from models.device_instance import DeviceInstance
from scan.name_resolution import NameResolver
from scan.device_heuristics import guess_icon, guess_type
from db.db_helper import sanitize_SQL_input, list_to_where
#-------------------------------------------------------------------------------
# Removing devices from the CurrentScan DB table which the user chose to ignore by MAC or IP
@@ -68,10 +69,23 @@ def update_devices_data_from_scan (db):
# Update IP
mylog('debug', '[Update Devices] - cur_IP -> devLastIP (always updated)')
sql.execute("""UPDATE Devices
SET devLastIP = (SELECT cur_IP FROM CurrentScan
WHERE devMac = cur_MAC)
WHERE EXISTS (SELECT 1 FROM CurrentScan
WHERE devMac = cur_MAC) """)
SET devLastIP = (
SELECT cur_IP
FROM CurrentScan
WHERE devMac = cur_MAC
AND cur_IP IS NOT NULL
AND cur_IP NOT IN ('', 'null', '(unknown)', '(Unknown)')
ORDER BY cur_DateTime DESC
LIMIT 1
)
WHERE EXISTS (
SELECT 1
FROM CurrentScan
WHERE devMac = cur_MAC
AND cur_IP IS NOT NULL
AND cur_IP NOT IN ('', 'null', '(unknown)', '(Unknown)')
)""")
# Update only devices with empty, NULL or (u(U)nknown) vendors
mylog('debug', '[Update Devices] - cur_Vendor -> (if empty) devVendor')

View File

@@ -6,7 +6,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan
from helper import timeNowTZ, print_table_schema, get_setting_value
from helper import timeNowTZ, get_setting_value
from db.db_helper import print_table_schema
from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications

121
test/test_device_endpoints.py Executable file
View File

@@ -0,0 +1,121 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
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
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 test_create_device(client, api_token, test_mac):
payload = {
"createNew": True,
"devType": "Test Device",
"devOwner": "Unit Test",
"devType": "Router",
"devVendor": "TestVendor",
}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_get_device(client, api_token, test_mac):
# First create it
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
# Then retrieve it
resp = client.get(f"/device/{test_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("devMac") == test_mac
def test_reset_device_props(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.post(f"/device/{test_mac}/reset-props", json={}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_device_events(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.delete(f"/device/{test_mac}/events/delete", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_device(client, api_token, test_mac):
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
resp = client.delete(f"/device/{test_mac}/delete", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_copy_device(client, api_token, test_mac):
# Step 1: Create the source device
payload = {"createNew": True}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# Step 2: Generate a target MAC
target_mac = "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
# Step 3: Copy device
copy_payload = {"macFrom": test_mac, "macTo": target_mac}
resp = client.post("/device/copy", json=copy_payload, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# Step 4: Verify new device exists
resp = client.get(f"/device/{target_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("devMac") == target_mac
# Cleanup: delete both devices
client.delete(f"/device/{test_mac}/delete", headers=auth_headers(api_token))
client.delete(f"/device/{target_mac}/delete", headers=auth_headers(api_token))
def test_update_device_column(client, api_token, test_mac):
# First, create the device
client.post(
f"/device/{test_mac}",
json={"createNew": True},
headers=auth_headers(api_token),
)
# Update its parent MAC
resp = client.post(
f"/device/{test_mac}/update-column",
json={"columnName": "devParentMAC", "columnValue": "Internet"},
headers=auth_headers(api_token),
)
assert resp.status_code == 200
assert resp.json.get("success") is True
# Try updating a non-existent device
resp_missing = client.post(
"/device/11:22:33:44:55:66/update-column",
json={"columnName": "devParentMAC", "columnValue": "Internet"},
headers=auth_headers(api_token),
)
assert resp_missing.status_code == 404
assert resp_missing.json.get("success") is False

196
test/test_devices_endpoints.py Executable file
View File

@@ -0,0 +1,196 @@
import sys
import pathlib
import sqlite3
import base64
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
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
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 create_dummy(client, api_token, test_mac):
payload = {
"createNew": True,
"devName": "Test Device",
"devOwner": "Unit Test",
"devType": "Router",
"devVendor": "TestVendor",
}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
def test_get_all_devices(client, api_token, test_mac):
# Ensure there is at least one device
create_dummy(client, api_token, test_mac)
# Fetch all devices
resp = client.get("/devices", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
devices = resp.json.get("devices")
assert isinstance(devices, list)
# Ensure our test device is in the list
assert any(d["devMac"] == test_mac for d in devices)
def test_delete_devices_with_macs(client, api_token, test_mac):
# First create device so it exists
create_dummy(client, api_token, test_mac)
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
# Delete by MAC
resp = client.delete("/devices", json={"macs": [test_mac]}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_delete_all_empty_macs(client, api_token):
resp = client.delete("/devices/empty-macs", headers=auth_headers(api_token))
assert resp.status_code == 200
# Expect success flag in response
assert resp.json.get("success") is True
def test_delete_unknown_devices(client, api_token):
resp = client.delete("/devices/unknown", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
def test_export_devices_csv(client, api_token, test_mac):
# Create a device first
create_dummy(client, api_token, test_mac)
# Export devices as CSV
resp = client.get("/devices/export/csv", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.mimetype == "text/csv"
assert "attachment; filename=devices.csv" in resp.headers.get("Content-disposition", "")
# CSV should contain test_mac
assert test_mac in resp.data.decode()
def test_export_devices_json(client, api_token, test_mac):
# Create a device first
create_dummy(client, api_token, test_mac)
# Export devices as JSON
resp = client.get("/devices/export/json", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.is_json
data = resp.get_json()
assert any(dev.get("devMac") == test_mac for dev in data["data"])
def test_export_devices_invalid_format(client, api_token):
# Request with unsupported format
resp = client.get("/devices/export/invalid", headers=auth_headers(api_token))
assert resp.status_code == 400
assert "Unsupported format" in resp.json.get("error")
def test_export_import_cycle_base64(client, api_token, test_mac):
# 1. Create a dummy device
create_dummy(client, api_token, test_mac)
# 2. Export devices as CSV
resp = client.get("/devices/export/csv", headers=auth_headers(api_token))
assert resp.status_code == 200
csv_data = resp.data.decode("utf-8")
print(csv_data)
# Ensure our dummy device is in the CSV
assert test_mac in csv_data
assert "Test Device" in csv_data
# 3. Base64-encode the CSV for JSON payload
csv_base64 = base64.b64encode(csv_data.encode("utf-8")).decode("utf-8")
json_payload = {"content": csv_base64}
# 4. POST to import endpoint with JSON content
resp = client.post(
"/devices/import",
json=json_payload,
headers={**auth_headers(api_token), "Content-Type": "application/json"}
)
assert resp.status_code == 200
assert resp.json.get("success") is True
# 5. Verify import results
assert resp.json.get("inserted") >= 1
assert resp.json.get("skipped_lines") == []
def test_devices_totals(client, api_token, test_mac):
# 1. Create a dummy device
create_dummy(client, api_token, test_mac)
# 2. Call the totals endpoint
resp = client.get("/devices/totals", headers=auth_headers(api_token))
assert resp.status_code == 200
# 3. Ensure the response is a JSON list
data = resp.json
assert isinstance(data, list)
assert len(data) == 6 # devices, connected, favorites, new, down, archived
# 4. Check that at least 1 device exists
assert data[0] >= 1 # 'devices' count includes the dummy device
def test_devices_by_status(client, api_token, test_mac):
# 1. Create a dummy device
create_dummy(client, api_token, test_mac)
# 2. Request devices by a valid status
resp = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
assert resp.status_code == 200
data = resp.json
assert isinstance(data, list)
assert any(d["id"] == test_mac for d in data)
# 3. Request devices with an invalid/unknown status
resp_invalid = client.get("/devices/by-status?status=invalid_status", headers=auth_headers(api_token))
assert resp_invalid.status_code == 200
# Should return empty list for unknown status
assert resp_invalid.json == []
# 4. Check favorite formatting if devFavorite = 1
# Update dummy device to favorite
client.post(
f"/device/{test_mac}",
json={"devFavorite": 1},
headers=auth_headers(api_token)
)
resp_fav = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
fav_data = next((d for d in resp_fav.json if d["id"] == test_mac), None)
assert fav_data is not None
assert "&#9733" in fav_data["title"]
def test_delete_test_devices(client, api_token, test_mac):
# Delete by MAC
resp = client.delete("/devices", json={"macs": ["AA:BB:CC:*"]}, headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True

104
test/test_events_endpoints.py Executable file
View File

@@ -0,0 +1,104 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
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
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 create_event(client, api_token, mac, event="UnitTest Event", days_old=None):
"""
Create event using API (POST /event/<mac>).
If days_old is set, adds it to payload for backdating support.
"""
payload = {
"event": event,
}
if days_old:
payload["days_old"] = days_old
return client.post(f"/event/{mac}", json=payload, headers=auth_headers(api_token))
def list_events(client, api_token, mac=None):
url = "/events" if mac is None else f"/events/{mac}"
return client.get(url, headers=auth_headers(api_token))
def test_delete_events_for_mac(client, api_token, test_mac):
# create event
resp = create_event(client, api_token, test_mac)
assert resp.status_code == 200
# confirm exists
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
assert any(ev["eve_MAC"] == test_mac for ev in resp.json)
# delete
resp = client.delete(f"/events/{test_mac}", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm deleted
resp = list_events(client, api_token, test_mac)
assert resp.status_code == 200
assert len(resp.json) == 0
def test_delete_all_events(client, api_token, test_mac):
# create two events
create_event(client, api_token, test_mac)
create_event(client, api_token, "FF:FF:FF:FF:FF:FF")
resp = list_events(client, api_token)
assert len(resp.json) >= 2
# delete all
resp = client.delete("/events", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm no events
resp = list_events(client, api_token)
assert len(resp.json) == 0
def test_delete_events_30days(client, api_token, test_mac):
# create old + new events
create_event(client, api_token, test_mac, days_old=40) # should be deleted
create_event(client, api_token, test_mac, days_old=5) # should remain
resp = list_events(client, api_token, test_mac)
assert len(resp.json) == 2
# delete events older than 30 days
resp = client.delete("/events/30days", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True
# confirm only recent remains
resp = list_events(client, api_token, test_mac)
mac_events = [ev for ev in resp.json if ev["eve_MAC"] == test_mac]
assert len(mac_events) == 1

View File

@@ -1,191 +0,0 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
from datetime import datetime, timedelta
sys.path.append(str(pathlib.Path(__file__).parent.parent.resolve()) + "/server/")
from helper import timeNowTZ, updateSubnets
# -------------------------------------------------------------------------------
def test_helper():
assert timeNow() == datetime.datetime.now().replace(microsecond=0)
# -------------------------------------------------------------------------------
def test_updateSubnets():
# test single subnet
subnet = "192.168.1.0/24 --interface=eth0"
result = updateSubnets(subnet)
assert type(result) is list
assert len(result) == 1
# test multiple subnets
subnet = ["192.168.1.0/24 --interface=eth0", "192.168.2.0/24 --interface=eth1"]
result = updateSubnets(subnet)
assert type(result) is list
assert len(result) == 2
# -------------------------------------------------------------------------------
# Function to insert N random device entries
def insert_devices(db_path, num_entries=1):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
print(f"{num_entries} entries to generate.")
# Function to generate a random MAC address
def generate_mac():
return '00:1A:2B:{:02X}:{:02X}:{:02X}'.format(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
# Function to generate a random string of given length
def generate_random_string(length):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
# Function to generate a random date within the last `n` days
def generate_random_date(n_days=365):
start_date = datetime.now() - timedelta(days=random.randint(0, n_days))
return start_date.strftime('%Y-%m-%d %H:%M:%S')
# Function to generate a GUID (Globally Unique Identifier)
def generate_guid():
return str(uuid.uuid4()) # Generates a unique GUID
# SQL query to insert a new row into Devices table
insert_query = """
INSERT INTO Devices (
devMac,
devName,
devOwner,
devType,
devVendor,
devFavorite,
devGroup,
devComments,
devFirstConnection,
devLastConnection,
devLastIP,
devStaticIP,
devScan,
devLogEvents,
devAlertEvents,
devAlertDown,
devSkipRepeated,
devLastNotification,
devPresentLastScan,
devIsNew,
devLocation,
devIsArchived,
devParentMAC,
devParentPort,
devIcon,
devGUID,
devSite,
devSSID,
devSyncHubNode,
devSourcePlugin,
devCustomProps,
devFQDN,
devParentRelType,
devReqNicsOnline
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"""
# List of device types, vendors, groups, locations
device_types = ['Phone', 'Laptop', 'Tablet', 'Other']
vendors = ['Vendor A', 'Vendor B', 'Vendor C']
groups = ['Group1', 'Group2']
locations = ['Location A', 'Location B']
# Insert the specified number of rows (default is 10,000)
for i in range(num_entries):
dev_mac = generate_mac()
dev_name = f'Device_{i:04d}'
dev_owner = f'Owner_{i % 100:03d}'
dev_type = random.choice(device_types)
dev_vendor = random.choice(vendors)
dev_favorite = random.choice([0, 1])
dev_group = random.choice(groups)
dev_comments = "" # Left as NULL
dev_first_connection = generate_random_date(365) # Within last 365 days
dev_last_connection = generate_random_date(30) # Within last 30 days
dev_last_ip = f'192.168.0.{random.randint(0, 255)}'
dev_static_ip = random.choice([0, 1])
dev_scan = random.randint(1, 10)
dev_log_events = random.choice([0, 1])
dev_alert_events = random.choice([0, 1])
dev_alert_down = random.choice([0, 1])
dev_skip_repeated = random.randint(0, 5)
dev_last_notification = "" # Left as NULL
dev_present_last_scan = random.choice([0, 1])
dev_is_new = random.choice([0, 1])
dev_location = random.choice(locations)
dev_is_archived = random.choice([0, 1])
dev_parent_mac = "" # Left as NULL
dev_parent_port = "" # Left as NULL
dev_icon = "" # Left as NULL
dev_guid = generate_guid() # Left as NULL
dev_site = "" # Left as NULL
dev_ssid = "" # Left as NULL
dev_sync_hub_node = "" # Left as NULL
dev_source_plugin = "" # Left as NULL
dev_devCustomProps = "" # Left as NULL
dev_devFQDN = "" # Left as NULL
# Execute the insert query
cursor.execute(insert_query, (
dev_mac,
dev_name,
dev_owner,
dev_type,
dev_vendor,
dev_favorite,
dev_group,
dev_comments,
dev_first_connection,
dev_last_connection,
dev_last_ip,
dev_static_ip,
dev_scan,
dev_log_events,
dev_alert_events,
dev_alert_down,
dev_skip_repeated,
dev_last_notification,
dev_present_last_scan,
dev_is_new,
dev_location,
dev_is_archived,
dev_parent_mac,
dev_parent_port,
dev_icon,
dev_guid,
dev_site,
dev_ssid,
dev_sync_hub_node,
dev_source_plugin,
dev_devCustomProps,
dev_devFQDN
))
# Commit after every 1000 rows to improve performance
if i % 1000 == 0:
conn.commit()
# Final commit to save all remaining data
conn.commit()
# Close the database connection
conn.close()
print(f"{num_entries} entries have been successfully inserted into the Devices table.")
# -------------------------------------------------------------------------------
if __name__ == "__main__":
# Call insert_devices with database path and number of entries as arguments
db_path = "/app/db/app.db"
num_entries = int(sys.argv[1]) if len(sys.argv) > 1 else 10000
insert_devices(db_path, num_entries)

35
test/test_history_endpoints.py Executable file
View File

@@ -0,0 +1,35 @@
import sys
import pathlib
import sqlite3
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
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
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 test_delete_history(client, api_token):
resp = client.delete(f"/history", headers=auth_headers(api_token))
assert resp.status_code == 200
assert resp.json.get("success") is True

76
test/test_nettools_endpoints.py Executable file
View File

@@ -0,0 +1,76 @@
import sys
import pathlib
import sqlite3
import base64
import random
import string
import uuid
import pytest
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value
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
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 create_dummy(client, api_token, test_mac):
payload = {
"createNew": True,
"devName": "Test Device",
"devOwner": "Unit Test",
"devType": "Router",
"devVendor": "TestVendor",
}
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
def test_wakeonlan_device(client, api_token, test_mac):
# 1. Ensure at least one device exists
create_dummy(client, api_token, test_mac)
# 2. Fetch all devices
resp = client.get("/devices", headers=auth_headers(api_token))
assert resp.status_code == 200
devices = resp.json.get("devices", [])
assert len(devices) > 0
# 3. Pick the first device (or the test device)
device_mac = devices[0]["devMac"]
# 4. Call the wakeonlan endpoint
resp = client.post(
"/nettools/wakeonlan",
json={"devMac": device_mac},
headers=auth_headers(api_token)
)
# 5. Conditional assertions based on MAC
if device_mac.lower() == 'internet' or device_mac == test_mac:
# For athe dummy "internet" or test MAC, expect a 400 response
assert resp.status_code == 400
else:
# For any other MAC, expect a 200 response
assert resp.status_code == 200
data = resp.json
assert data.get("success") is True
assert "WOL packet sent" in data.get("message", "")