@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.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/api/apikeys/index.js +107 -0
  3. package/api/apikeys/verify-api-or-token.js +47 -0
  4. package/api/index.js +37 -12
  5. package/api/jwt-helper.js +46 -0
  6. package/api/resources/index.js +30 -38
  7. package/api/scheduler/index.js +184 -0
  8. package/dist/3rdpartylicenses.txt +1 -1
  9. package/dist/assets/i18n/de.json +1 -1
  10. package/dist/assets/i18n/en.json +154 -15
  11. package/dist/assets/i18n/fr.json +1 -1
  12. package/dist/assets/i18n/ja.json +1 -1
  13. package/dist/assets/i18n/ru.json +1 -1
  14. package/dist/assets/i18n/sv.json +1 -1
  15. package/dist/assets/i18n/zh-cn.json +2 -2
  16. package/dist/assets/i18n/zh-tw.json +1718 -0
  17. package/dist/assets/images/nodered-icon.svg +19 -0
  18. package/dist/index.html +2 -2
  19. package/dist/{main.51a4dc379bb23222.js → main.a17204bdf90c1b7a.js} +115 -115
  20. package/dist/styles.7afa4817cbd46a9c.css +1 -0
  21. package/docs/openapi.yaml +1045 -0
  22. package/integrations/node-red/index.js +219 -0
  23. package/integrations/node-red/node-red-contrib-fuxa/index.js +1 -0
  24. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-ack-alarm.html +49 -0
  25. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-ack-alarm.js +27 -0
  26. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-emit-event.html +31 -0
  27. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-emit-event.js +17 -0
  28. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-enable-device.html +49 -0
  29. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-enable-device.js +25 -0
  30. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-execute-script.html +50 -0
  31. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-execute-script.js +49 -0
  32. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-alarms.html +27 -0
  33. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-alarms.js +19 -0
  34. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +100 -0
  35. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +25 -0
  36. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device-property.html +48 -0
  37. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device-property.js +25 -0
  38. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device.html +49 -0
  39. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-device.js +25 -0
  40. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-historical-tags.html +152 -0
  41. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-historical-tags.js +76 -0
  42. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-history-alarms.html +88 -0
  43. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-history-alarms.js +28 -0
  44. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +53 -0
  45. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +78 -0
  46. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +45 -0
  47. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +28 -0
  48. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-id.html +45 -0
  49. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-id.js +23 -0
  50. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +44 -0
  51. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +25 -0
  52. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-open-card.html +44 -0
  53. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-open-card.js +23 -0
  54. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-send-message.html +43 -0
  55. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-send-message.js +24 -0
  56. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-device-property.html +53 -0
  57. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-device-property.js +27 -0
  58. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +61 -0
  59. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +39 -0
  60. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +43 -0
  61. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +23 -0
  62. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-view.html +44 -0
  63. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-view.js +23 -0
  64. package/integrations/node-red/node-red-contrib-fuxa/package.json +41 -0
  65. package/main.js +44 -11
  66. package/package.json +11 -5
  67. package/runtime/apikeys/apiKeysStorage.js +125 -0
  68. package/runtime/apikeys/index.js +123 -0
  69. package/runtime/devices/device-utils.js +21 -2
  70. package/runtime/devices/fuxaserver/index.js +22 -2
  71. package/runtime/devices/modbus/index.js +76 -41
  72. package/runtime/devices/opcua/index.js +19 -6
  73. package/runtime/events.js +3 -0
  74. package/runtime/index.js +25 -4
  75. package/runtime/jobs/cleaner.js +34 -1
  76. package/runtime/notificator/index.js +34 -9
  77. package/runtime/plugins/index.js +3 -0
  78. package/runtime/scheduler/scheduler-service.js +1608 -0
  79. package/runtime/scheduler/scheduler-storage.js +184 -0
  80. package/runtime/scripts/index.js +1 -3
  81. package/runtime/storage/influxdb/index.js +1 -1
  82. package/settings.default.js +10 -2
  83. 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
  ![fuxa action](/screenshot/feature-action-move.gif)
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
- prjApi.init(runtime, authJwt.verifyToken, verifyGroups);
48
+ const authMiddleware = verifyApiOrToken(runtime);
49
+ prjApi.init(runtime, authMiddleware, verifyGroups);
43
50
  apiApp.use(prjApi.app());
44
- usersApi.init(runtime, authJwt.verifyToken, verifyGroups);
51
+ usersApi.init(runtime, authMiddleware, verifyGroups);
45
52
  apiApp.use(usersApi.app());
46
- alarmsApi.init(runtime, authJwt.verifyToken, verifyGroups);
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, authJwt.verifyToken, verifyGroups);
57
+ pluginsApi.init(runtime, authMiddleware, verifyGroups);
51
58
  apiApp.use(pluginsApi.app());
52
- diagnoseApi.init(runtime, authJwt.verifyToken, verifyGroups);
59
+ diagnoseApi.init(runtime, authMiddleware, verifyGroups);
53
60
  apiApp.use(diagnoseApi.app());
54
- daqApi.init(runtime, authJwt.verifyToken, verifyGroups);
61
+ daqApi.init(runtime, authMiddleware, verifyGroups);
55
62
  apiApp.use(daqApi.app());
56
- scriptsApi.init(runtime, authJwt.verifyToken, verifyGroups);
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, authJwt.verifyToken, verifyGroups);
67
+ resourcesApi.init(runtime, authMiddleware, verifyGroups);
59
68
  apiApp.use(resourcesApi.app());
60
- commandApi.init(runtime, authJwt.verifyToken, verifyGroups);
69
+ commandApi.init(runtime, authMiddleware, verifyGroups);
61
70
  apiApp.use(commandApi.app());
62
- reportsApi.init(runtime, authJwt.verifyToken, verifyGroups);
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", authJwt.verifyToken, function(req, res, next) {
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', authJwt.verifyToken, function (req, res) {
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 },
@@ -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
- const permission = checkGroupsFnc(req);
75
- if (res.statusCode === 403) {
76
- runtime.logger.error("api get resources/resources: Tocken Expired");
77
- } else if (!authJwt.haveAdminPermission(permission)) {
78
- res.status(401).json({ error: "unauthorized_error", message: "Unauthorized!" });
79
- runtime.logger.error("api get resources/resources: Unauthorized!");
80
- } else {
81
- try {
82
- const resourcesFilter = { fonts: ['ttf'] };
83
- const wwwSubDir = '_resources';
84
- const result = { ...req.query, ...{ groups: [] } };
85
- const group = { name: wwwSubDir, items: [] };
86
- var files = getFiles(runtime.settings.resourcesFileDir, ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.pdf', '.ttf', '.mp4', '.webm', '.ogg', '.ogv']);
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
- runtime.logger.error("api get resources/resources: " + err.message);
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
+ }
@@ -903,7 +903,7 @@ ISC
903
903
 
904
904
  ngraph.events
905
905
  BSD-3-Clause
906
- Copyright (c) 2013-2022 Andrei Kashcha
906
+ Copyright (c) 2013-2025 Andrei Kashcha
907
907
  All rights reserved.
908
908
 
909
909
  Redistribution and use in source and binary forms, with or without modification,
@@ -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-oriantation": "Ausrichtung",
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",