@frangoteam/fuxa-min 1.2.7 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/api/apikeys/index.js +107 -0
- package/api/apikeys/verify-api-or-token.js +47 -0
- package/api/index.js +37 -12
- package/api/jwt-helper.js +46 -0
- package/api/resources/index.js +30 -38
- package/api/scheduler/index.js +184 -0
- package/dist/3rdpartylicenses.txt +1 -1
- package/dist/assets/i18n/de.json +1 -1
- package/dist/assets/i18n/en.json +154 -15
- package/dist/assets/i18n/fr.json +1 -1
- package/dist/assets/i18n/ja.json +1 -1
- package/dist/assets/i18n/ru.json +1 -1
- package/dist/assets/i18n/sv.json +1 -1
- package/dist/assets/i18n/zh-cn.json +2 -2
- package/dist/assets/i18n/zh-tw.json +1718 -0
- package/dist/assets/images/nodered-icon.svg +19 -0
- package/dist/index.html +2 -2
- package/dist/{main.51a4dc379bb23222.js → main.a17204bdf90c1b7a.js} +115 -115
- package/dist/styles.7afa4817cbd46a9c.css +1 -0
- package/docs/openapi.yaml +1045 -0
- package/integrations/node-red/index.js +219 -0
- package/integrations/node-red/node-red-contrib-fuxa/index.js +1 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-ack-alarm.html +49 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-ack-alarm.js +27 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-emit-event.html +31 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-emit-event.js +17 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-enable-device.html +49 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-enable-device.js +25 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-execute-script.html +50 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-execute-script.js +49 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-alarms.html +27 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-alarms.js +19 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +100 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +25 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device-property.html +48 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device-property.js +25 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device.html +49 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device.js +25 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-historical-tags.html +152 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-historical-tags.js +76 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-history-alarms.html +88 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-history-alarms.js +28 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +53 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +78 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +45 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +28 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-id.html +45 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-id.js +23 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +44 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +25 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-open-card.html +44 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-open-card.js +23 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-send-message.html +43 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-send-message.js +24 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-device-property.html +53 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-device-property.js +27 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +61 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +39 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +43 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +23 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-view.html +44 -0
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-view.js +23 -0
- package/integrations/node-red/node-red-contrib-fuxa/package.json +41 -0
- package/main.js +44 -11
- package/package.json +11 -5
- package/runtime/apikeys/apiKeysStorage.js +125 -0
- package/runtime/apikeys/index.js +123 -0
- package/runtime/devices/device-utils.js +21 -2
- package/runtime/devices/fuxaserver/index.js +22 -2
- package/runtime/devices/modbus/index.js +76 -41
- package/runtime/devices/opcua/index.js +19 -6
- package/runtime/events.js +3 -0
- package/runtime/index.js +25 -4
- package/runtime/jobs/cleaner.js +34 -1
- package/runtime/notificator/index.js +34 -9
- package/runtime/plugins/index.js +3 -0
- package/runtime/scheduler/scheduler-service.js +1608 -0
- package/runtime/scheduler/scheduler-storage.js +184 -0
- package/runtime/scripts/index.js +1 -3
- package/runtime/storage/influxdb/index.js +1 -1
- package/settings.default.js +10 -2
- package/dist/styles.466e813c4ba26c88.css +0 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ FUXA is a web-based Process Visualization (SCADA/HMI/Dashboard) software. With F
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
|
-
- Devices connectivity with Modbus RTU/TCP, Siemens S7 Protocol, OPC-UA, BACnet IP, MQTT, WebAPI, Ethernet/IP (Allen Bradley), ODBC
|
|
12
|
+
- Devices connectivity with Modbus RTU/TCP, Siemens S7 Protocol, OPC-UA, BACnet IP, MQTT, WebAPI, Ethernet/IP (Allen Bradley), ADSclient, Gpio (Raspberry), WebCam, MELSEC, ODBC, Redis
|
|
13
13
|
- SCADA/HMI Web-Editor - Engineering and Design completely web-based
|
|
14
14
|
- Cross-Platform Full-Stack - Backend with NodeJs and Frontend with Web technologies (HTML5, CSS, Javascript, Angular, SVG)
|
|
15
15
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'api/apikeys': ApiKeys API to GET/POST/DELETE apikeys
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
var express = require("express");
|
|
6
|
+
const authJwt = require('../jwt-helper');
|
|
7
|
+
var runtime;
|
|
8
|
+
var secureFnc;
|
|
9
|
+
var checkGroupsFnc;
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
init: function (_runtime, _secureFnc, _checkGroupsFnc) {
|
|
13
|
+
runtime = _runtime;
|
|
14
|
+
secureFnc = _secureFnc;
|
|
15
|
+
checkGroupsFnc = _checkGroupsFnc;
|
|
16
|
+
},
|
|
17
|
+
app: function() {
|
|
18
|
+
var apiKeysApp = express();
|
|
19
|
+
apiKeysApp.use(function(req,res,next) {
|
|
20
|
+
if (!runtime.project) {
|
|
21
|
+
res.status(404).end();
|
|
22
|
+
} else {
|
|
23
|
+
next();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* GET ApiKeys
|
|
29
|
+
* Take from ApiKeys storage and reply
|
|
30
|
+
*/
|
|
31
|
+
apiKeysApp.get("/api/apikeys", secureFnc, function(req, res) {
|
|
32
|
+
const permission = checkGroupsFnc(req);
|
|
33
|
+
if (res.statusCode === 403) {
|
|
34
|
+
runtime.logger.error("api get apikeys: Tocken Expired");
|
|
35
|
+
} else if (!authJwt.haveAdminPermission(permission)) {
|
|
36
|
+
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
|
|
37
|
+
runtime.logger.error("api get apikeys: Unauthorized!");
|
|
38
|
+
} else {
|
|
39
|
+
runtime.apiKeys.getApiKeys(req.query).then(result => {
|
|
40
|
+
if (result) {
|
|
41
|
+
res.json(result);
|
|
42
|
+
} else {
|
|
43
|
+
res.end();
|
|
44
|
+
}
|
|
45
|
+
}).catch(function(err) {
|
|
46
|
+
if (err.code) {
|
|
47
|
+
res.status(400).json({error:err.code, message: err.message});
|
|
48
|
+
} else {
|
|
49
|
+
res.status(400).json({error:"unexpected_error", message:err.toString()});
|
|
50
|
+
}
|
|
51
|
+
runtime.logger.error("api get apikeys: " + err.message);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* POST ApiKeys
|
|
58
|
+
* Set apikeys storage
|
|
59
|
+
*/
|
|
60
|
+
apiKeysApp.post("/api/apikeys", secureFnc, function(req, res, next) {
|
|
61
|
+
const permission = checkGroupsFnc(req);
|
|
62
|
+
if (res.statusCode === 403) {
|
|
63
|
+
runtime.logger.error("api post apikeys: Tocken Expired");
|
|
64
|
+
} else if (!authJwt.haveAdminPermission(permission)) {
|
|
65
|
+
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
|
|
66
|
+
runtime.logger.error("api post apikeys: Unauthorized");
|
|
67
|
+
} else {
|
|
68
|
+
runtime.apiKeys.setApiKeys(req.body.params).then(function(data) {
|
|
69
|
+
res.end();
|
|
70
|
+
}).catch(function(err) {
|
|
71
|
+
if (err.code) {
|
|
72
|
+
res.status(400).json({error:err.code, message: err.message});
|
|
73
|
+
} else {
|
|
74
|
+
res.status(400).json({error:"unexpected_error", message:err.toString()});
|
|
75
|
+
}
|
|
76
|
+
runtime.logger.error("api post apikeys: " + err.message);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* DELETE Roles
|
|
83
|
+
* Delete apikeys from storage
|
|
84
|
+
*/
|
|
85
|
+
apiKeysApp.delete("/api/apikeys", secureFnc, function(req, res, next) {
|
|
86
|
+
const permission = checkGroupsFnc(req);
|
|
87
|
+
if (res.statusCode === 403) {
|
|
88
|
+
runtime.logger.error("api delete apikeys: Tocken Expired");
|
|
89
|
+
} else if (!authJwt.haveAdminPermission(permission)) {
|
|
90
|
+
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
|
|
91
|
+
runtime.logger.error("api delete apikeys: Unauthorized");
|
|
92
|
+
} else {
|
|
93
|
+
runtime.apiKeys.removeApiKeys(JSON.parse(req.query.apikeys)).then(function(data) {
|
|
94
|
+
res.end();
|
|
95
|
+
}).catch(function(err) {
|
|
96
|
+
if (err.code) {
|
|
97
|
+
res.status(400).json({error:err.code, message: err.message});
|
|
98
|
+
} else {
|
|
99
|
+
res.status(400).json({error:"unexpected_error", message:err.toString()});
|
|
100
|
+
}
|
|
101
|
+
runtime.logger.error("api delete apikeys: " + err.message);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return apiKeysApp;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const authJwt = require('../jwt-helper');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Middleware that accepts either JWT (x-access-token) or API key (x-api-key).
|
|
7
|
+
* It is active only when security is enabled.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = function verifyApiOrToken(runtime) {
|
|
10
|
+
return async function (req, res, next) {
|
|
11
|
+
if (!runtime?.settings?.secureEnabled) {
|
|
12
|
+
return next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const apiKey = req.headers['x-api-key'];
|
|
16
|
+
if (apiKey) {
|
|
17
|
+
try {
|
|
18
|
+
const stored = await runtime.apiKeys.getApiKeys();
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const validKey = stored.find(k => {
|
|
21
|
+
if (!k || k.key !== apiKey || k.enabled === false) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (!k.expires) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const expiresAt = new Date(k.expires).getTime();
|
|
28
|
+
return !isNaN(expiresAt) && expiresAt > now;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (validKey) {
|
|
32
|
+
req.apiKey = validKey;
|
|
33
|
+
// Grant admin group to match existing authorization checks.
|
|
34
|
+
req.userId = validKey.id || `apikey:${apiKey}`;
|
|
35
|
+
req.userGroups = authJwt.adminGroups[0];
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
runtime.logger.error(`api-key validation failed: ${err}`);
|
|
40
|
+
return res.status(500).json({ error: 'unexpected_error', message: 'ApiKey validation failed' });
|
|
41
|
+
}
|
|
42
|
+
return res.status(401).json({ error: 'unauthorized_error', message: 'Invalid API Key' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return authJwt.verifyToken(req, res, next);
|
|
46
|
+
};
|
|
47
|
+
};
|
package/api/index.js
CHANGED
|
@@ -12,15 +12,20 @@ const rateLimit = require("express-rate-limit");
|
|
|
12
12
|
var prjApi = require('./projects');
|
|
13
13
|
var authApi = require('./auth');
|
|
14
14
|
var usersApi = require('./users');
|
|
15
|
+
var apiKeysApi = require('./apikeys');
|
|
15
16
|
var alarmsApi = require('./alarms');
|
|
16
17
|
var pluginsApi = require('./plugins');
|
|
17
18
|
var diagnoseApi = require('./diagnose');
|
|
18
19
|
var scriptsApi = require('./scripts');
|
|
19
20
|
var resourcesApi = require('./resources');
|
|
20
21
|
var daqApi = require('./daq');
|
|
22
|
+
var schedulerApi = require('./scheduler');
|
|
21
23
|
var commandApi = require('./command');
|
|
22
24
|
const reports = require('../dist/reports.service');
|
|
23
25
|
const reportsApi = new reports.ReportsApiService();
|
|
26
|
+
const verifyApiOrToken = require('./apikeys/verify-api-or-token');
|
|
27
|
+
|
|
28
|
+
const version = '1.0.0';
|
|
24
29
|
|
|
25
30
|
var apiApp;
|
|
26
31
|
var server;
|
|
@@ -29,6 +34,7 @@ var runtime;
|
|
|
29
34
|
function init(_server, _runtime) {
|
|
30
35
|
server = _server;
|
|
31
36
|
runtime = _runtime;
|
|
37
|
+
|
|
32
38
|
return new Promise(function (resolve, reject) {
|
|
33
39
|
if (runtime.settings.disableServer !== false) {
|
|
34
40
|
apiApp = express();
|
|
@@ -39,28 +45,33 @@ function init(_server, _runtime) {
|
|
|
39
45
|
apiApp.use(bodyParser.json({limit:maxApiRequestSize}));
|
|
40
46
|
apiApp.use(bodyParser.urlencoded({limit:maxApiRequestSize, extended: true}));
|
|
41
47
|
authJwt.init(runtime.settings.secureEnabled, runtime.settings.secretCode, runtime.settings.tokenExpiresIn);
|
|
42
|
-
|
|
48
|
+
const authMiddleware = verifyApiOrToken(runtime);
|
|
49
|
+
prjApi.init(runtime, authMiddleware, verifyGroups);
|
|
43
50
|
apiApp.use(prjApi.app());
|
|
44
|
-
usersApi.init(runtime,
|
|
51
|
+
usersApi.init(runtime, authMiddleware, verifyGroups);
|
|
45
52
|
apiApp.use(usersApi.app());
|
|
46
|
-
alarmsApi.init(runtime,
|
|
53
|
+
alarmsApi.init(runtime, authMiddleware, verifyGroups);
|
|
47
54
|
apiApp.use(alarmsApi.app());
|
|
48
55
|
authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn);
|
|
49
56
|
apiApp.use(authApi.app());
|
|
50
|
-
pluginsApi.init(runtime,
|
|
57
|
+
pluginsApi.init(runtime, authMiddleware, verifyGroups);
|
|
51
58
|
apiApp.use(pluginsApi.app());
|
|
52
|
-
diagnoseApi.init(runtime,
|
|
59
|
+
diagnoseApi.init(runtime, authMiddleware, verifyGroups);
|
|
53
60
|
apiApp.use(diagnoseApi.app());
|
|
54
|
-
daqApi.init(runtime,
|
|
61
|
+
daqApi.init(runtime, authMiddleware, verifyGroups);
|
|
55
62
|
apiApp.use(daqApi.app());
|
|
56
|
-
|
|
63
|
+
schedulerApi.init(runtime, authMiddleware, verifyGroups);
|
|
64
|
+
apiApp.use(schedulerApi.app());
|
|
65
|
+
scriptsApi.init(runtime, authMiddleware, verifyGroups);
|
|
57
66
|
apiApp.use(scriptsApi.app());
|
|
58
|
-
resourcesApi.init(runtime,
|
|
67
|
+
resourcesApi.init(runtime, authMiddleware, verifyGroups);
|
|
59
68
|
apiApp.use(resourcesApi.app());
|
|
60
|
-
commandApi.init(runtime,
|
|
69
|
+
commandApi.init(runtime, authMiddleware, verifyGroups);
|
|
61
70
|
apiApp.use(commandApi.app());
|
|
62
|
-
reportsApi.init(runtime,
|
|
71
|
+
reportsApi.init(runtime, authMiddleware, verifyGroups);
|
|
63
72
|
apiApp.use(reportsApi.app());
|
|
73
|
+
apiKeysApi.init(runtime, authMiddleware, verifyGroups);
|
|
74
|
+
apiApp.use(apiKeysApi.app());
|
|
64
75
|
|
|
65
76
|
const limiter = rateLimit({
|
|
66
77
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
@@ -79,6 +90,13 @@ function init(_server, _runtime) {
|
|
|
79
90
|
next(err);
|
|
80
91
|
});
|
|
81
92
|
|
|
93
|
+
/**
|
|
94
|
+
* GET Server setting data
|
|
95
|
+
*/
|
|
96
|
+
apiApp.get('/api/version', function (req, res) {
|
|
97
|
+
res.json(version);
|
|
98
|
+
});
|
|
99
|
+
|
|
82
100
|
/**
|
|
83
101
|
* GET Server setting data
|
|
84
102
|
*/
|
|
@@ -101,7 +119,7 @@ function init(_server, _runtime) {
|
|
|
101
119
|
/**
|
|
102
120
|
* POST Server user settings
|
|
103
121
|
*/
|
|
104
|
-
apiApp.post("/api/settings",
|
|
122
|
+
apiApp.post("/api/settings", authMiddleware, function(req, res, next) {
|
|
105
123
|
const permission = verifyGroups(req);
|
|
106
124
|
if (res.statusCode === 403) {
|
|
107
125
|
runtime.logger.error("api post settings: Tocken Expired");
|
|
@@ -128,7 +146,7 @@ function init(_server, _runtime) {
|
|
|
128
146
|
/**
|
|
129
147
|
* GET Heartbeat to check token
|
|
130
148
|
*/
|
|
131
|
-
apiApp.post('/api/heartbeat',
|
|
149
|
+
apiApp.post('/api/heartbeat', authMiddleware, function (req, res) {
|
|
132
150
|
if (!runtime.settings.secureEnabled) {
|
|
133
151
|
res.end();
|
|
134
152
|
} else if (res.statusCode === 403) {
|
|
@@ -152,6 +170,7 @@ function init(_server, _runtime) {
|
|
|
152
170
|
res.end();
|
|
153
171
|
}
|
|
154
172
|
});
|
|
173
|
+
|
|
155
174
|
runtime.logger.info('api: init successful!', true);
|
|
156
175
|
} else {
|
|
157
176
|
}
|
|
@@ -179,10 +198,16 @@ function mergeUserSettings(settings) {
|
|
|
179
198
|
if (settings.alarms) {
|
|
180
199
|
runtime.settings.alarms = settings.alarms;
|
|
181
200
|
}
|
|
201
|
+
if (settings.logs) {
|
|
202
|
+
runtime.settings.logs = settings.logs;
|
|
203
|
+
}
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
function verifyGroups(req) {
|
|
185
207
|
if (runtime.settings && runtime.settings.secureEnabled) {
|
|
208
|
+
if (req.apiKey) {
|
|
209
|
+
return authJwt.adminGroups[0];
|
|
210
|
+
}
|
|
186
211
|
if (req.tokenExpired) {
|
|
187
212
|
return (runtime.settings.userRole) ? null : 0;
|
|
188
213
|
}
|
package/api/jwt-helper.js
CHANGED
|
@@ -76,6 +76,51 @@ function verifyToken (req, res, next) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function requireAuth (req, res, next) {
|
|
80
|
+
// Allow requests from FUXA interface (iframe embedding)
|
|
81
|
+
// Check for common FUXA referer patterns
|
|
82
|
+
const referer = req.headers.referer;
|
|
83
|
+
if (referer) {
|
|
84
|
+
// Allow if referer is from the same host (to support IP access without specific paths)
|
|
85
|
+
const requestHost = req.headers.host;
|
|
86
|
+
if (referer.startsWith(`http://${requestHost}`) || referer.startsWith(`https://${requestHost}`)) {
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
// Allow if referer contains common FUXA paths or is from the same server
|
|
90
|
+
const fuxaPatterns = [
|
|
91
|
+
'/fuxa', '/editor', '/viewer', '/lab', '/home',
|
|
92
|
+
'localhost:', '127.0.0.1:', '0.0.0.0:'
|
|
93
|
+
];
|
|
94
|
+
const hasFuxaReferer = fuxaPatterns.some(pattern => referer.includes(pattern));
|
|
95
|
+
if (hasFuxaReferer) {
|
|
96
|
+
return next();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// For direct access, require authentication
|
|
101
|
+
let token = req.headers['x-access-token'];
|
|
102
|
+
|
|
103
|
+
if (!token) {
|
|
104
|
+
return res.status(401).json({ error: "unauthorized_error", message: "Authentication required!" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
jwt.verify(token, secretCode, (err, decoded) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
return res.status(401).json({ error: "unauthorized_error", message: "Invalid token!" });
|
|
110
|
+
} else {
|
|
111
|
+
req.userId = decoded.id;
|
|
112
|
+
req.userGroups = decoded.groups;
|
|
113
|
+
if (req.headers['x-auth-user']) {
|
|
114
|
+
let user = JSON.parse(req.headers['x-auth-user']);
|
|
115
|
+
if (user && user.groups != req.userGroups) {
|
|
116
|
+
return res.status(403).json({ error: "unauthorized_error", message: "User Profile Corrupted!" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
next();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
function getNewToken(headers) {
|
|
80
125
|
const authUser = (headers['x-auth-user']) ? JSON.parse(headers['x-auth-user']) : null;
|
|
81
126
|
if (authUser) {
|
|
@@ -119,6 +164,7 @@ module.exports = {
|
|
|
119
164
|
init: init,
|
|
120
165
|
verify: verify,
|
|
121
166
|
verifyToken: verifyToken,
|
|
167
|
+
requireAuth: requireAuth,
|
|
122
168
|
getNewToken: getNewToken,
|
|
123
169
|
getGuestToken: getGuestToken,
|
|
124
170
|
get secretCode() { return secretCode },
|
package/api/resources/index.js
CHANGED
|
@@ -71,51 +71,43 @@ module.exports = {
|
|
|
71
71
|
* GET Server resources folder content
|
|
72
72
|
*/
|
|
73
73
|
resourcesApp.get('/api/resources/resources', secureFnc, function (req, res) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
runtime.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
for (var x = 0; x < files.length; x++) {
|
|
88
|
-
const fileName = files[x];
|
|
89
|
-
const filePath = path.join(wwwSubDir, files[x]).split(path.sep).join(path.posix.sep);
|
|
90
|
-
var fileLabel;
|
|
91
|
-
if (resourcesFilter.fonts.some(suffix => fileName.endsWith(suffix))) {
|
|
92
|
-
const font = fontkit.openSync(filePath);
|
|
93
|
-
fileLabel = font.fullName;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
group.items.push({
|
|
97
|
-
path: filePath,
|
|
98
|
-
name: fileName,
|
|
99
|
-
label: fileLabel
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
result.groups.push(group);
|
|
103
|
-
res.json(result);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
if (err.code) {
|
|
106
|
-
res.status(400).json({ error: err.code, message: err.message });
|
|
107
|
-
} else {
|
|
108
|
-
res.status(400).json({ error: "unexpected_error", message: err.toString() });
|
|
74
|
+
try {
|
|
75
|
+
const resourcesFilter = { fonts: ['ttf'] };
|
|
76
|
+
const wwwSubDir = '_resources';
|
|
77
|
+
const result = { ...req.query, ...{ groups: [] } };
|
|
78
|
+
const group = { name: wwwSubDir, items: [] };
|
|
79
|
+
var files = getFiles(runtime.settings.resourcesFileDir, ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.pdf', '.ttf', '.mp4', '.webm', '.ogg', '.ogv']);
|
|
80
|
+
for (var x = 0; x < files.length; x++) {
|
|
81
|
+
const fileName = files[x];
|
|
82
|
+
const filePath = path.join(wwwSubDir, files[x]).split(path.sep).join(path.posix.sep);
|
|
83
|
+
var fileLabel;
|
|
84
|
+
if (resourcesFilter.fonts.some(suffix => fileName.endsWith(suffix))) {
|
|
85
|
+
const font = fontkit.openSync(filePath);
|
|
86
|
+
fileLabel = font.fullName;
|
|
109
87
|
}
|
|
110
|
-
|
|
88
|
+
|
|
89
|
+
group.items.push({
|
|
90
|
+
path: filePath,
|
|
91
|
+
name: fileName,
|
|
92
|
+
label: fileLabel
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
result.groups.push(group);
|
|
96
|
+
res.json(result);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err.code) {
|
|
99
|
+
res.status(400).json({ error: err.code, message: err.message });
|
|
100
|
+
} else {
|
|
101
|
+
res.status(400).json({ error: "unexpected_error", message: err.toString() });
|
|
111
102
|
}
|
|
103
|
+
runtime.logger.error("api get resources/resources: " + err.message);
|
|
112
104
|
}
|
|
113
105
|
});
|
|
114
106
|
|
|
115
107
|
/**
|
|
116
108
|
* POST remove resource file
|
|
117
109
|
*/
|
|
118
|
-
resourcesApp.post('/api/resources/remove', function (req, res) {
|
|
110
|
+
resourcesApp.post('/api/resources/remove', secureFnc, function (req, res) {
|
|
119
111
|
const permission = checkGroupsFnc(req);
|
|
120
112
|
if (res.statusCode === 403) {
|
|
121
113
|
runtime.logger.error("api post device: Tocken Expired");
|
|
@@ -215,7 +207,7 @@ module.exports = {
|
|
|
215
207
|
/**
|
|
216
208
|
* POST template
|
|
217
209
|
*/
|
|
218
|
-
resourcesApp.post('/api/resources/template', function (req, res) {
|
|
210
|
+
resourcesApp.post('/api/resources/template', secureFnc, function (req, res) {
|
|
219
211
|
const permission = checkGroupsFnc(req);
|
|
220
212
|
if (res.statusCode === 403) {
|
|
221
213
|
runtime.logger.error("api post device: Tocken Expired");
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'api/scheduler': Scheduler API to GET/POST scheduler data
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
var express = require("express");
|
|
6
|
+
const authJwt = require('../jwt-helper');
|
|
7
|
+
|
|
8
|
+
var runtime;
|
|
9
|
+
var secureFnc;
|
|
10
|
+
var checkGroupsFnc;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
init: function (_runtime, _secureFnc, _checkGroupsFnc) {
|
|
14
|
+
runtime = _runtime;
|
|
15
|
+
secureFnc = _secureFnc;
|
|
16
|
+
checkGroupsFnc = _checkGroupsFnc;
|
|
17
|
+
},
|
|
18
|
+
app: function () {
|
|
19
|
+
var schedulerApp = express();
|
|
20
|
+
schedulerApp.use(function (req, res, next) {
|
|
21
|
+
if (!runtime.project) {
|
|
22
|
+
res.status(404).end();
|
|
23
|
+
} else {
|
|
24
|
+
next();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// GET scheduler data
|
|
29
|
+
schedulerApp.get("/api/scheduler", secureFnc, function(req, res) {
|
|
30
|
+
try {
|
|
31
|
+
if (req.query && req.query.id) {
|
|
32
|
+
var schedulerId = req.query.id;
|
|
33
|
+
runtime.schedulerStorage.getSchedulerData(schedulerId).then(result => {
|
|
34
|
+
if (result) {
|
|
35
|
+
res.json(result);
|
|
36
|
+
} else {
|
|
37
|
+
res.json({ schedules: {} });
|
|
38
|
+
}
|
|
39
|
+
}).catch(err => {
|
|
40
|
+
runtime.logger.error("get scheduler data error! " + err);
|
|
41
|
+
res.status(400).json({ error: err });
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
res.status(400).json({ error: 'Missing scheduler id parameter' });
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
runtime.logger.error("get scheduler data error! " + err);
|
|
48
|
+
res.status(400).json({ error: err });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// POST scheduler data
|
|
53
|
+
schedulerApp.post("/api/scheduler", secureFnc, function(req, res) {
|
|
54
|
+
try {
|
|
55
|
+
if (req.body && req.body.id && req.body.data !== undefined) {
|
|
56
|
+
var schedulerId = req.body.id;
|
|
57
|
+
var schedulerData = req.body.data;
|
|
58
|
+
|
|
59
|
+
// LOG INCOMING DATA
|
|
60
|
+
runtime.logger.info('[API POST SCHEDULER] Received data for scheduler: ' + schedulerId);
|
|
61
|
+
runtime.logger.info('[API POST SCHEDULER] Settings: ' + JSON.stringify(schedulerData.settings, null, 2));
|
|
62
|
+
runtime.logger.info('[API POST SCHEDULER] Device Actions: ' + JSON.stringify(schedulerData.settings?.deviceActions, null, 2));
|
|
63
|
+
|
|
64
|
+
const validation = validateSchedulerData(schedulerData);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
runtime.logger.error("Invalid scheduler data: " + validation.error);
|
|
67
|
+
return res.status(400).json({ error: 'Invalid scheduler data: ' + validation.error });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
runtime.schedulerStorage.getSchedulerData(schedulerId).then(oldData => {
|
|
71
|
+
return runtime.schedulerStorage.setSchedulerData(schedulerId, schedulerData).then(result => {
|
|
72
|
+
runtime.logger.info('[API POST SCHEDULER] Data saved successfully to database');
|
|
73
|
+
res.json({ result: 'ok' });
|
|
74
|
+
if (runtime.schedulerService) {
|
|
75
|
+
runtime.schedulerService.updateScheduler(schedulerId, schedulerData, oldData);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}).catch(err => {
|
|
79
|
+
runtime.logger.error("set scheduler data error! " + err);
|
|
80
|
+
res.status(400).json({ error: err });
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
res.status(400).json({ error: 'Missing scheduler id or data in request body' });
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
runtime.logger.error("set scheduler data error! " + err);
|
|
87
|
+
res.status(400).json({ error: err });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// DELETE scheduler data
|
|
92
|
+
schedulerApp.delete("/api/scheduler", secureFnc, function(req, res) {
|
|
93
|
+
try {
|
|
94
|
+
if (req.query && req.query.id) {
|
|
95
|
+
var schedulerId = req.query.id;
|
|
96
|
+
|
|
97
|
+
if (runtime.schedulerService && runtime.schedulerService.removeScheduler) {
|
|
98
|
+
runtime.schedulerService.removeScheduler(schedulerId).then(() => {
|
|
99
|
+
return runtime.schedulerStorage.deleteSchedulerData(schedulerId);
|
|
100
|
+
}).then(result => {
|
|
101
|
+
res.json({ result: 'ok', deleted: result.changes });
|
|
102
|
+
}).catch(err => {
|
|
103
|
+
runtime.logger.error("delete scheduler error! " + err);
|
|
104
|
+
res.status(400).json({ error: err.toString() });
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
runtime.schedulerStorage.deleteSchedulerData(schedulerId).then(result => {
|
|
108
|
+
res.json({ result: 'ok', deleted: result.changes });
|
|
109
|
+
}).catch(err => {
|
|
110
|
+
runtime.logger.error("delete scheduler data error! " + err);
|
|
111
|
+
res.status(400).json({ error: err.toString() });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
res.status(400).json({ error: 'Missing scheduler id parameter' });
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
runtime.logger.error("delete scheduler error! " + err);
|
|
119
|
+
res.status(400).json({ error: err.toString() });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return schedulerApp;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Validate scheduler data structure
|
|
128
|
+
function validateSchedulerData(data) {
|
|
129
|
+
if (!data || typeof data !== 'object') {
|
|
130
|
+
return { valid: false, error: 'Data must be an object' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!data.schedules || typeof data.schedules !== 'object') {
|
|
134
|
+
return { valid: false, error: 'Missing or invalid schedules object' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!data.settings || typeof data.settings !== 'object') {
|
|
138
|
+
return { valid: false, error: 'Missing or invalid settings object' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!data.settings.devices || !Array.isArray(data.settings.devices)) {
|
|
142
|
+
return { valid: false, error: 'Missing or invalid settings.devices array' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const deviceNames = new Set();
|
|
146
|
+
for (let device of data.settings.devices) {
|
|
147
|
+
if (!device.name || typeof device.name !== 'string') {
|
|
148
|
+
return { valid: false, error: 'Device missing name property' };
|
|
149
|
+
}
|
|
150
|
+
if (!device.variableId || typeof device.variableId !== 'string') {
|
|
151
|
+
return { valid: false, error: `Device "${device.name}" missing variableId` };
|
|
152
|
+
}
|
|
153
|
+
deviceNames.add(device.name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (let deviceName in data.schedules) {
|
|
157
|
+
if (!deviceNames.has(deviceName)) {
|
|
158
|
+
console.warn(`Warning: Schedule exists for "${deviceName}" but device not found in settings.devices`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const schedules = data.schedules[deviceName];
|
|
162
|
+
if (!Array.isArray(schedules)) {
|
|
163
|
+
return { valid: false, error: `Schedules for "${deviceName}" must be an array` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < schedules.length; i++) {
|
|
167
|
+
const schedule = schedules[i];
|
|
168
|
+
|
|
169
|
+
if (!schedule.startTime) {
|
|
170
|
+
return { valid: false, error: `Schedule ${i} for "${deviceName}" missing startTime` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!schedule.days || !Array.isArray(schedule.days) || schedule.days.length !== 7) {
|
|
174
|
+
return { valid: false, error: `Schedule ${i} for "${deviceName}" missing or invalid days array` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (schedule.deviceName !== deviceName) {
|
|
178
|
+
console.warn(`Warning: Schedule.deviceName "${schedule.deviceName}" doesn't match key "${deviceName}"`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { valid: true };
|
|
184
|
+
}
|
package/dist/assets/i18n/de.json
CHANGED
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
"graph.property-date-last-range": "Datumsbereich",
|
|
285
285
|
"graph.property-date-group": "Gruppe",
|
|
286
286
|
"graph.property-offline": "Online",
|
|
287
|
-
"graph.property-graph-
|
|
287
|
+
"graph.property-graph-orientation": "Ausrichtung",
|
|
288
288
|
"graph.property-ori-vartical": "Vertikal",
|
|
289
289
|
"graph.property-ori-horizontal": "Horizontal",
|
|
290
290
|
"graph.property-decimals": "Dezimalstellen",
|