Emperor Claudius

### Changelog

#### May 2025

- Update Drop In Events (FTP)
  - Allow API Key ending only in @
  - Clean up file and folder processing for trigger
  - Better Uploaded content cleanup
- Fix memory leak possibility in camera thread
- Add extender for onOnvifEventTrigger (not enabled)
- Fix timelapse frame path builder in cron
- Fix too long column insertion on Videos table objects column
- Remove fps changer in simple mode changer api endpoint
- Update actCheck.js

#### April 2025

- Fix default object detection dimensions at 1280x720
- Merge branch 'dev' into 'dev'
  - Added pl language (translated by an LLM)
- Clean up central connector, prevents connecting multiple times at start
- Added pl language (translated by an LLM)
- Fix some npm vulnerabilities
- Fix event filters getting broken in cleanStringsInObject
  - Make Event Filters disable submit on save
  - Change region editor to use configureMonitor function
- Add some debugging code to createEventBasedRecording
- Fix refactored Central Connector when lost connection
- General fixes on monitor startup
- Add missing Custom Settings table creation
- Make central connector only get IPv4 and ignore internal
- Cleanup some logging and spacing
- Add bad cseq log drop to prevent browser log flooding
- Fix failing input_map parse on some monitors, modernize some details
- Update pairServer.js
- Allow Central Connection without SSH
- Refactor central management connector
- Make Max Storage Amount a human inputable/readable value
- Fix broken monitor utils
- Allow Commas in cleanStringsInObject function
- Add "Alarms" logging/actions and PTZ Updates
  - Fix Alarms tab preview video link
  - Update alarmPopup.ejs
  - Make form dark on Alarm Popup
  - Clean up Alarm gamepad
  - Add height to Alarm popup
  - Add download button to Alarm Popup video
  - Add details from first event to alarm
  - Use normal form instead of save on change in alarm popup
  - Remove console.log from getEventBasedRecordingUponCompletion
  - Make Alarms use normal Videos instead of Notification video + Gamepad PTZ
  - Add multiple monitors logged to Alarm and updating Alarm
  - Fix timezone in alarm popup, add limit query option to Alarms listing
  - Alarms and Event-Based PTZ (Working 80%)
  - Alarms (Framework only) and Event-Based PTZ and Utility updates
- Add Max Days for Cloud Video Uploaders
- Make fetch ptz command provide response data
- Change color of status progress bar
- Central SSH reconnect with delay
- Add SSH Proxy Capability to Central Management

#### March 2025

- Fix libs/ffmpeg in gitignore
- Add option to periodically reset management connection
- Add offline activator
- Remove language loaded from account settings
- Add WireGuard VPN scripts (server uses docker)
- Key manages camera count
- Allow "&" in monitor config strings
- Allow "?" in monitor config strings
- Add server ip parse for Central Connect
- Fix Branding by removing User-Level language selection
- Reverse Videos list when merging to ensure proper order
- Save Frame from FTP Trigger in Timelapse
- Reapply "Fix Cross-site scripting vulnerability in Monitor Edit" (Fixed)
- Revert "Fix Cross-site scripting vulnerability in Monitor Edit"
- Update getVideoSearchRequestQueries to have operators
- Clean up Videos Table Search Execution
- Fix Cross-site scripting vulnerability in Monitor Edit
- Remove DB_DISABLE_INCLUDED from Docker image

#### February 2025

- Make Monitor Settings post with websocket instead of ajax
- Clean up websocket callback on complete
- Ignore ffmpeg folder within Shinobi folder (ffbinaries download)
- Add a cmd tool to mass modify monitor configs with a template
- Update removeSenstiveInfoFromMonitorConfig
- Allow Connecting Multiple Central Servers
- API Key Management Upgrades
  - Add API Endpoint for getting a single row
  - Update Central API Key Creation
  - Fix Central API Key acquisition
  - Upgrade API Key Management: Edit User Settings and Permission Sets
  - Upgrade API Key Management: Permission to allow managing API
  - Upgrade API Key Management: Permissions and Editing
- Add Custom Settings API
- Clean up getMonitors API and add websocket method
- Permission Groups + Websocket API for Editing Monitor
  - Add or Edit Monitor over Websocket with callback
  - Add method to add/edit Monitors with websocket
  - Fix applyPermissionsToUser in createSession for API Keys
  - Void failed proc.stdin.write("q\\r\\n")
  - Allow API Key Management of Sub-Accounts by Admin
  - Clean up selecting Monitors in Permission Groups
  - Add User Permission Management by Group
  - Fix permissions to view and edit Permission Groups
  - Change Sub-Account Monitor select to Table
- Load Recent Videos once on Dashboard Ready
merge-requests/534/merge
Moe 2025-06-03 13:19:09 +06:00
parent 37ebdba546
commit 7ede7ef208
133 changed files with 14088 additions and 8268 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ generatedLanguageFiles
faces
unknownFaces
.idea/
/ffmpeg

View File

@ -90,7 +90,7 @@ docker run -d --name='Shinobi' --memory=2g -p '8080:8080/tcp' -p '21:21/tcp' -v
> You must add (to the docker container) `/config/ssl/server.key` and `/config/ssl/server.cert`. The `/config` folder is mapped to `$HOME/Shinobi/config` on the host by default with the quick run methods. Place `key` and `cert` in `$HOME/Shinobi/config/ssl`. If `SSL_ENABLED=true` and these files don't exist they will be generated with `openssl`.
> For those using `DB_DISABLE_INCLUDED=true` please remember to create a user in your databse first. The Docker image will create the `DB_DATABASE` under the specified connection information.
> The Docker image will create the `DB_DATABASE` under the specified connection information.
### Power Video Viewer Blank or Not working

View File

@ -18,51 +18,47 @@ if [ "$SSL_ENABLED" = "true" ]; then
else
SSL_CONFIG='{}'
fi
if [ "$DB_DISABLE_INCLUDED" = "false" ]; then
echo "MariaDB Directory ..."
ls /var/lib/mysql
echo "MariaDB Directory ..."
ls /var/lib/mysql
if [ ! -f /var/lib/mysql/ibdata1 ]; then
echo "Installing MariaDB ..."
mysql_install_db --user=mysql --datadir=/var/lib/mysql --silent
fi
echo "Starting MariaDB ..."
/usr/bin/mysqld_safe --user=mysql &
sleep 5s
chown -R mysql /var/lib/mysql
if [ ! -f /var/lib/mysql/ibdata1 ]; then
mysql -u root --password="" -e "SET @@SESSION.SQL_LOG_BIN=0;
USE mysql;
DELETE FROM mysql.user ;
DROP USER IF EXISTS 'root'@'%','root'@'localhost','${DB_USER}'@'localhost','${DB_USER}'@'%';
CREATE USER 'root'@'%' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER 'root'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'%' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'localhost' WITH GRANT OPTION ;
DROP DATABASE IF EXISTS test ;
FLUSH PRIVILEGES ;"
fi
# Create MySQL database if it does not exists
if [ -n "${DB_HOST}" ]; then
echo "Wait for MySQL server" ...
while ! mysqladmin ping -h"$DB_HOST"; do
sleep 1
done
fi
echo "Create database user if it does not exists ..."
mysql -e "source /home/Shinobi/sql/user.sql" || true
else
echo "Create database schema if it does not exists ..."
if [ ! -f /var/lib/mysql/ibdata1 ]; then
echo "Installing MariaDB ..."
mysql_install_db --user=mysql --datadir=/var/lib/mysql --silent
fi
echo "Starting MariaDB ..."
/usr/bin/mysqld_safe --user=mysql &
sleep 5s
chown -R mysql /var/lib/mysql
if [ ! -f /var/lib/mysql/ibdata1 ]; then
mysql -u root --password="" -e "SET @@SESSION.SQL_LOG_BIN=0;
USE mysql;
DELETE FROM mysql.user ;
DROP USER IF EXISTS 'root'@'%','root'@'localhost','${DB_USER}'@'localhost','${DB_USER}'@'%';
CREATE USER 'root'@'%' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER 'root'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASS}' ;
CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'%' WITH GRANT OPTION ;
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'localhost' WITH GRANT OPTION ;
DROP DATABASE IF EXISTS test ;
FLUSH PRIVILEGES ;"
fi
# Create MySQL database if it does not exists
if [ -n "${DB_HOST}" ]; then
echo "Wait for MySQL server" ...
while ! mysqladmin ping -h"$DB_HOST"; do
sleep 1
done
fi
echo "Create database user if it does not exists ..."
mysql -e "source /home/Shinobi/sql/user.sql" || true
DATABASE_CONFIG='{"host": "'$DB_HOST'","user": "'$DB_USER'","password": "'$DB_PASSWORD'","database": "'$DB_DATABASE'","port":'$DB_PORT'}'

View File

@ -18,8 +18,7 @@ ENV DB_USER=majesticflame \
SSL_LOCATION='Vancouver' \
SSL_ORGANIZATION='Shinobi Systems' \
SSL_ORGANIZATION_UNIT='IT Department' \
SSL_COMMON_NAME='nvr.ninja' \
DB_DISABLE_INCLUDED=$EXCLUDE_DB
SSL_COMMON_NAME='nvr.ninja'
WORKDIR /home/Shinobi
COPY . ./

View File

@ -2,8 +2,9 @@
if [ -e "INSTALL/installed.txt" ]; then
echo "Starting Shinobi"
pm2 start camera.js
pm2 save
#pm2 start cron.js
pm2 logs
pm2 list
fi
if [ ! -e "INSTALL/installed.txt" ]; then
chmod +x INSTALL/now.sh&&INSTALL/now.sh

View File

@ -57,6 +57,8 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
require('./libs/monitor.js')(s,config,lang)
//event functions : motion, object matrix handler
require('./libs/events.js')(s,config,lang)
//alarm events
require('./libs/alarms.js')(s,config,lang,app)
//recording functions
require('./libs/videos.js')(s,config,lang)
//plugins : websocket connected services..

55
definitions/alarms.js Normal file
View File

@ -0,0 +1,55 @@
module.exports = (s,config,lang) => {
const {
yesNoPossibility,
} = require('./fieldValues.js')(s,config,lang);
return {
"section": "Alarms",
"blocks": {
"Alarms Search Settings": {
"name": lang["Alarms"],
"color": "green",
"section-pre-class": "col-md-4",
"info": [
{
"field": lang["Monitor"],
"fieldType": "select",
"class": "monitors_list",
"possible": []
},
{
"class": "date_selector",
"field": lang.Date,
},
{
"fieldType": "btn-group",
"btns": [
{
"fieldType": "btn",
"class": `btn-success fill refresh-data mb-3`,
"icon": `refresh`,
"btnContent": `${lang['Refresh']}`,
},
],
},
{
"fieldType": "div",
"id": "alarms_preview_area",
"divContent": ""
},
]
},
"theTable": {
noHeader: true,
"section-pre-class": "col-md-8",
"info": [
{
"fieldType": "table",
"attribute": `data-classes="table table-striped"`,
"id": "alarms_draw_area",
"divContent": ""
}
]
},
}
}
}

179
definitions/apiKeys.js Normal file
View File

@ -0,0 +1,179 @@
module.exports = (s,config,lang) => {
const {
yesNoPossibility,
} = require('./fieldValues.js')(s,config,lang);
return {
"section": "API Keys",
"blocks": {
"API Keys": {
"name": lang['API Keys'],
"color": "blue",
"isSection": true,
"id":"apiKeySectionList",
"info": [
{
"fieldType": "div",
"attribute": `style="max-height: 600px;overflow-y: auto;overflow-x: hidden;"`,
"id": "api_list",
}
]
},
"Add New": {
"name": `<span class="title">${lang['Add New']}</span>`,
"color": "forestgreen",
"isSection": true,
"isForm": true,
"id":"apiKeySectionAddNew",
"info": [
{
hidden: true,
"name": "code",
"fieldType": "text"
},
{
"name": "ip",
"field": lang['Allowed IPs'],
"default": `0.0.0.0`,
"placeholder": `0.0.0.0 ${lang['for Global Access']}`,
"description": lang[lang["fieldTextIp"]],
"fieldType": "text"
},
{
"name": "detail=treatAsSub",
"field": lang['Treated as Sub-Account'],
"default": "0",
"fieldType": "select",
"selector": "h_apiKey_treatAsSub",
"notForSubAccount": true,
"possible": yesNoPossibility,
},
{
"name": "detail=permissionSet",
"field": lang['Permission Group'],
"default": "",
"description": lang.fieldTextPermissionGroup,
"fieldType": "select",
// "notForSubAccount": true,
"possible": [
{
"name": lang.Default,
"value": "",
"info": lang.Default
},
{
"name": lang['Saved Permissions'],
"optgroup": []
}
]
},
{
"id": "apiKey_permissions",
"field": lang['Permissions'],
"default": "",
"fieldType": "select",
"attribute": `multiple style="height:150px;"`,
"possible": [
{
name: lang['Can Authenticate Websocket'],
value: 'auth_socket',
},
{
name: lang['Can Create API Keys'],
value: 'create_api_keys',
},
{
name: lang['Can Change User Settings'],
value: 'edit_user',
},
{
name: lang['Can Edit Permissions'],
value: 'edit_permissions',
},
{
name: lang['Can Get Monitors'],
value: 'get_monitors',
},
{
name: lang['Can Edit Monitors'],
value: 'edit_monitors',
},
{
name: lang['Can Control Monitors'],
value: 'control_monitors',
},
{
name: lang['Can Get Logs'],
value: 'get_logs',
},
{
name: lang['Can View Streams'],
value: 'watch_stream',
},
{
name: lang['Can View Snapshots'],
value: 'watch_snapshot',
},
{
name: lang['Can View Videos'],
value: 'watch_videos',
},
{
name: lang['Can Delete Videos'],
value: 'delete_videos',
},
{
name: lang['Can View Alarms'],
value: 'get_alarms',
},
{
name: lang['Can Edit Alarms'],
value: 'edit_alarms',
},
]
},
{
"name": "detail=monitorsRestricted",
"field": lang['Restricted Monitors'],
"default": "0",
"fieldType": "select",
"selector": "h_apiKey_monitorsRestricted",
// "notForSubAccount": true,
"possible": yesNoPossibility,
},
// {
// "forForm": true,
// "fieldType": "btn",
// "class": `btn-success`,
// "attribute": `type="submit"`,
// "btnContent": `<i class="fa fa-plus"></i> &nbsp; ${lang['Add New']}`,
// },
// {
// "forForm": true,
// "fieldType": "btn",
// "class": `btn-primary reset-form`,
// "attribute": `type="button"`,
// "btnContent": `<i class="fa fa-refresh"></i> &nbsp; ${lang['Clear']}`,
// },
]
},
"Monitors": {
noHeader: true,
styles: "display:none;",
"section-class": "search-parent h_apiKey_monitorsRestricted_input h_apiKey_monitorsRestricted_1",
"color": "green",
"info": [
{
"field": lang.Monitors,
"placeholder": lang.Search,
"class": "search-controller",
},
{
"fieldType": "table",
"class": "search-body",
id: "apiKeys_monitors",
},
]
},
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
module.exports = (s,config,lang) => {
const yesNoPossibility = [
{ "name": lang.No, "value": "0" },
{ "name": lang.Yes, "value": "1" }
];
function addMenuItem(newMenuItem, afterPageOpen){
// const newMenuItem = {
// icon: 'barcode',
// label: `${lang['Power Viewer']}`,
// pageOpen: 'powerVideo',
// }
const sideMenuContents = s.definitions.SideMenu.blocks.Container1.links;
const linkIndex = sideMenuContents.findIndex(x => x.pageOpen === afterPageOpen);
sideMenuContents.splice(linkIndex + 1, 0, newMenuItem)
}
return {
yesNoPossibility,
addMenuItem,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
module.exports = (s,config,lang) => {
const {
yesNoPossibility,
} = require('./fieldValues.js')(s,config,lang);
return {
"section": "User Permissions",
"blocks": {
"Info": {
"name": lang["User Permissions"],
"color": "blue",
"blockquoteClass": "global_tip",
"blockquote": lang.userPermissionsPageText,
"info": [
{
"fieldType": "btn",
"attribute": `page-open="userAccounts"`,
"class": `btn-primary`,
"btnContent": `<i class="fa fa-clock-o"></i> &nbsp; ${lang["User Accounts"]}`,
},
]
},
"Permissions": {
noHeader: true,
"color": "green",
"info": [
{
"id": "userPermissionsSelector",
"field": lang["Permissions"],
"fieldType": "select",
"possible": [
{
"name": lang['Add New'],
"value": ""
},
{
"name": lang['Saved Permissions'],
"optgroup": []
},
]
},
]
},
"Preset": {
"name": lang["Permission"],
"color": "green",
"info": [
{
"fieldType": "btn",
"attribute": `type="button" style="display:none"`,
"class": `btn-danger delete`,
"btnContent": `<i class="fa fa-trash"></i> &nbsp; ${lang.Delete}`,
},
{
"name": "name",
"field": lang.Name,
"example": lang.Operator,
},
{
"name": "detail=allmonitors",
"field": lang['All Monitors and Privileges'],
"default": "0",
"fieldType": "select",
"selector": "h_perm_allmonitors",
"possible": yesNoPossibility
},
{
"name": "detail=monitor_create",
"field": lang['Can Create and Delete Monitors'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=user_change",
"field": lang['Can Change User Settings'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=view_logs",
"field": lang['Can View Logs'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=edit_permissions",
"field": lang['Can Edit Permissions'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
}
]
},
"Monitors": {
noHeader: true,
"section-class": "search-parent",
"color": "green",
"info": [
{
"field": lang.Monitors,
"placeholder": lang.Search,
"class": "search-controller",
},
{
"fieldType": "table",
"class": "search-body",
id: "permissionSets_monitors",
},
]
},
}
}
}

186
definitions/regionEditor.js Normal file
View File

@ -0,0 +1,186 @@
module.exports = (s,config,lang) => {
const {
yesNoPossibility,
} = require('./fieldValues.js')(s,config,lang);
return {
"section": "Region Editor",
"blocks": {
"Regions": {
"color": "green",
isFormGroupGroup: true,
"noHeader": true,
"section-class": "col-md-6",
"noDefaultSectionClasses": true,
"info": [
{
"name": lang["Regions"],
"headerTitle": `<span class="cord_name">&nbsp;</span>
<div class="pull-right">
<a href=# class="btn btn-success btn-sm add"><i class="fa fa-plus"></i></a>
<a href=# class="btn btn-danger btn-sm erase"><i class="fa fa-trash-o"></i></a>
</div>`,
"color": "orange",
"box-wrapper-class": "row",
isFormGroupGroup: true,
"info": [
{
"field": lang["Monitor"],
"id": "region_editor_monitors",
"fieldType": "select",
"form-group-class": "col-md-6",
},
{
"id": "regions_list",
"field": lang["Regions"],
"fieldType": "select",
"possible": [],
"form-group-class": "col-md-6",
},
{
"name": "name",
"field": lang['Region Name'],
},
{
"name": "sensitivity",
"field": lang['Minimum Change'],
"form-group-class": "col-md-6",
},
{
"name": "max_sensitivity",
"field": lang['Maximum Change'],
"form-group-class": "col-md-6",
},
{
"name": "threshold",
"field": lang['Trigger Threshold'],
"form-group-class": "col-md-6",
},
{
"name": "color_threshold",
"field": lang['Color Threshold'],
"form-group-class": "col-md-6",
},
{
hidden: true,
id: "regions_points",
"fieldType": "table",
"class": 'table table-striped',
},
{
"class": 'col-md-12',
"fieldType": 'div',
info: [
{
"fieldType": "btn",
attribute: "href=#",
"class": `btn-info toggle-region-still-image`,
"btnContent": `<i class="fa fa-retweet"></i> &nbsp; ${lang['Live Stream Toggle']}`,
},
{
"fieldType": "btn",
forForm: true,
attribute: "href=#",
"class": `btn-success`,
"btnContent": `<i class="fa fa-check"></i> &nbsp; ${lang['Save']}`,
},
]
},
]
},
{
"name": lang["Primary"],
"color": "blue",
"section-class": "hide-box-wrapper",
"box-wrapper-class": "row",
isFormGroupGroup: true,
"info": [
{
"name": "detail=detector_sensitivity",
"field": lang['Minimum Change'],
"description": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".",
"default": "10",
"example": "10",
},
{
"name": "detail=detector_max_sensitivity",
"field": lang["Maximum Change"],
"description": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".",
"default": "",
"example": "75",
},
{
"name": "detail=detector_threshold",
"field": lang["Trigger Threshold"],
"description": lang["fieldTextDetectorThreshold"],
"default": "1",
"example": "3",
"possible": "Any non-negative integer."
},
{
"name": "detail=detector_color_threshold",
"field": lang["Color Threshold"],
"description": lang["fieldTextDetectorColorThreshold"],
"default": "9",
"example": "9",
"possible": "Any non-negative integer."
},
{
"name": "detail=detector_frame",
"field": lang["Full Frame Detection"],
"description": lang["fieldTextDetectorFrame"],
"default": "1",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=detector_motion_tile_mode",
"field": lang['Accuracy Mode'],
"default": "1",
"example": "",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=detector_tile_size",
"field": lang["Tile Size"],
"description": lang.fieldTextTileSize,
"default": "20",
},
{
"name": "detail=use_detector_filters",
"field": lang['Event Filters'],
"description": lang.fieldTextEventFilters,
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=use_detector_filters_object",
"field": lang['Filter for Objects only'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
]
},
]
},
"Points": {
"name": lang["Points"],
"color": "orange",
"section-pre-class": "col-md-6",
"style": "overflow:auto",
"blockquoteClass": "global_tip",
"blockquote": lang.RegionNote,
"info": [
{
"fieldType": "div",
class: "canvas_holder",
divContent: `<div id="region_editor_live"><iframe></iframe><img></div>
<div class="grid"></div><textarea id="regions_canvas" rows=3 class="hidden canvas-area input-xxlarge" disabled></textarea>`,
}
]
}
}
}
}

View File

@ -0,0 +1,195 @@
module.exports = (s,config,lang) => {
const {
yesNoPossibility,
} = require('./fieldValues.js')(s,config,lang);
return {
"section": "Sub-Account Manager",
"blocks": {
"Sub-Accounts": {
"name": lang['Sub-Accounts'],
"color": "orange",
"isSection": true,
"id":"monSectionAccountList",
"info": [
{
"fieldType": "div",
"style": "max-height: 400px;overflow: auto;",
id: "subAccountsList",
}
]
},
// "Currently Active": {
// "name": lang['Currently Active'],
// "section-pre-class": "col-md-6 search-parent",
// "color": "green",
// "isSection": true,
// "info": [
// {
// "field": lang['Search'],
// "class": 'search-controller',
// },
// {
// "fieldType": "div",
// "class": "search-body",
// "id": "currently-active-users",
// "attribute": `style="max-height: 400px;overflow: auto;"`,
// }
// ]
// },
"Account Information": {
"name": lang['Account Information'],
"color": "blue",
"isSection": true,
"isForm": true,
"id":"monSectionAccountInformation",
"info": [
{
hidden: true,
"name": "uid",
"field": "UID",
"fieldType": "text"
},
{
"name": "mail",
"field": lang.Email,
"fieldType": "text",
"default": "",
"possible": ""
},
{
"name": "pass",
"field": lang.Password,
"fieldType": "password",
"default": "",
"possible": ""
},
{
"name": "password_again",
"field": lang['Password Again'],
"fieldType": "password",
"default": "",
"possible": ""
},
{
forForm: true,
"fieldType": "btn",
"attribute": `type="reset"`,
"class": `btn-default reset-form`,
"btnContent": `<i class="fa fa-undo"></i> &nbsp; ${lang['Clear']}`,
},
{
"fieldType": "btn",
"class": `btn-success submit-form`,
"btnContent": `<i class="fa fa-plus"></i> &nbsp; ${lang['Add New']}`,
},
{
hidden: true,
"name": "details",
"preFill": "{}",
},
]
},
"Account Privileges": {
"name": lang['Account Privileges'],
"color": "red",
"isSection": true,
"id":"monSectionAccountPrivileges",
"info": [
{
"name": "detail=permissionSet",
"field": lang['Permission Group'],
"default": "",
"description": lang.fieldTextPermissionGroup,
"fieldType": "select",
"selector": "h_perm_permissionSet",
"possible": [
{
"name": lang.Default,
"value": "",
"info": lang.Default
},
{
"name": lang['Saved Permissions'],
"optgroup": []
}
]
},
{
"name": "detail=allmonitors",
"field": lang['All Monitors and Privileges'],
"default": "0",
"fieldType": "select",
"selector": "h_perm_allmonitors",
"possible": yesNoPossibility
},
{
"name": "detail=monitor_create",
"field": lang['Can Create and Delete Monitors'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=user_change",
"field": lang['Can Change User Settings'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=view_logs",
"field": lang['Can View Logs'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=edit_permissions",
"field": lang['Can Edit Permissions'],
"default": "0",
"fieldType": "select",
"possible": yesNoPossibility
},
{
"name": "detail=landing_page",
"field": lang['Landing Page'],
"default": "",
"fieldType": "select",
"possible": [
{
"name": lang.Default,
"value": ""
},
{
"name": lang.Timelapse,
"value": "timelapse"
}
]
},
]
},
"Monitors": {
noHeader: true,
"section-class": "search-parent h_perm_allmonitors_input h_perm_allmonitors_1",
"color": "green",
"info": [
{
"field": lang.Monitors,
"placeholder": lang.Search,
"class": "search-controller",
},
{
"fieldType": "btn",
"class": `btn-success submit-form`,
"btnContent": `<i class="fa fa-plus"></i> &nbsp; ${lang['Add New']}`,
},
{
"fieldType": "table",
"class": "search-body",
id: "sub_accounts_permissions",
},
]
},
}
}
}

View File

@ -16,6 +16,7 @@
"monitorDeleted": "Monitor Deleted",
"accountCreationError": "Account Creation Error",
"accountEditError": "Account Edit Error",
"No Snippet Found": "No Snippet Found",
"Monitor Map": "Monitor Map",
"Geolocation": "Geolocation",
"Configure": "Configure",
@ -74,6 +75,7 @@
"Failed to Edit Account": "Failed to Edit Account",
"How to Connect": "How to Connect",
"Cycle": "Cycle",
"Auto Placement": "Auto Placement",
"Cycle Interval": "Cycle Interval",
"Rows and Columns": "Rows and Columns",
"Number of Monitors": "Number of Monitors",
@ -272,6 +274,7 @@
"Order Streams": "Order Streams",
"Original Aspect Ratio": "Original Aspect Ratio",
"Remember Positions": "Remember Positions",
"Event Opens Alarm": "Event Opens Alarm",
"Hide Notes": "Hide Notes",
"Example": "Example",
"Logout": "Logout",
@ -340,6 +343,7 @@
"Group Key": "Group Key",
"Allowed IPs": "Allowed IPs",
"Separate with commas, no spaces": "Separate with commas, no spaces",
"Can Create API Keys": "Can Create API Keys",
"Can Get Monitors": "Can Get Monitors",
"Can Get Logs": "Can Get Logs",
"Can Authenticate Websocket": "Can Authenticate Websocket",
@ -349,10 +353,16 @@
"Can View Streams": "Can View Streams",
"Can View Videos": "Can View Videos",
"Can View Monitor": "Can View Monitor",
"Can Edit Permissions": "Can Edit Permissions",
"Can Change User Settings": "Can Change User Settings",
"Can Create and Delete Monitors": "Can Create and Delete Monitors",
"Can Edit Monitor": "Can Edit Monitor",
"Can Delete Videos": "Can Delete Videos",
"Can View Alarms": "Can View Alarms",
"Can Edit Alarms": "Can Edit Alarms",
"Delete Alarm": "Delete Alarm",
"Delete Alarms": "Delete Alarms",
"Update Alarm": "Update Alarm",
"Delete Video": "Delete Video",
"Delete Videos": "Delete Videos",
"Batch Download": "Batch Download",
@ -362,6 +372,12 @@
"Can Delete Videos and Events": "Can Delete Videos and Events",
"Saved Filters": "Saved Filters",
"Saved Presets": "Saved Presets",
"Start Patrol": "Start Patrol",
"Stop Patrol": "Stop Patrol",
"Add Preset": "Add Preset",
"PTZ Presets": "PTZ Presets",
"Save PTZ Preset": "Save PTZ Preset",
"Delete PTZ Preset": "Delete PTZ Preset",
"Saved Schedules": "Saved Schedules",
"Filter Name": "Filter Name",
"Find Where": "Find Where",
@ -531,6 +547,7 @@
"SFTP (SSH File Transfer)": "SFTP (SSH File Transfer)",
"SFTP Error": "SFTP Error",
"SFTP": "SFTP",
"FTPMonitorIdNotFound": "Monitor ID Not Found or Not Active. Check the Monitor ID configured in the FTP settings.",
"accountSettingsError": "Account Settings Error",
"Could not create Bucket.": "Could not create Bucket.",
"Amazon S3": "Amazon S3",
@ -601,6 +618,7 @@
"Recent Videos": "Recent Videos",
"Videos List": "Videos List",
"Monitor Settings": "Monitor Settings",
"Alarms": "Alarms",
"Enlarge": "Enlarge",
"Fullscreen": "Fullscreen",
"Value": "Value",
@ -677,10 +695,20 @@
"Start Time cannot be empty.": "Start Time cannot be empty.",
"Must be atleast one row": "Must be atleast one row",
"InvalidJSONText": "Please ensure this is a valid JSON string for Shinobi monitor configuration.",
"Passwords don't match": "Passwords don't match",
"Passwords Don't Match": "Passwords Don't Match",
"Email address is in use.": "Email address is in use.",
"Account Created": "Account Created",
"Account Edited": "Account Edited",
"Edited By": "Edited By",
"Attention": "Attention",
"Acknowledged": "Acknowledged",
"InProgress": "In Progress",
"Resolved": "Resolved",
"Cleared": "Cleared",
"Dismissed": "Dismissed",
"Verified": "Verified",
"Escalated": "Escalated",
"FalseAlarm": "False Alarm",
"Compression Info": "Compression Info",
"Compression Error": "Compression Error",
"Group Key is in use.": "Group Key is in use.",
@ -699,6 +727,7 @@
"monitorEditFailedMaxReached": "Your account has reached the maximum number of cameras that can be created. Speak to an administrator if you would like this changed.",
"monitorEditFailedMaxReachedUnactivated": "Your system has reached the maximum number of cameras that can be created. You must activate your installation to create more.",
"Sub-Accounts": "Sub-Accounts",
"Treated as Sub-Account": "Treated as Sub-Account",
"Stream in Background": "Stream in Background",
"Carousel in Background": "Carousel in Background",
"Last": "Last",
@ -846,6 +875,8 @@
"HLS List Size": "List Size",
"Recording Complete": "Recording Complete",
"Event-Based Recording": "Event-Based Recording",
"Event-Based PTZ": "Event-Based PTZ",
"eventBasedPtzDescription": "When this Monitor has an Event Trigger you can have a separate Monitor automatically PTZ to a specified ONVIF Preset. The specified Monitor must have ONVIF capability and must already be configured to allow movement via their own Preset selection.",
"Recorded Buffer": "Recorded Buffer",
"Buffer Preview": "Buffer Preview",
"HLS Start Number": "HLS Start Number",
@ -1996,5 +2027,21 @@
"rejectUnauth": "Ignore server certificate",
"Central Management" : "Central Management",
"centralManagementSaved" : "Settings saved. Restarting connection to Central Managment Server.",
"centralManagementNotEnabled" : "Management Server connectivity is not enabled. Activate your installation and add <code>\"enableMgmtConnect\": true</code> to your conf.json."
"centralManagementNotEnabled" : "Management Server connectivity is not enabled. Activate your installation and add <code>\"enableMgmtConnect\": true</code> to your conf.json.",
"Failed Action": "Failed Action",
"Operator": "Operator",
"Permission": "Permission",
"Permission Group": "Permission Group",
"fieldTextPermissionGroup": "Setting this to anything other than Default will override any choices made specifically for this account.",
"Delete User": "Delete User",
"Delete Permission": "Delete Permission",
"All Permissions": "All Permissions",
"Saved Permissions": "Saved Permissions",
"Permission Groups": "Permission Groups",
"User Permissions": "User Permissions",
"User Accounts": "User Accounts",
"Restricted Monitors": "Restricted Monitors",
"Available Pages": "Available Pages",
"userPermissionsPageText": "Here you can create Permission Groups for this Management Panel. To set add a User to these Permission Groups access the User Accounts page.",
"userAccountsPageText": "Here you can create Sub-Accounts. These accounts can have full access or limited access based on your selections when creating or editing them. To create Permission Groups you can access the User Permissions page."
}

1942
languages/pl.json Normal file

File diff suppressed because it is too large Load Diff

207
libs/alarms.js Normal file
View File

@ -0,0 +1,207 @@
module.exports = function(s,config,lang,app){
if(config.alarmManagement){
const {
getAlarm,
createAlarm,
updateAlarm,
deleteAlarm,
sanitizeOperator,
} = require('./events/alarms.js')(s,config,lang);
const {
getAssociatedMonitorPtzTargets,
getEventBasedRecordingsUponCompletion,
} = require('./events/utils.js')(s,config,lang)
const {
addMenuItem,
} = require('../definitions/fieldValues.js')(s,config,lang);
if(config.renderPaths.alarmPopup === undefined){config.renderPaths.alarmPopup='pages/alarmPopup'};
const onGoingAlarms = {};
const onGoingAlarmTimeouts = {};
function sendWebsocketMessage(type, data){
const sendData = Object.assign({ f: type }, data)
s.tx(sendData,`GRP_${data.ke}`);
}
s.onEventTrigger(function(d,filter,eventTime){
const groupKey = d.ke
const monitorId = d.id || d.mid;
const alarmTarget = `${groupKey}${monitorId}`
if(!onGoingAlarms[alarmTarget]){
const startTime = s.formattedTime(eventTime);
onGoingAlarms[alarmTarget] = { startTime };
const createData = {
ke: groupKey,
mid: monitorId,
time: startTime,
details: d.details
};
const associatedMonitors = [monitorId, ...Object.keys(getAssociatedMonitorPtzTargets(groupKey, monitorId))]
createAlarm(createData)
sendWebsocketMessage('alarm_updated',createData)
getEventBasedRecordingsUponCompletion(groupKey, associatedMonitors, false, true, true).then((recordedFiles) => {
const updateData = {
ke: groupKey,
mid: monitorId,
time: startTime,
videos: recordedFiles,
}
updateAlarm(updateData);
sendWebsocketMessage('alarm_updated',updateData)
})
}
clearTimeout(onGoingAlarmTimeouts[alarmTarget])
onGoingAlarmTimeouts[alarmTarget] = setTimeout(() => {
const { startTime } = onGoingAlarms[alarmTarget];
const endTime = new Date();
const updateData = {
ke: groupKey,
mid: monitorId,
time: startTime,
end: s.formattedTime(endTime)
}
updateAlarm(updateData).then(() => {
sendWebsocketMessage('alarm_updated',updateData)
delete(onGoingAlarms[alarmTarget]);
delete(onGoingAlarmTimeouts[alarmTarget]);
})
},10000)
})
/**
* API : Get Alarm(s)
*/
app.get([
config.webPaths.apiPrefix+':auth/alarms/:ke',
config.webPaths.apiPrefix+':auth/alarms/:ke/:id',
], function (req,res){
res.setHeader('Content-Type', 'application/json');
s.auth(req.params, async function(user){
const monitorId = req.params.id
const groupKey = req.params.ke
const {
monitorPermissions,
monitorRestrictions,
} = s.getMonitorsPermitted(user.details,monitorId)
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
if(
isRestrictedApiKey && apiKeyPermissions.get_alarms_disallowed ||
isRestricted && (
monitorId && !monitorPermissions[`${monitorId}_get_alarms`] ||
monitorRestrictions.length === 0
)
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], alarms: []});
return
}
const { name, start, startOperator, end, endOperator, limit } = req.query;
const response = { ok: true }
const rows = await getAlarm({
ke: groupKey,
mid: monitorId,
name,
start,
end,
startOperator: sanitizeOperator(startOperator),
endOperator: sanitizeOperator(endOperator),
limit,
});
response.alarms = rows;
s.closeJsonResponse(res,response)
})
})
/**
* API : Update Alarm
*/
app.post(config.webPaths.apiPrefix+':auth/alarms/:ke/:id', function (req,res){
res.setHeader('Content-Type', 'application/json');
s.auth(req.params, async function(user){
const monitorId = req.params.id
const groupKey = req.params.ke
const {
monitorPermissions,
monitorRestrictions,
} = s.getMonitorsPermitted(user.details,monitorId)
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
if(
isRestrictedApiKey && apiKeyPermissions.edit_alarms_disallowed ||
isRestricted && (
monitorId && !monitorPermissions[`${monitorId}_edit_alarms`] ||
monitorRestrictions.length === 0
)
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], alarms: []});
return
}
const { name, videos, videoTime, notes, status, editedBy, details, time, start, end } = req.body;
const response = await updateAlarm({
ke: groupKey,
mid: monitorId,
name,
videos: s.parseJSON(videos),
videoTime,
notes,
status,
editedBy: user.uid,
details,
time,
start,
end,
});
s.closeJsonResponse(res,response)
})
})
/**
* Page : Get Alarm Popup Window
*/
app.get([config.webPaths.apiPrefix+':auth/alarm/:ke/:id',config.webPaths.apiPrefix+':auth/alarm/:ke/:id/:addon'], function (req,res){
s.auth(req.params,function(user){
const { auth: authKey, ke: groupKey, id: monitorId } = req.params;
var $user = {
auth_token: authKey,
ke: groupKey,
uid: user.uid,
mail: user.mail,
details: {},
};
s.renderPage(req,res,config.renderPaths.alarmPopup,{
forceUrlPrefix: req.query.host || '',
protocol: req.protocol,
baseUrl: req.protocol+'://'+req.hostname,
config: s.getConfigWithBranding(req.hostname),
define: s.getDefinitonFile(user.details ? user.details.lang : config.lang),
lang,
$user,
groupKey,
monitorId,
monitor: Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId]),
originalURL: s.getOriginalUrl(req)
});
},res,req);
});
//
addMenuItem({
icon: 'pencil-square-o',
label: `${lang['Alarms']}`,
pageOpen: 'alarms',
addUl: true,
ulItems: [
{
label: lang['Event Opens Alarm'],
class: 'cursor-pointer',
attributes: 'shinobi-switch="alarmOpenedByEvent" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"',
color: 'grey',
},
]
},'videosTableView');
config.webBlocksPreloaded.push('home/alarms');
}
}

View File

@ -1,5 +1,8 @@
var fs = require('fs');
module.exports = function(s,config,lang){
const {
applyPermissionsToUser,
} = require('./user/permissionSets.js')(s,config,lang)
//Authenticator functions
s.api = {}
s.superUsersApi = {}
@ -78,17 +81,17 @@ module.exports = function(s,config,lang){
var isSessionKey = false
if(apiKey){
var sessionKey = params.auth
getUserByUid(apiKey,'mail,details',function(err,user){
getUserByUid(apiKey,'mail,details',async function(err,user){
if(user){
createSession(apiKey,{
await createSession(apiKey,{
auth: sessionKey,
permissions: s.parseJSON(apiKey.details),
permissions: s.parseJSON(apiKey.details) || {},
mail: user.mail,
details: s.parseJSON(user.details),
lang: s.getLanguageFile(user.details.lang)
})
}else{
createSession(apiKey,{
await createSession(apiKey,{
auth: sessionKey,
permissions: s.parseJSON(apiKey.details),
details: {}
@ -97,9 +100,9 @@ module.exports = function(s,config,lang){
callback(err,s.api[params.auth])
})
}else{
getUserBySessionKey(params,function(err,user){
getUserBySessionKey(params,async function(err,user){
if(user){
createSession(user,{
await createSession(user,{
auth: params.auth,
details: JSON.parse(user.details),
isSessionKey: true,
@ -113,7 +116,7 @@ module.exports = function(s,config,lang){
}
})
}
var createSession = function(user,additionalData){
var createSession = async function(user,additionalData){
if(user){
var generatedId
if(!additionalData)additionalData = {}
@ -124,8 +127,14 @@ module.exports = function(s,config,lang){
generatedId = user.auth || user.code
}
user.details = s.parseJSON(user.details)
const apiKeyPermissions = additionalData.permissions || {};
const permissionSet = apiKeyPermissions.permissionSet;
const treatAsSub = apiKeyPermissions.treatAsSub === '1';
if(permissionSet)additionalData.details.permissionSet = permissionSet;
if(treatAsSub)additionalData.details.sub = '1';
user.permissions = {}
s.api[generatedId] = Object.assign({},user,additionalData)
await applyPermissionsToUser(s.api[generatedId])
return generatedId
}
}
@ -172,10 +181,6 @@ module.exports = function(s,config,lang){
params.ip && (params.ip.indexOf(activeSession.ip) > -1)
)
){
if(!user.lang){
var details = s.parseJSON(user.details).lang
user.lang = s.getLanguageFile(user.details.lang) || s.copySystemDefaultLanguage()
}
onSuccessComplete(user)
}else{
onFail()
@ -184,9 +189,6 @@ module.exports = function(s,config,lang){
if(s.group[params.ke] && s.group[params.ke].users && s.group[params.ke].users[params.auth] && s.group[params.ke].users[params.auth].details){
var activeSession = s.group[params.ke].users[params.auth]
activeSession.permissions = {}
if(!activeSession.lang){
activeSession.lang = s.copySystemDefaultLanguage()
}
onSuccessComplete(activeSession)
}else if(s.api[params.auth] && s.api[params.auth].details){
var activeSession = s.api[params.auth]
@ -195,10 +197,10 @@ module.exports = function(s,config,lang){
resetActiveSessionTimer(activeSession)
}
}else if(params.username && params.username !== '' && params.password && params.password !== ''){
loginWithUsernameAndPassword(params,'*',function(err,user){
loginWithUsernameAndPassword(params,'*',async function(err,user){
if(user){
params.auth = user.auth
createSession(user)
await createSession(user)
resetActiveSessionTimer(s.api[params.auth])
onSuccess(user)
}else{
@ -253,7 +255,7 @@ module.exports = function(s,config,lang){
ip : ip,
$user: userSelected,
config: chosenConfig,
lang: lang
lang
})
}
if(params.auth && Object.keys(s.superUsersApi).indexOf(params.auth) > -1){
@ -300,7 +302,7 @@ module.exports = function(s,config,lang){
}
s.basicOrApiAuthentication = function(username,password,callback){
var splitUsername = username.split('@')
if(splitUsername[1] && splitUsername[1].toLowerCase().indexOf('shinobi') > -1){
if(username.endsWith('@') || (splitUsername[1] && splitUsername[1].toLowerCase().indexOf('shinobi') > -1)){
getApiKey({
auth: splitUsername[0],
ke: password

View File

@ -108,7 +108,7 @@ module.exports = (s,config,lang,app) => {
app.get(config.webPaths.apiPrefix+':auth/loginTokenAddGoogle/:ke', function (req,res){
s.auth(req.params,(user) => {
s.renderPage(req,res,config.renderPaths.loginTokenAddGoogle,{
lang: lang,
lang,
config: s.getConfigWithBranding(req.hostname),
$user: user
})

View File

@ -159,7 +159,7 @@ module.exports = (s,config,lang,app) => {
app.get(config.webPaths.apiPrefix+':auth/loginTokenAddLDAP/:ke', function (req,res){
s.auth(req.params,(user) => {
s.renderPage(req,res,config.renderPaths.loginTokenAddLDAP,{
lang: lang,
lang,
define: s.getDefinitonFile(user.details.lang),
config: s.getConfigWithBranding(req.hostname),
$user: user

View File

@ -152,8 +152,6 @@ module.exports = function(s,config,lang){
function twoFactorVerification(params){
const response = { ok: false }
const factorAuthKey = (params.factorAuthKey || '00').trim()
console.log(params)
console.log(s.factorAuth[params.ke][params.id])
if(
s.factorAuth[params.ke] &&
s.factorAuth[params.ke][params.id] &&
@ -182,8 +180,7 @@ module.exports = function(s,config,lang){
}
}
const pageTarget = factorAuthObject.function
factorAuthObject.info.lang = s.getLanguageFile(userDetails.lang)
response.info = Object.assign(factorAuthObject.info,{})
response.info = Object.assign({},factorAuthObject.info)
clearTimeout(factorAuthObject.expireAuth)
s.deleteFactorAuth({
ke: params.ke,

View File

@ -136,71 +136,6 @@ module.exports = (processCwd,config) => {
}
return theRequester(requestUrl,requestOptions)
}
const checkSubscription = (subscriptionId,callback,suppressCheckNotice = false) => {
function subscriptionFailed(){
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
console.error('This Install of Shinobi is NOT Activated')
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
s.systemLog('This Install of Shinobi is NOT Activated')
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
console.log('https://licenses.shinobi.video/subscribe')
}
if(subscriptionId && subscriptionId !== 'sub_XXXXXXXXXXXX' && !config.disableOnlineSubscriptionCheck){
var url = 'https://licenses.shinobi.video/subscribe/check?subscriptionId=' + subscriptionId
var hasSubcribed = false
fetchTimeout(url,30000,{
method: 'GET',
})
.then(response => response.text())
.then(function(body){
var json = s.parseJSON(body)
hasSubcribed = json && !!json.ok
var i;
for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) {
const extender = s.onSubscriptionCheckExtensions[i]
hasSubcribed = extender(hasSubcribed,json,subscriptionId)
}
callback(hasSubcribed)
if(hasSubcribed){
if(!suppressCheckNotice){
s.systemLog('This Install of Shinobi is Activated')
if(!json.expired && json.timeExpires){
s.systemLog(`This License expires on ${json.timeExpires}`)
}
}
}else{
subscriptionFailed()
}
}).catch((err) => {
if(err)console.log(err)
subscriptionFailed()
callback(false)
})
}else{
var i;
for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) {
const extender = s.onSubscriptionCheckExtensions[i]
hasSubcribed = extender(false,{},subscriptionId)
}
if(hasSubcribed === false){
subscriptionFailed()
}
callback(hasSubcribed)
}
}
function checkAgainSubscription(){
let checkCount = 1
return setInterval(function(){
if(checkCount === 28){
checkSubscription(config.subscriptionId || config.peerConnectKey || config.p2pApiKey, function(hasSubcribed){
config.userHasSubscribed = hasSubcribed
}, true);
checkCount = 1;
}
++checkCount;
}, 1000 * 60 * 60 * 24);
}
function isEven(value) {
if (value%2 == 0)
return true;
@ -273,6 +208,86 @@ module.exports = (processCwd,config) => {
console.error(`Error deleting files: ${error.message}`);
}
}
function setTimeoutPromise(theTime){
return new Promise((resolve) => {
setTimeout(() => {
resolve()
},theTime)
})
}
function cleanStringsInObject(obj, isWithinDetectorFilters = false) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// Check if we're entering the detector_filters structure
const enteringDetectorFilters = !isWithinDetectorFilters &&
(key === 'detector_filters' ||
(key === 'details' &&
typeof obj[key] === 'object' &&
obj[key].hasOwnProperty('detector_filters')));
// Determine if we're currently within detector_filters
let currentIsWithinDetectorFilters = isWithinDetectorFilters || enteringDetectorFilters;
// Special handling for stringified detector_filters
if ((key === 'detector_filters' || (key === 'details' && obj[key].hasOwnProperty('detector_filters')))) {
const detectorFiltersTarget = key === 'details' ? obj[key] : obj;
const detectorFiltersKey = key === 'details' ? 'detector_filters' : key;
if (typeof detectorFiltersTarget[detectorFiltersKey] === 'string') {
try {
const parsed = JSON.parse(detectorFiltersTarget[detectorFiltersKey]);
detectorFiltersTarget[detectorFiltersKey] = cleanStringsInObject(parsed, true);
continue; // Skip further processing as we've replaced and cleaned it
} catch (e) {
currentIsWithinDetectorFilters = true;
}
}
}
if (typeof obj[key] === 'string') {
try {
const parsed = JSON.parse(obj[key]);
obj[key] = cleanStringsInObject(parsed, currentIsWithinDetectorFilters);
} catch (e) {
if (currentIsWithinDetectorFilters) {
// Special handling for detector_filters - allow comparison operators
obj[key] = obj[key].replace(/[^\w\s.\-=+()\[\]*$@!`^%#:?\/&,><=!]/gi, '');
} else {
// Normal string cleaning
obj[key] = obj[key].replace(/[^\w\s.\-=+()\[\]*$@!`^%#:?\/&,]/gi, '');
}
}
}
else if (typeof obj[key] === 'object' && obj[key] !== null) {
if (enteringDetectorFilters && key === 'details') {
cleanStringsInObject(obj[key].detector_filters, true);
cleanStringsInObject(obj[key], false);
} else {
cleanStringsInObject(obj[key], currentIsWithinDetectorFilters);
}
}
else if (Array.isArray(obj[key])) {
obj[key].forEach((item, index) => {
if (typeof item === 'string') {
try {
const parsed = JSON.parse(item);
obj[key][index] = cleanStringsInObject(parsed, currentIsWithinDetectorFilters);
} catch (e) {
if (currentIsWithinDetectorFilters) {
obj[key][index] = item.replace(/[^\w\s.\-=+()\[\]*$@!`^%#:?\/&,><=!]/gi, '');
} else {
obj[key][index] = item.replace(/[^\w\s.\-=+()\[\]*$@!`^%#:?\/&,]/gi, '');
}
}
} else if (typeof item === 'object' && item !== null) {
cleanStringsInObject(item, currentIsWithinDetectorFilters);
}
});
}
}
}
return obj;
}
return {
parseJSON: parseJSON,
stringJSON: stringJSON,
@ -285,8 +300,6 @@ module.exports = (processCwd,config) => {
utcToLocal: utcToLocal,
localToUtc: localToUtc,
formattedTime: formattedTime,
checkSubscription: checkSubscription,
checkAgainSubscription,
isEven: isEven,
fetchTimeout: fetchTimeout,
fetchDownloadAndWrite: fetchDownloadAndWrite,
@ -297,5 +310,7 @@ module.exports = (processCwd,config) => {
setDefaultIfUndefined,
deleteFilesInFolder,
moveFile,
setTimeoutPromise,
cleanStringsInObject,
}
}

View File

@ -31,6 +31,6 @@ module.exports = function(s,config,lang,app,io){
if(config.brandingConfig && config.brandingConfig[domain]){
return Object.assign(configCopy,config.brandingConfig[domain])
}
return config
return configCopy
}
}

View File

@ -1,235 +1,176 @@
const fs = require('fs')
const exec = require('child_process').exec
const spawn = require('child_process').spawn
const isWindows = (process.platform === 'win32' || process.platform === 'win64')
process.send = process.send || function () {};
const { exec, spawn } = require('child_process')
const isWindows = process.platform === 'win32' || process.platform === 'win64'
// --- helper -----------------------------------------------------------
const safeWriteStderr = (payload) => {
try {
const out = Array.isArray(payload) ? JSON.stringify(payload) : String(payload)
process.stderr.write(Buffer.from(out.slice(0, 2 * 1024), 'utf8')) // cap 2 KiB
} catch (_) {}
}
process.logData = safeWriteStderr
process.send = process.send || function () {}
// --- config ----------------------------------------------------------
if (!process.argv[2] || !process.argv[3]) {
return safeWriteStderr('Missing FFMPEG path or JSON payload')
}
var jsonData = JSON.parse(fs.readFileSync(process.argv[3],'utf8'))
const config = jsonData.globalInfo.config
const ffmpegAbsolutePath = process.argv[2].trim()
const ffmpegCommandString = jsonData.cmd
const rawMonitorConfig = jsonData.rawMonitorConfig
const stdioPipes = jsonData.pipes || []
var newPipes = []
var stdioWriters = [];
const jsonData = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'))
const {
globalInfo: { config },
cmd: ffmpegCommandString,
rawMonitorConfig,
pipes: stdioPipes = []
} = jsonData
const { fetchTimeout } = require('../basic/utils.js')(process.cwd(),config)
const { fetchTimeout } = require('../basic/utils.js')(process.cwd(), config)
const dataPort = require('./libs/dataPortConnection.js')(jsonData,
// onConnected
() => {
dataPort.send(jsonData.dataPortToken)
},
// onError
(err) => {
writeToStderr([
'dataPort:Connection:Error',
err
])
},
// onClose
(e) => {
writeToStderr([
'dataPort:Connection:Closed',
e
])
})
() => dataPort.send(jsonData.dataPortToken), // onConnected
(err) => safeWriteStderr(['dataPort:Connection:Error', err]), // onError
(e) => safeWriteStderr(['dataPort:Connection:Closed', e]) // onClose
)
var writeToStderr = function(argsAsArray){
try{
process.stderr.write(Buffer.from(`${JSON.stringify(argsAsArray)}`, 'utf8' ))
// dataPort.send({
// f: 'debugLog',
// data: argsAsArray,
// })
// stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' ))
}catch(err){
// Make sure we can terminate the websocket cleanly later
const destroyDataPort = () => {
if (dataPort && typeof dataPort.close === 'function') dataPort.close()
}
// --- global resource holders -----------------------------------------
let stdioWriters = []
let cameraProcess = null
let jpegInterval = null
// Build stdio array (keeping original loop logic per request suggestion #2 skipped)
const newPipes = []
for (let i = 0; i < stdioPipes; i++) {
switch (i) {
case 0:
newPipes[i] = 'pipe'
break
case 1:
newPipes[i] = 1
break
case 2:
newPipes[i] = 2
break
case 3:
stdioWriters[i] = fs.createWriteStream(null, { fd: i, end: false })
newPipes[i] = (rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1') ? 'pipe' : stdioWriters[i]
break
case 5:
stdioWriters[i] = fs.createWriteStream(null, { fd: i, end: false })
newPipes[i] = 'pipe'
break
default:
stdioWriters[i] = fs.createWriteStream(null, { fd: i, end: false })
newPipes[i] = stdioWriters[i]
break
}
// fs.appendFileSync('/home/ubuntu/cdn-site/tools/compilers/diycam/Shinobi/test.log',text + '\n','utf8')
}
process.logData = writeToStderr
if(!process.argv[2] || !process.argv[3]){
return writeToStderr('Missing FFMPEG Command String or no command operator')
}
const buildMonitorUrl = function(e,noPath){
var authd = ''
var url
if(e.details.muser&&e.details.muser!==''&&e.host.indexOf('@')===-1) {
e.username = e.details.muser
e.password = e.details.mpass
authd = e.details.muser+':'+e.details.mpass+'@'
}
if(e.port==80&&e.details.port_force!=='1'){e.porty=''}else{e.porty=':'+e.port}
url = e.protocol+'://'+authd+e.host+e.porty
if(noPath !== true)url += e.path
return url
}
// [CTRL] + [C] = exit
process.on('uncaughtException', function (err) {
writeToStderr('Uncaught Exception occured!');
writeToStderr(err.stack);
});
const exitAction = function(){
try{
if(isWindows){
spawn("taskkill", ["/pid", cameraProcess.pid, '/f', '/t'])
}else{
process.kill(-cameraProcess.pid)
}
}catch(err){
// Guard writers against silent errors
stdioWriters.forEach((w) => w && w.on('error', (e) => safeWriteStderr(e)))
}
}
process.on('SIGTERM', exitAction);
process.on('SIGINT', exitAction);
process.on('exit', exitAction);
// --- child process ----------------------------------------------------
const spawnCamera = () => {
cameraProcess = spawn(ffmpegAbsolutePath, ffmpegCommandString, { detached: true, stdio: newPipes })
for(var i=0; i < stdioPipes; i++){
switch(i){
case 0:
newPipes[i] = 'pipe'
break;
case 1:
newPipes[i] = 1
break;
case 2:
newPipes[i] = 2
break;
case 3:
stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false});
if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){
newPipes[i] = 'pipe'
}else{
newPipes[i] = stdioWriters[i]
}
break;
case 5:
stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false});
newPipes[i] = 'pipe'
break;
default:
stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false});
newPipes[i] = stdioWriters[i]
break;
}
}
stdioWriters.forEach((writer)=>{
writer.on('error', (err) => {
writeToStderr(err.stack);
});
})
var cameraProcess = spawn(ffmpegAbsolutePath,ffmpegCommandString,{detached: true,stdio:newPipes})
cameraProcess.on('close',()=>{
writeToStderr('Process Closed')
stdioWriters.forEach((writer)=>{
writer.end()
// forward extra stdout (pipe 5) with automatic listener removal
const pipe5 = cameraProcess.stdio[5]
const onPipe5Data = (d) => stdioWriters[5]?.write(d)
pipe5?.on('data', onPipe5Data)
cameraProcess.once('close', () => {
safeWriteStderr('FFmpeg process closed')
pipe5?.off('data', onPipe5Data)
cleanupWriters()
clearInterval(jpegInterval)
jpegInterval = null
destroyDataPort()
process.exit()
})
process.exit();
})
cameraProcess.stdio[5].on('data',(data)=>{
stdioWriters[5].write(data)
})
writeToStderr('Thread Opening')
}
// ------------------------------------------------ jpeg puller ---------
const startJpegPuller = () => {
if (rawMonitorConfig.type !== 'jpeg') return
safeWriteStderr('JPEG Input Type Detected')
if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){
try{
const attachPamDetector = require(config.monitorDetectorDaemonPath ? config.monitorDetectorDaemonPath : __dirname + '/detector.js')(jsonData,(detectorObject) => {
dataPort.send(JSON.stringify(detectorObject))
},dataPort)
attachPamDetector(cameraProcess)
}catch(err){
writeToStderr(err.stack)
const fps = parseInt(rawMonitorConfig.details.sfps) || 1
if (Number.isNaN(fps) || fps <= 0) {
safeWriteStderr('Invalid capture FPS')
return
}
safeWriteStderr(`Running at ${fps} FPS`)
const fetchAndPipe = async () => {
try {
const res = await fetchTimeout(buildMonitorUrl(rawMonitorConfig), 15000)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
res.body.pipe(cameraProcess.stdin, { end: false })
} catch (err) {
safeWriteStderr(err.message)
}
}
jpegInterval = setInterval(fetchAndPipe, 1000 / fps)
}
if(rawMonitorConfig.type === 'jpeg'){
writeToStderr('JPEG Input Type Detected')
var recordingSnapper
var errorTimeout
var errorCount = 0
var capture_fps = parseInt(rawMonitorConfig.details.sfps) || 1
if(isNaN(capture_fps)){
writeToStderr(`${lang['Error While Decoding']}, ${lang['Field Missing Value']} : ${lang['Monitor Capture Rate']}`)
return;
}
writeToStderr(`Running at ${capture_fps} FPS`)
try{
cameraProcess.stdio[0].on('error',function(err){
if(err && rawMonitorConfig.details.loglevel !== 'quiet'){
// s.userLog(e,{type:'STDIN ERROR',msg:err});
}
})
}catch(err){
writeToStderr(err.stack)
}
setTimeout(() => {
if(!cameraProcess.stdin)return writeToStderr('No Camera Process Found for Snapper');
const captureOne = function(f){
fetchTimeout(buildMonitorUrl(rawMonitorConfig),15000,{
method: 'GET',
})
.then(response => response.arrayBuffer())
.then(function(content){
cameraProcess.stdin.write(new Uint8Array(content))
recordingSnapper = setTimeout(function(){
captureOne()
},1000 / capture_fps)
if(!errorTimeout){
clearTimeout(errorTimeout)
errorTimeout = setTimeout(function(){
errorCount = 0;
delete(errorTimeout)
},3000)
}
}).catch(function(err){
++errorCount
clearTimeout(errorTimeout)
errorTimeout = null
writeToStderr(err.stack)
if(rawMonitorConfig.details.loglevel !== 'quiet'){
// s.userLog(e,{
// type: lang['JPEG Error'],
// msg: {
// msg: lang.JPEGErrorText,
// info: err
// }
// });
switch(err.code){
case'ESOCKETTIMEDOUT':
case'ETIMEDOUT':
// ++s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount
// if(
// rawMonitorConfig.details.fatal_max !== 0 &&
// s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount > rawMonitorConfig.details.fatal_max
// ){
// // s.userLog(e,{type:lang['Fatal Maximum Reached'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}});
// // s.camera('stop',e)
// }else{
// // s.userLog(e,{type:lang['Restarting Process'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}});
// // s.camera('restart',e)
// }
// return;
break;
}
}
// if(rawMonitorConfig.details.fatal_max !== 0 && errorCount > rawMonitorConfig.details.fatal_max){
// clearTimeout(recordingSnapper)
// process.exit()
// }
})
}
captureOne()
},5000)
// ------------------------------------------------ misc helpers --------
const buildMonitorUrl = (e, noPath) => {
let auth = ''
if (e.details.muser && e.details.muser !== '' && !e.host.includes('@')) {
auth = `${e.details.muser}:${e.details.mpass}@`
}
const portPart = (e.port == 80 && e.details.port_force !== '1') ? '' : `:${e.port}`
return `${e.protocol}://${auth}${e.host}${portPart}${noPath ? '' : e.path}`
}
if(
rawMonitorConfig.type === 'dashcam' ||
rawMonitorConfig.type === 'socket'
){
process.stdin.on('data',(data) => {
//confirmed receiving data this way.
cameraProcess.stdin.write(data)
})
const cleanupWriters = () => {
stdioWriters.forEach((w, idx) => {
if (!w) return
w.removeAllListeners()
w.end()
w.destroy?.()
stdioWriters[idx] = null
})
stdioWriters = []
}
// ------------------------------------------------ exit handling -------
const exitAction = () => {
try {
if (jpegInterval) clearInterval(jpegInterval)
cleanupWriters()
destroyDataPort()
if (cameraProcess) {
if (isWindows) spawn('taskkill', ['/pid', cameraProcess.pid, '/f', '/t'])
else process.kill(-cameraProcess.pid)
}
} catch (_) {}
}
const registerOnce = (evt, fn) => {
if (!process.listenerCount(evt)) process.once(evt, fn)
}
registerOnce('SIGTERM', exitAction)
registerOnce('SIGINT', exitAction)
registerOnce('exit', exitAction)
registerOnce('uncaughtException', (err) => {
safeWriteStderr(['Uncaught Exception', err.message])
exitAction()
})
// ------------------------------------------------ bootstrap -----------
spawnCamera()
startJpegPuller()
// dashcam / socket passthrough remains unchanged
if (rawMonitorConfig.type === 'dashcam' || rawMonitorConfig.type === 'socket') {
process.stdin.on('data', (d) => cameraProcess.stdin.write(d))
}

1
libs/checker/actCheck.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -12,14 +12,12 @@ module.exports = (s,config,lang) => {
const cameraCountChecks = [
{ kind: 'ec2', maxCameras: 2, condition: config.isEC2 },
{ kind: 'highCoreCount', maxCameras: 50, condition: config.isHighCoreCount },
{ kind: 'default', maxCameras: 15, condition: true },
{ kind: 'default', maxCameras: s.cameraCount, condition: true },
];
if (!config.userHasSubscribed) {
const monitorCountOnSystem = getTotalMonitorCount();
for (const check of cameraCountChecks) {
if (check.condition && monitorCountOnSystem >= check.maxCameras) {
return false;
}
const monitorCountOnSystem = getTotalMonitorCount();
for (const check of cameraCountChecks) {
if (check.condition && monitorCountOnSystem >= check.maxCameras) {
return false;
}
}
return true;

View File

@ -107,7 +107,7 @@ module.exports = function(s,config,lang,app){
const workerProcess = new Worker(pathToWorkerScript,{
workerData: {
config: config,
lang: lang
lang
}
})
workerProcess.on('message',function(data){

View File

@ -1,36 +1,44 @@
const fs = require('fs').promises
const { Worker } = require('worker_threads')
const testMode = process.argv[2] === 'test'
let passedJSON = false
let passedConfig = {}
const moduleName = 'connectToManagementServer'
module.exports = (s,config,lang,app) => {
if(!config.enableMgmtConnect){
return;
}
const { modifyConfiguration, getConfiguration } = require('../system/utils.js')(config)
require('./libs/centralConnect.js')(s,config,lang)
require('./libs/pairServer.js')(s,config)
require('./libs/pairServer.js')(s,config,lang)
const {
getManagementServers,
addManagementServer,
removeManagementServer,
connectToManagementServer,
disconnectFromManagmentServer,
connectAllManagementServers,
migrateOldConfiguration,
} = require('./utils.js')(s,config,lang)
s.onLoadedUsersAtStartup(() => {
connectAllManagementServers()
if(config.managementServer && config.peerConnectKey){
console.log(`Migrating Old Central Configuration`)
migrateOldConfiguration()
}
})
/**
* API : Superuser : Get Management Server Settings
*/
app.get(config.webPaths.superApiPrefix+':auth/mgmt/list', function (req,res){
s.superAuth(req.params,(resp) => {
const response = getManagementServers()
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Superuser : Save Management Server Settings
*/
app.post(config.webPaths.superApiPrefix+':auth/mgmt/save', function (req,res){
s.superAuth(req.params,async (resp) => {
// for saving :
// form.peerConnectKey
// form.managementServer
const response = {ok: true};
const form = s.getPostData(req,'data',true);
config = Object.assign(config,form)
const currentConfig = await getConfiguration()
const configError = await modifyConfiguration(Object.assign(currentConfig,form))
if(configError)s.systemLog(configError)
try{
s.restartCentralManagement()
}catch(err){
s.debugLog(err)
}
const managementServer = req.body.managementServer;
const peerConnectKey = req.body.peerConnectKey;
const response = await addManagementServer(managementServer, peerConnectKey)
await connectToManagementServer(managementServer, peerConnectKey)
s.closeJsonResponse(res,response)
},res,req)
})
@ -40,23 +48,10 @@ module.exports = (s,config,lang,app) => {
*/
app.post(config.webPaths.superApiPrefix+':auth/mgmt/disconnect', async function (req,res){
s.superAuth(req.params,async (resp) => {
const response = {ok: true};
const peerConnectKey = s.getPostData(req,'peerConnectKey');
const currentConfig = await getConfiguration()
if(currentConfig.peerConnectKey === peerConnectKey){
delete(config.managementServer);
delete(currentConfig.managementServer);
const configError = await modifyConfiguration(currentConfig);
if(configError)s.systemLog(configError);
try{
s.restartCentralManagement()
}catch(err){
s.debugLog(err)
}
}else{
response.ok = false;
response.msg = 'Peer Connect Key not matching! Cannot disconnect.';
}
const managementServer = req.body.managementServer;
const peerConnectKey = req.body.peerConnectKey;
const response = await removeManagementServer(managementServer, peerConnectKey)
await disconnectFromManagmentServer(managementServer, peerConnectKey)
s.closeJsonResponse(res,response)
},res,req)
})

View File

@ -1,366 +1,436 @@
const { spawn } = require('child_process');
const { parentPort, workerData } = require('worker_threads');
process.on("uncaughtException", function(error) {
console.error(error);
});
const activeTerminalCommands = {}
let config = workerData.config
let lang = workerData.lang
let sslInfo = config.ssl || {}
const expectedConfigPath = `./conf.json`
const hostPeerServer = config.managementServer;
const peerConnectKey = config.peerConnectKey;
if(!peerConnectKey || !hostPeerServer){
console.log(`Management Server Connection Not Configured!`)
setInterval(() => {
}, 1000 * 60 * 60 * 24)
return;
}
const fs = require("fs").promises
const net = require("net")
const bson = require('bson')
const WebSocket = require('cws')
const fs = require("fs").promises;
const net = require("net");
const bson = require('bson');
const WebSocket = require('cws');
const os = require('os');
const { EventEmitter } = require('node:events');
const internalEvents = new EventEmitter();
const s = {
debugLog: () => {},
systemLog: (...args) => {
parentPort.postMessage({
f: 'systemLog',
data: args
})
},
}
console.log('hostPeerServer',hostPeerServer)
if(config.debugLog){
s.debugLog = (...args) => {
parentPort.postMessage({
f: 'debugLog',
data: args
})
}
}
parentPort.on('message',(data) => {
switch(data.f){
case'init':
initialize()
break;
case'connectDetails':
data.connectDetails.peerConnectKey = peerConnectKey;
internalEvents.emit('connectDetails', data.connectDetails);
// outboundMessage('connectDetailsForManagement', data.connectDetails, '1');
break;
case'exit':
s.debugLog('Closing Central Connection...')
process.exit(0)
break;
}
})
let outboundMessage = null
var socketCheckTimer = null
var heartbeatTimer = null
var heartBeatCheckTimout = null
var onClosedTimeout = null
let stayDisconnected = false
const requestConnections = {}
const requestConnectionsData = {}
function getServerIPAddresses() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (let interfaceName in interfaces) {
for (let i = 0; i < interfaces[interfaceName].length; i++) {
const iface = interfaces[interfaceName][i];
if (iface.family === 'IPv4' && !iface.internal) {
addresses.push(iface.address);
// Constants
const EXPECTED_CONFIG_PATH = './conf.json';
const HEARTBEAT_INTERVAL = 10000; // 10 seconds
const SOCKET_CHECK_INTERVAL = 20000; // 20 seconds
const RECONNECT_DELAY = 2000; // 2 seconds
const HEARTBEAT_TIMEOUT_MULTIPLIER = 1.5;
// Error handling
process.on("uncaughtException", (error) => {
console.error('Uncaught Exception:', error);
});
class CentralConnection {
constructor() {
this.activeTerminalCommands = {};
this.requestConnections = {};
this.requestConnectionsData = {};
this.responseTunnels = {};
this.timers = {
socketCheck: null,
heartbeat: null,
heartbeatCheck: null,
onClosed: null
};
this.stayDisconnected = false;
this.allMessageHandlers = [];
this.internalEvents = new EventEmitter();
this.initConfig();
this.initLogging();
this.setupParentPortHandlers();
}
initConfig() {
const { config, serverIp: hostPeerServer, p2pKey: peerConnectKey } = workerData;
this.config = config;
this.hostPeerServer = hostPeerServer;
this.peerConnectKey = peerConnectKey;
this.sslInfo = config.ssl || {};
}
initLogging() {
this.logger = {
debugLog: () => {},
systemLog: (...args) => {
parentPort.postMessage({ f: 'systemLog', data: args });
}
};
// if (this.config.debugLog) {
this.logger.debugLog = (...args) => {
parentPort.postMessage({ f: 'debugLog', data: args });
};
// }
}
setupParentPortHandlers() {
const _this = this;
parentPort.on('message', (data) => {
switch (data.f) {
case 'init':
_this.initialize();
break;
case 'connectDetails':
data.connectDetails.peerConnectKey = this.peerConnectKey;
_this.internalEvents.emit('connectDetails', data.connectDetails);
break;
case 'exit':
_this.logger.debugLog('Closing Central Connection...');
process.exit(0);
break;
}
});
}
clearAllTimers() {
Object.values(this.timers).forEach(timer => {
if (timer) clearTimeout(timer);
});
this.timers = {
socketCheck: null,
heartbeat: null,
heartbeatCheck: null,
onClosed: null
};
}
getServerIPAddresses() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const interfaceName in interfaces) {
for (const iface of interfaces[interfaceName]) {
if (iface.family === 'IPv4' && !iface.internal && iface.address !== '127.0.0.1' && !iface.address.includes(':')) {
addresses.push(iface.address);
}
}
}
return addresses;
}
return addresses;
}
function getRequestConnection(requestId){
return requestConnections[requestId] || {
write: () => {}
}
}
function clearAllTimeouts(){
clearInterval(heartbeatTimer)
clearTimeout(heartBeatCheckTimout)
clearTimeout(onClosedTimeout)
}
function getConnectionDetails(){
getRequestConnection(requestId) {
return this.requestConnections[requestId] || {
write: () => {}
};
}
async getConnectionDetails() {
return new Promise((resolve) => {
internalEvents.once('connectDetails' ,(data) => {
resolve(data)
})
parentPort.postMessage({ f: 'connectDetailsRequest' })
})
}
function requestConnectionDetails(){
}
function startConnection(){
let tunnelToP2P
stayDisconnected = false
const allMessageHandlers = []
async function startWebsocketConnection(key,callback){
s.debugLog(`startWebsocketConnection EXECUTE`,new Error())
console.log('Central : Connecting to Central Server...')
function createWebsocketConnection(){
clearAllTimeouts()
return new Promise((resolve,reject) => {
try{
stayDisconnected = true
if(tunnelToP2P)tunnelToP2P.close()
}catch(err){
console.log(err)
}
tunnelToP2P = new WebSocket(hostPeerServer);
stayDisconnected = false;
tunnelToP2P.on('open', function(){
resolve(tunnelToP2P)
})
tunnelToP2P.on('error', (err) => {
console.log(`Central tunnelToCentral Error : `,err)
console.log(`Central Restarting...`)
// disconnectedConnection()
})
tunnelToP2P.on('close', () => {
console.log(`Central Connection Closed!`)
clearAllTimeouts()
// onClosedTimeout = setTimeout(() => {
// disconnectedConnection();
// },5000)
});
tunnelToP2P.onmessage = function(event){
const data = bson.deserialize(Buffer.from(event.data))
allMessageHandlers.forEach((handler) => {
if(data.f === handler.key){
handler.callback(data.data,data.rid)
}
})
}
this.internalEvents.once('connectDetails', (data) => {
resolve(data);
});
parentPort.postMessage({ f: 'connectDetailsRequest' });
});
}
clearInterval(socketCheckTimer)
socketCheckTimer = setInterval(() => {
// s.debugLog('Tunnel Ready State :',tunnelToP2P.readyState)
if(tunnelToP2P.readyState !== 1){
s.debugLog('Tunnel NOT Ready! Reconnecting...')
disconnectedConnection()
}
},1000 * 20)
})
}
function disconnectedConnection(code,reason){
s.debugLog('stayDisconnected',stayDisconnected)
clearAllTimeouts()
s.debugLog('DISCONNECTED!')
if(stayDisconnected)return;
s.debugLog('RESTARTING!')
setTimeout(() => {
if(tunnelToP2P && tunnelToP2P.readyState !== 1)startWebsocketConnection()
},2000)
}
s.debugLog(hostPeerServer)
await createWebsocketConnection(hostPeerServer,allMessageHandlers)
console.log('Central : Connected! Authenticating...')
const connectDetails = await getConnectionDetails()
sendDataToTunnel({
isShinobi: !!config.passwordType,
peerConnectKey,
connectDetails,
ipAddresses: getServerIPAddresses(),
config: JSON.parse(await fs.readFile(expectedConfigPath,'utf8')),
})
clearInterval(heartbeatTimer)
heartbeatTimer = setInterval(() => {
sendDataToTunnel({
f: 'ping',
})
}, 1000 * 10)
setTimeout(() => {
if(tunnelToP2P.readyState !== 1)refreshHeartBeatCheck()
},5000)
}
function sendDataToTunnel(data){
tunnelToP2P.send(bson.serialize(data))
}
startWebsocketConnection()
function onIncomingMessage(key,callback){
allMessageHandlers.push({
key: key,
callback: callback,
})
}
outboundMessage = (key,data,requestId) => {
sendDataToTunnel({
f: key,
data: data,
rid: requestId
})
}
async function createRemoteSocket(host,port,requestId,initData){
// if(requestConnections[requestId]){
// remotesocket.off('data')
// remotesocket.off('drain')
// remotesocket.off('close')
// requestConnections[requestId].end()
// }
const responseTunnel = await getResponseTunnel(requestId)
let remotesocket = new net.Socket();
remotesocket.on('ready',() => {
remotesocket.write(initData.buffer)
})
remotesocket.on('error',(err) => {
s.debugLog('createRemoteSocket ERROR',err)
})
remotesocket.on('data', function(data) {
requestConnectionsData[requestId] = data.toString()
responseTunnel.send('data',data)
})
remotesocket.on('drain', function() {
responseTunnel.send('resume',{})
});
remotesocket.on('close', function() {
delete(requestConnectionsData[requestId])
responseTunnel.send('end',{})
setTimeout(() => {
if(
responseTunnel &&
(responseTunnel.readyState === 0 || responseTunnel.readyState === 1)
){
responseTunnel.close()
}
},5000)
});
remotesocket.connect(port, host || 'localhost');
requestConnections[requestId] = remotesocket
return remotesocket
}
function writeToServer(data,requestId){
var flushed = getRequestConnection(requestId).write(data.buffer)
if (!flushed) {
outboundMessage('pause',{},requestId)
}
}
function refreshHeartBeatCheck(){
clearTimeout(heartBeatCheckTimout)
heartBeatCheckTimout = setTimeout(() => {
startWebsocketConnection()
},1000 * 10 * 1.5)
}
onIncomingMessage('connect',async (data,requestId) => {
s.debugLog('New Request Incoming', 'localhost', config.port, requestId);
const socket = await createRemoteSocket('localhost', config.port, requestId, data.init)
})
onIncomingMessage('data',writeToServer)
async startConnection() {
this.stayDisconnected = false;
await this.startWebsocketConnection();
}
onIncomingMessage('resume',function(data,requestId){
requestConnections[requestId].resume()
})
onIncomingMessage('pause',function(data,requestId){
requestConnections[requestId].pause()
})
onIncomingMessage('pong',function(data,requestId){
refreshHeartBeatCheck()
})
onIncomingMessage('init',function(data,requestId){
console.log(`Central : Authenticated!`)
})
onIncomingMessage('modifyConfiguration',function(data,requestId){
parentPort.postMessage({
f: 'modifyConfiguration',
data: data
})
})
onIncomingMessage('getConfiguration',function(data, requestId){
outboundMessage('getConfigurationResponse', Object.assign({}, config), requestId)
})
onIncomingMessage('restart',function(data,requestId){
parentPort.postMessage({ f: 'restart' })
})
onIncomingMessage('end',function(data,requestId){
try{
requestConnections[requestId].end()
}catch(err){
s.debugLog(`Reqest Failed to END ${requestId}`)
s.debugLog(`Failed Request ${requestConnectionsData[requestId]}`)
delete(requestConnectionsData[requestId])
s.debugLog(err)
// console.log('requestConnections',requestConnections)
}
})
onIncomingMessage('disconnect',function(data,requestId){
console.log(`FAILED LICENSE CHECK ON P2P`)
const retryLater = data && data.retryLater;
stayDisconnected = !retryLater
if(retryLater)console.log(`Retrying Central Later...`)
})
}
const responseTunnels = {}
async function getResponseTunnel(originalRequestId){
return responseTunnels[originalRequestId] || await createResponseTunnel(originalRequestId)
}
function createResponseTunnel(originalRequestId){
const responseTunnelMessageHandlers = []
function onMessage(key,callback){
responseTunnelMessageHandlers.push({
key: key,
callback: callback,
})
async startWebsocketConnection() {
this.logger.debugLog('startWebsocketConnection EXECUTE', new Error());
console.log('Central : Connecting to Central Server...');
try {
this.clearAllTimers();
// this.stayDisconnected = true;
if (this.tunnelToP2P){
this.tunnelToP2P.removeAllListeners('open');
this.tunnelToP2P.removeAllListeners('error');
this.tunnelToP2P.removeAllListeners('close');
this.tunnelToP2P.close();
}
} catch (err) {
console.log('Error closing previous connection:', err);
}
return new Promise((resolve,reject) => {
const responseTunnel = new WebSocket(hostPeerServer);
function sendToResponseTunnel(data){
responseTunnel.send(
bson.serialize(data)
)
}
function sendData(key,data){
sendToResponseTunnel({
f: key,
data: data,
rid: originalRequestId
})
}
responseTunnel.on('error', (err) => {
s.debugLog('responseTunnel ERROR',err)
})
responseTunnel.on('open', function(){
sendToResponseTunnel({
responseTunnel: originalRequestId,
peerConnectKey,
})
})
responseTunnel.on('close', function(){
delete(responseTunnels[originalRequestId])
})
onMessage('ready', function(){
const finalData = {
onMessage,
send: sendData,
sendRaw: sendToResponseTunnel,
close: responseTunnel.close
}
responseTunnels[originalRequestId] = finalData;
resolve(finalData)
})
responseTunnel.onmessage = function(event){
const data = bson.deserialize(Buffer.from(event.data))
responseTunnelMessageHandlers.forEach((handler) => {
if(data.f === handler.key){
handler.callback(data.data,data.rid)
}
})
}
})
}
function closeResponseTunnel(originalRequestId){
// also should be handled server side
try{
responseTunnels[originalRequestId].close()
}catch(err){
s.debugLog('closeResponseTunnel',err)
// this.stayDisconnected = false;
this.tunnelToP2P = new WebSocket(this.hostPeerServer);
this.tunnelToP2P.on('open', () => this.onWebsocketOpen());
this.tunnelToP2P.on('error', (err) => this.onWebsocketError(err));
this.tunnelToP2P.on('close', () => this.onWebsocketClose());
this.tunnelToP2P.onmessage = (event) => this.handleWebsocketMessage(event);
this.setupSocketCheckTimer();
}
onWebsocketOpen() {
console.log('Central : Connected! Authenticating...');
this.authenticateConnection();
}
onWebsocketError(err) {
console.log('Central tunnelToCentral Error:', err);
console.log('Central Restarting...');
this.disconnectedConnection();
}
onWebsocketClose() {
console.log('Central Connection Closed!');
this.clearAllTimers();
this.timers.onClosed = setTimeout(() => {
this.disconnectedConnection();
}, 5000);
}
async authenticateConnection() {
const connectDetails = await this.getConnectionDetails();
const configData = JSON.parse(await fs.readFile(EXPECTED_CONFIG_PATH, 'utf8'));
this.sendDataToTunnel({
isShinobi: !!this.config.passwordType,
peerConnectKey: this.peerConnectKey,
connectDetails,
ipAddresses: this.getServerIPAddresses(),
config: configData,
});
this.setupHeartbeat();
this.scheduleHeartbeatCheck();
}
sendDataToTunnel(data) {
if (this.tunnelToP2P.readyState === 1) {
this.tunnelToP2P.send(bson.serialize(data));
}else{
console.log('Cant Send Data, Tunnel Not Ready!')
}
}
setupSocketCheckTimer() {
this.timers.socketCheck = setInterval(() => {
if (this.tunnelToP2P.readyState !== 1) {
this.logger.debugLog('Tunnel NOT Ready! Reconnecting...');
this.disconnectedConnection();
}
}, SOCKET_CHECK_INTERVAL);
}
setupHeartbeat() {
this.clearAllTimers();
this.timers.heartbeat = setInterval(() => {
this.sendDataToTunnel({ f: 'ping' });
}, HEARTBEAT_INTERVAL);
}
scheduleHeartbeatCheck() {
setTimeout(() => {
if (this.tunnelToP2P.readyState !== 1) {
this.refreshHeartBeatCheck();
}
}, 5000);
}
refreshHeartBeatCheck() {
this.clearAllTimers();
this.timers.heartbeatCheck = setTimeout(() => {
this.startWebsocketConnection();
}, HEARTBEAT_INTERVAL * HEARTBEAT_TIMEOUT_MULTIPLIER);
}
disconnectedConnection() {
this.logger.debugLog('stayDisconnected', this.stayDisconnected);
this.clearAllTimers();
this.logger.debugLog('DISCONNECTED!');
if (this.stayDisconnected) return;
this.logger.debugLog('RESTARTING!');
setTimeout(() => {
if (!this.tunnelToP2P || this.tunnelToP2P.readyState !== 1) {
this.startWebsocketConnection();
}
}, RECONNECT_DELAY);
}
handleWebsocketMessage(event) {
const data = bson.deserialize(Buffer.from(event.data));
this.allMessageHandlers.forEach((handler) => {
if (data.f === handler.key) {
handler.callback(data.data, data.rid);
}
});
}
onIncomingMessage(key, callback) {
this.allMessageHandlers.push({ key, callback });
}
outboundMessage(key, data, requestId) {
this.sendDataToTunnel({
f: key,
data: data,
rid: requestId
});
}
async createRemoteSocket(host, port, requestId, initData) {
const responseTunnel = await this.getResponseTunnel(requestId);
const remoteSocket = new net.Socket();
remoteSocket.on('ready', () => {
remoteSocket.write(initData.buffer);
});
remoteSocket.on('error', (err) => {
this.logger.debugLog('createRemoteSocket ERROR', err);
});
remoteSocket.on('data', (data) => {
this.requestConnectionsData[requestId] = data.toString();
responseTunnel.send('data', data);
});
remoteSocket.on('drain', () => {
responseTunnel.send('resume', {});
});
remoteSocket.on('close', () => {
delete this.requestConnectionsData[requestId];
responseTunnel.send('end', {});
setTimeout(() => {
if (responseTunnel && (responseTunnel.readyState === 0 || responseTunnel.readyState === 1)) {
responseTunnel.close();
}
}, 5000);
});
remoteSocket.connect(port, host || 'localhost');
this.requestConnections[requestId] = remoteSocket;
return remoteSocket;
}
writeToServer(data, requestId) {
const flushed = this.getRequestConnection(requestId).write(data.buffer);
if (!flushed) {
this.outboundMessage('pause', {}, requestId);
}
}
async getResponseTunnel(originalRequestId) {
return this.responseTunnels[originalRequestId] || await this.createResponseTunnel(originalRequestId);
}
createResponseTunnel(originalRequestId) {
return new Promise((resolve) => {
const responseTunnelMessageHandlers = [];
const responseTunnel = new WebSocket(this.hostPeerServer);
const sendToResponseTunnel = (data) => {
responseTunnel.send(bson.serialize(data));
};
const sendData = (key, data) => {
sendToResponseTunnel({
f: key,
data: data,
rid: originalRequestId
});
};
const onMessage = (key, callback) => {
responseTunnelMessageHandlers.push({ key, callback });
};
responseTunnel.on('error', (err) => {
this.logger.debugLog('responseTunnel ERROR', err);
});
responseTunnel.on('open', () => {
sendToResponseTunnel({
responseTunnel: originalRequestId,
peerConnectKey: this.peerConnectKey,
});
});
responseTunnel.on('close', () => {
delete this.responseTunnels[originalRequestId];
});
responseTunnel.onmessage = (event) => {
const data = bson.deserialize(Buffer.from(event.data));
responseTunnelMessageHandlers.forEach((handler) => {
if (data.f === handler.key) {
handler.callback(data.data, data.rid);
}
});
};
onMessage('ready', () => {
const finalData = {
onMessage,
send: sendData,
sendRaw: sendToResponseTunnel,
close: responseTunnel.close.bind(responseTunnel)
};
this.responseTunnels[originalRequestId] = finalData;
resolve(finalData);
});
});
}
closeResponseTunnel(originalRequestId) {
try {
this.responseTunnels[originalRequestId]?.close();
} catch (err) {
this.logger.debugLog('closeResponseTunnel', err);
}
}
initialize() {
this.setupMessageHandlers();
this.startConnection();
}
setupMessageHandlers() {
this.onIncomingMessage('connect', async (data, requestId) => {
this.logger.debugLog('New Request Incoming', 'localhost', this.config.port, requestId);
await this.createRemoteSocket('localhost', this.config.port, requestId, data.init);
});
this.onIncomingMessage('data', (data, requestId) => this.writeToServer(data, requestId));
this.onIncomingMessage('resume', (data, requestId) => this.requestConnections[requestId].resume());
this.onIncomingMessage('pause', (data, requestId) => this.requestConnections[requestId].pause());
this.onIncomingMessage('pong', () => {}); // Heartbeat response
this.onIncomingMessage('init', () => {
console.log('Central : Authenticated!');
parentPort.postMessage({ f: 'authenticated' });
});
this.onIncomingMessage('modifyConfiguration', (data) => {
parentPort.postMessage({ f: 'modifyConfiguration', data });
});
this.onIncomingMessage('getConfiguration', (data, requestId) => {
this.outboundMessage('getConfigurationResponse', { ...this.config }, requestId);
});
this.onIncomingMessage('restart', () => {
parentPort.postMessage({ f: 'restart' });
});
this.onIncomingMessage('end', (data, requestId) => {
try {
this.requestConnections[requestId]?.end();
} catch (err) {
this.logger.debugLog(`Request Failed to END ${requestId}`);
this.logger.debugLog(`Failed Request ${this.requestConnectionsData[requestId]}`);
delete this.requestConnectionsData[requestId];
this.logger.debugLog(err);
}
});
this.onIncomingMessage('disconnect', (data) => {
console.log('FAILED LICENSE CHECK ON P2P');
this.stayDisconnected = !data?.retryLater;
if (data?.retryLater) console.log('Retrying Central Later...');
});
}
}
startConnection()
// Start the connection
const centralConnection = new CentralConnection();
centralConnection.initialize();

View File

@ -0,0 +1,97 @@
const WebSocket = require('cws');
const net = require('net');
const process = require('process');
const { parentPort, workerData } = require('worker_threads');
const WS_SERVER = workerData.wsServer;
const IDENTIFIER = workerData.peerConnectKey;
const SSH_PORT = workerData.sshPort || 22;
const RECONNECT_INTERVAL = workerData.reconnectInterval || 10000;
process.on("uncaughtException", function(error) {
console.error(error);
});
let ws = null;
let sshSocket = null;
let assignedPort = null;
let reconnectTimer = null;
function connectWebSocket() {
if (ws) {
if (ws.readyState === ws.OPEN) return;
}
console.log(`Connecting SSH to ${WS_SERVER}...`);
ws = new WebSocket(`${WS_SERVER}`);
ws.on('open', () => {
console.log('Connected SSH, registering...');
ws.send(JSON.stringify({
type: 'register',
identifier: IDENTIFIER
}));
});
ws.on('message', (message) => {
const msg = JSON.parse(message);
if (msg.type === 'registered') {
assignedPort = msg.port;
console.log(`Registered SSH! Forwarding port ${assignedPort} to SSH`);
}
else if (msg.type === 'data') {
if (!sshSocket) connectSSH();
sshSocket.write(Buffer.from(msg.data, 'base64'));
}
});
ws.on('close', () => {
console.log('Disconnected SSH from server');
if (sshSocket) sshSocket.end();
scheduleReconnect();
});
ws.on('error', (err) => {
console.error('SSH Socket error:', err);
scheduleReconnect();
});
}
function connectSSH() {
if (sshSocket) sshSocket.end();
sshSocket = new net.Socket();
sshSocket.connect(SSH_PORT, '127.0.0.1', () => {
console.log('Connected to SSH server');
});
sshSocket.on('data', (data) => {
if (ws && ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'data',
identifier: IDENTIFIER,
data: data.toString('base64')
}));
}
});
sshSocket.on('error', (err) => {
console.error('SSH error:', err);
sshSocket = null;
});
sshSocket.on('close', () => {
sshSocket = null;
});
}
function scheduleReconnect() {
if (!reconnectTimer) {
console.log(`Reconnecting in ${RECONNECT_INTERVAL/1000} seconds...`);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectWebSocket();
}, RECONNECT_INTERVAL);
}
}
// Start connection
connectWebSocket();

View File

@ -1,6 +1,23 @@
const fs = require("fs").promises
module.exports = (s,config) => {
const configPath = config.thisIsDocker ? "/config/super.json" : s.location.super;
module.exports = (s,config,lang) => {
const { getNewApiKey } = require('../../user/apiKeys.js')(s,config,lang)
const configPath = s.location.super;
const requiredApiKeyPermissions = {
"auth_socket": "1",
"create_api_keys": "1",
"edit_user": "1",
"edit_permissions": "1",
"get_monitors": "1",
"edit_monitors": "1",
"control_monitors": "1",
"get_logs": "1",
"watch_stream": "1",
"watch_snapshot": "1",
"watch_videos": "1",
"delete_videos": "1",
"get_alarms": "1",
"edit_alarms": "1",
};
async function generateSuperUserJson(){
const baseConfig = [
{
@ -55,13 +72,20 @@ module.exports = (s,config) => {
table: "API",
where: { ke: groupKey, uid: userId }
});
var requiredPermissions = Object.keys(requiredApiKeyPermissions);
let suitableKey = null;
for(row of rows){
row.details = JSON.parse(row.details)
const detailValues = Object.values(row.details);
const theFiltered = detailValues.filter(item => item != 1);
if(theFiltered.length === 0){
suitableKey = row.code
var details = JSON.parse(row.details)
var cantUse = details.permissionSet || details.treatAsSub === '1' || details.monitorsRestricted === '1';
if(!cantUse){
var canUse = true;
for(permission of requiredPermissions){
if(details[permission] !== '1')canUse = false;
}
if(canUse){
suitableKey = row.code
break;
}
}
};
if(!suitableKey){
@ -70,26 +94,16 @@ module.exports = (s,config) => {
return suitableKey;
}
async function createApiKey(groupKey, userId){
const newApiKey = s.gid(30);
const newApiKey = await getNewApiKey(groupKey);
await s.knexQueryPromise({
action: "insert",
table: "API",
insert: {
ke : groupKey,
uid : userId,
code : newApiKey,
ip : '0.0.0.0',
details : s.stringJSON({
"auth_socket": "1",
"get_monitors": "1",
"edit_monitors": "1",
"control_monitors": "1",
"get_logs": "1",
"watch_stream": "1",
"watch_snapshot": "1",
"watch_videos": "1",
"delete_videos": "1"
})
ke: groupKey,
uid: userId,
code: newApiKey,
ip: '0.0.0.0',
details: s.stringJSON(requiredApiKeyPermissions)
}
});
return newApiKey

View File

@ -3,7 +3,7 @@ const express = require('express');
const app = express();
var cors = require('cors');
var bodyParser = require('body-parser');
module.exports = (s,config) => {
module.exports = (s,config,lang) => {
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
const pairPort = config.pairPort || 8091
const bindIp = config.bindip
@ -16,33 +16,25 @@ module.exports = (s,config) => {
console.log('Management Pair Server Listening on '+pairPort);
});
const {
addManagementServer,
removeManagementServer,
connectToManagementServer,
connectAllManagementServers,
} = require('../utils.js')(s,config,lang)
/**
* API : Superuser : Save Management Server Settings
*/
app.post('/mgmt/connect', async function (req,res){
// form.managementServer
// Example of Shinobi and MGMT on same server
// ws://127.0.0.1:8663
const response = {ok: true};
if(!config.managementServer){
const managementServer = s.getPostData(req,'managementServer');
const peerConnectKey = s.getPostData(req,'peerConnectKey');
let response = {ok: true};
const managementServer = req.body.managementServer;
if(!config.mgmtServers[managementServer]){
const peerConnectKey = req.body.peerConnectKey;
if(peerConnectKey){
config = Object.assign(config, { managementServer, peerConnectKey })
const currentConfig = await getConfiguration()
if(peerConnectKey){
currentConfig.peerConnectKey = peerConnectKey
}
// else if(!currentConfig.peerConnectKey){
// currentConfig.peerConnectKey = `bihan${s.gid(20)}`
// }
const configError = await modifyConfiguration(Object.assign(currentConfig, { managementServer }))
if(configError)s.systemLog(configError)
try{
s.centralManagementWorker.terminate()
}catch(err){
s.debugLog(err)
}
response = await addManagementServer(managementServer, peerConnectKey)
await connectToManagementServer(managementServer, peerConnectKey)
}else{
response.ok = false;
response.msg = 'No P2P API Key Provided';

View File

@ -0,0 +1,247 @@
const { Worker } = require('worker_threads')
module.exports = (s,config,lang) => {
const { getConnectionDetails } = require('./libs/connectDetails.js')(s,config,lang)
const { modifyConfiguration, getConfiguration } = require('../system/utils.js')(config)
const sshDisabled = config.noCentralSsh === true;
const queuedSshRestart = {}
if(!s.connectedMgmtServers)s.connectedMgmtServers = {}
function parseNewConnectionAddress(serverIp){
let parsedIp = `${serverIp}`
if(parsedIp.indexOf('://') === -1)parsedIp = `ws://${parsedIp}`
if(parsedIp.split(':').length === 2)parsedIp = `ws://${parsedIp}:8663`
return parsedIp;
}
function getManagementServers(){
const response = { ok: true }
response.mgmtServers = config.mgmtServers || {};
return response
}
async function setManagementServers(mgmtServers){
const response = { ok: true }
config = Object.assign(config,{ mgmtServers })
const currentConfig = await getConfiguration()
currentConfig.mgmtServer = mgmtServers;
const configError = await modifyConfiguration(currentConfig)
if(configError){
response.ok = false;
response.err = configError
s.systemLog(configError)
}
return response
}
async function addManagementServer(serverIp, p2pKey){
const response = { ok: true }
const parsedIp = parseNewConnectionAddress(serverIp)
const currentConfig = await getConfiguration();
if(!currentConfig.mgmtServers)currentConfig.mgmtServers = {};
currentConfig.mgmtServers[serverIp] = p2pKey;
config = Object.assign(config, { mgmtServers: currentConfig.mgmtServers })
const configError = await modifyConfiguration(currentConfig)
if(configError){
response.ok = false;
response.err = configError
s.systemLog(configError)
}
return response
}
async function removeManagementServer(serverIp, p2pKey){
const response = { ok: true }
let foundMatching = false;
const currentConfig = await getConfiguration();
if(!currentConfig.mgmtServers)currentConfig.mgmtServers = {};
const currentPeerConnectKey = currentConfig.mgmtServers[serverIp];
if(currentPeerConnectKey === p2pKey){
foundMatching = true
delete(currentConfig.mgmtServers[serverIp])
config = Object.assign(config, { mgmtServers: currentConfig.mgmtServers })
const configError = await modifyConfiguration(currentConfig)
if(configError){
response.ok = false;
response.err = configError
s.systemLog(configError)
}
}else{
response.ok = false;
response.msg = 'Peer Connect Key not matching! Cannot disconnect.';
}
return response
}
async function queueToggleSshToManagement(serverIp, p2pKey, onlyClose){
if(sshDisabled)return;
clearTimeout(queuedSshRestart[serverIp])
queuedSshRestart[serverIp] = setTimeout(() => {
delete(queuedSshRestart[serverIp])
if(onlyClose){
if(s.connectedMgmtServers[serverIp])s.connectedMgmtServers[serverIp].sshWorker.terminate()
}else{
provideSshToManagement(serverIp, p2pKey)
}
},1000 * 60)
}
async function provideSshToManagement(serverIp, p2pKey){
if(sshDisabled)return;
if(queuedSshRestart[serverIp]){
clearTimeout(queuedSshRestart[serverIp]);
return s.connectedMgmtServers[serverIp].sshWorker
}
const configFromFile = await getConfiguration()
const wsServerParts = serverIp.split(':')
wsServerParts[serverIp.includes('://') ? 2 : 1] = configFromFile.sshSocketPort || 9021
const wsServer = wsServerParts.join(':')
console.log('Central SSH Connector Starting...', wsServer)
const worker = new Worker(`${__dirname}/libs/centralConnect/ssh.js`, {
workerData: {
config: configFromFile,
wsServer: wsServer,
peerConnectKey: p2pKey,
}
});
worker.on('message', async (data) => {
switch(data.f){
case'restart':
s.systemLog('Restarting SSH Connection...', serverIp)
worker.terminate()
break;
}
});
worker.on('error', (err) => {
console.error('cameraPeer SSH Error', serverIp, err)
});
worker.on('exit', (code) => {
if(!s.connectedMgmtServers[serverIp].wantTerminate){
console.log('cameraPeer SSH Exited, Restarting...', serverIp, code)
queueToggleSshToManagement(serverIp, p2pKey, true)
}else{
console.log('cameraPeer SSH Exited, NOT Restarting...', serverIp, code)
}
});
return worker
}
async function connectToManagementServer(serverIp, p2pKey){
if(!config.userHasSubscribed){
return console.log(lang.centralManagementNotEnabled)
}
if(s.connectedMgmtServers[serverIp]){
disconnectFromManagmentServer(serverIp)
}
s.connectedMgmtServers[serverIp] = {}
const configFromFile = await getConfiguration()
configFromFile.timezone = config.timezone;
console.log('Central Worker Starting...', serverIp)
const worker = new Worker(`${__dirname}/libs/centralConnect/index.js`, {
workerData: {
config: configFromFile,
serverIp,
p2pKey,
}
});
worker.on('message', async (data) => {
switch(data.f){
case'authenticated':
const sshWorker = await provideSshToManagement(serverIp, p2pKey)
s.connectedMgmtServers[serverIp].sshWorker = sshWorker;
break;
case'connectDetailsRequest':
getConnectionDetails().then((connectDetails) => {
worker.postMessage({ f: 'connectDetails', connectDetails })
}).catch((error) => {
console.error('FAILED TO GET connectDetails', serverIp, error)
worker.postMessage({ f: 'connectDetails', connectDetails: {} })
})
break;
case'modifyConfiguration':
console.log('Editing Configuration...', serverIp, data.data.form)
const configFromFile = await getConfiguration()
const mgmtServers = JSON.stringify(configFromFile.mgmtServers)
const newConfig = data.data.form;
const mgmtServersFromNewConfig = JSON.stringify(configFromFile.mgmtServers)
if(mgmtServers !== mgmtServersFromNewConfig){
resetAllManagementServers()
}
modifyConfiguration(newConfig)
break;
case'restart':
s.systemLog('Restarting Central Connection...', serverIp)
worker.terminate()
break;
}
});
worker.on('error', (err) => {
console.error('cameraPeer Error', serverIp, err)
});
worker.on('exit', (code) => {
console.log('cameraPeer Exited, Restarting...', serverIp, code)
if(!s.connectedMgmtServers[serverIp].wantTerminate)connectToManagementServer(serverIp, p2pKey)
});
s.connectedMgmtServers[serverIp].worker = worker;
s.connectedMgmtServers[serverIp].wantTerminate = false;
}
function disconnectFromManagmentServer(serverIp){
const mgmtConnection = s.connectedMgmtServers[serverIp];
try{
if(!mgmtConnection)return;
mgmtConnection.wantTerminate = true;
mgmtConnection.worker.terminate();
}catch(err){
s.debugLog('disconnectFromManagmentServer ERR',err)
}
try{
queueToggleSshToManagement(serverIp, null, true);
}catch(err){
s.debugLog('disconnectFromManagmentSshServer ERR',err)
}
}
function resetConnectionToManagementServer(serverIp){
const mgmtConnection = s.connectedMgmtServers[serverIp];
if(!mgmtConnection)return;
mgmtConnection.wantTerminate = false;
mgmtConnection.worker.terminate();
try{
queueToggleSshToManagement(serverIp, null, true);
}catch(err){
s.debugLog('resetConnectionToManagementSshServer ERR',err)
}
}
function resetAllManagementServers(){
for(serverIp in s.connectedMgmtServers){
disconnectFromManagmentServer(serverIp)
}
connectAllManagementServers()
}
async function connectAllManagementServers(){
const configFromFile = await getConfiguration()
const mgmtServers = configFromFile.mgmtServers
if(mgmtServers){
for(serverIp in mgmtServers){
var p2pKey = mgmtServers[serverIp]
await connectToManagementServer(serverIp, p2pKey)
}
}else{
console.log(`Management Server Connection Not Configured!`)
}
}
async function migrateOldConfiguration(){
await addManagementServer(config.managementServer, config.peerConnectKey)
await connectToManagementServer(config.managementServer, config.peerConnectKey)
const configFromFile = await getConfiguration()
delete(configFromFile.managementServer)
delete(configFromFile.peerConnectKey)
modifyConfiguration(configFromFile)
}
if(config.autoRestartManagementConnectionInterval){
setInterval(() => {
resetAllManagementServers()
}, 1000 * 60 * 15)
}
return {
getManagementServers,
addManagementServer,
removeManagementServer,
connectToManagementServer,
disconnectFromManagmentServer,
resetConnectionToManagementServer,
resetAllManagementServers,
connectAllManagementServers,
migrateOldConfiguration,
}
}

View File

@ -1,6 +1,5 @@
var os = require('os');
var exec = require('child_process').exec;
const onvif = require("shinobi-onvif");
const {
addCredentialsToUrl,
} = require('../common.js')
@ -9,28 +8,16 @@ module.exports = function(s,config,lang,app,io){
createSnapshot,
addCredentialsToStreamLink,
} = require('../monitor/utils.js')(s,config,lang)
const createOnvifDevice = async (onvifAuth) => {
var response = {ok: false}
const monitorConfig = s.group[onvifAuth.ke].rawMonitorConfigurations[onvifAuth.id]
const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true)
const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl,monitorConfig)
//create onvif connection
const device = new onvif.OnvifDevice({
address : controlURLOptions.host + ':' + controlURLOptions.port,
user : controlURLOptions.username,
pass : controlURLOptions.password
})
s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection = device
try{
const info = await device.init()
response.ok = true
response.device = device
}catch(err){
response.msg = 'Device responded with an error'
response.error = err
}
return response
}
const {
getOnvifDevice,
createOnvifDevice,
startPatrolPresets,
stopPatrolPresets,
removePreset,
getPresets,
setPreset,
goToPreset,
} = require('../onvifDeviceManager/utils.js')(s,config,lang)
const replaceDynamicInOptions = (Camera,options) => {
const newOptions = {}
Object.keys(options).forEach((key) => {
@ -181,6 +168,167 @@ module.exports = function(s,config,lang,app,io){
})
},res,req);
})
/**
* API : ONVIF Get Presets
*/
app.get(config.webPaths.apiPrefix+':auth/onvifPresets/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[monitorId].details.is_onvif === '1';
if(onvifEnabled){
const profileToken = s.getPostData(req,'profileToken') || "__CURRENT_TOKEN";
const asObject = s.getPostData(req,'asObject') === '1';
const numberOf = parseInt(s.getPostData(req,'numberOf')) || undefined;
const onvifDevice = await getOnvifDevice(groupKey, monitorId);
endData.presets = await getPresets(onvifDevice, asObject, profileToken, numberOf)
}else{
endData.ok = false;
endData.err = lang.ONVIFNotEnabled;
}
}catch(err){
endData.ok = false;
endData.err = err.toString()
}
s.closeJsonResponse(res,endData)
},res,req);
})
/**
* API : ONVIF Set Presets
*/
app.post(config.webPaths.apiPrefix+':auth/onvifSetPreset/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[monitorId].details.is_onvif === '1';
if(onvifEnabled){
const presetToken = s.getPostData(req,'presetToken') || "2";
const presetName = s.getPostData(req,'presetName') || "newPreset";
const onvifDevice = await getOnvifDevice(groupKey, monitorId);
endData.responseFromDevice = await setPreset(onvifDevice, presetToken, presetName)
}else{
endData.ok = false;
endData.err = lang.ONVIFNotEnabled;
}
}catch(err){
endData.ok = false;
endData.err = err.toString()
}
s.closeJsonResponse(res,endData)
},res,req);
})
/**
* API : ONVIF Go To Preset
*/
app.post(config.webPaths.apiPrefix+':auth/onvifGoToPreset/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[monitorId].details.is_onvif === '1';
if(onvifEnabled){
const presetToken = s.getPostData(req,'presetToken') || "2";
const speed = parseFloat(s.getPostData(req,'speed')) || undefined;
const onvifDevice = await getOnvifDevice(groupKey, monitorId);
endData.responseFromDevice = await goToPreset(onvifDevice, presetToken, speed)
}else{
endData.ok = false;
endData.err = lang.ONVIFNotEnabled;
}
}catch(err){
endData.ok = false;
endData.err = err.toString()
}
s.closeJsonResponse(res,endData)
},res,req);
})
/**
* API : ONVIF Remove Preset
*/
app.post(config.webPaths.apiPrefix+':auth/onvifRemovePreset/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[monitorId].details.is_onvif === '1';
if(onvifEnabled){
const presetToken = s.getPostData(req,'presetToken') || "2";
const onvifDevice = await getOnvifDevice(groupKey, monitorId);
endData.responseFromDevice = await removePreset(onvifDevice, presetToken)
}else{
endData.ok = false;
endData.err = lang.ONVIFNotEnabled;
}
}catch(err){
endData.ok = false;
endData.err = err.toString()
}
s.closeJsonResponse(res,endData)
},res,req);
})
/**
* API : ONVIF Start Patrol
*/
app.post(config.webPaths.apiPrefix+':auth/onvifStartPatrol/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[monitorId].details.is_onvif === '1';
if(onvifEnabled){
const patrolId = `${groupKey}_${monitorId}`;
const onvifDevice = await getOnvifDevice(groupKey, monitorId);
const startingPresetToken = s.getPostData(req,'startingPresetToken');
const patrolIndexTimeout = s.getPostData(req,'patrolIndexTimeout');
const speed = s.getPostData(req,'speed');
const activeMonitor = s.group[groupKey].activeMonitors[monitorId];
await startPatrolPresets(patrolId, onvifDevice, startingPresetToken, patrolIndexTimeout, speed, (currentToken) => {
activeMonitor.lastOnvifPresetFromPatrol = currentToken;
s.tx({
f: 'control_ptz_preset_changed',
mid: monitorId,
ke: groupKey,
profileToken: currentToken
},'GRP_'+groupKey);
})
}else{
endData.ok = false;
endData.err = lang.ONVIFNotEnabled;
}
}catch(err){
endData.ok = false;
endData.err = err.toString()
console.log(err)
}
s.closeJsonResponse(res,endData)
},res,req);
})
/**
* API : ONVIF Stop Patrol
*/
app.get(config.webPaths.apiPrefix+':auth/onvifStopPatrol/:ke/:id',function (req,res){
s.auth(req.params, async function(user){
const endData = { ok: true }
try{
const groupKey = req.params.ke;
const monitorId = req.params.id;
const patrolId = `${groupKey}_${monitorId}`;
await stopPatrolPresets(patrolId)
}catch(err){
endData.ok = false;
endData.err = err.toString()
console.log(err)
}
s.closeJsonResponse(res,endData)
},res,req);
})
s.getSnapshotFromOnvif = getSnapshotFromOnvif
s.createOnvifDevice = createOnvifDevice
s.runOnvifMethod = runOnvifMethod

View File

@ -64,6 +64,7 @@ module.exports = function(s,config,lang){
const theRequest = fetchWithAuthentication(requestUrl,fetchWithAuthData);
theRequest.then(res => res.text())
.then((data) => {
response.msg = data;
if(doStart){
const stopCommandEnabled = monitorConfig.details.control_stop === '1' || monitorConfig.details.control_stop === '2';
if(stopCommandEnabled && options.direction !== 'center'){
@ -425,7 +426,7 @@ module.exports = function(s,config,lang){
})
})
}
const moveToHomePositionTimeout = (event) => {
const moveToHomePositionTimeout = (event, returnTime = 7000) => {
const groupKey = event.ke
const monitorId = event.id
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
@ -439,7 +440,7 @@ module.exports = function(s,config,lang){
},(endData) => {
s.debugLog(endData)
})
},7000)
},returnTime)
}
const getLargestMatrix = (matrices,imgWidth,imgHeight) => {
var largestMatrix = {width: 0, height: 0}
@ -540,5 +541,6 @@ module.exports = function(s,config,lang){
moveCameraPtzToMatrix,
setHomePositionPreset,
moveToHomePosition,
moveToHomePositionTimeout,
}
}

View File

@ -55,6 +55,27 @@ module.exports = (s,config,lang) => {
case's.onCronGroupProcessedAwaited':
s.runExtensionsForArrayAwaited('onCronGroupProcessedAwaited', null, data.args)
break;
case'getCloudVideoMaxDays':
var maxDays = null;
try{
maxDays = s.group[data.ke].cloudDiskUse[data.type].maxDays
}catch(err){
s.debugLog('Failed to get Max Days for Cloud Disk Use', data.ke, data.type)
}
workerProcess.postMessage({
f: 'callback',
rid: data.rid,
args: [maxDays],
})
break;
case'getAllCloudVideoMaxDays':
var cloudDiskUse = s.group[data.ke].cloudDiskUse;
workerProcess.postMessage({
f: 'callback',
rid: data.rid,
args: [cloudDiskUse],
})
break;
case's.setDiskUsedForGroup':
function doOnMain(){
s.setDiskUsedForGroup(data.ke,data.size,data.target || undefined)

View File

@ -117,6 +117,26 @@ function beginProcessing(){
const setDiskUsedForGroup = (groupKey,size,target,videoRow) => {
postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow})
}
const getCloudVideoMaxDays = (user, storageType) => {
return new Promise((resolve) => {
const groupKey = user.ke;
const requestId = generateRandomId();
pendingCallbacks[requestId] = (value) => {
resolve(value)
}
postMessage({f:'getCloudVideoMaxDays', ke: groupKey, rid: requestId, type: storageType })
})
}
const getAllCloudVideoMaxDays = (user) => {
return new Promise((resolve) => {
const groupKey = user.ke;
const requestId = generateRandomId();
pendingCallbacks[requestId] = (value) => {
resolve(value)
}
postMessage({f:'getAllCloudVideoMaxDays', ke: groupKey, rid: requestId })
})
}
const knexQuery = (...args) => {
const requestId = generateRandomId();
const callback = args.pop();
@ -440,7 +460,7 @@ function beginProcessing(){
const dir = getTimelapseFrameDirectory(row)
const filename = row.filename
const theDate = filename.split('T')[0]
const enclosingFolder = `${dir}/${theDate}/`
const enclosingFolder = `${dir}${theDate}/`
try{
const fileSizeMB = row.size / 1048576;
setDiskUsedForGroup(groupKey,-fileSizeMB,null,row)
@ -507,6 +527,30 @@ function beginProcessing(){
}
})
}
//events - alarms
const deleteOldAlarms = function(v){
return new Promise((resolve,reject) => {
const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10
if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){
knexQuery({
action: "delete",
table: "Alarms",
where: [
['ke','=',v.ke],
['time','<', sqlDate(daysOldForDeletion + ' DAY')],
]
},(err,rrr) => {
resolve()
if(err)return errorLog(err);
if(rrr && rrr > 0 || config.debugLog === true){
postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'})
}
})
}else{
resolve()
}
})
}
//event counts
const deleteOldEventCounts = function(v){
return new Promise((resolve,reject) => {
@ -568,6 +612,57 @@ function beginProcessing(){
}
})
}
//cloud video max days
const deleteCloudVideosByDays = async function(user){
const cloudDiskUse = await getAllCloudVideoMaxDays(user);
const groupKey = user.ke;
let affectedRows = 0;
for(storageType in cloudDiskUse){
var maxDays = cloudDiskUse[storageType].maxDays
if(maxDays){
const { err, rows: videos } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Cloud Videos",
where: [
['type','=', storageType],
['ke','=', groupKey],
['archive','!=', `1`],
['time','<', sqlDate(maxDays+' DAY')],
]
});
if(videos.length > 0){
affectedRows += videos.length;
for(video of videos){
s.setCloudDiskUsedForGroup(groupKey,{
amount : -(video.size/1048576),
storageType : storageType
})
s.deleteVideoFromCloudExtensionsRunner({ke: groupKey},storageType,video)
}
}
}
}
return {
ok: !err,
err,
affectedRows,
}
}
const deleteOldCloudVideos = async (v) => {
// v = group, admin user
if(config.cron.deleteOld === true){
const { affectedRows } = await deleteCloudVideosByDays(v,cloudDiskUse)
if(affectedRows > 0 || config.debugLog === true){
postMessage({
f: 'deleteOldCloudVideos',
msg: `${affectedRows} Cloud Videos deleted`,
ke: v.ke,
time: 'moment()',
})
}
}
}
//user processing function
const processUser = async (v) => {
if(!v){
@ -594,6 +689,8 @@ function beginProcessing(){
debugLog('--- deleteOldFileBins Complete')
await deleteOldEvents(v)
debugLog('--- deleteOldEvents Complete')
await deleteOldAlarms(v)
debugLog('--- deleteOldAlarms Complete')
await deleteOldEventCounts(v)
debugLog('--- deleteOldEventCounts Complete')
await checkFilterRules(v)

View File

@ -0,0 +1,9 @@
module.exports = async function(s,config){
s.debugLog('Updating database to 2025-03-05')
const {
addColumn,
} = require('../utils.js')(s,config)
await addColumn('Alarms',[
{name: 'videos', type: 'text'},
])
}

View File

@ -178,10 +178,42 @@ module.exports = function(s,config){
{name: 'end', length: 10, type: 'string'},
{name: 'enabled', type: 'integer', length: 1, defaultTo: 1},
]);
await createTable('Permission Sets',[
isMySQL ? {name: 'utf8', type: 'charset'} : null,
isMySQL ? {name: 'utf8_general_ci', type: 'collate'} : null,
{name: 'ke', length: 50, type: 'string'},
{name: 'name', length: 100, type: 'string'},
{name: 'details', type: 'text'},
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
]);
await createTable('Alarms',[
isMySQL ? {name: 'utf8', type: 'charset'} : null,
isMySQL ? {name: 'utf8_general_ci', type: 'collate'} : null,
{name: 'ke', length: 50, type: 'string'},
{name: 'mid', length: 100, type: 'string'},
{name: 'name', length: 100, type: 'string'},
{name: 'videos', type: 'text'},
{name: 'notes', length: 100, type: 'string'},
{name: 'status', type: 'tinyint', length: 1, defaultTo: 0},
{name: 'editedBy', length: 50, type: 'string'},
{name: 'details', type: 'text'},
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
{name: 'end', type: 'timestamp', defaultTo: currentTimestamp()},
]);
await createTable('Custom Settings',[
isMySQL ? {name: 'utf8', type: 'charset'} : null,
isMySQL ? {name: 'utf8_general_ci', type: 'collate'} : null,
{name: 'ke', length: 50, type: 'string'},
{name: 'uid', length: 50, type: 'string'},
{name: 'name', length: 100, type: 'string'},
{name: 'details', type: 'text'},
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
]);
// additional requirements for older installs
await require('./migrate/2022-08-22.js')(s,config)
await require('./migrate/2022-12-18.js')(s,config)
await require('./migrate/2023-03-11.js')(s,config)
await require('./migrate/2025-03-05.js')(s,config)
delete(s.preQueries)
}
}

View File

@ -1,4 +1,5 @@
var fs = require('fs')
var path = require('path')
var exec = require('child_process').exec
module.exports = function(s,config,lang,app,io){
const base64Prefix = '=?UTF-8?B?';
@ -56,7 +57,7 @@ module.exports = function(s,config,lang,app,io){
var snapPath = s.dir.streams + ke + '/' + mid + '/s.jpg'
fs.rm(snapPath,async function(err){
await copyFileAsync(filePath, snapPath)
triggerEvent({
const eventData = {
id: mid,
ke: ke,
details: {
@ -64,8 +65,14 @@ module.exports = function(s,config,lang,app,io){
name: filename,
plug: "dropInEvent",
reason: "ftpServer"
},
},config.dropInEventForceSaveEvent)
}
}
try{
eventData.frame = await fs.promises.readFile(filePath);
}catch(err){
}
triggerEvent(eventData,config.dropInEventForceSaveEvent)
})
}else{
var reason = "ftpServer"
@ -79,7 +86,7 @@ module.exports = function(s,config,lang,app,io){
var writeStream = fs.createWriteStream(recordingPath)
fs.createReadStream(filePath).pipe(writeStream)
writeStream.on('finish', () => {
s.insertCompletedVideo(s.group[monitorConfig.ke].rawMonitorConfigurations[monitorConfig.mid],{
s.insertCompletedVideo(monitorConfig,{
file: shinobiFilename,
events: [
{
@ -126,42 +133,52 @@ module.exports = function(s,config,lang,app,io){
}
}
var onFileOrFolderFound = function(filePath,deletionKey,monitorConfig){
fs.stat(filePath,function(err,stats){
if(!err){
if(stats.isDirectory()){
fs.readdir(filePath,function(err,files){
if(files){
files.forEach(function(filename){
onFileOrFolderFound(clipPathEnding(filePath) + '/' + filename,deletionKey,monitorConfig)
})
}else if(err){
console.log(err)
}
function deleteFile(filePath, numberOfMinutes = 5){
// console.log(`QUEUE deleteFile in ${numberOfMinutes} minutes : `, filePath)
clearTimeout(fileQueue[filePath])
fileQueue[filePath] = setTimeout(async function(){
try{
await fs.promises.rm(filePath, { recursive: true });
// console.log('DONE deleteFile : ', filePath)
}catch(err){
// console.log('ERROR deleteFile : ', filePath, err)
}
delete(fileQueue[filePath])
},1000 * 60 * numberOfMinutes)
}
async function onFileOrFolderFound(filePath, monitorConfig){
try{
const stats = await fs.promises.stat(filePath)
const isDirectory = stats.isDirectory();
if(isDirectory){
const files = await fs.promises.readdir(filePath)
if(files){
files.forEach(function(filename){
const fileInDirectory = path.join(filePath, filename);
// console.log('File Found in FTP Directory : ', fileInDirectory)
onFileOrFolderFound(fileInDirectory, monitorConfig)
})
}else{
if(!fileQueue[filePath]){
processFile(filePath,monitorConfig)
if(config.dropInEventDeleteFileAfterTrigger){
clearTimeout(fileQueue[filePath])
fileQueue[filePath] = setTimeout(function(){
fs.rm(filePath, { recursive: true },(err) => {
delete(fileQueue[filePath])
})
},1000 * 60 * 5)
}
deleteFile(filePath, 6)
}else{
if(!fileQueue[filePath]){
// console.log('Processing File in FTP : ', filePath)
processFile(filePath, monitorConfig)
if(config.dropInEventDeleteFileAfterTrigger){
const aboveFolder = path.dirname(filePath);
const monitorDirectory = path.join(s.dir.dropInEvents, monitorConfig.ke, monitorConfig.mid);
if(aboveFolder !== monitorDirectory){
// console.log('Delete aboveFolder', aboveFolder)
deleteFile(aboveFolder)
}else{
deleteFile(filePath)
}
}
}
if(config.dropInEventDeleteFileAfterTrigger){
clearTimeout(fileQueueForDeletion[deletionKey])
fileQueueForDeletion[deletionKey] = setTimeout(function(){
fs.rm(filePath, { recursive: true },(err) => {
delete(fileQueueForDeletion[deletionKey])
})
},1000 * 60 * 5)
}
}
})
}
}catch(err){
console.log(err)
}
}
var createDropInEventsDirectory = function(){
try{
@ -243,21 +260,35 @@ module.exports = function(s,config,lang,app,io){
ftpServer.on('login', ({connection, username, password}, resolve, reject) => {
s.basicOrApiAuthentication(username,password,function(err,user){
if(user){
// console.log('FTP : login',username, password)
connection.on('STOR', (error, fileName) => {
// console.log('FTP : STOR',fileName,error)
if(!fileName)return;
var pathPieces = fileName.replace(s.dir.dropInEvents,'').split('/')
var ke = pathPieces[0]
var mid = pathPieces[1]
var firstDroppedPart = pathPieces[2]
var monitorEventDropDir = s.dir.dropInEvents + ke + '/' + mid + '/'
var deleteKey = monitorEventDropDir + firstDroppedPart
fs.mkdir(pathPieces.join('/'), { recursive: true }, (err) => {
onFileOrFolderFound(monitorEventDropDir + firstDroppedPart,deleteKey,Object.assign({},s.group[ke].rawMonitorConfigurations[mid]))
})
try{
const pathPieces = fileName.replace(s.dir.dropInEvents,'').split('/')
const ke = user.ke
const mid = pathPieces[1]
const monitorConfig = s.group[ke].rawMonitorConfigurations[mid];
const monitorDirectory = path.join(s.dir.dropInEvents, user.ke, mid);
if(monitorConfig){
onFileOrFolderFound(fileName, monitorConfig)
}else{
deleteFile(monitorDirectory, 0.1)
s.userLog({ ke, mid: '$USER' }, {
type: 'FTP Upload Error',
msg: lang.FTPMonitorIdNotFound
});
// console.log('Monitor ID Not Found or Not Active')
}
}catch(err){
deleteFile(fileName, 0.1)
console.log('FTP Failed Processing')
}
})
resolve({root: s.dir.dropInEvents + user.ke})
}else{
// reject(new Error('Failed Authorization'))
// console.log('FTP : AUTH FAIL')
reject(new Error('Failed Authorization'))
}
})
})

107
libs/events/alarms.js Normal file
View File

@ -0,0 +1,107 @@
module.exports = function(s,config,lang){
const acceptableOperators = ['>=','>','<','<=','=']
function sanitizeOperator(startOrEndOperator = ''){
const theOperator = `${startOrEndOperator}`.trim()
if(!theOperator || acceptableOperators.indexOf(theOperator) === -1){
return undefined
}else{
return theOperator
}
}
async function getAlarm({ ke, mid, name, status, editedBy, time, start, startOperator = '>=', end, endOperator = '<=', limit }){
const whereQuery = [
['ke','=',ke],
];
if(mid)whereQuery.push(['mid','=',mid]);
if(name)whereQuery.push(['name','=',name]);
if(status !== undefined && status !== null)whereQuery.push(['status','=',status]);
if(editedBy)whereQuery.push(['editedBy','=',editedBy]);
if(time || start)whereQuery.push(['time',startOperator,time || start]);
if(end)whereQuery.push(['end',endOperator,end]);
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Alarms",
orderBy: ['time','desc'],
where: whereQuery,
limit
});
for(row of rows){
row.details = JSON.parse(row.details);
row.videos = JSON.parse(row.videos || '{}');
}
return rows
}
function getAlarmParams({ mid, name, videos, videoTime, notes, status, editedBy, details, time, start, end }, isForUpdate){
const params = {};
if(isForUpdate && details)params.details = s.stringJSON(details || {});
if(isForUpdate && videos)params.videos = s.stringJSON(videos || {});
if(mid)params.mid = mid;
if(name)params.name = name;
if(videoTime)params.videoTime = videoTime;
if(notes)params.notes = notes;
if(status !== undefined && status !== null)params.status = status;
if(editedBy)params.editedBy = editedBy;
if(start)params.time = start;
if(time)params.time = time;
if(end)params.end = end;
return params
}
async function createAlarm({ ke, mid, name, videos, videoTime, notes, status, editedBy, details = {}, time, end }){
const insertQuery = {
ke,
};
const alarmParams = getAlarmParams({ ke, mid, name, videos, videoTime, notes, status, editedBy, details, time, end });
for(param in alarmParams){
insertQuery[param] = alarmParams[param]
}
await s.knexQueryPromise({
action: "insert",
table: "Alarms",
insert: insertQuery
})
return insertQuery;
}
async function updateAlarm({ ke, mid, name, videos, videoTime, notes, status, editedBy, details, time, start, end }){
const whereQuery = {
ke,
mid,
time: time || start,
};
const updateQuery = getAlarmParams({ ke, name, videos, videoTime, notes, status, editedBy, details, end }, true);
const response = { ok: true }
try{
if(Object.keys(updateQuery).length > 0){
await s.knexQueryPromise({
action: "update",
table: "Alarms",
where: whereQuery,
update: updateQuery
})
}
}catch(err){
response.ok = false;
response.err = err.toString();
}
return response;
}
async function deleteAlarm({ ke, mid, start }){
const whereQuery = {
ke,
mid,
time,
};
return await s.knexQueryPromise({
action: "delete",
table: "Alarms",
where: whereQuery
})
}
return {
getAlarm,
createAlarm,
updateAlarm,
deleteAlarm,
sanitizeOperator,
}
}

View File

@ -19,8 +19,14 @@ module.exports = (s,config,lang) => {
splitForFFMPEG
} = require('../ffmpeg/utils.js')(s,config,lang)
const {
moveCameraPtzToMatrix
moveCameraPtzToMatrix,
moveToHomePositionTimeout,
} = require('../control/ptz.js')(s,config,lang)
const {
getOnvifDevice,
getPresets,
goToPreset,
} = require('../onvifDeviceManager/utils.js')(s,config,lang)
const {
cutVideoLength,
reEncodeVideoAndBinOriginalAddToQueue
@ -34,6 +40,7 @@ module.exports = (s,config,lang) => {
const {
isEven,
fetchTimeout,
copyFile,
} = require('../basic/utils.js')(process.cwd(),config)
const glyphs = require('../../definitions/glyphs.js')
async function saveImageFromEvent(options,frameBuffer){
@ -477,29 +484,66 @@ module.exports = (s,config,lang) => {
})
}
moveAssociatedMonitorPtzTargets(groupKey, monitorId)
for (var i = 0; i < s.onEventTriggerExtensions.length; i++) {
const extender = s.onEventTriggerExtensions[i]
await extender(d,filter)
await extender(d,filter,eventTime)
}
}
const getEventBasedRecordingUponCompletion = function(options){
const saveEventBaseRecordingClip = async function({
groupKey,
monitorId,
filename,
filePath,
details = {}
}){
const response = { ok: true }
try{
const fileBinFilePath = s.getFileBinDirectory({ ke: groupKey, mid: monitorId }) + filename;
const copyResponse = await copyFile(filePath,fileBinFilePath)
const fileSize = (await fs.stat(fileBinFilePath)).size
// s.file('delete',filePath)
const fileBinInsertQuery = {
ke: groupKey,
mid: monitorId,
name: filename,
size: fileSize,
details: JSON.stringify(details),
status: 1,
time: new Date(),
}
await s.insertFileBinEntry(fileBinInsertQuery)
response.fileBinInsertQuery = fileBinInsertQuery
response.fileBinPath = fileBinFilePath
}catch(err){
response.ok = false;
console.log(err)
response.err = err.toString();
}
return response;
}
const getEventBasedRecordingUponCompletion = function(options, getNonCut){
const response = {ok: true}
return new Promise((resolve,reject) => {
return new Promise(async (resolve,reject) => {
const groupKey = options.ke
const monitorId = options.mid
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
if(activeMonitor && activeMonitor.eventBasedRecording && activeMonitor.eventBasedRecording.process){
const eventBasedRecording = activeMonitor.eventBasedRecording
if(!activeMonitor || !activeMonitor.eventBasedRecording){
return resolve(response)
}
const fileTime = options.fileTime || activeMonitor.eventBasedRecordingLastFileTime;
if(activeMonitor.eventBasedRecording[fileTime] && activeMonitor.eventBasedRecording[fileTime].process){
const eventBasedRecording = activeMonitor.eventBasedRecording[fileTime]
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const videoLength = parseInt(monitorConfig.details.detector_send_video_length) || 10
const recordingDirectory = s.getVideoDirectory(monitorConfig)
const fileTime = eventBasedRecording.lastFileTime
const filename = `${fileTime}.mp4`
response.filename = `${filename}`
response.filePath = `${recordingDirectory}${filename}`
eventBasedRecording.process.on('exit',function(){
eventBasedRecording.process.on('exit', async function(){
setTimeout(async () => {
if(!isNaN(videoLength)){
if(!getNonCut && !isNaN(videoLength)){
const cutResponse = await cutVideoLength({
ke: groupKey,
mid: monitorId,
@ -507,8 +551,21 @@ module.exports = (s,config,lang) => {
cutLength: videoLength,
})
if(cutResponse.ok){
const { ok, fileBinPath, fileBinInsertQuery } = await saveEventBaseRecordingClip({
groupKey,
monitorId,
filename: cutResponse.filename,
filePath: cutResponse.filePath,
details: {
source: `${response.filePath}`
}
});
response.filename = cutResponse.filename
response.filePath = cutResponse.filePath
if(ok){
response.fileBinPath = fileBinPath;
response.fileBinInsertQuery = fileBinInsertQuery;
}
}else{
s.debugLog('cutResponse',cutResponse)
}
@ -525,6 +582,36 @@ module.exports = (s,config,lang) => {
}
})
}
const getEventBasedRecordingsUponCompletion = function(groupKey, monitorIds, withPath = false, asObject = true, getNonCut){
return new Promise((resolve) => {
const response = asObject ? {} : [];
const total = monitorIds.length;
let currentCount = 0;
monitorIds.forEach((monitorId) => {
getEventBasedRecordingUponCompletion({
ke: groupKey,
mid: monitorId
}, getNonCut).then(({ filename, filePath }) => {
if(filename && filePath){
if(asObject){
response[monitorId] = filename;
}else{
const foundData = {
mid: monitorId,
filename,
}
if(withPath)foundData.filePath = filePath;
response.push(foundData)
}
}
++currentCount;
if(currentCount === total){
resolve(response)
}
});
})
})
}
const createEventBasedRecording = function(d,fileTime){
if(!fileTime)fileTime = s.formattedTime()
const logTitleText = lang["Traditional Recording"]
@ -569,6 +656,9 @@ module.exports = (s,config,lang) => {
let outputMap = `-map 0:0 `
const analyzeDuration = parseInt(monitorDetails.event_record_aduration) || 1000
const probeSize = parseInt(monitorDetails.event_record_probesize) || 32
const audioCodec = monitorDetails.detector_buffer_acodec;
const noAudio = audioCodec === 'no';
const autoAudio = !audioCodec || audioCodec === 'auto';
s.userLog(d,{
type: logTitleText,
msg: lang["Started"]
@ -579,31 +669,37 @@ module.exports = (s,config,lang) => {
}
//-t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+'
if(
monitorDetails.detector_buffer_acodec &&
monitorDetails.detector_buffer_acodec !== 'no' &&
monitorDetails.detector_buffer_acodec !== 'auto'
audioCodec &&
audioCodec !== 'no' &&
audioCodec !== 'auto'
){
outputMap += `-map 0:1? `
}
const secondsBefore = parseInt(monitorDetails.detector_buffer_seconds_before) || 5
let LiveStartIndex = parseInt(secondsBefore / 2 + 1)
const ffmpegCommand = `-loglevel warning -live_start_index -${LiveStartIndex} -analyzeduration ${analyzeDuration} -probesize ${probeSize} -re -i "${s.dir.streams+groupKey+'/'+monitorId}/detectorStream.m3u8" ${outputMap}-movflags faststart -fflags +igndts -c:v copy -c:a aac -strict -2 -strftime 1 -y "${s.getVideoDirectory(monitorConfig) + filename}"`
const ffmpegCommand = `-loglevel warning -live_start_index -${LiveStartIndex} -analyzeduration ${analyzeDuration} -probesize ${probeSize} -re -i "${s.dir.streams+groupKey+'/'+monitorId}/detectorStream.m3u8" ${outputMap}-movflags faststart -fflags +igndts -c:v copy ${noAudio ? '-an' : autoAudio ? '' : `-c:a aac`} -strict -2 -strftime 1 -y "${s.getVideoDirectory(monitorConfig) + filename}"`
s.debugLog(ffmpegCommand)
activeMonitor.eventBasedRecording[fileTime].process = spawn(
config.ffmpegDir,
splitForFFMPEG(ffmpegCommand)
)
activeMonitor.eventBasedRecording[fileTime].process.stdout.on('data',function(data){
s.userLog(d,{
type: `${logTitleText} : STDOUT`,
msg: data.toString()
})
})
activeMonitor.eventBasedRecording[fileTime].process.stderr.on('data',function(data){
s.userLog(d,{
type: logTitleText,
type: `${logTitleText} : STDERR`,
msg: data.toString()
})
})
activeMonitor.eventBasedRecording[fileTime].process.on('close',function(){
if(!activeMonitor.eventBasedRecording[fileTime].allowEnd){
s.userLog(d,{
type: logTitleText,
msg: lang["Detector Recording Process Exited Prematurely. Restarting."]
type: `${logTitleText} : ${lang["Detector Recording Process Exited Prematurely. Restarting."]}`,
msg: ffmpegCommand
})
runRecord()
return
@ -827,6 +923,7 @@ module.exports = (s,config,lang) => {
f: 'detector_trigger',
id: d.id,
ke: d.ke,
time: eventTime,
details: eventDetails,
doObjectDetection: d.doObjectDetection
},`DETECTOR_${monitorConfig.ke}${monitorConfig.mid}`);
@ -874,7 +971,33 @@ module.exports = (s,config,lang) => {
const tags = getObjectTagsFromMatrices(d)
return `${tags.join(', ')} ${lang.detected} in ${monitorName}`
}
function getAssociatedMonitorPtzTargets(groupKey, monitorId){
const monitorDetails = s.group[groupKey].rawMonitorConfigurations[monitorId].details;
const detectorEventPtz = monitorDetails.detectorEventPtz === '1';
if(detectorEventPtz){
const triggerMonitorsPtzTargets = monitorDetails.triggerMonitorsPtzTargets || {}
return triggerMonitorsPtzTargets;
}else{
return {}
}
}
async function moveAssociatedMonitorPtzTargets(groupKey, monitorId){
const response = { ok: true, responseFromDevices: {} };
const triggerMonitorsPtzTargets = getAssociatedMonitorPtzTargets(groupKey, monitorId);
for(targetMonitorId in triggerMonitorsPtzTargets){
const presetToken = triggerMonitorsPtzTargets[targetMonitorId]
const onvifEnabled = s.group[groupKey].rawMonitorConfigurations[targetMonitorId].details.is_onvif === '1';
if(onvifEnabled){
var onvifDevice = await getOnvifDevice(groupKey, targetMonitorId);
response.responseFromDevices[targetMonitorId] = await goToPreset(onvifDevice, presetToken);
moveToHomePositionTimeout({ id: targetMonitorId, ke: groupKey }, 30000)
}
}
return response
}
return {
getAssociatedMonitorPtzTargets,
moveAssociatedMonitorPtzTargets,
getObjectTagNotifyText,
getObjectTagsFromMatrices,
countObjects: countObjects,
@ -896,5 +1019,6 @@ module.exports = (s,config,lang) => {
triggerEvent: triggerEvent,
addEventDetailsToString: addEventDetailsToString,
getEventBasedRecordingUponCompletion: getEventBasedRecordingUponCompletion,
getEventBasedRecordingsUponCompletion,
}
}

View File

@ -59,6 +59,7 @@ module.exports = function(s,config){
createExtension(`onEventTrigger`)
createExtension(`onEventTriggerBeforeFilter`)
createExtension(`onFilterEvent`)
createExtension(`onOnvifEventTrigger`)
////// MONITOR //////
createExtension(`onMonitorInit`)
createExtension(`onMonitorStart`)

View File

@ -89,11 +89,15 @@ module.exports = async (s,config,lang,onFinish) => {
]
const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes})
if(config.debugLog === true && config.debugLogMonitors === true){
cameraProcess.stderr.on('close',(data) => {
delete(s.dataPortTokens[dataPortToken])
})
cameraProcess.stderr.on('data',(data) => {
const string = data.toString()
var checkLog = function(x){return string.indexOf(x)>-1}
switch(true){
case checkLog('pkt->duration = 0'):
case checkLog('bad cseq'):
case checkLog('[hls @'):
case checkLog('Past duration'):
case checkLog('Last message repeated'):

View File

@ -636,7 +636,7 @@ module.exports = (s,config,lang) => {
const baseDimensionsFlag = `-s ${baseWidth}x${baseHeight}`
const baseFps = e.details.detector_fps ? e.details.detector_fps : '2'
const baseFpsFilter = 'fps=' + baseFps
const objectDetectorDimensionsFlag = `-s ${e.details.detector_scale_x_object ? e.details.detector_scale_x_object : baseWidth}x${e.details.detector_scale_y_object ? e.details.detector_scale_y_object : baseHeight}`
const objectDetectorDimensionsFlag = `-s ${e.details.detector_scale_x_object ? e.details.detector_scale_x_object : 1280}x${e.details.detector_scale_y_object ? e.details.detector_scale_y_object : 720}`
const objectDetectorFpsFilter = 'fps=' + (e.details.detector_fps_object ? e.details.detector_fps_object : baseFps)
const cudaVideoFilters = 'hwdownload,format=nv12'
const videoFilters = []

View File

@ -249,7 +249,7 @@ module.exports = function(s,config,lang,app,io){
app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:file', async (req,res) => {
s.auth(req.params,function(user){
var failed = function(){
res.end(user.lang['File Not Found'])
res.end(lang['File Not Found'])
}
const groupKey = req.params.ke
const monitorId = req.params.id
@ -356,11 +356,11 @@ module.exports = function(s,config,lang,app,io){
await deleteFileBinEntry(file)
break;
default:
response.msg = user.lang.modifyVideoText1;
response.msg = lang.modifyVideoText1;
break;
}
}else{
response.msg = user.lang['No such file'];
response.msg = lang['No such file'];
}
s.closeJsonResponse(res,response);
})

View File

@ -10,6 +10,7 @@ const { queryStringToObject, createQueryStringFromObject } = require('./common.j
module.exports = function(s,config,lang){
const {
asyncSetTimeout,
cleanStringsInObject,
} = require('./basic/utils.js')(process.cwd(),config)
const {
splitForFFMPEG,
@ -73,10 +74,14 @@ module.exports = function(s,config,lang){
if(!e.status || !e.code)console.error(JSON.stringify(e),new Error());
const groupKey = e.ke
const monitorId = e.mid || e.id
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
activeMonitor.monitorStatus = `${e.status}`
activeMonitor.monitorStatusCode = `${e.code}`
s.tx(Object.assign(e,{f:'monitor_status'}),'GRP_'+e.ke)
try{
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
activeMonitor.monitorStatus = `${e.status}`
activeMonitor.monitorStatusCode = `${e.code}`
s.tx(Object.assign(e,{f:'monitor_status'}),'GRP_'+e.ke)
}catch(err){
}
}
s.getMonitorCpuUsage = function(e,callback){
if(s.group[e.ke].activeMonitors[e.mid] && s.group[e.ke].activeMonitors[e.mid].spawn){
@ -155,6 +160,7 @@ module.exports = function(s,config,lang){
return s.dir.streams + monitor.ke + '/' + (monitor.mid || monitor.id) + '/'
}
s.getRawSnapshotFromMonitor = function(monitor,options){
if(!monitor || !monitor.details)return {};
return new Promise((resolve,reject) => {
options = options instanceof Object ? options : {flags: ''}
s.checkDetails(monitor)
@ -557,6 +563,7 @@ module.exports = function(s,config,lang){
if(callback)callback(endData);
return
}
cleanStringsInObject(form)
form.mid = form.mid.replace(/[^\w\s]/gi,'').replace(/ /g,'')
const selectResponse = await s.knexQueryPromise({
action: "select",
@ -607,7 +614,7 @@ module.exports = function(s,config,lang){
}
})
s.userLog(form,{type:'Monitor Updated',msg:'by user : '+user.uid})
endData.msg = user.lang['Monitor Updated by user']+' : '+user.uid
endData.msg = lang['Monitor Updated by user']+' : '+user.uid
s.knexQuery({
action: "update",
table: "Monitors",
@ -629,7 +636,7 @@ module.exports = function(s,config,lang){
}
})
s.userLog(form,{type:'Monitor Added',msg:'by user : '+user.uid})
endData.msg = user.lang['Monitor Added by user']+' : '+user.uid
endData.msg = lang['Monitor Added by user']+' : '+user.uid
s.knexQuery({
action: "insert",
table: "Monitors",
@ -639,7 +646,7 @@ module.exports = function(s,config,lang){
}else{
txData.f = 'monitor_edit_failed'
txData.ff = 'max_reached'
endData.msg = !systemMax ? user.lang.monitorEditFailedMaxReachedUnactivated : user.lang.monitorEditFailedMaxReached
endData.msg = !systemMax ? lang.monitorEditFailedMaxReachedUnactivated : lang.monitorEditFailedMaxReached
}
if(affectMonitor === true){
form.details = JSON.parse(form.details)
@ -789,12 +796,12 @@ module.exports = function(s,config,lang){
s.tx({f:'change_group_state',ke:groupKey,name:stateName},'GRP_'+groupKey)
callback(endData)
}else{
endData.msg = user.lang['State Configuration has no monitors associated']
endData.msg = lang['State Configuration has no monitors associated']
callback(endData)
}
})
}else{
endData.msg = user.lang['State Configuration Not Found']
endData.msg = lang['State Configuration Not Found']
callback(endData)
}
})
@ -851,6 +858,9 @@ module.exports = function(s,config,lang){
const details = user.details;
[
'auth_socket',
'create_api_keys',
'edit_user',
'edit_permissions',
'get_monitors',
'edit_monitors',
'control_monitors',
@ -875,6 +885,7 @@ module.exports = function(s,config,lang){
'monitor_create',
'user_change',
'view_logs',
'edit_permissions',
].forEach((key) => {
response.userPermissions[key] = details[key] === '1' || !details[key];
response.userPermissions[`${key}_disallowed`] = details[key] === '0';

View File

@ -81,7 +81,9 @@ module.exports = (s,config,lang) => {
treekill(processPID)
});
if(proc && proc.stdin) {
proc.stdin.write("q\r\n");
try{
proc.stdin.write("q\r\n");
}catch(err){}
}
let killTimer = setTimeout(() => {
if(proc && proc.kill){
@ -677,10 +679,9 @@ module.exports = (s,config,lang) => {
type: lang.monitorDeleted,
msg: `${lang.byUser} : ${userId}`
});
s.camera('stop', {
await s.camera('stop', {
ke: groupKey,
mid: monitorId,
delete: 1,
});
s.tx({
f: 'monitor_delete',
@ -1422,6 +1423,7 @@ module.exports = (s,config,lang) => {
break;
case checkLog(d,'pkt->duration = 0'):
case checkLog(d,'[hls @'):
case checkLog(d,'bad cseq'):
case checkLog(d,'Past duration'):
case checkLog(d,'Last message repeated'):
case checkLog(d,'Non-monotonous DTS'):
@ -1631,7 +1633,7 @@ module.exports = (s,config,lang) => {
}
}
s.onMonitorStartExtensions.forEach(function(extender){
extender(Object.assign(theGroup.rawMonitorConfigurations[monitorId],{}),e)
extender(Object.assign({},theGroup.rawMonitorConfigurations[monitorId]),e)
})
resolve()
})
@ -1738,12 +1740,13 @@ module.exports = (s,config,lang) => {
async function monitorStart(e){
const groupKey = e.ke
const monitorId = e.mid || e.id
const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
let monitorConfig = getMonitorConfiguration(groupKey,monitorId);
monitorConfigurationMigrator(e)
s.initiateMonitorObject({ke:groupKey,mid:monitorId})
const activeMonitor = getActiveMonitor(groupKey,monitorId)
if(!monitorConfig){
s.group[groupKey].rawMonitorConfigurations[monitorId] = s.cleanMonitorObject(e)
monitorConfig = s.cleanMonitorObject(e)
s.group[groupKey].rawMonitorConfigurations[monitorId] = monitorConfig
}
if(activeMonitor.isStarted === true){
s.debugLog('Monitor Already Started!')
@ -1803,7 +1806,7 @@ module.exports = (s,config,lang) => {
const monitorId = e.mid || e.id
const activeMonitor = getActiveMonitor(groupKey,monitorId);
//parse Objects
(['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){
(['cords','detector_filters','input_map_choices']).forEach(function(v){
if(e.details && e.details[v]){
try{
if(!e.details[v] || e.details[v] === '')e.details[v] = '{}'
@ -1838,8 +1841,8 @@ module.exports = (s,config,lang) => {
(['stream_channels','input_maps']).forEach(function(v){
if(e.details&&e.details[v]&&(e.details[v] instanceof Array)===false){
try{
e.details[v]=JSON.parse(e.details[v]);
if(!e.details[v])e.details[v]=[];
e.details[v] = s.parseJSON(e.details[v]);
if(!e.details[v])e.details[v] = [];
}catch(err){
e.details[v]=[];
}
@ -1887,10 +1890,117 @@ module.exports = (s,config,lang) => {
monitorConfig.details.stream_channels = ''
monitorConfig.details.input_maps = ''
delete(monitorConfig.details.input_map_choices)
delete(monitorConfig.details.substream)
if(monitorConfig.details.substream && monitorConfig.details.substream.fulladdress)delete(monitorConfig.details.substream.fulladdress);
return monitorConfig
}
function getMonitors(groupKey, monitorId, authKey, isRestricted, monitorPermissions, monitorRestrictions, cannotSeeImportantSettings, search){
return new Promise((resolve) => {
const whereQuery = [
['ke','=',groupKey],
monitorRestrictions
];
if(!!search){
const searchQuery = search.split(',');
const whereQuerySearch = []
for(item of searchQuery){
if(item){
whereQuerySearch.push(
whereQuerySearch.length === 0 ? ['name','LIKE',`%${item.trim()}%`] : ['or', 'name','LIKE',`%${item}%`],
['or','mid','LIKE',`%${item.trim()}%`]
);
}
}
whereQuery.push(whereQuerySearch)
}
s.knexQuery({
action: "select",
columns: "*",
table: "Monitors",
where: whereQuery
},(err,r) => {
if(err){
return []
}
r.forEach(function(v,n){
const monitorId = v.mid;
v.details = JSON.parse(v.details)
var details = v.details;
if(isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`] || cannotSeeImportantSettings){
r[n] = removeSenstiveInfoFromMonitorConfig(v);
}
if(s.group[v.ke] && s.group[v.ke].activeMonitors[v.mid]){
const activeMonitor = s.group[v.ke].activeMonitors[v.mid]
r[n].currentlyWatching = Object.keys(activeMonitor.watch).length
r[n].currentCpuUsage = activeMonitor.currentCpuUsage
r[n].status = activeMonitor.monitorStatus
r[n].code = activeMonitor.monitorStatusCode
r[n].subStreamChannel = activeMonitor.subStreamChannel
r[n].subStreamActive = !!activeMonitor.subStreamProcess
}
function getStreamUrl(type,channelNumber){
var streamURL
if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''}
switch(type){
case'mjpeg':
streamURL='/'+authKey+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber
break;
case'hls':
streamURL='/'+authKey+'/hls/'+v.ke+'/'+v.mid+channelNumber+'/s.m3u8'
break;
case'h264':
streamURL='/'+authKey+'/h264/'+v.ke+'/'+v.mid+channelNumber
break;
case'flv':
streamURL='/'+authKey+'/flv/'+v.ke+'/'+v.mid+channelNumber+'/s.flv'
break;
case'mp4':
streamURL='/'+authKey+'/mp4/'+v.ke+'/'+v.mid+channelNumber+'/s.mp4'
break;
case'useSubstream':
try{
const monitorConfig = s.group[v.ke].rawMonitorConfigurations[v.mid]
const monitorDetails = monitorConfig.details
const subStreamChannelNumber = 1 + (monitorDetails.stream_channels || []).length
const subStreamType = monitorConfig.details.substream.output.stream_type
streamURL = getStreamUrl(subStreamType,subStreamChannelNumber)
}catch(err){
s.debugLog(err)
}
break;
}
return streamURL
}
var buildStreamURL = function(type,channelNumber){
var streamURL = getStreamUrl(type,channelNumber)
if(streamURL){
if(!r[n].streamsSortedByType[type]){
r[n].streamsSortedByType[type]=[]
}
r[n].streamsSortedByType[type].push(streamURL)
r[n].streams.push(streamURL)
}
return streamURL
}
if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5)
if(details.snap==='1'){
r[n].snapshot = '/'+authKey+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg'
}
r[n].streams=[]
r[n].streamsSortedByType={}
buildStreamURL(details.stream_type)
if(details.stream_channels&&details.stream_channels!==''){
details.stream_channels=s.parseJSON(details.stream_channels)
details.stream_channels.forEach(function(b,m){
buildStreamURL(b.stream_type,m.toString())
})
}
})
resolve(r);
})
})
}
return {
getMonitors,
monitorStop,
monitorIdle,
monitorStart,

76
libs/monitor/websocket.js Normal file
View File

@ -0,0 +1,76 @@
module.exports = function(s,config,lang,io){
const { getMonitors } = require('./utils.js')(s,config,lang)
s.onOtherWebSocketMessages(async (d,cn,tx) => {
const authKey = cn.auth
const groupKey = cn.ke
const user = s.group[groupKey].users[authKey];
const monitorId = d.mid || d.id;
const callbackId = d.callbackId;
const response = { f: 'callback', callbackId, args: [true] }
switch(d.f){
case'getMonitors':
response.ff = 'getMonitors'
var {
monitorPermissions,
monitorRestrictions,
} = s.getMonitorsPermitted(user.details,monitorId)
var {
isRestricted,
userPermissions,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
if(
isRestrictedApiKey && apiKeyPermissions.get_monitors_disallowed ||
isRestricted && (
monitorId && !monitorPermissions[`${monitorId}_monitors`] ||
monitorRestrictions.length === 0
)
){
//not authorized
}else{
const cannotSeeImportantSettings = (isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed) || userPermissions.monitor_create_disallowed;
const monitors = await getMonitors(groupKey, monitorId, authKey, isRestricted, monitorPermissions, monitorRestrictions, cannotSeeImportantSettings, d.search)
response.args = [false, monitors]
}
tx(response);
break;
case'addOrEditMonitor':
response.ff = 'addOrEditMonitor'
var {
monitorPermissions,
monitorRestrictions,
} = s.getMonitorsPermitted(user.details,monitorId)
var {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user);
if(
userPermissions.monitor_create_disallowed ||
isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed ||
isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`]
){
response.msg = lang['Not Authorized'];
}else{
var form = d.form;
if(!form){
response.msg = lang.monitorEditText1;
}else{
form.mid = monitorId.replace(/[^\w\s]/gi,'').replace(/ /g,'')
if(form && form.name){
s.checkDetails(form)
form.ke = groupKey
const editResponse = await s.addOrEditMonitor(form,null,user);
response.args = [!editResponse.ok, editResponse];
}else{
response.args = [lang.monitorEditText1];
}
}
}
tx(response);
break;
}
})
}

View File

@ -93,7 +93,7 @@ module.exports = function(s,config,lang,getSnapshot){
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
mid: d.mid || d.id
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath
@ -129,11 +129,11 @@ module.exports = function(s,config,lang,getSnapshot){
if(r.details.factor_discord === '1'){
sendMessage({
author: {
name: r.lang['2-Factor Authentication'],
name: lang['2-Factor Authentication'],
icon_url: config.iconURL
},
title: r.lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1,
title: lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+lang.FactorAuthText1,
fields: [],
timestamp: new Date(),
footer: messageFooter

View File

@ -54,11 +54,11 @@ module.exports = function(s,config,lang,getSnapshot){
sendMessage({
from: config.mail.from,
to: checkEmail(r.mail),
subject: r.lang['2-Factor Authentication'],
html: r.lang['Enter this code to proceed']+' <b>'+s.factorAuth[r.ke][r.uid].key+'</b>. '+r.lang.FactorAuthText1,
subject: lang['2-Factor Authentication'],
html: lang['Enter this code to proceed']+' <b>'+s.factorAuth[r.ke][r.uid].key+'</b>. '+lang.FactorAuthText1,
}, (error, info) => {
if (error) {
s.systemLog(r.lang.MailError,error)
s.systemLog(lang.MailError,error)
return
}
})
@ -160,7 +160,7 @@ module.exports = function(s,config,lang,getSnapshot){
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
mid: d.mid || d.id
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath

View File

@ -102,11 +102,11 @@ module.exports = function (s, config, lang, getSnapshot) {
// r = user
if (r.details.factor_emailClient === '1') {
sendMessage({
subject: r.lang['2-Factor Authentication'],
subject: lang['2-Factor Authentication'],
html: template.createFramework({
title: r.lang['2-Factor Authentication'],
subtitle: r.lang['Enter this code to proceed'],
body: '<b style="font-size: 20pt;">'+s.factorAuth[r.ke][r.uid].key+'</b><br><br>'+r.lang.FactorAuthText1,
title: lang['2-Factor Authentication'],
subtitle: lang['Enter this code to proceed'],
body: '<b style="font-size: 20pt;">'+s.factorAuth[r.ke][r.uid].key+'</b><br><br>'+lang.FactorAuthText1,
}),
},[],r.ke);
}
@ -165,7 +165,7 @@ module.exports = function (s, config, lang, getSnapshot) {
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
mid: d.mid || d.id
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath

View File

@ -110,7 +110,7 @@ module.exports = async function(s,config,lang,getSnapshot){
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
mid: d.mid || d.id
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath
@ -141,7 +141,7 @@ module.exports = async function(s,config,lang,getSnapshot){
const onTwoFactorAuthCodeNotificationForMatrixBot = function(user){
// r = user
if(r.details.factor_matrixbot === '1'){
const eventText = `${user.lang['2-Factor Authentication']} : ${user.lang['Enter this code to proceed']} **${factorAuthKey}** ${user.lang.FactorAuthText1}`
const eventText = `${lang['2-Factor Authentication']} : ${lang['Enter this code to proceed']} **${factorAuthKey}** ${lang.FactorAuthText1}`
sendMessage({
text: eventText,
},[],d.ke)

View File

@ -118,9 +118,11 @@ module.exports = function(s,config,lang,getSnapshot){
//
const groupKey = d.ke
await getSnapshot(d,monitorConfig)
sendToMqttConnections(groupKey,'onEventTrigger',[Object.assign({},d,{
screenshotBuffer: d.screenshotBuffer.toString('base64')
}),filter],true)
if(d.screenshotBuffer){
sendToMqttConnections(groupKey,'onEventTrigger',[Object.assign({},d,{
screenshotBuffer: d.screenshotBuffer.toString('base64')
}),filter],true)
}
}
}
const onMonitorSave = (monitorConfig) => {
@ -141,7 +143,7 @@ module.exports = function(s,config,lang,getSnapshot){
}
const onEventBasedRecordingComplete = (response,monitorConfig) => {
const groupKey = monitorConfig.ke
sendToMqttConnections(groupKey,'onEventBasedRecordingComplete',[monitorConfig],true)
sendToMqttConnections(groupKey,'onEventBasedRecordingComplete',[response,monitorConfig],true)
}
const insertCompletedVideoExtender = (activeMonitor,temp,insertQuery,response) => {
const groupKey = insertQuery.ke

View File

@ -96,12 +96,12 @@ module.exports = function (s, config, lang, getSnapshot) {
if (r.details.factor_pushover === '1') {
sendMessage(
{
title: r.lang['Enter this code to proceed'],
title: lang['Enter this code to proceed'],
description:
'**' +
s.factorAuth[r.ke][r.uid].key +
'** ' +
r.lang.FactorAuthText1,
lang.FactorAuthText1,
},
[],
r.ke

View File

@ -132,7 +132,7 @@ module.exports = function(s,config,lang,getSnapshot){
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
mid: d.mid || d.id
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath
@ -166,8 +166,8 @@ module.exports = function(s,config,lang,getSnapshot){
// r = user
if(r.details.factor_telegram === '1'){
sendMessage({
title: r.lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1,
title: lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+lang.FactorAuthText1,
},[],r.ke)
}
}

View File

@ -131,8 +131,8 @@ module.exports = function(s,config,lang,getSnapshot){
// r = user
if(r.details.factor_global_webhook === '1'){
sendMessage({
title: r.lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1,
title: lang['Enter this code to proceed'],
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+lang.FactorAuthText1,
},[],r.ke)
}
}

View File

@ -1,22 +1,22 @@
const {
getDeviceInformation,
setHostname,
setProtocols,
setGateway,
setDNS,
setNTP,
rebootCamera,
setDateAndTime,
createUser,
deleteUser,
setVideoConfiguration,
setNetworkInterface,
setImagingSettings,
setDiscoveryMode,
getUIFieldValues,
} = require('./onvifDeviceManager/utils.js')
module.exports = function(s,config,lang,app,io){
const {
getDeviceInformation,
setHostname,
setProtocols,
setGateway,
setDNS,
setNTP,
rebootCamera,
setDateAndTime,
createUser,
deleteUser,
setVideoConfiguration,
setNetworkInterface,
setImagingSettings,
setDiscoveryMode,
getUIFieldValues,
} = require('./onvifDeviceManager/utils.js')(s,config,lang)
async function getOnvifDevice(groupKey,monitorId){
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({id: monitorId, ke: groupKey})).device
return onvifDevice

File diff suppressed because it is too large Load Diff

View File

@ -132,7 +132,7 @@ module.exports = function(s,config,lang,app,io){
uid: 'System',
details: {},
permissions: {},
lang: lang
lang
},function(endData){
// console.log(endData)
})
@ -248,7 +248,7 @@ module.exports = function(s,config,lang,app,io){
var form = s.getPostData(req)
s.checkDetails(form)
if(!form || !form.details){
endData.msg = user.lang['Form Data Not Found']
endData.msg = lang['Form Data Not Found']
s.closeJsonResponse(res,endData)
return
}
@ -317,7 +317,7 @@ module.exports = function(s,config,lang,app,io){
case'delete':
s.findSchedule(req.params.ke,req.params.name,function(notFound,schedule){
if(notFound === true){
endData.msg = user.lang['Schedule Configuration Not Found']
endData.msg = lang['Schedule Configuration Not Found']
s.closeJsonResponse(res,endData)
}else{
s.knexQuery({

View File

@ -118,7 +118,7 @@ module.exports = function(s,config,lang,app,io){
}else{
s.closeJsonResponse(res,{
ok: false,
msg: user.lang['No API Key']
msg: lang['No API Key']
})
}
},res,req)
@ -159,7 +159,7 @@ module.exports = function(s,config,lang,app,io){
}else{
s.closeJsonResponse(res,{
ok: false,
msg: user.lang['No API Key']
msg: lang['No API Key']
})
}
},res,req)

View File

@ -8,8 +8,8 @@ const {
} = require('./common.js')
module.exports = function(s,config,lang,io){
const {
ptzControl
} = require('./control/ptz.js')(s,config,lang)
applyPermissionsToUser,
} = require('./user/permissionSets.js')(s,config,lang)
const {
legacyFilterEvents
} = require('./events/utils.js')(s,config,lang)
@ -302,9 +302,9 @@ module.exports = function(s,config,lang,io){
cn.ip = (ipAddress.indexOf('127.0.0.1') > -1 || ipAddress.indexOf('localhost') > -1) && d.ipAddress ? d.ipAddress : ipAddress;
tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);}
const onFail = (msg) => {
tx({ok:false,msg:'Not Authorized',token_used:d.auth,ke:d.ke});cn.disconnect();
tx({ok:false,msg: msg ? msg.stack || msg : lang['Not Authorized'],token_used:d.auth,ke:d.ke});cn.disconnect();
}
const onSuccess = (r) => {
const onSuccess = async (r) => {
r = r[0];
cn.join('GRP_'+d.ke);cn.join('CPU');
cn.ke=d.ke,
@ -314,11 +314,14 @@ module.exports = function(s,config,lang,io){
// if(!s.group[d.ke].vid)s.group[d.ke].vid={};
if(!s.group[d.ke].users)s.group[d.ke].users={};
// s.group[d.ke].vid[cn.id]={uid:d.uid};
r.details = JSON.parse(r.details);
await applyPermissionsToUser(r)
// cn.checkedPermissions = s.checkPermission(r)
s.group[d.ke].users[d.auth] = {
cnid: cn.id,
uid: r.uid,
mail: r.mail,
details: JSON.parse(r.details),
details: r.details,
logged_in_at: s.timeObject(new Date).format(),
login_type: 'Dashboard'
}
@ -327,8 +330,7 @@ module.exports = function(s,config,lang,io){
if(s.group[d.ke].users[d.auth].details.get_server_log!=='0'){
cn.join('GRPLOG_'+d.ke)
}
s.group[d.ke].users[d.auth].lang = s.getLanguageFile(s.group[d.ke].users[d.auth].details.lang)
s.userLog({ke:d.ke,mid:'$USER'},{type:s.group[d.ke].users[d.auth].lang['Websocket Connected'],msg:{mail:r.mail,id:d.uid,ip:cn.ip}})
s.userLog({ke:d.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{mail:r.mail,id:d.uid,ip:cn.ip}})
if(!s.group[d.ke].activeMonitors){
s.group[d.ke].activeMonitors={}
if(!s.group[d.ke].activeMonitors){s.group[d.ke].activeMonitors={}}
@ -368,7 +370,6 @@ module.exports = function(s,config,lang,io){
}
if((d.id||d.uid||d.mid)&&cn.ke){
try{
d.callbackResponse = {ok: true}
switch(d.f){
case'monitorOrder':
if(d.monitorOrder && d.monitorOrder instanceof Object){
@ -709,13 +710,6 @@ module.exports = function(s,config,lang,io){
})
break;
}
if(d.callbackId && !d.hasResponded){
tx({
f:'callback',
callbackId: d.callbackId,
args: [d.callbackResponse]
})
}
}catch(er){
s.systemLog('ERROR CATCH 1',er)
}
@ -847,19 +841,21 @@ module.exports = function(s,config,lang,io){
['uid','=',d.uid],
],
limit: 1
},(err,r) => {
},async (err,r) => {
if(r && r[0]){
r = r[0]
cn.ke=d.ke,cn.uid=d.uid,cn.auth=d.auth;
if(!s.group[d.ke])s.group[d.ke]={};
if(!s.group[d.ke].users)s.group[d.ke].users={};
if(!s.group[d.ke].dashcamUsers)s.group[d.ke].dashcamUsers={};
r.details = JSON.parse(r.details);
await applyPermissionsToUser(r)
s.group[d.ke].users[d.auth]={
cnid: cn.id,
ke : d.ke,
uid:r.uid,
mail:r.mail,
details:JSON.parse(r.details),
details: r.details,
logged_in_at:s.timeObject(new Date).format(),
login_type:'Streamer'
}

View File

@ -11,7 +11,7 @@ module.exports = function(s,config,lang,io){
const {
checkSubscription,
checkAgainSubscription,
} = require('./basic/utils.js')(process.cwd(),config)
} = require('./checker/actCheck.js')(s,config)
const {
checkForStaticUsers
} = require('./user/startup.js')(s,config,lang,io)
@ -60,7 +60,10 @@ module.exports = function(s,config,lang,io){
columns: "*",
table: "Monitors",
},function(err,monitors) {
foundMonitors = monitors
foundMonitors = monitors.map(item => {
item.details = JSON.parse(item.details)
return item
})
if(err){s.systemLog('Startup Error', err.toString())}
if(monitors && monitors[0]){
var didNotLoad = 0
@ -69,7 +72,7 @@ module.exports = function(s,config,lang,io){
var loadMonitor = function(monitor){
const checkAnother = function(){
++loadCompleted
if(monitors[loadCompleted]){
if(loadCompleted <= s.cameraCount && monitors[loadCompleted]){
loadMonitor(monitors[loadCompleted])
}else{
if(didNotLoad > 0)console.log(`${didNotLoad} Monitor${didNotLoad === 1 ? '' : 's'} not loaded because Admin user does not exist for them. It may have been deleted.`);
@ -402,7 +405,6 @@ module.exports = function(s,config,lang,io){
}
})
}
config.userHasSubscribed = false
//check disk space every 20 minutes
if(config.autoDropCache===true){
setInterval(function(){
@ -422,8 +424,7 @@ module.exports = function(s,config,lang,io){
setTimeout(async () => {
await checkForStaticUsers()
//check for subscription
checkSubscription(config.subscriptionId || config.peerConnectKey || config.p2pApiKey, function(hasSubcribed){
config.userHasSubscribed = hasSubcribed
checkSubscription(config.subscriptionId || config.peerConnectKey || config.p2pApiKey, function(){
//check terminal commander
checkForTerminalCommands(function(){
//load administrators (groups)

View File

@ -10,6 +10,7 @@ module.exports = (config) => {
const response = {
"Time Started": s.timeStarted,
"Time Ready": s.timeReady,
"Maximum Cameras": s.cameraCount,
Versions: {
"Shinobi": s.currentVersion,
"Node.js": process.version,
@ -29,8 +30,7 @@ module.exports = (config) => {
},
getConfiguration: () => {
return new Promise((resolve,reject) => {
const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config;
const configPath = s.location.config;
fs.readFile(configPath, 'utf8', (err, data) => {
resolve(JSON.parse(data))
});

View File

@ -636,7 +636,7 @@ module.exports = function(s,config,lang,app,io){
req.params.protocol=req.protocol;
s.auth(req.params,function(user){
// if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){
// res.end(user.lang['Not Permitted'])
// res.end(lang['Not Permitted'])
// return
// }
req.params.uid = user.uid
@ -644,7 +644,7 @@ module.exports = function(s,config,lang,app,io){
$user: user,
data: req.params,
config: s.getConfigWithBranding(req.hostname),
lang: user.lang,
lang,
originalURL: s.getOriginalUrl(req)
})
},res,req);

View File

@ -32,6 +32,7 @@ module.exports = function(s,config,lang){
}
function cloudDiskUseStartup(group,userDetails){
group.cloudDiskUse['s3'].name = 'Amazon S3'
group.cloudDiskUse['s3'].maxDays = parseInt(userDetails.aws_s3_max_days);
group.cloudDiskUse['s3'].sizeLimitCheck = (userDetails.use_aws_s3_size_limit === '1')
if(!userDetails.aws_s3_size_limit || userDetails.aws_s3_size_limit === ''){
group.cloudDiskUse['s3'].sizeLimit = 10000
@ -491,14 +492,25 @@ module.exports = function(s,config,lang){
},
{
"hidden": true,
"name": "detail=aws_s3_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=aws_s3_size_limit]'`,
"form-group-class":"autosave_aws_s3_input autosave_aws_s3_1",
"form-group-class-pre-layer":"h_s3sld_input h_s3sld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=aws_s3_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=aws_s3_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_aws_s3_input autosave_aws_s3_1",
"form-group-class-pre-layer":"h_s3sld_input h_s3sld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -11,6 +11,7 @@ module.exports = function(s,config,lang){
}
var cloudDiskUseStartupForBackblazeB2 = function(group,userDetails){
group.cloudDiskUse[serviceProvider].name = 'Backblaze B2'
group.cloudDiskUse[serviceProvider].maxDays = parseInt(userDetails.bb_b2_max_days);
group.cloudDiskUse[serviceProvider].sizeLimitCheck = (userDetails.use_bb_b2_size_limit === '1')
if(!userDetails.bb_b2_size_limit || userDetails.bb_b2_size_limit === ''){
group.cloudDiskUse[serviceProvider].sizeLimit = 10000
@ -305,14 +306,25 @@ module.exports = function(s,config,lang){
},
{
"hidden": true,
"name": "detail=bb_b2_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=bb_b2_size_limit]'`,
"form-group-class":"autosave_bb_b2_input autosave_bb_b2_1",
"form-group-class-pre-layer":"h_b2sld_input h_b2sld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=bb_b2_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=bb_b2_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_bb_b2_input autosave_bb_b2_1",
"form-group-class-pre-layer":"h_b2sld_input h_b2sld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -59,6 +59,7 @@ module.exports = (s,config,lang,app,io) => {
}
var cloudDiskUseStartupForGoogleDrive = function(group,userDetails){
group.cloudDiskUse['googd'].name = 'Google Drive Storage'
group.cloudDiskUse['googd'].maxDays = parseInt(userDetails.googd_max_days);
group.cloudDiskUse['googd'].sizeLimitCheck = (userDetails.use_googd_size_limit === '1')
if(!userDetails.googd_size_limit || userDetails.googd_size_limit === ''){
group.cloudDiskUse['googd'].sizeLimit = 10000
@ -397,14 +398,25 @@ module.exports = (s,config,lang,app,io) => {
},
{
"hidden": true,
"name": "detail=googd_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=googd_size_limit]'`,
"form-group-class":"autosave_googd_input autosave_googd_1",
"form-group-class-pre-layer":"h_googdsld_input h_googdsld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=googd_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=googd_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_googd_input autosave_googd_1",
"form-group-class-pre-layer":"h_googdsld_input h_googdsld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -38,6 +38,7 @@ module.exports = function(s,config,lang){
}
function cloudDiskUseStartup(group,userDetails){
group.cloudDiskUse['mnt'].name = 'Mounted Drive'
group.cloudDiskUse['mnt'].maxDays = parseInt(userDetails.mnt_max_days);
group.cloudDiskUse['mnt'].sizeLimitCheck = (userDetails.use_mnt_size_limit === '1')
if(!userDetails.mnt_size_limit || userDetails.mnt_size_limit === ''){
group.cloudDiskUse['mnt'].sizeLimit = 10000
@ -303,14 +304,25 @@ module.exports = function(s,config,lang){
},
{
"hidden": true,
"name": "detail=mnt_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=mnt_size_limit]'`,
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=mnt_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=mnt_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -33,6 +33,7 @@ module.exports = function(s,config,lang){
}
function cloudDiskUseStartup(group,userDetails){
group.cloudDiskUse['whcs'].name = 'S3-Based Network Storage'
group.cloudDiskUse['whcs'].maxDays = parseInt(userDetails.whcs_max_days);
group.cloudDiskUse['whcs'].sizeLimitCheck = (userDetails.use_whcs_size_limit === '1')
if(!userDetails.whcs_size_limit || userDetails.whcs_size_limit === ''){
group.cloudDiskUse['whcs'].sizeLimit = 10000
@ -494,14 +495,25 @@ module.exports = function(s,config,lang){
},
{
"hidden": true,
"name": "detail=whcs_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=whcs_size_limit]'`,
"form-group-class":"autosave_whcs_input autosave_whcs_1",
"form-group-class-pre-layer":"h_whcssld_input h_whcssld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=whcs_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=whcs_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_whcs_input autosave_whcs_1",
"form-group-class-pre-layer":"h_whcssld_input h_whcssld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -12,6 +12,7 @@ module.exports = async function(s,config,lang){
}
var cloudDiskUseStartupForWebDav = function(group,userDetails){
group.cloudDiskUse['webdav'].name = 'WebDAV'
group.cloudDiskUse['webdav'].maxDays = parseInt(userDetails.webdav_max_days);
group.cloudDiskUse['webdav'].sizeLimitCheck = (userDetails.use_webdav_size_limit === '1')
if(!userDetails.webdav_size_limit || userDetails.webdav_size_limit === ''){
group.cloudDiskUse['webdav'].sizeLimit = 10000
@ -336,14 +337,25 @@ module.exports = async function(s,config,lang){
},
{
"hidden": true,
"name": "detail=webdav_size_limit",
"field": lang['Max Storage Amount'],
"attribute": `size-adjust='[detail=webdav_size_limit]'`,
"form-group-class":"autosave_webdav_input autosave_webdav_1",
"form-group-class-pre-layer":"h_webdavsld_input h_webdavsld_1",
"description": "",
"field": lang["Max Storage Amount"],
"default": "10 GB",
},
{
"hidden": true,
"name": "detail=webdav_size_limit",
"field": lang['Max Storage Amount'],
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=webdav_max_days",
"field": lang['Number of Days to keep'],
"form-group-class":"autosave_webdav_input autosave_webdav_1",
"form-group-class-pre-layer":"h_webdavsld_input h_webdavsld_1",
"example": "30",
},
{
"hidden": true,

View File

@ -355,6 +355,8 @@ module.exports = function(s,config,lang){
if(details.video_delete){formDetails.video_delete = details.video_delete;}
if(details.video_view){formDetails.video_view = details.video_view;}
if(details.monitor_edit){formDetails.monitor_edit = details.monitor_edit;}
if(details.edit_permissions){formDetails.edit_permissions = details.edit_permissions;}
if(details.permissionSet){formDetails.permissionSet = details.permissionSet;}
if(details.size){formDetails.size = details.size;}
if(details.days){formDetails.days = details.days;}
}

140
libs/user/apiKeys.js Normal file
View File

@ -0,0 +1,140 @@
module.exports = function(s,config,lang){
const availablePermissions = [
{ name: lang['Can Authenticate Websocket'], value: 'auth_socket' },
{ name: lang['Can Get Monitors'], value: 'get_monitors' },
{ name: lang['Can Edit Monitors'], value: 'edit_monitors' },
{ name: lang['Can Control Monitors'], value: 'control_monitors' },
{ name: lang['Can Get Logs'], value: 'get_logs' },
{ name: lang['Can View Streams'], value: 'watch_stream' },
{ name: lang['Can View Snapshots'], value: 'watch_snapshot' },
{ name: lang['Can View Videos'], value: 'watch_videos' },
{ name: lang['Can Delete Videos'], value: 'delete_videos' },
{ name: lang['Can View Alarm'], value: 'get_alarms' },
{ name: lang['Can Edit Alarm'], value: 'edit_alarms' },
]
function createFullAccessDetails(){
const details = {}
for(item of availablePermissions){
details[item.value] = '1'
}
return details
}
async function getApiKeys({ ke, uid }){
const whereQuery = {
ke,
};
if(uid)whereQuery.uid = uid;
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "API",
where: whereQuery
});
for(row of rows){
row.details = JSON.parse(row.details);
}
return rows
}
async function getApiKey({ ke, code, uid }){
const whereQuery = {
ke,
code
};
if(uid)whereQuery.uid = uid;
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "API",
where: whereQuery
});
if(rows[0])rows[0].details = JSON.parse(rows[0].details);
return rows[0]
}
async function getNewApiKey(ke){
let newApiKey = s.gid(30)
const foundRow = await getApiKey({ ke, code: newApiKey })
if(foundRow){
return await getNewApiKey(ke)
}else{
return newApiKey
}
}
async function createApiKey({ ke, uid, ip = '0.0.0.0', details = createFullAccessDetails() }){
const newApiKey = await getNewApiKey(ke);
const insertQuery = {
ke,
uid,
code: newApiKey,
ip,
details: s.stringJSON(details)
};
await s.knexQueryPromise({
action: "insert",
table: "API",
insert: insertQuery
})
return insertQuery;
}
async function updateApiKey({ ke, code, ip, details }){
const whereQuery = {
ke,
code,
};
const updateQuery = {};
if(ip)updateQuery.ip = ip;
if(details)updateQuery.details = details;
if(ip || details){
await s.knexQueryPromise({
action: "update",
table: "API",
where: whereQuery,
update: updateQuery
})
}
return { ke, code };
}
async function editApiKey({ ke, code, uid, ip, details }){
const response = { ok: true }
try{
let exists = false;
if(code){
const row = await getApiKey({ ke, code, uid, ip, details });
exists = !!row;
}
if(!exists){
response.editResponse = await createApiKey({ ke, uid, ip, details })
}else{
response.editResponse = await updateApiKey({ ke, code, uid, ip, details })
delete(s.api[response.editResponse.code])
}
response.api = await getApiKey({ ke, code: response.editResponse.code });
}catch(err){
response.ok = false;
response.err = err.toString();
}
return response;
}
async function deleteApiKey({ ke, code, uid }){
const whereQuery = {
ke,
code
};
if(uid)whereQuery.uid = uid;
return await s.knexQueryPromise({
action: "delete",
table: "API",
where: whereQuery
})
}
return {
availablePermissions,
createFullAccessDetails,
getApiKey,
getApiKeys,
getNewApiKey,
createApiKey,
updateApiKey,
editApiKey,
deleteApiKey,
}
}

View File

@ -0,0 +1,83 @@
module.exports = function(s,config,lang){
async function getCustomSetting({ ke, uid, name }){
const whereQuery = {
ke,
};
if(uid)whereQuery.uid = uid;
if(name)whereQuery.name = name;
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Custom Settings",
where: whereQuery
});
for(row of rows){
row.details = JSON.parse(row.details)
}
return rows
}
async function addCustomSetting({ ke, uid, name, details = {}, time = new Date() }){
return await s.knexQueryPromise({
action: "insert",
table: "Custom Settings",
insert: {
ke,
uid,
name,
details: s.stringJSON(details),
time: new Date(),
}
})
}
async function updateCustomSetting({ ke, uid, name, details = {} }){
return await s.knexQueryPromise({
action: "update",
table: "Custom Settings",
where: {
ke,
uid,
name,
},
update: {
details: s.stringJSON(details),
time: new Date(),
}
})
}
async function editCustomSetting({ ke, uid, name, details = {} }){
const response = { ok: true }
try{
if(typeof details !== 'object'){
details = {}
}
const existAlready = await getCustomSetting({ ke, uid, name });
if(existAlready[0]){
response.editResponse = await updateCustomSetting({ ke, uid, name, details })
}else{
response.editResponse = await addCustomSetting({ ke, uid, name, details })
}
}catch(err){
response.ok = false;
response.err = err.toString();
}
return response
}
async function deleteCustomSetting({ ke, uid, name }){
return await s.knexQueryPromise({
action: "delete",
table: "Custom Settings",
where: {
ke,
uid,
name
}
})
}
return {
getCustomSetting,
addCustomSetting,
updateCustomSetting,
editCustomSetting,
deleteCustomSetting,
}
}

112
libs/user/permissionSets.js Normal file
View File

@ -0,0 +1,112 @@
module.exports = function(s,config,lang){
const yesNoPossibility = [
{ "name": lang.No, "value": "0" },
{ "name": lang.Yes, "value": "1" }
];
const baseItems = [
{ name: "allmonitors", label: lang['All Monitors and Privileges'], possible: yesNoPossibility },
{ name: "monitor_create", label: lang['Can Create and Delete Monitors'], possible: yesNoPossibility },
{ name: "user_change", label: lang['Can Change User Settings'], possible: yesNoPossibility },
{ name: "view_logs", label: lang['Can View Logs'], possible: yesNoPossibility },
{ name: "edit_permissions", label: lang['Can Edit Permissions'], possible: yesNoPossibility },
];
const monitorSpecific = [
{ name: 'monitors', label: lang['Can View Monitor'] },
{ name: 'monitor_edit', label: lang['Can Edit Monitor'] },
{ name: 'video_view', label: lang['Can View Videos and Events'] },
{ name: 'video_delete', label: lang['Can Delete Videos and Events'] },
];
async function getPermissionSets(groupKey, name){
const whereQuery = [
['ke','=',groupKey],
];
if(name)whereQuery.push(['name','=',name]);
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Permission Sets",
where: whereQuery
});
rows.forEach((row) => {
row.details = s.parseJSON(row.details);
});
return rows
}
async function insertPermissionSet(groupKey, { name, details }){
const insertResponse = await s.knexQueryPromise({
action: "insert",
table: "Permission Sets",
insert: {
ke: groupKey,
name: name,
details: s.stringJSON(details),
}
});
return insertResponse
}
async function updatePermissionSet(groupKey, { name, details }){
const updateResponse = await s.knexQueryPromise({
action: "update",
table: "Permission Sets",
where: [
['ke','=', groupKey],
['name','=', name],
],
update: {
details: s.stringJSON(details),
}
});
return updateResponse
}
async function deletePermissionSet(groupKey, name){
const deleteResponse = await s.knexQueryPromise({
action: "delete",
table: "Permission Sets",
where: [
['ke','=', groupKey],
['name','=', name],
]
});
return deleteResponse
}
async function editPermissionSet(groupKey, { name, details }){
const response = { ok: true }
try{
const rows = await getPermissionSets(groupKey, name);
const exists = !!rows[0];
if(!exists){
response.editResponse = await insertPermissionSet(groupKey, { name, details })
}else{
response.editResponse = await updatePermissionSet(groupKey, { name, details })
}
}catch(err){
response.ok = false;
response.err = err.toString();
}
return response;
}
async function applyPermissionsToUser(user){
const groupKey = user.ke;
const name = (user.details.permissionSet || '').trim();
const apiKeyPermissions = user.permissions || {};
if(name){
const rows = await getPermissionSets(groupKey, name)
const foundRow = rows[0];
if(foundRow){
user.details = Object.assign(user.details, foundRow.details)
}
}
if(apiKeyPermissions.monitorsRestricted === '1' && apiKeyPermissions.monitorPermissions){
user.details = Object.assign(user.details, apiKeyPermissions.monitorPermissions)
}
return user
}
return {
getPermissionSets,
insertPermissionSet,
updatePermissionSet,
deletePermissionSet,
editPermissionSet,
applyPermissionsToUser,
}
}

View File

@ -0,0 +1,177 @@
module.exports = function(s,config,lang){
const { createApiKey } = require('./apiKeys.js')(s,config,lang)
async function getSubAccounts({ ke: groupKey, uid }){
const whereQuery = [
['ke','=', groupKey],
['details','LIKE','%"sub"%'],
];
if(uid){
whereQuery.push(['uid','=', uid])
}
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "ke,uid,mail,details",
table: "Users",
where: whereQuery
});
for(row of rows){
row.details = JSON.parse(row.details);
}
return rows
}
async function addSubAccount({ ke: groupKey, mail, pass, password_again = '', pass_again = '', details, alsoCreateApiKey = false }){
const response = { ok: false }
if(mail !== '' && pass !== ''){
if(pass === password_again || pass === pass_again){
const { rows: foundUsers } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=',mail],
]
})
if(foundUsers && foundUsers[0]){
response.msg = lang['Email address is in use.']
}else{
const uid = s.gid()
const postDetails = Object.assign({
allmonitors: "1"
},s.parseJSON(details) || {});
postDetails.sub = 1
const insertQuery = {
ke: groupKey,
uid: uid,
mail: mail,
pass: s.createHash(pass),
details: JSON.stringify(postDetails),
};
await s.knexQueryPromise({
action: "insert",
table: "Users",
insert: insertQuery
});
if(alsoCreateApiKey){
response.apiKey = await createApiKey({ ke: groupKey, uid });
}
response.msg = lang.accountAddedText
response.ok = true
response.user = insertQuery
}
}else{
response.msg = lang["Passwords Don't Match"]
}
}else{
response.msg = lang['Fields cannot be empty']
}
return response
}
async function updateSubAccount({ ke: groupKey, mail, uid, pass, password_again, pass_again, details }){
const response = { ok: false }
details = s.parseJSON(details) || {"sub": 1, "allmonitors": "1"}
details.sub = 1
if(mail){
const { rows: foundUsers } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=', mail],
]
})
const foundUser = foundUsers[0];
if(foundUser){
const passwordsMatch = pass && (pass === password_again || pass === pass_again);
if(!pass || passwordsMatch){
const updateQuery = {
details: s.stringJSON(details)
}
if(passwordsMatch){
updateQuery.pass = s.createHash(pass)
}
if(foundUser.uid === uid){
updateQuery.mail = mail
await s.knexQueryPromise({
action: "update",
table: "Users",
update: updateQuery,
where: [
['ke','=', groupKey],
['uid','=', uid],
]
})
const { rows: apiKeys } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "API",
where: [
['ke','=', groupKey],
['uid','=', uid],
]
});
if(apiKeys && apiKeys[0]){
apiKeys.forEach(function(apiKey){
delete(s.api[apiKey.code])
})
}
response.ok = true
}else{
response.msg = lang['Invalid Data']
}
}else{
response.msg = lang["Passwords Don't Match"]
}
}
}else{
response.msg = lang['Invalid Data']
}
return response
}
async function deleteSubAccount({ ke: groupKey, uid }){
const response = { ok: false }
const usersFound = await getSubAccounts({ ke: groupKey, uid });
const theUserUpForDeletion = usersFound[0]
if(theUserUpForDeletion){
await s.knexQueryPromise({
action: "delete",
table: "Users",
where: {
ke: groupKey,
uid: uid,
}
})
const { err, rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "API",
where: [
['ke','=',groupKey],
['uid','=',uid],
]
});
if(rows && rows[0]){
for(row of rows){
delete(s.api[row.code])
}
await s.knexQueryPromise({
action: "delete",
table: "API",
where: {
ke: groupKey,
uid: uid,
}
})
}
response.ok = true
}else{
response.msg = lang['User Not Found']
}
return response
}
return {
addSubAccount,
getSubAccounts,
deleteSubAccount,
updateSubAccount,
}
}

View File

@ -726,7 +726,7 @@ module.exports = (s,config,lang) => {
}catch(err){
}
const filePaths = videos.map(video => {
const filePaths = videos.reverse().map(video => {
const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid];
const filePath = path.join(s.getVideoDirectory(video), `${s.formattedTime(video.time)}.mp4`);
return filePath
@ -890,7 +890,7 @@ module.exports = (s,config,lang) => {
if(code === 0){
resolve(buffer);
}else{
reject(new Error(`FFmpeg process exited with code ${code}`));
reject(new Error(`FFmpeg process exited with code ${code} : ${ffmpegArgs.join(' ')}`));
}
});

View File

@ -80,7 +80,7 @@ module.exports = function(s,config,lang){
ext: k.ext || e.ext,
status: 1,
details: s.s(k.details),
objects: k.objects || '',
objects: (k.objects || '').substring(0,509),
size: k.filesize,
end: k.endTime,
}
@ -148,7 +148,7 @@ module.exports = function(s,config,lang){
ext: k.ext,
size: k.filesize,
filesize: k.filesize,
objects: k.objects.substring(0, 510),
objects: k.objects.substring(0, 509),
time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'),
end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss')
}

160
libs/webPaths/apiKeys.js Normal file
View File

@ -0,0 +1,160 @@
module.exports = function(s,config,lang,app){
const { getApiKey, getApiKeys, createApiKey, editApiKey, deleteApiKey } = require('../user/apiKeys.js')(s,config,lang)
/**
* API : Add/Edit API Key, binded to the user who created it
*/
app.post([
config.webPaths.adminApiPrefix+':auth/api/:ke/add',
config.webPaths.apiPrefix+':auth/api/:ke/add',
],function (req,res){
var endData = {ok:false}
s.auth(req.params,async function(user){
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
const endData = {
ok : false
}
if(isRestrictedApiKey && apiKeyPermissions.create_api_keys_disallowed){
endData.msg = lang['Not Authorized']
}else{
const groupKey = req.params.ke;
var form = s.getPostData(req) || {}
try{
const targetUID = form.uid || req.body.uid;
const code = form.code;
const editResponse = await editApiKey({
code,
ke : groupKey,
uid : !isSubAccount && targetUID ? targetUID : user.uid,
ip : typeof form.ip === 'string' ? form.ip.trim() : '',
details : form.details ? s.stringJSON(form.details) : undefined
});
if(editResponse.ok){
s.tx({
f: 'api_key_added',
uid: user.uid,
form: editResponse.api
},'GRP_' + groupKey)
}
endData.ok = editResponse.ok
endData.api = editResponse.api
}catch(err){
console.error(err)
}
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Delete API Key
*/
app.post([
config.webPaths.adminApiPrefix+':auth/api/:ke/delete',
config.webPaths.apiPrefix+':auth/api/:ke/delete',
],function (req,res){
var endData = {ok:false}
s.auth(req.params, async function(user){
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
const endData = {
ok : false
}
if(isRestrictedApiKey && apiKeyPermissions.create_api_keys_disallowed){
endData.msg = lang['Not Authorized']
}else{
var form = s.getPostData(req) || {}
const code = form.code || s.getPostData(req,'code',false)
if(!code){
endData.msg = lang.postDataBroken
}else{
const groupKey = req.params.ke;
const targetUID = req.query.uid;
endData.uid = !isSubAccount && targetUID ? targetUID : user.uid;
const { ok } = await deleteApiKey({ ke: groupKey, code, uid: endData.uid })
if(ok){
s.tx({
f: 'api_key_deleted',
uid: user.uid,
form: {
code: code
}
},'GRP_' + groupKey)
endData.ok = ok
delete(s.api[code])
}
}
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : List API Keys for Authenticated user
*/
app.get([
config.webPaths.adminApiPrefix+':auth/api/:ke/list',
config.webPaths.apiPrefix+':auth/api/:ke/list',
],function (req,res){
var endData = {ok:false}
s.auth(req.params, async function(user){
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
const endData = {
ok : false,
keys: []
}
if(isRestrictedApiKey && apiKeyPermissions.create_api_keys_disallowed){
endData.msg = lang['Not Authorized']
}else{
const groupKey = req.params.ke;
const targetUID = req.query.uid;
endData.uid = !isSubAccount && targetUID ? targetUID : user.uid;
const rows = await getApiKeys({ ke: groupKey, uid: endData.uid })
endData.ok = true
endData.keys = rows
endData.ke = user.ke
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get API Key for Authenticated user
*/
app.get([
config.webPaths.adminApiPrefix+':auth/api/:ke/get/:code',
config.webPaths.apiPrefix+':auth/api/:ke/get/:code',
],function (req,res){
var endData = {ok:false}
s.auth(req.params, async function(user){
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
const endData = {
ok : false,
keys: []
}
if(isRestrictedApiKey && apiKeyPermissions.create_api_keys_disallowed){
endData.msg = lang['Not Authorized']
}else{
const groupKey = req.params.ke;
const targetUID = req.query.uid;
const code = req.params.code;
const uid = !isSubAccount && targetUID ? targetUID : user.uid;
const row = await getApiKey({ ke: groupKey, uid, code })
endData.ok = true
endData.key = row
}
s.closeJsonResponse(res,endData)
},res,req)
})
}

View File

@ -0,0 +1,66 @@
module.exports = function(s,config,lang,app){
const {
getCustomSetting,
addCustomSetting,
updateCustomSetting,
editCustomSetting,
deleteCustomSetting,
} = require('../user/customSettings.js')(s,config,lang)
/**
* API : Permission Set : Get
*/
app.get([
config.webPaths.apiPrefix+':auth/customSettings/:ke',
config.webPaths.apiPrefix+':auth/customSettings/:ke/:name',
], function (req,res){
s.auth(req.params, async function(user){
const response = { ok: true }
const groupKey = req.params.ke;
const userId = user.uid;
const name = req.params.name;
const rows = await getCustomSetting({ ke: groupKey, uid: userId, name })
if(name){
response.row = rows[0];
}else{
response.rows = rows;
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Permission Set : Edit
*/
app.post(config.webPaths.apiPrefix+':auth/customSettings/:ke', function (req,res){
s.auth(req.params, async function(user){
let response = { ok: false }
const groupKey = req.params.ke;
const form = req.body || {};
form.ke = groupKey;
form.uid = user.uid;
if(form.name && form.details){
response = await editCustomSetting(form)
}else{
response.msg = lang['Invalid Data'];
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Permission Set : Delete
*/
app.get(config.webPaths.apiPrefix+':auth/customSettings/:ke/:name/delete', function (req,res){
s.auth(req.params, async function(user){
const response = { ok: false }
const groupKey = req.params.ke;
const userId = user.uid;
const name = req.params.name;
try{
response.deleteResponse = await deleteCustomSetting({ ke: groupKey, uid: userId, name })
response.ok = true;
}catch(err){
response.err = err.toString()
}
s.closeJsonResponse(res,response)
},res,req)
})
}

View File

@ -0,0 +1,165 @@
module.exports = function(s,config,lang,app){
/**
* API : Administrator : Get Monitor State Presets List
*/
app.all([
config.webPaths.apiPrefix+':auth/monitorStates/:ke',
config.webPaths.adminApiPrefix+':auth/monitorStates/:ke'
],function (req,res){
s.auth(req.params,function(user){
var endData = {
ok : false
}
const groupKey = req.params.ke
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
if(
userPermissions.monitor_create_disallowed ||
isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']});
return
}
s.knexQuery({
action: "select",
columns: "*",
table: "Presets",
where: [
['ke','=',req.params.ke],
['type','=','monitorStates'],
]
},function(err,presets) {
if(presets && presets[0]){
endData.ok = true
presets.forEach(function(preset){
preset.details = JSON.parse(preset.details)
})
}
endData.presets = presets || []
s.closeJsonResponse(res,endData)
})
})
})
/**
* API : Administrator : Change Group Preset. Currently affects Monitors only.
*/
app.all([
config.webPaths.apiPrefix+':auth/monitorStates/:ke/:stateName',
config.webPaths.apiPrefix+':auth/monitorStates/:ke/:stateName/:action',
config.webPaths.adminApiPrefix+':auth/monitorStates/:ke/:stateName',
config.webPaths.adminApiPrefix+':auth/monitorStates/:ke/:stateName/:action',
],function (req,res){
s.auth(req.params,function(user){
var endData = {
ok : false
}
const groupKey = req.params.ke
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
if(
userPermissions.monitor_create_disallowed ||
isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']});
return
}
var presetQueryVals = [req.params.ke,'monitorStates',req.params.stateName]
switch(req.params.action){
case'insert':case'edit':
var form = s.getPostData(req)
s.checkDetails(form)
if(!form || !form.monitors){
endData.msg = lang['Form Data Not Found']
s.closeJsonResponse(res,endData)
return
}
s.findPreset(presetQueryVals,function(notFound,preset){
if(notFound === true){
endData.msg = lang["Inserted State Configuration"]
var details = {
monitors : form.monitors
}
var insertData = {
ke: req.params.ke,
name: req.params.stateName,
details: s.s(details),
type: 'monitorStates'
}
s.knexQuery({
action: "insert",
table: "Presets",
insert: insertData
})
s.tx({
f: 'add_group_state',
details: details,
ke: req.params.ke,
name: req.params.stateName
},'GRP_'+req.params.ke)
}else{
endData.msg = lang["Edited State Configuration"]
var details = Object.assign(preset.details,{
monitors : form.monitors
})
s.knexQuery({
action: "update",
table: "Presets",
update: {
details: s.s(details)
},
where: [
['ke','=',req.params.ke],
['name','=',req.params.stateName],
]
})
s.tx({
f: 'edit_group_state',
details: details,
ke: req.params.ke,
name: req.params.stateName
},'GRP_'+req.params.ke)
}
endData.ok = true
s.closeJsonResponse(res,endData)
})
break;
case'delete':
s.findPreset(presetQueryVals,function(notFound,preset){
if(notFound === true){
endData.msg = lang['State Configuration Not Found']
s.closeJsonResponse(res,endData)
}else{
s.knexQuery({
action: "delete",
table: "Presets",
where: {
ke: req.params.ke,
name: req.params.stateName,
}
},(err) => {
if(!err){
endData.msg = lang["Deleted State Configuration"]
endData.ok = true
}
s.closeJsonResponse(res,endData)
})
}
})
break;
default://change monitors according to state
s.activateMonitorStates(req.params.ke,req.params.stateName,user,function(endData){
s.closeJsonResponse(res,endData)
})
break;
}
},res,req)
})
}

View File

@ -0,0 +1,88 @@
module.exports = function(s,config,lang,app){
const {
getPermissionSets,
insertPermissionSet,
updatePermissionSet,
deletePermissionSet,
editPermissionSet,
applyPermissionsToUser,
} = require('../user/permissionSets.js')(s,config,lang)
/**
* API : Permission Set : Get
*/
app.get([
config.webPaths.apiPrefix+':auth/permissions/:ke',
config.webPaths.apiPrefix+':auth/permissions/:ke/:name',
], function (req,res){
s.auth(req.params, async function(user){
const response = { ok: false }
const {
isSubAccount,
userPermissions,
apiKeyPermissions,
isRestrictedApiKey,
} = s.checkPermission(user)
const canEditPermissions = !isSubAccount || userPermissions.edit_permissions || isRestrictedApiKey && (apiKeyPermissions.edit_permissions || apiKeyPermissions.create_api_keys);
if(!canEditPermissions){
s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']});
}else{
const groupKey = req.params.ke;
const name = req.params.name;
const rows = await getPermissionSets(groupKey,name)
response.permissions = rows;
s.closeJsonResponse(res,response)
}
},res,req)
})
/**
* API : Permission Set : Edit
*/
app.post(config.webPaths.apiPrefix+':auth/permissions/:ke', function (req,res){
s.auth(req.params, async function(user){
let response = { ok: false }
const {
isSubAccount,
userPermissions,
apiKeyPermissions,
isRestrictedApiKey,
} = s.checkPermission(user)
const canEditPermissions = !isSubAccount || userPermissions.edit_permissions || isRestrictedApiKey && apiKeyPermissions.edit_permissions;
if(!canEditPermissions){
response.msg = lang['Not Authorized'];
}else{
const groupKey = req.params.ke;
const form = s.getPostData(req) || {};
if(form.name && form.details){
response = await editPermissionSet(groupKey,form)
}else{
response.msg = lang['Invalid Data'];
}
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Permission Set : Delete
*/
app.get(config.webPaths.apiPrefix+':auth/permissions/:ke/:name/delete', function (req,res){
s.auth(req.params, async function(user){
const response = { ok: false }
const {
isSubAccount,
userPermissions,
apiKeyPermissions,
isRestrictedApiKey,
} = s.checkPermission(user)
const canEditPermissions = !isSubAccount || userPermissions.edit_permissions || isRestrictedApiKey && apiKeyPermissions.edit_permissions;
if(!canEditPermissions){
response.msg = lang['Not Authorized'];
}else{
const groupKey = req.params.ke;
const name = req.params.name;
response.ok = true;
response.deleteResponse = await deletePermissionSet(groupKey,name)
}
s.closeJsonResponse(res,response)
},res,req)
})
}

View File

@ -0,0 +1,117 @@
module.exports = function(s,config,lang,app){
const { addSubAccount, getSubAccounts, deleteSubAccount, updateSubAccount } = require('../user/subAccountManager.js')(s,config,lang)
/**
* API : Administrator : Edit Sub-Account (Account to share cameras with)
*/
app.post(config.webPaths.adminApiPrefix+':auth/accounts/:ke/edit', function (req,res){
s.auth(req.params, async (user) => {
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
let response = { ok: false }
if(
isSubAccount ||
isRestrictedApiKey && apiKeyPermissions.edit_user_disallowed
){
response.msg = lang['Not Authorized']
}else{
const groupKey = req.params.ke;
let { mail, uid, pass, password_again, pass_again, details } = s.getPostData(req);
if(!uid)uid = s.getPostData(req,'uid',false)
if(!mail)mail = (s.getPostData(req,'mail',false) || '').trim()
if(mail && uid && details){
response = await updateSubAccount({ ke: groupKey, mail, uid, pass, password_again, pass_again, details })
}else{
response.msg = lang.postDataBroken
}
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Administrator : Delete Sub-Account (Account to share cameras with)
*/
app.post(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){
s.auth(req.params, async function(user){
const groupKey = req.params.ke;
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
let response = { ok: false }
if(
isSubAccount ||
isRestrictedApiKey && apiKeyPermissions.edit_user_disallowed
){
response.msg = lang['Not Authorized']
}else{
var form = s.getPostData(req) || {}
var uid = form.uid || s.getPostData(req,'uid',false)
response = await deleteSubAccount({ ke: groupKey, uid })
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Administrator : Get Sub-Account List
*/
app.get([
config.webPaths.adminApiPrefix+':auth/accounts/:ke',
config.webPaths.adminApiPrefix+':auth/accounts/:ke/:uid',
], function (req,res){
s.auth(req.params,async function(user){
const groupKey = req.params.ke;
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
let response = { ok: false }
if(
isSubAccount ||
isRestrictedApiKey && apiKeyPermissions.edit_user_disallowed
){
response.msg = lang['Not Authorized']
}else{
response.ok = true
const uid = req.params.uid;
response.accounts = await getSubAccounts({ ke: groupKey, uid })
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Administrator : Add Sub-Account (Account to share cameras with)
*/
app.post(config.webPaths.adminApiPrefix+':auth/accounts/:ke/register',function (req,res){
s.auth(req.params, async function(user){
const {
isSubAccount,
isRestrictedApiKey,
apiKeyPermissions,
userPermissions,
} = s.checkPermission(user)
const endData = {
ok : false
}
if(
isSubAccount ||
isRestrictedApiKey && apiKeyPermissions.edit_user_disallowed
){
endData.msg = lang['Not Authorized']
}else{
const groupKey = req.params.ke;
const { mail, pass, password_again, pass_again, details } = s.getPostData(req);
const alsoCreateApiKey = s.getPostData(req,'createApiKey') === '1';
response = await addSubAccount({ ke: groupKey, mail, pass, password_again, pass_again, details, alsoCreateApiKey })
}
s.closeJsonResponse(res,response)
},res,req)
})
}

View File

@ -5,6 +5,7 @@ const https = require('https');
const express = require('express');
const app = express()
module.exports = function(s,config,lang,io){
require('./monitor/websocket.js')(s,config,lang,io);
app.disable('x-powered-by');
//get page URL
if(!config.baseURL){
@ -66,6 +67,7 @@ module.exports = function(s,config,lang,io){
'home/videoPlayer',
'home/monitorsList',
'home/subAccountManager',
'home/permissionSets',
'home/accountSettings',
'home/apiKeys',
'home/monitorSettings',

View File

@ -1,289 +1,13 @@
var fs = require('fs');
var os = require('os');
var moment = require('moment')
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var execSync = require('child_process').execSync;
module.exports = function(s,config,lang,app){
const {
deleteMonitor,
} = require('./monitor/utils.js')(s,config,lang)
/**
* API : Administrator : Edit Sub-Account (Account to share cameras with)
*/
app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/edit', function (req,res){
s.auth(req.params,async (user) => {
var endData = {
ok : false
}
const {
isSubAccount,
} = s.checkPermission(user)
if(isSubAccount){
s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']});
return
}
var form = s.getPostData(req)
var uid = form.uid || s.getPostData(req,'uid',false)
var mail = (form.mail || s.getPostData(req,'mail',false) || '').trim()
if(form){
var keys = ['details']
form.details = s.parseJSON(form.details) || {"sub": 1, "allmonitors": "1"}
form.details.sub = 1
const updateQuery = {
details: s.stringJSON(form.details)
}
if(form.pass && form.pass === form.password_again){
updateQuery.pass = s.createHash(form.pass)
}
if(form.mail){
const userCheck = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=',form.mail],
]
})
if(userCheck.rows[0]){
const foundUser = userCheck.rows[0]
if(foundUser.uid === form.uid){
updateQuery.mail = form.mail
}else{
endData.msg = lang['Email address is in use.']
s.closeJsonResponse(res,endData)
return
}
}else{
updateQuery.mail = form.mail
}
}
await s.knexQueryPromise({