mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-12 05:01:27 -07:00
Compare commits
34 Commits
v25.8.6
...
f78c84d9a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f78c84d9a8 | ||
|
|
2d11d3dd3e | ||
|
|
39c556576c | ||
|
|
73fd094cfc | ||
|
|
cbf2cd0ee8 | ||
|
|
915bb523d6 | ||
|
|
3dc87d2adb | ||
|
|
9155303674 | ||
|
|
0777824d96 | ||
|
|
b170ca3e18 | ||
|
|
5fd30fe3c8 | ||
|
|
2fa181ffbc | ||
|
|
a2bccdfb8e | ||
|
|
f3b159116f | ||
|
|
03b9a9cf0d | ||
|
|
bf2fae6e1a | ||
|
|
086fa54035 | ||
|
|
962bbaa5a1 | ||
|
|
9c71a8ecab | ||
|
|
deff5a4ed0 | ||
|
|
e10c1c9c8d | ||
|
|
b155fe2b06 | ||
|
|
840bfe32d2 | ||
|
|
f33ef9861b | ||
|
|
cbe71cc203 | ||
|
|
beaf8131ae | ||
|
|
99bfbb56de | ||
|
|
e73c8e830a | ||
|
|
1c4e6c7e38 | ||
|
|
1319c3380d | ||
|
|
dce8c34064 | ||
|
|
9502ee0cd0 | ||
|
|
8eb4ffe3ed | ||
|
|
4be59807e5 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
||||
github: jokob-sk
|
||||
patreon: 84385063
|
||||
patreon: netalertx
|
||||
buy_me_a_coffee: jokobsk
|
||||
|
||||
81
.github/workflows/docker_rewrite.yml
vendored
Executable file
81
.github/workflows/docker_rewrite.yml
vendored
Executable file
@@ -0,0 +1,81 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- rewrite
|
||||
tags:
|
||||
- '*.*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- rewrite
|
||||
|
||||
jobs:
|
||||
docker_rewrite:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
if: >
|
||||
contains(github.event.head_commit.message, 'PUSHPROD') != 'True' &&
|
||||
github.repository == 'jokob-sk/NetAlertX'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up dynamic build ARGs
|
||||
id: getargs
|
||||
run: echo "version=$(cat ./stable/VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get release version
|
||||
id: get_version
|
||||
run: echo "version=Dev" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create .VERSION file
|
||||
run: echo "${{ steps.get_version.outputs.version }}" >> .VERSION
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/jokob-sk/netalertx-dev-rewrite
|
||||
jokobsk/netalertx-dev-rewrite
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Log in to Github Container Registry (GHCR)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: jokob-sk
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . ${INSTALL_DIR}/
|
||||
|
||||
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
|
||||
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
|
||||
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
|
||||
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
|
||||
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"
|
||||
|
||||
@@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3
|
||||
RUN apt-get install -y python3-venv
|
||||
RUN python3 -m venv myenv
|
||||
|
||||
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
|
||||
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
|
||||
|
||||
# Create a buildtimestamp.txt to later check if a new version was released
|
||||
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt
|
||||
|
||||
34
docs/DEBUG_PHP.md
Executable file
34
docs/DEBUG_PHP.md
Executable file
@@ -0,0 +1,34 @@
|
||||
# Debugging backend PHP issues
|
||||
|
||||
## Logs in UI
|
||||
|
||||

|
||||
|
||||
You can view recent backend PHP errors directly in the **Maintenance > Logs** section of the UI. This provides quick access to logs without needing terminal access.
|
||||
|
||||
## Accessing logs directly
|
||||
|
||||
Sometimes, the UI might not be accessible. In that case, you can access the logs directly inside the container.
|
||||
|
||||
### Step-by-step:
|
||||
|
||||
1. **Open a shell into the container:**
|
||||
|
||||
```bash
|
||||
docker exec -it netalertx /bin/sh
|
||||
```
|
||||
|
||||
2. **Check the NGINX error log:**
|
||||
|
||||
```bash
|
||||
cat /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
3. **Check the PHP application error log:**
|
||||
|
||||
```bash
|
||||
cat /app/log/app.php_errors.log
|
||||
```
|
||||
|
||||
These logs will help identify syntax issues, fatal errors, or startup problems when the UI fails to load properly.
|
||||
|
||||
@@ -53,6 +53,9 @@ The function `guess_device_attributes(...)` runs a series of matching functions
|
||||
4. IP pattern → `match_ip()`
|
||||
5. Final fallback → defaults defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings.
|
||||
|
||||
> [!NOTE]
|
||||
> The app will try guessing the device type or icon if `devType` or `devIcon` are `""` or `"null"`.
|
||||
|
||||
### Use of default values
|
||||
|
||||
The guessing process runs for every device **as long as the current type or icon still matches the default values**. Even if earlier heuristics return a match, the system continues evaluating additional clues — like name or IP — to try and replace placeholders.
|
||||
|
||||
@@ -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 | | |
|
||||
|
||||
BIN
docs/img/DEBUG/maintenance_debug_php.png
Executable file
BIN
docs/img/DEBUG/maintenance_debug_php.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
<!-- ----------------------------------------------------------------------- -->
|
||||
|
||||
|
||||
@@ -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
|
||||
----------------------------------------------------------------------------- */
|
||||
@@ -1726,6 +1741,16 @@ input[readonly] {
|
||||
width: 92%;
|
||||
}
|
||||
|
||||
#modal-ok
|
||||
{
|
||||
z-index: 1051; /*highest priority*/
|
||||
}
|
||||
|
||||
#modal-form-plc
|
||||
{
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- */
|
||||
/* NETWORK page */
|
||||
/* ----------------------------------------------------------------- */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Content header--------------------------------------------------------- -->
|
||||
<section class="content-header">
|
||||
<?php require 'php/templates/notification.php'; ?>
|
||||
<?php require 'php/templates/modals.php'; ?>
|
||||
|
||||
<h1 id="pageTitle">
|
||||
 <small>Quering device info...</small>
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
@@ -290,18 +284,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------
|
||||
// Show the description of a setting
|
||||
function showDescriptionPopup(e) {
|
||||
|
||||
console.log($(e).attr("my-set-key"));
|
||||
|
||||
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Save device data to DB
|
||||
function setDeviceData(direction = '', refreshCallback = '') {
|
||||
|
||||
@@ -683,7 +683,9 @@ function initializeDatatable (status) {
|
||||
|
||||
return JSON.stringify(query); // Send the JSON request
|
||||
},
|
||||
"dataSrc": function (json) {
|
||||
"dataSrc": function (res) {
|
||||
|
||||
json = res["data"];
|
||||
// Set the total number of records for pagination
|
||||
json.recordsTotal = json.devices.count || 0;
|
||||
json.recordsFiltered = json.devices.count || 0;
|
||||
@@ -1004,9 +1006,7 @@ function initializeDatatable (status) {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "",
|
||||
"src": "/img/NetAlertX_logo.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -1032,7 +1029,8 @@ function getDevDataByMac(macAddress, dbColumn) {
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown"; // Return a default value if MAC address is not found
|
||||
console.error("⚠ Device with MAC not found:" + macAddress)
|
||||
return null; // Return a default value if MAC address is not found
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -161,6 +161,139 @@ function showModalFieldInput(
|
||||
$(`#${prefix}`).modal("show");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function showModalPopupForm(
|
||||
title,
|
||||
message,
|
||||
btnCancel = getString("Gen_Cancel"),
|
||||
btnOK = getString("Gen_Okay"),
|
||||
curValue = null,
|
||||
popupFormJson = null,
|
||||
parentSettingKey = null,
|
||||
triggeredBy = null
|
||||
) {
|
||||
// set captions
|
||||
prefix = "modal-form";
|
||||
console.log(popupFormJson);
|
||||
|
||||
$(`#${prefix}-title`).html(title);
|
||||
$(`#${prefix}-message`).html(message);
|
||||
$(`#${prefix}-cancel`).html(btnCancel);
|
||||
$(`#${prefix}-OK`).html(btnOK);
|
||||
|
||||
// if curValue not null
|
||||
|
||||
if (curValue)
|
||||
{
|
||||
initialValues = JSON.parse(atob(curValue));
|
||||
}
|
||||
|
||||
outputHtml = "";
|
||||
|
||||
if (Array.isArray(popupFormJson)) {
|
||||
popupFormJson.forEach((field, index) => {
|
||||
// You'll need to define these or map them from `field`
|
||||
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
|
||||
let initialValue = '';
|
||||
if (curValue && Array.isArray(initialValues)) {
|
||||
const match = initialValues.find(
|
||||
v => v[1] == setKey
|
||||
);
|
||||
if (match) {
|
||||
initialValue = match[3];
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const inputFormHtml = `
|
||||
<div class="form-group col-xs-12">
|
||||
<label id="${setKey}_label" class="${labelClasses}"> ${setName}
|
||||
<i my-set-key="${parentSettingKey}_popupform_${setKey}"
|
||||
title="${getString("Settings_Show_Description")}"
|
||||
class="fa fa-circle-info pointer helpIconSmallTopRight"
|
||||
onclick="showDescriptionPopup(this)">
|
||||
</i>
|
||||
</label>
|
||||
<div class="${inputClasses}">
|
||||
${generateFormHtml(
|
||||
null, // settingsData only required for datatables
|
||||
setObj,
|
||||
null,
|
||||
fieldOptionsOverride,
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append to result
|
||||
outputHtml += inputFormHtml;
|
||||
});
|
||||
}
|
||||
|
||||
$(`#modal-form-plc`).html(outputHtml);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
function modalDefaultOK() {
|
||||
// Hide modal
|
||||
|
||||
@@ -67,6 +67,15 @@ function getPluginConfig(pluginsData, prefix) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Show the description of a setting
|
||||
function showDescriptionPopup(e) {
|
||||
|
||||
console.log($(e).attr("my-set-key"));
|
||||
|
||||
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Generate plugin HTML card based on prefixes in an array
|
||||
function pluginCards(prefixesOfEnabledPlugins, includeSettings) {
|
||||
@@ -237,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){
|
||||
@@ -299,6 +301,33 @@ function removeDataTableRow(el) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Add item via pop up form dialog
|
||||
function addViaPopupForm(element) {
|
||||
console.log(element);
|
||||
|
||||
const toId = $(element).attr("my-input-to");
|
||||
const curValue = $(`#${toId}`).val();
|
||||
const parsed = JSON.parse(atob($(`#${toId}`).data("elementoptionsbase64")));
|
||||
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
|
||||
|
||||
console.log(`toId | curValue: ${toId} | ${curValue}`);
|
||||
|
||||
showModalPopupForm(
|
||||
`<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
|
||||
);
|
||||
|
||||
// flag something changes to prevent navigating from page
|
||||
settingsChanged();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Add item to list
|
||||
function addList(element, clearInput = true) {
|
||||
@@ -427,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);
|
||||
@@ -622,8 +674,6 @@ function generateOptionsOrSetOptions(
|
||||
// obj.push({ id: item, name: item })
|
||||
options = arrayToObject(createArray(overrideOptions ? overrideOptions : getSettingOptions(setKey)))
|
||||
|
||||
|
||||
|
||||
// Call to render lists
|
||||
renderList(
|
||||
options,
|
||||
@@ -633,8 +683,6 @@ function generateOptionsOrSetOptions(
|
||||
targetField,
|
||||
transformers
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -655,6 +703,13 @@ function applyTransformers(val, transformers) {
|
||||
val = btoa(val);
|
||||
}
|
||||
break;
|
||||
case "name|base64":
|
||||
// // Implement base64 logic
|
||||
// if (!isBase64(val)) {
|
||||
// val = btoa(val);
|
||||
// }
|
||||
val = val; // probably TODO ⚠
|
||||
break;
|
||||
case "getString":
|
||||
// no change
|
||||
val = val;
|
||||
@@ -667,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) {
|
||||
@@ -681,6 +736,13 @@ function reverseTransformers(val, transformers) {
|
||||
val = atob(val);
|
||||
}
|
||||
break;
|
||||
case "name|base64":
|
||||
// Implement base64 decoding logic
|
||||
if (isBase64(val)) {
|
||||
val = JSON.parse(atob(val))[0][3];
|
||||
}
|
||||
val = val; // probably TODO ⚠
|
||||
break;
|
||||
case "getString":
|
||||
// retrieve string
|
||||
val = getString(val);
|
||||
@@ -720,8 +782,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
|
||||
let customParams = "";
|
||||
let customId = "";
|
||||
let columns = [];
|
||||
let base64Regex = "";
|
||||
|
||||
let base64Regex = "";
|
||||
let elementOptionsBase64 = btoa(JSON.stringify(elementOptions));
|
||||
|
||||
elementOptions.forEach((option) => {
|
||||
if (option.prefillValue) {
|
||||
@@ -804,7 +866,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
|
||||
customParams,
|
||||
customId,
|
||||
columns,
|
||||
base64Regex
|
||||
base64Regex,
|
||||
elementOptionsBase64
|
||||
};
|
||||
};
|
||||
|
||||
@@ -934,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'];
|
||||
@@ -955,6 +1107,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
|
||||
// }
|
||||
|
||||
// Parse the setType JSON string
|
||||
// console.log(processQuotes(setType));
|
||||
|
||||
const setTypeObject = JSON.parse(processQuotes(setType))
|
||||
const dataType = setTypeObject.dataType;
|
||||
const elements = setTypeObject.elements || [];
|
||||
@@ -982,7 +1136,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
|
||||
customParams,
|
||||
customId,
|
||||
columns,
|
||||
base64Regex
|
||||
base64Regex,
|
||||
elementOptionsBase64
|
||||
} = handleElementOptions(setKey, elementOptions, transformers, inVal);
|
||||
|
||||
// Override value
|
||||
@@ -1014,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>
|
||||
@@ -1051,6 +1207,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
|
||||
my-originalSetKey="${originalSetKey}"
|
||||
my-input-from="${sourceIds}"
|
||||
my-input-to="${setKey}"
|
||||
data-elementoptionsbase64="${elementOptionsBase64}"
|
||||
onclick="${onClick}">
|
||||
${getString(getStringKey)}
|
||||
</button>`;
|
||||
|
||||
@@ -782,17 +782,24 @@ function initSelect2() {
|
||||
// ------------------------------------------
|
||||
// Render a device link with hover-over functionality
|
||||
function renderDeviceLink(data, container, useName = false) {
|
||||
if (!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,
|
||||
device.devMac
|
||||
);
|
||||
|
||||
// Add badge class and hover-info class to container
|
||||
// badge class and hover-info class to container
|
||||
$(container)
|
||||
.addClass(`${badge.cssClass} hover-node-info`)
|
||||
.attr({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
|
||||
<!-- Page ------------------------------------------------------------------ -->
|
||||
|
||||
@@ -136,7 +136,8 @@
|
||||
customParams,
|
||||
customId,
|
||||
columns,
|
||||
base64Regex
|
||||
base64Regex,
|
||||
elementOptionsBase64
|
||||
} = handleElementOptions('none', elementOptions, transformers, val = "");
|
||||
|
||||
// render based on element type
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
|
||||
<script>
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
require dirname(__FILE__).'/../server/init.php';
|
||||
|
||||
// EQUIVALENT: /nettools/speedtest
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// check if authenticated
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
|
||||
|
||||
@@ -19,6 +19,8 @@ require dirname(__FILE__).'/../server/init.php';
|
||||
// check if authenticated
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
|
||||
|
||||
// NEW ENDPOINT EQUIVALENT: /nettools/traceroute
|
||||
|
||||
// Get IP
|
||||
$ip = $_GET['ip'];
|
||||
|
||||
|
||||
@@ -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": "Перш ніж перевіряти налаштування, збережіть зміни."
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
//------------------------------------------------------------------------------
|
||||
// check if authenticated
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
|
||||
|
||||
@@ -117,6 +117,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal form input -->
|
||||
<div class="modal fade" id="modal-form" data-myparam-triggered-by="" style="display: none;">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 id="modal-form-title" class="modal-title"> Modal Title </h4>
|
||||
</div>
|
||||
|
||||
<div id="modal-form-message" class="modal-body"> Modal message </div>
|
||||
|
||||
<div id="modal-form-plc"></div>
|
||||
|
||||
<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;" > OK </button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal field input -->
|
||||
<div class="modal modal-warning fade" id="modal-field-input" data-myparam-triggered-by="" style="display: none;">
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
|
||||
<!-- Page ------------------------------------------------------------------ -->
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
||||
25
front/plugins/unifi_api_import/README.md
Executable file
25
front/plugins/unifi_api_import/README.md
Executable file
@@ -0,0 +1,25 @@
|
||||
## Overview
|
||||
|
||||
Unifi import plugin using the Site Manager API.
|
||||
|
||||
> [!TIP]
|
||||
> The Site Manager API doesn't seems to have feature parity with the old API yet, so certain limitations apply.
|
||||
|
||||
### Quick setup guide
|
||||
|
||||
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
|
||||
|
||||
- Head to **Settings** > **Plugin name** to adjust the default values.
|
||||
|
||||
### Notes
|
||||
|
||||
- Version: 1.0.0
|
||||
- Author: `jokob-sk`
|
||||
- Release Date: `Aug 2025`
|
||||
717
front/plugins/unifi_api_import/config.json
Executable file
717
front/plugins/unifi_api_import/config.json
Executable file
@@ -0,0 +1,717 @@
|
||||
{
|
||||
"code_name": "unifi_api_import",
|
||||
"unique_prefix": "UNIFIAPI",
|
||||
"plugin_type": "device_scanner",
|
||||
"execution_order": "Layer_0",
|
||||
"enabled": true,
|
||||
"data_source": "script",
|
||||
"mapped_to_table": "CurrentScan",
|
||||
"data_filters": [
|
||||
{
|
||||
"compare_column": "Object_PrimaryID",
|
||||
"compare_operator": "==",
|
||||
"compare_field_id": "txtMacFilter",
|
||||
"compare_js_template": "'{value}'.toString()",
|
||||
"compare_use_quotes": true
|
||||
}
|
||||
],
|
||||
"show_ui": true,
|
||||
"localized": [
|
||||
"display_name",
|
||||
"description",
|
||||
"icon"
|
||||
],
|
||||
"display_name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "UniFi import (API)"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "This plugin is used to import devices from an UNIFI controller via the Site Manager API."
|
||||
}
|
||||
],
|
||||
"icon": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "<i class=\"fa-solid fa-shield-halved\"></i>"
|
||||
}
|
||||
],
|
||||
"params": [],
|
||||
"settings": [
|
||||
{
|
||||
"function": "RUN",
|
||||
"events": [
|
||||
"run"
|
||||
],
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "disabled",
|
||||
"options": [
|
||||
"disabled",
|
||||
"once",
|
||||
"schedule"
|
||||
],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "When to run"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "When the plugin should run. Good options are <code>schedule</code>, <code>once</code>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "RUN_SCHD",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "span",
|
||||
"elementOptions": [
|
||||
{
|
||||
"cssClasses": "input-group-addon validityCheck"
|
||||
},
|
||||
{
|
||||
"getStringKey": "Gen_ValidIcon"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
},
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"onChange": "validateRegex(this)"
|
||||
},
|
||||
{
|
||||
"base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "*/5 * * * *",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Schedule"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#UNIFIAPI_RUN\"><code>UNIFIAPI_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "CMD",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"readonly": "true"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "python3 /app/front/plugins/unifi_api_import/unifi_api_import.py",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Command"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Command to run. This can not be changed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "RUN_TIMEOUT",
|
||||
"type": {
|
||||
"dataType": "integer",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": 10,
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Run timeout"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "sites",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "button",
|
||||
"elementOptions": [
|
||||
{
|
||||
"sourceSuffixes": []
|
||||
},
|
||||
{
|
||||
"separator": ""
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-xs-12"
|
||||
},
|
||||
{
|
||||
"onClick": "addViaPopupForm(this)"
|
||||
},
|
||||
{
|
||||
"getStringKey": "Gen_Add"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
},
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementHasInputValue": 1,
|
||||
"elementOptions": [
|
||||
{
|
||||
"multiple": "true"
|
||||
},
|
||||
{
|
||||
"readonly": "true"
|
||||
},
|
||||
{
|
||||
"editable": "true"
|
||||
},
|
||||
{
|
||||
"popupForm": [
|
||||
{
|
||||
"function": "UNIFIAPI_site_name",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"placeholder": "Enter value"
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-sm-10"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "default",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Site name"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "The name of your site. Use a descriptive name."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "UNIFIAPI_base_url",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"placeholder": "https://host_ip/proxy/network/integration/"
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-sm-10"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "default",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Base URL"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "You can find your base url in the UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i> in the <i>API Request Format</i> section, (e.g. <code>https://host_ip/proxy/network/integration/</code>)."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "UNIFIAPI_api_version",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"placeholder": "v1"
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-sm-10"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "default",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "API version"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "The version of the API (e.g.: <code>v1</code>)."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "UNIFIAPI_api_key",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"placeholder": "Enter value"
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-sm-10"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": "default",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "API key"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "You can get an API key in your UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i>."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "UNIFIAPI_verify_ssl",
|
||||
"type": {
|
||||
"dataType": "boolean",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "input",
|
||||
"elementOptions": [
|
||||
{
|
||||
"type": "checkbox"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": 1,
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Verify SSL"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Disable if you do not have an SSL certificate set up."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
],
|
||||
"transformers": [
|
||||
"name|base64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"elementType": "button",
|
||||
"elementOptions": [
|
||||
{
|
||||
"sourceSuffixes": []
|
||||
},
|
||||
{
|
||||
"separator": ""
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-xs-6"
|
||||
},
|
||||
{
|
||||
"onClick": "removeFromList(this)"
|
||||
},
|
||||
{
|
||||
"getStringKey": "Gen_Remove_Last"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
},
|
||||
{
|
||||
"elementType": "button",
|
||||
"elementOptions": [
|
||||
{
|
||||
"sourceSuffixes": []
|
||||
},
|
||||
{
|
||||
"separator": ""
|
||||
},
|
||||
{
|
||||
"cssClasses": "col-xs-6"
|
||||
},
|
||||
{
|
||||
"onClick": "removeAllOptions(this)"
|
||||
},
|
||||
{
|
||||
"getStringKey": "Gen_Remove_All"
|
||||
}
|
||||
],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Sites"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"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>."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
{
|
||||
"column": "Index",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "none",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Index"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Object_PrimaryID",
|
||||
"mapped_to_column": "cur_MAC",
|
||||
"css_classes": "col-sm-3",
|
||||
"show": true,
|
||||
"type": "device_name_mac",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "MAC (name)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Object_SecondaryID",
|
||||
"mapped_to_column": "cur_IP",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "device_ip",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "IP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value1",
|
||||
"mapped_to_column": "cur_Name",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value2",
|
||||
"mapped_to_column": "cur_Type",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Device Type"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Watched_Value3",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Dummy",
|
||||
"mapped_to_column": "cur_ScanMethod",
|
||||
"mapped_to_column_data": {
|
||||
"value": "UNIFIAPI"
|
||||
},
|
||||
"css_classes": "col-sm-2",
|
||||
"show": false,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Scan method"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "DateTimeCreated",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Created"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "DateTimeChanged",
|
||||
"css_classes": "col-sm-2",
|
||||
"show": true,
|
||||
"type": "label",
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Changed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"column": "Status",
|
||||
"css_classes": "col-sm-1",
|
||||
"show": true,
|
||||
"type": "replace",
|
||||
"default_value": "",
|
||||
"options": [
|
||||
{
|
||||
"equals": "watched-not-changed",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
|
||||
},
|
||||
{
|
||||
"equals": "watched-changed",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
|
||||
},
|
||||
{
|
||||
"equals": "new",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
|
||||
},
|
||||
{
|
||||
"equals": "missing-in-last-scan",
|
||||
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
|
||||
}
|
||||
],
|
||||
"localized": [
|
||||
"name"
|
||||
],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Status"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
182
front/plugins/unifi_api_import/unifi_api_import.py
Executable file
182
front/plugins/unifi_api_import/unifi_api_import.py
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
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, decode_settings_base64
|
||||
from plugin_utils import get_plugins_configs
|
||||
from logger import mylog, Logger
|
||||
from const import pluginsPath, fullDbPath, logPath
|
||||
from helper import timeNowTZ, get_setting_value
|
||||
|
||||
from messaging.in_app import write_notification
|
||||
import conf
|
||||
|
||||
# Make sure the TIMEZONE for logging is correct
|
||||
conf.tz = timezone(get_setting_value('TIMEZONE'))
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
pluginName = 'UNIFIAPI'
|
||||
|
||||
# Define the current path and log file paths
|
||||
LOG_PATH = logPath + '/plugins'
|
||||
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
|
||||
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
|
||||
|
||||
# Initialize the Plugin obj output file
|
||||
plugin_objects = Plugin_Objects(RESULT_FILE)
|
||||
|
||||
|
||||
def main():
|
||||
mylog('verbose', [f'[{pluginName}] In script'])
|
||||
|
||||
# Retrieve configuration settings
|
||||
unifi_sites_configs = get_setting_value('UNIFIAPI_sites')
|
||||
|
||||
mylog('verbose', [f'[{pluginName}] number of unifi_sites_configs: {len(unifi_sites_configs)}'])
|
||||
|
||||
for site_config in unifi_sites_configs:
|
||||
|
||||
siteDict = decode_settings_base64(site_config)
|
||||
|
||||
mylog('verbose', [f'[{pluginName}] siteDict: {json.dumps(siteDict)}'])
|
||||
mylog('none', [f'[{pluginName}] Connecting to: {siteDict["UNIFIAPI_site_name"]}'])
|
||||
|
||||
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"]
|
||||
)
|
||||
|
||||
sites_resp = api.get_sites()
|
||||
sites = sites_resp.get("data", [])
|
||||
|
||||
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(site, api):
|
||||
device_data = []
|
||||
|
||||
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 device_data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
|
||||
?>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -58,6 +58,12 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
|
||||
|
||||
<div id="settingsPage" class="content-wrapper">
|
||||
|
||||
<a style="cursor:pointer">
|
||||
<span>
|
||||
<i id='toggleSettings' onclick="toggleAllSettings()" class="settings-expand-icon fa fa-angle-double-down"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Content header--------------------------------------------------------- -->
|
||||
|
||||
<section class="content-header">
|
||||
@@ -193,8 +199,8 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
|
||||
console.log("Response:", response);
|
||||
|
||||
// Handle the successful response
|
||||
if (response && response.settings) {
|
||||
const settingsData = response.settings.settings;
|
||||
if (response && response.data && response.data.settings && response.data.settings.settings) {
|
||||
const settingsData = response.data.settings.settings;
|
||||
console.log("Settings:", settingsData);
|
||||
|
||||
// Wrong number of settings processing
|
||||
@@ -547,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
|
||||
@@ -559,120 +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
|
||||
} = 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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
require 'php/templates/header.php';
|
||||
?>
|
||||
<?php require 'php/templates/notification.php'; ?>
|
||||
<?php require 'php/templates/modals.php'; ?>
|
||||
<!-- ----------------------------------------------------------------------- -->
|
||||
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ function fetchUsedIps(callback) {
|
||||
|
||||
console.log(response);
|
||||
|
||||
const usedIps = (response?.devices?.devices || [])
|
||||
const usedIps = (response?.data?.devices?.devices || [])
|
||||
.map(d => d.devLastIP)
|
||||
.filter(ip => ip && ip.includes('.'));
|
||||
callback(usedIps);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/notification.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
<!-- ----------------------------------------------------------------------- -->
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@ source myenv/bin/activate
|
||||
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
|
||||
|
||||
# install packages thru pip3
|
||||
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
|
||||
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ nav:
|
||||
- Debugging Tips: DEBUG_TIPS.md
|
||||
- Debugging GraphQL: DEBUG_GRAPHQL.md
|
||||
- Debugging Invalid JSON: DEBUG_INVALID_JSON.md
|
||||
- Debugging PHP: DEBUG_PHP.md
|
||||
- Debugging Plugins: DEBUG_PLUGINS.md
|
||||
- Debugging Web UI Port: WEB_UI_PORT_DEBUG.md
|
||||
- Debugging Workflows: WORKFLOWS_DEBUGGING.md
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
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_older_than, get_events, create_event, get_events_totals
|
||||
from .history_endpoint import delete_online_history
|
||||
from .prometheus_endpoint import get_metric_stats
|
||||
from .sessions_endpoint import get_sessions, delete_session, create_session, get_sessions_calendar, get_device_sessions, get_session_events
|
||||
from .nettools_endpoint import wakeonlan, traceroute, speedtest, nslookup, nmap_scan, internet_info
|
||||
from .sync_endpoint import handle_sync_post, handle_sync_get
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
@@ -12,12 +18,26 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
from logger import mylog
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
from db.db_helper import get_date_from_period
|
||||
from app_state import updateState
|
||||
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"/sessions/*": {"origins": "*"},
|
||||
r"/events/*": {"origins": "*"}
|
||||
},
|
||||
supports_credentials=True,
|
||||
allow_headers=["Authorization", "Content-Type"]
|
||||
)
|
||||
|
||||
# --------------------------
|
||||
# GraphQL Endpoints
|
||||
@@ -45,33 +65,392 @@ def graphql_endpoint():
|
||||
# Execute the GraphQL query
|
||||
result = devicesSchema.execute(data.get("query"), variables=data.get("variables"))
|
||||
|
||||
# Return the result as JSON
|
||||
return jsonify(result.data)
|
||||
# Initialize response
|
||||
response = {}
|
||||
|
||||
if result.errors:
|
||||
response["errors"] = [str(e) for e in result.errors]
|
||||
if result.data:
|
||||
response["data"] = result.data
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
# --------------------------
|
||||
# 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)
|
||||
|
||||
@app.route("/nettools/traceroute", methods=["POST"])
|
||||
def api_traceroute():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
ip = request.json.get("devLastIP")
|
||||
return traceroute(ip)
|
||||
|
||||
@app.route("/nettools/speedtest", methods=["GET"])
|
||||
def api_speedtest():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return speedtest()
|
||||
|
||||
@app.route("/nettools/nslookup", methods=["POST"])
|
||||
def api_nslookup():
|
||||
"""
|
||||
API endpoint to handle nslookup requests.
|
||||
Expects JSON with 'devLastIP'.
|
||||
"""
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "devLastIP" not in data:
|
||||
return jsonify({"success": False, "error": "Missing 'devLastIP'"}), 400
|
||||
|
||||
ip = data["devLastIP"]
|
||||
return nslookup(ip)
|
||||
|
||||
@app.route("/nettools/nmap", methods=["POST"])
|
||||
def api_nmap():
|
||||
"""
|
||||
API endpoint to handle nmap scan requests.
|
||||
Expects JSON with 'scan' (IP address) and 'mode' (scan mode).
|
||||
"""
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "scan" not in data or "mode" not in data:
|
||||
return jsonify({"success": False, "error": "Missing 'scan' or 'mode'"}), 400
|
||||
|
||||
ip = data["scan"]
|
||||
mode = data["mode"]
|
||||
return nmap_scan(ip, mode)
|
||||
|
||||
|
||||
@app.route("/nettools/internetinfo", methods=["GET"])
|
||||
def api_internet_info():
|
||||
if not is_authorized():
|
||||
return jsonify({"success": False, "error": "Forbidden"}), 403
|
||||
return internet_info()
|
||||
|
||||
# --------------------------
|
||||
# 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/create/<mac>", methods=["POST"])
|
||||
def api_create_event(mac):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.json or {}
|
||||
ip = data.get("ip", "0.0.0.0")
|
||||
event_type = data.get("event_type", "Device Down")
|
||||
additional_info = data.get("additional_info", "")
|
||||
pending_alert = data.get("pending_alert", 1)
|
||||
event_time = data.get("event_time", None)
|
||||
|
||||
# Call the helper to insert into DB
|
||||
create_event(mac, ip, event_type, additional_info, pending_alert, event_time)
|
||||
|
||||
# Return consistent JSON response
|
||||
return jsonify({"success": True, "message": f"Event created for {mac}"})
|
||||
|
||||
|
||||
@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.args.get("mac")
|
||||
return get_events(mac)
|
||||
|
||||
@app.route("/events/<int:days>", methods=["DELETE"])
|
||||
def api_delete_old_events(days: int):
|
||||
"""
|
||||
Delete events older than <days> days.
|
||||
Example: DELETE /events/30
|
||||
"""
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
return delete_events_older_than(days)
|
||||
|
||||
@app.route("/sessions/totals", methods=["GET"])
|
||||
def api_get_events_totals():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
period = get_date_from_period(request.args.get("period", "7 days"))
|
||||
return get_events_totals(period)
|
||||
|
||||
# --------------------------
|
||||
# Sessions
|
||||
# --------------------------
|
||||
|
||||
@app.route("/sessions/create", methods=["POST"])
|
||||
def api_create_session():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
data = request.json
|
||||
mac = data.get("mac")
|
||||
ip = data.get("ip")
|
||||
start_time = data.get("start_time")
|
||||
end_time = data.get("end_time")
|
||||
event_type_conn = data.get("event_type_conn", "Connected")
|
||||
event_type_disc = data.get("event_type_disc", "Disconnected")
|
||||
|
||||
if not mac or not ip or not start_time:
|
||||
return jsonify({"success": False, "error": "Missing required parameters"}), 400
|
||||
|
||||
return create_session(mac, ip, start_time, end_time, event_type_conn, event_type_disc)
|
||||
|
||||
|
||||
@app.route("/sessions/delete", methods=["DELETE"])
|
||||
def api_delete_session():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
mac = request.json.get("mac") if request.is_json else None
|
||||
if not mac:
|
||||
return jsonify({"success": False, "error": "Missing MAC parameter"}), 400
|
||||
|
||||
return delete_session(mac)
|
||||
|
||||
|
||||
@app.route("/sessions/list", methods=["GET"])
|
||||
def api_get_sessions():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
mac = request.args.get("mac")
|
||||
start_date = request.args.get("start_date")
|
||||
end_date = request.args.get("end_date")
|
||||
|
||||
return get_sessions(mac, start_date, end_date)
|
||||
|
||||
@app.route("/sessions/calendar", methods=["GET"])
|
||||
def api_get_sessions_calendar():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
# Query params: /sessions/calendar?start=2025-08-01&end=2025-08-21
|
||||
start_date = request.args.get("start")
|
||||
end_date = request.args.get("end")
|
||||
|
||||
return get_sessions_calendar(start_date, end_date)
|
||||
|
||||
@app.route("/sessions/<mac>", methods=["GET"])
|
||||
def api_device_sessions(mac):
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
period = request.args.get("period", "1 day")
|
||||
return get_device_sessions(mac, period)
|
||||
|
||||
@app.route("/sessions/session-events", methods=["GET"])
|
||||
def api_get_session_events():
|
||||
if not is_authorized():
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
session_event_type = request.args.get("type", "all")
|
||||
period = get_date_from_period(request.args.get("period", "7 days"))
|
||||
return get_session_events(session_event_type, period)
|
||||
|
||||
# --------------------------
|
||||
# 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")
|
||||
return Response(get_metric_stats(), 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 +458,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)
|
||||
|
||||
146
server/api_server/events_endpoint.py
Executable file
146
server/api_server/events_endpoint.py
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/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, format_date_iso, format_event_date, timeNowTZ, mylog, ensure_datetime
|
||||
from db.db_helper import row_to_json, get_date_from_period
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Events Endpoints Functions
|
||||
# --------------------------
|
||||
|
||||
|
||||
def create_event(
|
||||
mac: str,
|
||||
ip: str,
|
||||
event_type: str = "Device Down",
|
||||
additional_info: str = "",
|
||||
pending_alert: int = 1,
|
||||
event_time: datetime | None = None
|
||||
):
|
||||
"""
|
||||
Insert a single event into the Events table and return a standardized JSON response.
|
||||
Exceptions will propagate to the caller.
|
||||
"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
if isinstance(event_time, str):
|
||||
start_time = ensure_datetime(event_time)
|
||||
|
||||
start_time = ensure_datetime(event_time)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType, eve_AdditionalInfo, eve_PendingAlertEmail)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (mac, ip, start_time, event_type, additional_info, pending_alert))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
mylog("debug", f"[Events] Created event for {mac} ({event_type})")
|
||||
return jsonify({"success": True, "message": f"Created event for {mac}"})
|
||||
|
||||
|
||||
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_older_than(days):
|
||||
"""Delete all events older than a specified number of days"""
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Use a parameterized query with sqlite date function
|
||||
sql = "DELETE FROM Events WHERE eve_DateTime <= date('now', ?)"
|
||||
cur.execute(sql, [f'-{days} days'])
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Deleted events older than {days} 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"})
|
||||
|
||||
|
||||
|
||||
def get_events_totals(period: str = "7 days"):
|
||||
"""
|
||||
Return counts for events and sessions totals over a given period.
|
||||
period: "7 days", "1 month", "1 year", "100 years"
|
||||
"""
|
||||
# Convert period to SQLite date expression
|
||||
period_date_sql = get_date_from_period(period)
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql}) AS all_events,
|
||||
(SELECT COUNT(*) FROM Sessions WHERE
|
||||
ses_DateTimeConnection >= {period_date_sql}
|
||||
OR ses_DateTimeDisconnection >= {period_date_sql}
|
||||
OR ses_StillConnected = 1
|
||||
) AS sessions,
|
||||
(SELECT COUNT(*) FROM Sessions WHERE
|
||||
(ses_DateTimeConnection IS NULL AND ses_DateTimeDisconnection >= {period_date_sql})
|
||||
OR (ses_DateTimeDisconnection IS NULL AND ses_StillConnected = 0 AND ses_DateTimeConnection >= {period_date_sql})
|
||||
) AS missing,
|
||||
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'VOIDED%') AS voided,
|
||||
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'New Device') AS new,
|
||||
(SELECT COUNT(*) FROM Events WHERE eve_DateTime >= {period_date_sql} AND eve_EventType LIKE 'Device Down') AS down
|
||||
"""
|
||||
|
||||
cur.execute(sql)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
# Return as JSON array
|
||||
result_json = [row[0], row[1], row[2], row[3], row[4], row[5]]
|
||||
return jsonify(result_json)
|
||||
|
||||
@@ -113,6 +113,7 @@ class Query(ObjectType):
|
||||
try:
|
||||
with open(folder + 'table_devices.json', 'r') as f:
|
||||
devices_data = json.load(f)["data"]
|
||||
total_count = len(devices_data)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog('none', f'[graphql_schema] Error loading devices data: {e}')
|
||||
return DeviceResult(devices=[], count=0)
|
||||
@@ -124,7 +125,7 @@ class Query(ObjectType):
|
||||
device["devParentChildrenCount"] = get_number_of_children(device["devMac"], devices_data)
|
||||
device["devIpLong"] = format_ip_long(device.get("devLastIP", ""))
|
||||
|
||||
mylog('verbose', f'[graphql_schema] devices_data: {devices_data}')
|
||||
mylog('trace', f'[graphql_schema] devices_data: {devices_data}')
|
||||
|
||||
|
||||
# Apply sorting if options are provided
|
||||
@@ -133,16 +134,16 @@ class Query(ObjectType):
|
||||
# Define status-specific filtering
|
||||
if options.status:
|
||||
status = options.status
|
||||
mylog('verbose', f'[graphql_schema] Applying status filter: {status}')
|
||||
mylog('trace', f'[graphql_schema] Applying status filter: {status}')
|
||||
|
||||
# Include devices matching criteria in UI_MY_DEVICES
|
||||
allowed_statuses = get_setting_value("UI_MY_DEVICES")
|
||||
hidden_relationships = get_setting_value("UI_hide_rel_types")
|
||||
network_dev_types = get_setting_value("NETWORK_DEVICE_TYPES")
|
||||
|
||||
mylog('verbose', f'[graphql_schema] allowed_statuses: {allowed_statuses}')
|
||||
mylog('verbose', f'[graphql_schema] hidden_relationships: {hidden_relationships}')
|
||||
mylog('verbose', f'[graphql_schema] network_dev_types: {network_dev_types}')
|
||||
mylog('trace', f'[graphql_schema] allowed_statuses: {allowed_statuses}')
|
||||
mylog('trace', f'[graphql_schema] hidden_relationships: {hidden_relationships}')
|
||||
mylog('trace', f'[graphql_schema] network_dev_types: {network_dev_types}')
|
||||
|
||||
# Filtering based on the "status"
|
||||
if status == "my_devices":
|
||||
@@ -248,7 +249,7 @@ class Query(ObjectType):
|
||||
return SettingResult(settings=[], count=0)
|
||||
|
||||
|
||||
mylog('verbose', f'[graphql_schema] settings_data: {settings_data}')
|
||||
mylog('trace', f'[graphql_schema] settings_data: {settings_data}')
|
||||
|
||||
# Convert to Setting objects
|
||||
settings = [Setting(**setting) for setting in settings_data]
|
||||
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"})
|
||||
222
server/api_server/nettools_endpoint.py
Executable file
222
server/api_server/nettools_endpoint.py
Executable file
@@ -0,0 +1,222 @@
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import ipaddress
|
||||
from flask import jsonify
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
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
|
||||
|
||||
def traceroute(ip):
|
||||
"""
|
||||
Executes a traceroute to the given IP address.
|
||||
|
||||
Parameters:
|
||||
ip (str): The target IP address to trace.
|
||||
|
||||
Returns:
|
||||
JSON response with:
|
||||
- success (bool)
|
||||
- output (str) if successful
|
||||
- error (str) and details (str) if failed
|
||||
"""
|
||||
# --------------------------
|
||||
# Step 1: Validate IP address
|
||||
# --------------------------
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
# Return 400 if IP is invalid
|
||||
return jsonify({"success": False, "error": f"Invalid IP: {ip}"}), 400
|
||||
|
||||
# --------------------------
|
||||
# Step 2: Execute traceroute
|
||||
# --------------------------
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["traceroute", ip], # Command and argument
|
||||
capture_output=True, # Capture stdout/stderr
|
||||
text=True, # Return output as string
|
||||
check=True # Raise CalledProcessError on non-zero exit
|
||||
)
|
||||
# Return success response with traceroute output
|
||||
return jsonify({"success": True, "output": result.stdout.strip()})
|
||||
|
||||
# --------------------------
|
||||
# Step 3: Handle command errors
|
||||
# --------------------------
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Return 500 if traceroute fails
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Traceroute failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def speedtest():
|
||||
"""
|
||||
API endpoint to run a speedtest using speedtest-cli.
|
||||
Returns JSON with the test output or error.
|
||||
"""
|
||||
try:
|
||||
# Run speedtest-cli command
|
||||
result = subprocess.run(
|
||||
[f"{INSTALL_PATH}/back/speedtest-cli", "--secure", "--simple"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
# Return each line as a list
|
||||
output_lines = result.stdout.strip().split("\n")
|
||||
return jsonify({"success": True, "output": output_lines})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Speedtest failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def nslookup(ip):
|
||||
"""
|
||||
Run an nslookup on the given IP address.
|
||||
Returns JSON with the lookup output or error.
|
||||
"""
|
||||
# Validate IP
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid IP address"
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Run nslookup command
|
||||
result = subprocess.run(
|
||||
["nslookup", ip],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output_lines = result.stdout.strip().split("\n")
|
||||
return jsonify({"success": True, "output": output_lines})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "nslookup failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def nmap_scan(ip, mode):
|
||||
"""
|
||||
Run an nmap scan on the given IP address with the requested mode.
|
||||
Modes supported:
|
||||
- "fast" → nmap -F <ip>
|
||||
- "normal" → nmap <ip>
|
||||
- "detail" → nmap -A <ip>
|
||||
- "skipdiscovery" → nmap -Pn <ip>
|
||||
Returns JSON with the scan output or error.
|
||||
"""
|
||||
# Validate IP
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Invalid IP address"
|
||||
}), 400
|
||||
|
||||
# Map scan modes to nmap arguments
|
||||
mode_args = {
|
||||
"fast": ["-F"],
|
||||
"normal": [],
|
||||
"detail": ["-A"],
|
||||
"skipdiscovery": ["-Pn"]
|
||||
}
|
||||
|
||||
if mode not in mode_args:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Invalid scan mode '{mode}'"
|
||||
}), 400
|
||||
|
||||
try:
|
||||
# Build and run nmap command
|
||||
cmd = ["nmap"] + mode_args[mode] + [ip]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output_lines = result.stdout.strip().split("\n")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
"ip": ip,
|
||||
"output": output_lines
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "nmap scan failed",
|
||||
"details": e.stderr.strip()
|
||||
}), 500
|
||||
|
||||
|
||||
def internet_info():
|
||||
"""
|
||||
API endpoint to fetch internet info using ipinfo.io.
|
||||
Returns JSON with the info or error.
|
||||
"""
|
||||
try:
|
||||
# Perform the request via curl
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "https://ipinfo.io"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
output = result.stdout.strip()
|
||||
if not output:
|
||||
raise ValueError("Empty response from ipinfo.io")
|
||||
|
||||
# Clean up the JSON-like string by removing { } , and "
|
||||
cleaned_output = output.replace("{", "").replace("}", "").replace(",", "").replace('"', "")
|
||||
|
||||
return jsonify({"success": True, "output": cleaned_output})
|
||||
|
||||
except (subprocess.CalledProcessError, ValueError) as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Failed to fetch internet info",
|
||||
"details": str(e)
|
||||
}), 500
|
||||
@@ -18,7 +18,7 @@ def escape_label_value(val):
|
||||
# Define a base URL with the user's home directory
|
||||
folder = apiPath
|
||||
|
||||
def getMetricStats():
|
||||
def get_metric_stats():
|
||||
output = []
|
||||
|
||||
# 1. Dashboard totals
|
||||
383
server/api_server/sessions_endpoint.py
Executable file
383
server/api_server/sessions_endpoint.py
Executable file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import time
|
||||
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, format_date_iso, format_event_date, mylog, timeNowTZ, format_date_diff, format_ip_long, parse_datetime
|
||||
from db.db_helper import row_to_json, get_date_from_period
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Sessions Endpoints Functions
|
||||
# --------------------------
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def create_session(mac, ip, start_time, end_time=None, event_type_conn="Connected", event_type_disc="Disconnected"):
|
||||
"""Insert a new session into Sessions table"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO Sessions (ses_MAC, ses_IP, ses_DateTimeConnection, ses_DateTimeDisconnection,
|
||||
ses_EventTypeConnection, ses_EventTypeDisconnection)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (mac, ip, start_time, end_time, event_type_conn, event_type_disc))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": f"Session created for MAC {mac}"})
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def delete_session(mac):
|
||||
"""Delete all sessions for a given MAC"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("DELETE FROM Sessions WHERE ses_MAC = ?", (mac,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": f"Deleted sessions for MAC {mac}"})
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def get_sessions(mac=None, start_date=None, end_date=None):
|
||||
"""Retrieve sessions optionally filtered by MAC and date range"""
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = "SELECT * FROM Sessions WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if mac:
|
||||
sql += " AND ses_MAC = ?"
|
||||
params.append(mac)
|
||||
if start_date:
|
||||
sql += " AND ses_DateTimeConnection >= ?"
|
||||
params.append(start_date)
|
||||
if end_date:
|
||||
sql += " AND ses_DateTimeDisconnection <= ?"
|
||||
params.append(end_date)
|
||||
|
||||
cur.execute(sql, tuple(params))
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Convert rows to list of dicts
|
||||
table_data = [dict(r) for r in rows]
|
||||
|
||||
return jsonify({"success": True, "sessions": table_data})
|
||||
|
||||
|
||||
|
||||
def get_sessions_calendar(start_date, end_date):
|
||||
"""
|
||||
Fetch sessions between a start and end date for calendar display.
|
||||
Returns JSON list of calendar sessions.
|
||||
"""
|
||||
|
||||
if not start_date or not end_date:
|
||||
return jsonify({"success": False, "error": "Missing start or end date"}), 400
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = """
|
||||
-- Correct missing connection/disconnection sessions:
|
||||
-- If ses_EventTypeConnection is missing, backfill from last disconnection
|
||||
-- If ses_EventTypeDisconnection is missing, forward-fill from next connection
|
||||
|
||||
SELECT
|
||||
SES1.ses_MAC, SES1.ses_EventTypeConnection, SES1.ses_DateTimeConnection,
|
||||
SES1.ses_EventTypeDisconnection, SES1.ses_DateTimeDisconnection, SES1.ses_IP,
|
||||
SES1.ses_AdditionalInfo, SES1.ses_StillConnected,
|
||||
|
||||
CASE
|
||||
WHEN SES1.ses_EventTypeConnection = '<missing event>' THEN
|
||||
IFNULL(
|
||||
(SELECT MAX(SES2.ses_DateTimeDisconnection)
|
||||
FROM Sessions AS SES2
|
||||
WHERE SES2.ses_MAC = SES1.ses_MAC
|
||||
AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection
|
||||
AND SES2.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?)
|
||||
),
|
||||
DATETIME(SES1.ses_DateTimeDisconnection, '-1 hour')
|
||||
)
|
||||
ELSE SES1.ses_DateTimeConnection
|
||||
END AS ses_DateTimeConnectionCorrected,
|
||||
|
||||
CASE
|
||||
WHEN SES1.ses_EventTypeDisconnection = '<missing event>' THEN
|
||||
(SELECT MIN(SES2.ses_DateTimeConnection)
|
||||
FROM Sessions AS SES2
|
||||
WHERE SES2.ses_MAC = SES1.ses_MAC
|
||||
AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection
|
||||
AND SES2.ses_DateTimeConnection BETWEEN Date(?) AND Date(?)
|
||||
)
|
||||
ELSE SES1.ses_DateTimeDisconnection
|
||||
END AS ses_DateTimeDisconnectionCorrected
|
||||
|
||||
FROM Sessions AS SES1
|
||||
WHERE (SES1.ses_DateTimeConnection BETWEEN Date(?) AND Date(?))
|
||||
OR (SES1.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?))
|
||||
OR SES1.ses_StillConnected = 1
|
||||
"""
|
||||
|
||||
cur.execute(sql, (start_date, end_date, start_date, end_date, start_date, end_date, start_date, end_date))
|
||||
rows = cur.fetchall()
|
||||
|
||||
table_data = []
|
||||
for r in rows:
|
||||
row = dict(r)
|
||||
|
||||
# Determine color
|
||||
if row["ses_EventTypeConnection"] == "<missing event>" or row["ses_EventTypeDisconnection"] == "<missing event>":
|
||||
color = "#f39c12"
|
||||
elif row["ses_StillConnected"] == 1:
|
||||
color = "#00a659"
|
||||
else:
|
||||
color = "#0073b7"
|
||||
|
||||
# Tooltip
|
||||
tooltip = (
|
||||
f"Connection: {format_event_date(row['ses_DateTimeConnection'], row['ses_EventTypeConnection'])}\n"
|
||||
f"Disconnection: {format_event_date(row['ses_DateTimeDisconnection'], row['ses_EventTypeDisconnection'])}\n"
|
||||
f"IP: {row['ses_IP']}"
|
||||
)
|
||||
|
||||
# Append calendar entry
|
||||
table_data.append({
|
||||
"resourceId": row["ses_MAC"],
|
||||
"title": "",
|
||||
"start": format_date_iso(row["ses_DateTimeConnectionCorrected"]),
|
||||
"end": format_date_iso(row["ses_DateTimeDisconnectionCorrected"]),
|
||||
"color": color,
|
||||
"tooltip": tooltip,
|
||||
"className": "no-border"
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return jsonify({"success": True, "sessions": table_data})
|
||||
|
||||
|
||||
|
||||
def get_device_sessions(mac, period):
|
||||
"""
|
||||
Fetch device sessions for a given MAC address and period.
|
||||
"""
|
||||
period_date = get_date_from_period(period)
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
IFNULL(ses_DateTimeConnection, ses_DateTimeDisconnection) AS ses_DateTimeOrder,
|
||||
ses_EventTypeConnection,
|
||||
ses_DateTimeConnection,
|
||||
ses_EventTypeDisconnection,
|
||||
ses_DateTimeDisconnection,
|
||||
ses_StillConnected,
|
||||
ses_IP,
|
||||
ses_AdditionalInfo
|
||||
FROM Sessions
|
||||
WHERE ses_MAC = ?
|
||||
AND (
|
||||
ses_DateTimeConnection >= {period_date}
|
||||
OR ses_DateTimeDisconnection >= {period_date}
|
||||
OR ses_StillConnected = 1
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
cur.execute(sql, (mac,))
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
|
||||
table_data = {"data": []}
|
||||
|
||||
for row in rows:
|
||||
# Connection DateTime
|
||||
if row["ses_EventTypeConnection"] == "<missing event>":
|
||||
ini = row["ses_EventTypeConnection"]
|
||||
else:
|
||||
ini = format_date(row["ses_DateTimeConnection"])
|
||||
|
||||
# Disconnection DateTime
|
||||
if row["ses_StillConnected"]:
|
||||
end = "..."
|
||||
elif row["ses_EventTypeDisconnection"] == "<missing event>":
|
||||
end = row["ses_EventTypeDisconnection"]
|
||||
else:
|
||||
end = format_date(row["ses_DateTimeDisconnection"])
|
||||
|
||||
# Duration
|
||||
if row["ses_EventTypeConnection"] in ("<missing event>", None) or row["ses_EventTypeDisconnection"] in ("<missing event>", None):
|
||||
dur = "..."
|
||||
elif row["ses_StillConnected"]:
|
||||
dur = format_date_diff(row["ses_DateTimeConnection"], None)["text"]
|
||||
else:
|
||||
dur = format_date_diff(row["ses_DateTimeConnection"], row["ses_DateTimeDisconnection"])["text"]
|
||||
|
||||
# Additional Info
|
||||
info = row["ses_AdditionalInfo"]
|
||||
if row["ses_EventTypeConnection"] == "New Device":
|
||||
info = f"{row['ses_EventTypeConnection']}: {info}"
|
||||
|
||||
# Push row data
|
||||
table_data["data"].append({
|
||||
"ses_MAC": mac,
|
||||
"ses_DateTimeOrder": row["ses_DateTimeOrder"],
|
||||
"ses_Connection": ini,
|
||||
"ses_Disconnection": end,
|
||||
"ses_Duration": dur,
|
||||
"ses_IP": row["ses_IP"],
|
||||
"ses_Info": info,
|
||||
})
|
||||
|
||||
# Control no rows
|
||||
if not table_data["data"]:
|
||||
table_data["data"] = []
|
||||
|
||||
sessions = table_data["data"]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"sessions": sessions
|
||||
})
|
||||
|
||||
|
||||
def get_session_events(event_type, period_date):
|
||||
"""
|
||||
Fetch events or sessions based on type and period.
|
||||
"""
|
||||
conn = get_temp_db_connection()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
# Base SQLs
|
||||
sql_events = f"""
|
||||
SELECT
|
||||
eve_DateTime AS eve_DateTimeOrder,
|
||||
devName,
|
||||
devOwner,
|
||||
eve_DateTime,
|
||||
eve_EventType,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
eve_IP,
|
||||
NULL,
|
||||
eve_AdditionalInfo,
|
||||
NULL,
|
||||
devMac,
|
||||
eve_PendingAlertEmail
|
||||
FROM Events_Devices
|
||||
WHERE eve_DateTime >= {period_date}
|
||||
"""
|
||||
|
||||
sql_sessions = f"""
|
||||
SELECT
|
||||
IFNULL(ses_DateTimeConnection, ses_DateTimeDisconnection) AS ses_DateTimeOrder,
|
||||
devName,
|
||||
devOwner,
|
||||
NULL,
|
||||
NULL,
|
||||
ses_DateTimeConnection,
|
||||
ses_DateTimeDisconnection,
|
||||
NULL,
|
||||
NULL,
|
||||
ses_IP,
|
||||
NULL,
|
||||
ses_AdditionalInfo,
|
||||
ses_StillConnected,
|
||||
devMac
|
||||
FROM Sessions_Devices
|
||||
"""
|
||||
|
||||
# Build SQL based on type
|
||||
if event_type == "all":
|
||||
sql = sql_events
|
||||
elif event_type == "sessions":
|
||||
sql = sql_sessions + f"""
|
||||
WHERE (
|
||||
ses_DateTimeConnection >= {period_date}
|
||||
OR ses_DateTimeDisconnection >= {period_date}
|
||||
OR ses_StillConnected = 1
|
||||
)
|
||||
"""
|
||||
elif event_type == "missing":
|
||||
sql = sql_sessions + f"""
|
||||
WHERE (
|
||||
(ses_DateTimeConnection IS NULL AND ses_DateTimeDisconnection >= {period_date})
|
||||
OR (ses_DateTimeDisconnection IS NULL AND ses_StillConnected = 0 AND ses_DateTimeConnection >= {period_date})
|
||||
)
|
||||
"""
|
||||
elif event_type == "voided":
|
||||
sql = sql_events + ' AND eve_EventType LIKE "VOIDED%"'
|
||||
elif event_type == "new":
|
||||
sql = sql_events + ' AND eve_EventType = "New Device"'
|
||||
elif event_type == "down":
|
||||
sql = sql_events + ' AND eve_EventType = "Device Down"'
|
||||
else:
|
||||
sql = sql_events + ' AND 1=0'
|
||||
|
||||
cur.execute(sql)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
|
||||
table_data = {"data": []}
|
||||
|
||||
for row in rows:
|
||||
row = list(row) # make mutable
|
||||
|
||||
if event_type in ("sessions", "missing"):
|
||||
# Duration
|
||||
if row[5] and row[6]:
|
||||
delta = format_date_diff(row[5], row[6])
|
||||
row[7] = delta["text"]
|
||||
row[8] = int(delta["total_minutes"] * 60) # seconds
|
||||
elif row[12] == 1:
|
||||
delta = format_date_diff(row[5], None)
|
||||
row[7] = delta["text"]
|
||||
row[8] = int(delta["total_minutes"] * 60) # seconds
|
||||
else:
|
||||
row[7] = "..."
|
||||
row[8] = 0
|
||||
|
||||
# Connection
|
||||
row[5] = format_date(row[5]) if row[5] else "<missing event>"
|
||||
|
||||
# Disconnection
|
||||
if row[6]:
|
||||
row[6] = format_date(row[6])
|
||||
elif row[12] == 0:
|
||||
row[6] = "<missing event>"
|
||||
else:
|
||||
row[6] = "..."
|
||||
|
||||
else:
|
||||
# Event Date
|
||||
row[3] = format_date(row[3])
|
||||
|
||||
# IP Order
|
||||
row[10] = format_ip_long(row[9])
|
||||
|
||||
table_data["data"].append(row)
|
||||
|
||||
return jsonify(table_data)
|
||||
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)
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
301
server/helper.py
301
server/helper.py
@@ -7,6 +7,7 @@ import os
|
||||
import re
|
||||
import unicodedata
|
||||
import subprocess
|
||||
from typing import Union
|
||||
import pytz
|
||||
from pytz import timezone
|
||||
import json
|
||||
@@ -16,9 +17,9 @@ import requests
|
||||
import base64
|
||||
import hashlib
|
||||
import random
|
||||
import email
|
||||
import string
|
||||
import ipaddress
|
||||
import dns.resolver
|
||||
|
||||
import conf
|
||||
from const import *
|
||||
@@ -54,20 +55,114 @@ def get_timezone_offset():
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
def updateSubnets(scan_subnets):
|
||||
subnets = []
|
||||
# Date and time methods
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
# multiple interfaces
|
||||
if type(scan_subnets) is list:
|
||||
for interface in scan_subnets :
|
||||
subnets.append(interface)
|
||||
# one interface only
|
||||
# # -------------------------------------------------------------------------------------------
|
||||
# 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')
|
||||
|
||||
# # -------------------------------------------------------------------------------------------
|
||||
# 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
|
||||
|
||||
# days = delta.days
|
||||
# hours, remainder = divmod(delta.seconds, 3600)
|
||||
# minutes = remainder // 60
|
||||
|
||||
# return f"{days}d {hours:02}:{minutes:02}"
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def format_date_iso(date1: str) -> str:
|
||||
"""Return ISO 8601 string for a date or None if empty"""
|
||||
if date1 is None:
|
||||
return None
|
||||
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
|
||||
return dt.isoformat()
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def format_event_date(date_str: str, event_type: str) -> str:
|
||||
"""Format event date with fallback rules."""
|
||||
if date_str:
|
||||
return format_date(date_str)
|
||||
elif event_type == "<missing event>":
|
||||
return "<missing event>"
|
||||
else:
|
||||
subnets.append(scan_subnets)
|
||||
return "<still connected>"
|
||||
|
||||
return subnets
|
||||
# -------------------------------------------------------------------------------------------
|
||||
def ensure_datetime(dt: Union[str, datetime, None]) -> datetime:
|
||||
if dt is None:
|
||||
return timeNowTZ()
|
||||
if isinstance(dt, str):
|
||||
return datetime.datetime.fromisoformat(dt)
|
||||
return dt
|
||||
|
||||
|
||||
def parse_datetime(dt_str):
|
||||
if not dt_str:
|
||||
return None
|
||||
try:
|
||||
# Try ISO8601 first
|
||||
return datetime.datetime.fromisoformat(dt_str)
|
||||
except ValueError:
|
||||
# Try RFC1123 / HTTP format
|
||||
try:
|
||||
return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT')
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def format_date(date_str: str) -> str:
|
||||
dt = parse_datetime(date_str)
|
||||
return dt.strftime('%Y-%m-%d %H:%M') if dt else "invalid"
|
||||
|
||||
def format_date_diff(date1, date2):
|
||||
"""
|
||||
Return difference between two datetimes as 'Xd HH:MM'.
|
||||
Uses app timezone if datetime is naive.
|
||||
date2 can be None (uses now).
|
||||
"""
|
||||
# Get timezone from settings
|
||||
tz_name = get_setting_value("TIMEZONE") or "UTC"
|
||||
tz = pytz.timezone(tz_name)
|
||||
|
||||
def parse_dt(dt):
|
||||
if dt is None:
|
||||
return datetime.datetime.now(tz)
|
||||
if isinstance(dt, str):
|
||||
try:
|
||||
dt_parsed = email.utils.parsedate_to_datetime(dt)
|
||||
except Exception:
|
||||
# fallback: parse ISO string
|
||||
dt_parsed = datetime.datetime.fromisoformat(dt)
|
||||
# convert naive GMT/UTC to app timezone
|
||||
if dt_parsed.tzinfo is None:
|
||||
dt_parsed = tz.localize(dt_parsed)
|
||||
else:
|
||||
dt_parsed = dt_parsed.astimezone(tz)
|
||||
return dt_parsed
|
||||
return dt if dt.tzinfo else tz.localize(dt)
|
||||
|
||||
dt1 = parse_dt(date1)
|
||||
dt2 = parse_dt(date2)
|
||||
|
||||
delta = dt2 - dt1
|
||||
total_minutes = int(delta.total_seconds() // 60)
|
||||
days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day
|
||||
hours, minutes = divmod(rem_minutes, 60)
|
||||
|
||||
return {
|
||||
"text": f"{days}d {hours:02}:{minutes:02}",
|
||||
"days": days,
|
||||
"hours": hours,
|
||||
"minutes": minutes,
|
||||
"total_minutes": total_minutes
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# File system permission handling
|
||||
@@ -217,12 +312,6 @@ def get_setting(key):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Settings
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Return setting value
|
||||
def get_setting_value(key):
|
||||
@@ -248,8 +337,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 +428,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 +471,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 +508,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 +563,6 @@ def removeDuplicateNewLines(text):
|
||||
return text
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
def sanitize_string(input):
|
||||
if isinstance(input, bytes):
|
||||
input = input.decode('utf-8')
|
||||
@@ -482,15 +570,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 +580,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 +614,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 +627,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 +641,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 +683,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 +692,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):
|
||||
@@ -632,31 +704,6 @@ def collect_lang_strings(json, pref, stringSqlParams):
|
||||
|
||||
return stringSqlParams
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Misc
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
def print_table_schema(db, table):
|
||||
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)
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
def checkNewVersion():
|
||||
mylog('debug', [f"[Version check] Checking if new version available"])
|
||||
@@ -698,22 +745,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:
|
||||
@@ -275,6 +275,15 @@ def importConfigs (db, all_plugins):
|
||||
# Save the user defined value into the object
|
||||
set["value"] = v
|
||||
|
||||
# Now check for popupForm inside elements → elementOptions
|
||||
elements = set.get("type", {}).get("elements", [])
|
||||
for element in elements:
|
||||
for option in element.get("elementOptions", []):
|
||||
if "popupForm" in option:
|
||||
for popup_entry in option["popupForm"]:
|
||||
popup_pref = key + "_popupform_" + popup_entry.get("function", "")
|
||||
stringSqlParams = collect_lang_strings(popup_entry, popup_pref, stringSqlParams)
|
||||
|
||||
# Collect settings related language strings
|
||||
# Creates an entry with key, for example ARPSCAN_CMD_name
|
||||
stringSqlParams = collect_lang_strings(set, pref + "_" + set["function"], stringSqlParams)
|
||||
|
||||
@@ -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
|
||||
148
test/test_events_endpoints.py
Executable file
148
test/test_events_endpoints.py
Executable file
@@ -0,0 +1,148 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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):
|
||||
payload = {"ip": "0.0.0.0", "event_type": event}
|
||||
|
||||
# Calculate the event_time if days_old is given
|
||||
if days_old is not None:
|
||||
event_time = timeNowTZ() - timedelta(days=days_old)
|
||||
# ISO 8601 string
|
||||
payload["event_time"] = event_time.isoformat()
|
||||
|
||||
return client.post(f"/events/create/{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={mac}"
|
||||
return client.get(url, headers=auth_headers(api_token))
|
||||
|
||||
def test_create_event(client, api_token, test_mac):
|
||||
# create event
|
||||
resp = create_event(client, api_token, test_mac)
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data.get("success") is True
|
||||
|
||||
# confirm event exists
|
||||
resp = list_events(client, api_token, test_mac)
|
||||
assert resp.status_code == 200
|
||||
events = resp.get_json().get("events", [])
|
||||
assert any(ev.get("eve_MAC") == test_mac for ev in events)
|
||||
|
||||
|
||||
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
|
||||
events = resp.json.get("events", [])
|
||||
assert any(ev["eve_MAC"] == test_mac for ev in events)
|
||||
|
||||
# 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.get("events", [])) == 0
|
||||
|
||||
def test_get_events_totals(client, api_token):
|
||||
# 1. Request totals with default period
|
||||
resp = client.get(
|
||||
"/sessions/totals",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
data = resp.json
|
||||
assert isinstance(data, list)
|
||||
# Expecting 6 counts: all_events, sessions, missing, voided, new, down
|
||||
assert len(data) == 6
|
||||
for count in data:
|
||||
assert isinstance(count, int) # each should be a number
|
||||
|
||||
# 2. Request totals with custom period
|
||||
resp_month = client.get(
|
||||
"/sessions/totals?period=1 month",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp_month.status_code == 200
|
||||
data_month = resp_month.json
|
||||
assert isinstance(data_month, list)
|
||||
assert len(data_month) == 6
|
||||
|
||||
|
||||
|
||||
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.get("events", [])) == 0
|
||||
|
||||
|
||||
def test_delete_events_dynamic_days(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/30", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
assert "Deleted events older than 30 days" in resp.json.get("message", "")
|
||||
|
||||
# confirm only recent remains
|
||||
resp = list_events(client, api_token, test_mac)
|
||||
events = resp.get_json().get("events", [])
|
||||
mac_events = [ev for ev in events if ev.get("eve_MAC") == test_mac]
|
||||
assert len(mac_events) == 1
|
||||
|
||||
|
||||
93
test/test_graphq_endpoints.py
Executable file
93
test/test_graphq_endpoints.py
Executable file
@@ -0,0 +1,93 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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_graphql_debug_get(client):
|
||||
"""GET /graphql should return the debug string"""
|
||||
resp = client.get("/graphql")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.decode() == "NetAlertX GraphQL server running."
|
||||
|
||||
def test_graphql_post_unauthorized(client):
|
||||
"""POST /graphql without token should return 401"""
|
||||
query = {"query": "{ devices { devName devMac } }"}
|
||||
resp = client.post("/graphql", json=query)
|
||||
assert resp.status_code == 401
|
||||
assert "Unauthorized access attempt" in resp.json.get("error", "")
|
||||
|
||||
def test_graphql_post_devices(client, api_token):
|
||||
"""POST /graphql with a valid token should return device data"""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
devices {
|
||||
devices {
|
||||
devGUID
|
||||
devGroup
|
||||
devIsRandomMac
|
||||
devParentChildrenCount
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
resp = client.post("/graphql", json=query, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
body = resp.get_json()
|
||||
|
||||
# GraphQL spec: response always under "data"
|
||||
assert "data" in body
|
||||
data = body["data"]
|
||||
|
||||
assert "devices" in data
|
||||
assert isinstance(data["devices"]["devices"], list)
|
||||
assert isinstance(data["devices"]["count"], int)
|
||||
|
||||
def test_graphql_post_settings(client, api_token):
|
||||
"""POST /graphql should return settings data"""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
settings {
|
||||
settings { setKey setValue setGroup }
|
||||
count
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
resp = client.post("/graphql", json=query, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json.get("data", {})
|
||||
assert "settings" in data
|
||||
assert isinstance(data["settings"]["settings"], list)
|
||||
@@ -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
|
||||
203
test/test_nettools_endpoints.py
Executable file
203
test/test_nettools_endpoints.py
Executable file
@@ -0,0 +1,203 @@
|
||||
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", "")
|
||||
|
||||
def test_speedtest_endpoint(client, api_token):
|
||||
# 1. Call the speedtest endpoint
|
||||
resp = client.get("/nettools/speedtest", headers=auth_headers(api_token))
|
||||
|
||||
# 2. Assertions
|
||||
if resp.status_code == 403:
|
||||
# Unauthorized access
|
||||
data = resp.json
|
||||
assert "error" in data
|
||||
assert data["error"] == "Forbidden"
|
||||
else:
|
||||
# Expect success
|
||||
assert resp.status_code == 200
|
||||
data = resp.json
|
||||
assert data.get("success") is True
|
||||
assert "output" in data
|
||||
assert isinstance(data["output"], list)
|
||||
# Optionally check that output lines are strings
|
||||
assert all(isinstance(line, str) for line in data["output"])
|
||||
|
||||
def test_traceroute_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
|
||||
device_ip = devices[0].get("devLastIP", "192.168.1.1") # fallback if dummy has no IP
|
||||
|
||||
# 4. Call the traceroute endpoint
|
||||
resp = client.post(
|
||||
"/nettools/traceroute",
|
||||
json={"devLastIP": device_ip},
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
|
||||
# 5. Assertions
|
||||
if not device_ip or device_ip.lower() == 'invalid':
|
||||
# Expect 400 if IP is missing or invalid
|
||||
assert resp.status_code == 400
|
||||
data = resp.json
|
||||
assert data.get("success") is False
|
||||
else:
|
||||
# Expect 200 and valid traceroute output
|
||||
assert resp.status_code == 200
|
||||
data = resp.json
|
||||
assert data.get("success") is True
|
||||
assert "output" in data
|
||||
assert isinstance(data["output"], str)
|
||||
|
||||
@pytest.mark.parametrize("ip,expected_status", [
|
||||
("8.8.8.8", 200),
|
||||
("256.256.256.256", 400), # Invalid IP
|
||||
("", 400), # Missing IP
|
||||
])
|
||||
def test_nslookup_endpoint(client, api_token, ip, expected_status):
|
||||
payload = {"devLastIP": ip} if ip else {}
|
||||
resp = client.post("/nettools/nslookup", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
assert resp.status_code == expected_status
|
||||
data = resp.json
|
||||
|
||||
if expected_status == 200:
|
||||
assert data.get("success") is True
|
||||
assert isinstance(data["output"], list)
|
||||
assert all(isinstance(line, str) for line in data["output"])
|
||||
else:
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
|
||||
@pytest.mark.parametrize("ip,mode,expected_status", [
|
||||
("127.0.0.1", "fast", 200),
|
||||
("127.0.0.1", "normal", 200),
|
||||
("127.0.0.1", "detail", 200),
|
||||
("127.0.0.1", "skipdiscovery", 200),
|
||||
("127.0.0.1", "invalidmode", 400),
|
||||
("999.999.999.999", "fast", 400),
|
||||
])
|
||||
def test_nmap_endpoint(client, api_token, ip, mode, expected_status):
|
||||
payload = {"scan": ip, "mode": mode}
|
||||
resp = client.post("/nettools/nmap", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
assert resp.status_code == expected_status
|
||||
data = resp.json
|
||||
|
||||
if expected_status == 200:
|
||||
assert data.get("success") is True
|
||||
assert data.get("mode") == mode
|
||||
assert data.get("ip") == ip
|
||||
assert isinstance(data["output"], list)
|
||||
assert all(isinstance(line, str) for line in data["output"])
|
||||
else:
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
|
||||
def test_nslookup_unauthorized(client):
|
||||
# No auth headers
|
||||
resp = client.post("/nettools/nslookup", json={"devLastIP": "8.8.8.8"})
|
||||
assert resp.status_code == 403
|
||||
data = resp.json
|
||||
assert data.get("success") is False
|
||||
assert data.get("error") == "Forbidden"
|
||||
|
||||
def test_nmap_unauthorized(client):
|
||||
# No auth headers
|
||||
resp = client.post("/nettools/nmap", json={"scan": "127.0.0.1", "mode": "fast"})
|
||||
assert resp.status_code == 403
|
||||
data = resp.json
|
||||
assert data.get("success") is False
|
||||
assert data.get("error") == "Forbidden"
|
||||
|
||||
|
||||
def test_internet_info_endpoint(client, api_token):
|
||||
resp = client.get("/nettools/internetinfo", headers=auth_headers(api_token))
|
||||
data = resp.json
|
||||
|
||||
if resp.status_code == 200:
|
||||
assert data.get("success") is True
|
||||
assert isinstance(data.get("output"), str)
|
||||
assert len(data["output"]) > 0 # ensure output is not empty
|
||||
else:
|
||||
# Handle errors, e.g., curl failure
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
assert "details" in data
|
||||
257
test/test_sessions_endpoints.py
Executable file
257
test/test_sessions_endpoints.py
Executable file
@@ -0,0 +1,257 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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_create_session(client, api_token, test_mac):
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowTZ(),
|
||||
"event_type_conn": "Connected",
|
||||
"event_type_disc": "Disconnected"
|
||||
}
|
||||
resp = client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
# -----------------------------
|
||||
def test_list_sessions(client, api_token, test_mac):
|
||||
# Ensure at least one session exists
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowTZ()
|
||||
}
|
||||
client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
# List sessions for MAC
|
||||
resp = client.get(f"/sessions/list?mac={test_mac}", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
sessions = resp.json.get("sessions")
|
||||
assert any(ses["ses_MAC"] == test_mac for ses in sessions)
|
||||
|
||||
|
||||
def test_device_sessions_by_period(client, api_token, test_mac):
|
||||
# 1. Create a dummy session so we have data
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.200",
|
||||
"start_time": timeNowTZ()
|
||||
}
|
||||
resp_create = client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
assert resp_create.status_code == 200
|
||||
assert resp_create.json.get("success") is True
|
||||
|
||||
# 2. Query sessions for the device with a valid period
|
||||
resp = client.get(
|
||||
f"/sessions/{test_mac}?period=7 days",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
data = resp.json
|
||||
assert data.get("success") is True
|
||||
assert "sessions" in data
|
||||
|
||||
sessions = data["sessions"]
|
||||
|
||||
print(sessions)
|
||||
print(test_mac)
|
||||
|
||||
assert isinstance(sessions, list)
|
||||
assert any(s["ses_MAC"] == test_mac for s in sessions)
|
||||
|
||||
|
||||
def test_device_session_events(client, api_token, test_mac):
|
||||
"""
|
||||
Test fetching session/events from the /sessions/session-events endpoint.
|
||||
"""
|
||||
|
||||
# 1. Create a dummy session to ensure we have data
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.250",
|
||||
"start_time": timeNowTZ()
|
||||
}
|
||||
resp_create = client.post(
|
||||
"/sessions/create",
|
||||
json=payload,
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp_create.status_code == 200
|
||||
assert resp_create.json.get("success") is True
|
||||
|
||||
# 2. Fetch session events with default type ('all') and period ('7 days')
|
||||
resp = client.get(
|
||||
f"/sessions/session-events?type=all&period=7 days",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
data = resp.json
|
||||
assert "data" in data # table data key
|
||||
events = data["data"]
|
||||
|
||||
# 3. Validate the response structure
|
||||
assert isinstance(events, list)
|
||||
|
||||
# If there is at least one row, check fields for sessions
|
||||
if events:
|
||||
row = events[0]
|
||||
# Expecting row as list with at least expected columns
|
||||
assert isinstance(row, list)
|
||||
# IP and datetime fields should exist
|
||||
assert row[9] # IP column
|
||||
assert row[3] # Event datetime column
|
||||
|
||||
# 4. Optionally, test filtering by session type
|
||||
resp_sessions = client.get(
|
||||
"/sessions/session-events?type=sessions&period=7 days",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp_sessions.status_code == 200
|
||||
sessions = resp_sessions.json["data"]
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
# -----------------------------
|
||||
def test_delete_session(client, api_token, test_mac):
|
||||
# First create session
|
||||
payload = {
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": timeNowTZ()
|
||||
}
|
||||
client.post("/sessions/create", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
# Delete session
|
||||
resp = client.delete("/sessions/delete", json={"mac": test_mac}, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
# Confirm deletion
|
||||
resp = client.get(f"/sessions/list?mac={test_mac}", headers=auth_headers(api_token))
|
||||
sessions = resp.json.get("sessions")
|
||||
assert not any(ev["ses_MAC"] == test_mac for ses in sessions)
|
||||
|
||||
|
||||
|
||||
def test_get_sessions_calendar(client, api_token, test_mac):
|
||||
"""
|
||||
Test the /sessions/calendar endpoint.
|
||||
Creates session and ensures the calendar output is correct.
|
||||
Cleans up test sessions after test.
|
||||
"""
|
||||
|
||||
|
||||
# --- Setup: create two sessions for the test MAC ---
|
||||
now = timeNowTZ()
|
||||
start1 = (now - timedelta(days=2)).isoformat(timespec="seconds")
|
||||
end1 = (now - timedelta(days=1, hours=20)).isoformat(timespec="seconds")
|
||||
|
||||
start2 = (now - timedelta(days=1)).isoformat(timespec="seconds")
|
||||
end2 = (now - timedelta(hours=20)).isoformat(timespec="seconds")
|
||||
|
||||
# Create sessions using your endpoint
|
||||
client.post("/sessions/create", json={
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": start1,
|
||||
"end_time": end1,
|
||||
"event_type_conn": "connect",
|
||||
"event_type_disc": "disconnect"
|
||||
}, headers=auth_headers(api_token))
|
||||
|
||||
client.post("/sessions/create", json={
|
||||
"mac": test_mac,
|
||||
"ip": "192.168.1.100",
|
||||
"start_time": start2,
|
||||
"end_time": end2,
|
||||
"event_type_conn": "connect",
|
||||
"event_type_disc": "disconnect"
|
||||
}, headers=auth_headers(api_token))
|
||||
|
||||
# --- Call the /sessions/calendar API ---
|
||||
start_date = (now - timedelta(days=3)).strftime("%Y-%m-%d")
|
||||
end_date = (now + timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
|
||||
resp = client.get(
|
||||
f"/sessions/calendar?start={start_date}&end={end_date}",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json
|
||||
assert data.get("success") is True
|
||||
assert "sessions" in data
|
||||
sessions = data["sessions"]
|
||||
|
||||
# --- Verify calendar sessions ---
|
||||
assert len(sessions) >= 2 # at least our two sessions
|
||||
|
||||
# Check expected keys
|
||||
expected_keys = {"resourceId", "title", "start", "end", "color", "tooltip", "className"}
|
||||
for ses in sessions:
|
||||
assert set(ses.keys()) == expected_keys
|
||||
|
||||
# Check that all sessions belong to the test MAC
|
||||
mac_sessions = [ses for ses in sessions if ses["resourceId"] == test_mac]
|
||||
assert len(mac_sessions) == 2 # or exact number if you know it
|
||||
|
||||
# Check ISO date formatting for start/end
|
||||
for ses in mac_sessions:
|
||||
# start must always be present
|
||||
assert ses["start"] is not None, f"Session start is None: {ses}"
|
||||
datetime.fromisoformat(ses["start"])
|
||||
|
||||
# end can be None only if tooltip mentions "<still connected>"
|
||||
if ses["end"] is not None:
|
||||
datetime.fromisoformat(ses["end"])
|
||||
else:
|
||||
assert "<still connected>" in ses["tooltip"], f"End is None but session not marked as still connected: {ses}"
|
||||
|
||||
# --- Cleanup: delete all test sessions for this MAC ---
|
||||
client.delete(f"/sessions/delete?mac={test_mac}", headers=auth_headers(api_token))
|
||||
Reference in New Issue
Block a user