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
parent
37ebdba546
commit
7ede7ef208
|
|
@ -15,3 +15,4 @@ generatedLanguageFiles
|
|||
faces
|
||||
unknownFaces
|
||||
.idea/
|
||||
/ffmpeg
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'}'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 . ./
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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..
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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> ${lang['Add New']}`,
|
||||
// },
|
||||
// {
|
||||
// "forForm": true,
|
||||
// "fieldType": "btn",
|
||||
// "class": `btn-primary reset-form`,
|
||||
// "attribute": `type="button"`,
|
||||
// "btnContent": `<i class="fa fa-refresh"></i> ${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",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
5258
definitions/base.js
5258
definitions/base.js
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
|
@ -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> ${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> ${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",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"> </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> ${lang['Live Stream Toggle']}`,
|
||||
},
|
||||
{
|
||||
"fieldType": "btn",
|
||||
forForm: true,
|
||||
attribute: "href=#",
|
||||
"class": `btn-success`,
|
||||
"btnContent": `<i class="fa fa-check"></i> ${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>`,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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> ${lang['Clear']}`,
|
||||
},
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-success submit-form`,
|
||||
"btnContent": `<i class="fa fa-plus"></i> ${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> ${lang['Add New']}`,
|
||||
},
|
||||
{
|
||||
"fieldType": "table",
|
||||
"class": "search-body",
|
||||
id: "sub_accounts_permissions",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
38
libs/auth.js
38
libs/auth.js
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 pass‑through remains unchanged
|
||||
if (rawMonitorConfig.type === 'dashcam' || rawMonitorConfig.type === 'socket') {
|
||||
process.stdin.on('data', (d) => cameraProcess.stdin.write(d))
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
libs/cron.js
21
libs/cron.js
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
])
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ module.exports = function(s,config){
|
|||
createExtension(`onEventTrigger`)
|
||||
createExtension(`onEventTriggerBeforeFilter`)
|
||||
createExtension(`onFilterEvent`)
|
||||
createExtension(`onOnvifEventTrigger`)
|
||||
////// MONITOR //////
|
||||
createExtension(`onMonitorInit`)
|
||||
createExtension(`onMonitorStart`)
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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(' ')}`));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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({
|
||||