mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
Compare commits
20 Commits
rewrite
...
5fd30fe3c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fd30fe3c8 | ||
|
|
2fa181ffbc | ||
|
|
a2bccdfb8e | ||
|
|
f3b159116f | ||
|
|
03b9a9cf0d | ||
|
|
bf2fae6e1a | ||
|
|
086fa54035 | ||
|
|
962bbaa5a1 | ||
|
|
9c71a8ecab | ||
|
|
deff5a4ed0 | ||
|
|
e10c1c9c8d | ||
|
|
b155fe2b06 | ||
|
|
840bfe32d2 | ||
|
|
f33ef9861b | ||
|
|
cbe71cc203 | ||
|
|
beaf8131ae | ||
|
|
99bfbb56de | ||
|
|
e73c8e830a | ||
|
|
1c4e6c7e38 | ||
|
|
1319c3380d |
2
.github/workflows/docker_rewrite.yml
vendored
2
.github/workflows/docker_rewrite.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- rewrite
|
||||
|
||||
jobs:
|
||||
docker_dev:
|
||||
docker_rewrite:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
|
||||
@@ -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 | | |
|
||||
|
||||
@@ -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
|
||||
----------------------------------------------------------------------------- */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -833,4 +833,4 @@
|
||||
"settings_system_label": "System",
|
||||
"settings_update_item_warning": "",
|
||||
"test_event_tooltip": "Speichere die Änderungen, bevor Sie die Einstellungen testen."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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:])
|
||||
|
||||
763
front/php/templates/language/pt_pt.json
Executable file
763
front/php/templates/language/pt_pt.json
Executable 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": ""
|
||||
}
|
||||
@@ -760,4 +760,4 @@
|
||||
"settings_system_label": "Система",
|
||||
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
|
||||
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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**.
|
||||
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
336
server/api_server/device_endpoint.py
Executable file
336
server/api_server/device_endpoint.py
Executable 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()
|
||||
|
||||
263
server/api_server/devices_endpoint.py
Executable file
263
server/api_server/devices_endpoint.py
Executable 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">★</span> {dev_name}'
|
||||
|
||||
table_data.append({
|
||||
"id": r.get("devMac", ""),
|
||||
"title": dev_name,
|
||||
"favorite": r.get("devFavorite", 0)
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return jsonify(table_data)
|
||||
|
||||
72
server/api_server/events_endpoint.py
Executable file
72
server/api_server/events_endpoint.py
Executable 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"})
|
||||
|
||||
|
||||
35
server/api_server/history_endpoint.py
Executable file
35
server/api_server/history_endpoint.py
Executable 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"})
|
||||
21
server/api_server/nettools_endpoint.py
Executable file
21
server/api_server/nettools_endpoint.py
Executable 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
|
||||
|
||||
71
server/api_server/sync_endpoint.py
Executable file
71
server/api_server/sync_endpoint.py
Executable 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
|
||||
@@ -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
269
server/db/db_helper.py
Executable 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]
|
||||
@@ -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)
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
216
server/helper.py
216
server/helper.py
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
121
test/test_device_endpoints.py
Executable 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
196
test/test_devices_endpoints.py
Executable 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 "★" 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
104
test/test_events_endpoints.py
Executable 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
|
||||
@@ -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
35
test/test_history_endpoints.py
Executable 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
76
test/test_nettools_endpoints.py
Executable 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", "")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user