@frangoteam/fuxa-min 1.2.10 → 1.3.0
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/api/auth/index.js +141 -3
- package/api/command/index.js +10 -4
- package/api/diagnose/index.js +12 -4
- package/api/index.js +41 -8
- package/api/jwt-helper.js +15 -2
- package/api/path-helper.js +41 -0
- package/api/projects/index.js +27 -14
- package/api/reports/reports.service.ts +12 -2
- package/api/resources/index.js +30 -9
- package/api/scheduler/index.js +21 -1
- package/dist/3rdpartylicenses.txt +139 -7
- package/dist/assets/i18n/de.json +10 -0
- package/dist/assets/i18n/en.json +17 -3
- package/dist/assets/i18n/es.json +12 -0
- package/dist/assets/i18n/fr.json +10 -0
- package/dist/assets/i18n/ja.json +15 -6
- package/dist/assets/i18n/ko.json +12 -0
- package/dist/assets/i18n/pt.json +9 -2
- package/dist/assets/i18n/ru.json +11 -0
- package/dist/assets/i18n/sv.json +10 -1
- package/dist/assets/i18n/tr.json +8 -1
- package/dist/assets/i18n/ua.json +9 -2
- package/dist/assets/i18n/zh-cn.json +10 -0
- package/dist/assets/i18n/zh-tw.json +11 -1
- package/dist/index.html +2 -2
- package/dist/main.bafae830903d548e.js +329 -0
- package/dist/polyfills.d7de05f9af2fb559.js +1 -0
- package/dist/reports.service.js +11 -1
- package/dist/{runtime.8ef63094e52a66ba.js → runtime.9136a61a9b98f987.js} +1 -1
- package/dist/{scripts.40b60f02658462e4.js → scripts.d9e6ee984bf6f3b7.js} +1 -1
- package/dist/styles.545e37beb3e671ba.css +1 -0
- package/integrations/node-red/index.js +91 -24
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +8 -2
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +12 -12
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +14 -10
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +8 -2
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +24 -20
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +8 -2
- package/main.js +41 -17
- package/package.json +10 -5
- package/runtime/devices/adsclient/index.js +1 -1
- package/runtime/devices/bacnet/index.js +66 -32
- package/runtime/devices/ethernetip/index.js +1 -1
- package/runtime/devices/gpio/index.js +1 -1
- package/runtime/devices/odbc/index.js +5 -5
- package/runtime/devices/template/index.js +14 -14
- package/runtime/storage/daqstorage.js +28 -2
- package/runtime/storage/influxdb/index.js +1 -1
- package/runtime/storage/questdb/index.js +224 -0
- package/runtime/utils.js +5 -0
- package/settings.default.js +13 -3
- package/dist/main.020ca34630a3363a.js +0 -329
- package/dist/polyfills.c8e7db9850a3ad8b.js +0 -1
- package/dist/styles.03cc550382689976.css +0 -1
package/api/auth/index.js
CHANGED
|
@@ -10,12 +10,78 @@ const authJwt = require('../jwt-helper');
|
|
|
10
10
|
var runtime;
|
|
11
11
|
var secretCode;
|
|
12
12
|
var tokenExpiresIn;
|
|
13
|
+
var enableRefreshCookieAuth = false;
|
|
14
|
+
var refreshTokenExpiresIn = '7d';
|
|
15
|
+
const refreshCookieName = 'fuxa_refresh';
|
|
16
|
+
|
|
17
|
+
function parseExpiresToMs(expiresIn) {
|
|
18
|
+
if (expiresIn === undefined || expiresIn === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof expiresIn === 'number') {
|
|
22
|
+
return expiresIn * 1000;
|
|
23
|
+
}
|
|
24
|
+
if (typeof expiresIn !== 'string') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const match = expiresIn.trim().match(/^(\d+)\s*([smhd])?$/i);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const value = Number(match[1]);
|
|
32
|
+
const unit = (match[2] || 's').toLowerCase();
|
|
33
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
34
|
+
return value * (multipliers[unit] || 1000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCookieValue(req, name) {
|
|
38
|
+
const cookieHeader = req.headers.cookie;
|
|
39
|
+
if (!cookieHeader) return null;
|
|
40
|
+
const cookies = cookieHeader.split(';');
|
|
41
|
+
for (const cookie of cookies) {
|
|
42
|
+
const [key, ...rest] = cookie.trim().split('=');
|
|
43
|
+
if (key === name) {
|
|
44
|
+
return decodeURIComponent(rest.join('='));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildAccessToken(user) {
|
|
51
|
+
return jwt.sign({ id: user.username, groups: user.groups }, secretCode, { expiresIn: tokenExpiresIn });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildRefreshToken(user) {
|
|
55
|
+
return jwt.sign({ id: user.username, groups: user.groups, type: 'refresh' }, secretCode, { expiresIn: refreshTokenExpiresIn });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setRefreshCookie(res, token) {
|
|
59
|
+
const maxAge = parseExpiresToMs(refreshTokenExpiresIn);
|
|
60
|
+
const options = {
|
|
61
|
+
httpOnly: true,
|
|
62
|
+
sameSite: 'lax',
|
|
63
|
+
secure: !!runtime?.settings?.https,
|
|
64
|
+
path: '/api/refresh'
|
|
65
|
+
};
|
|
66
|
+
if (maxAge) {
|
|
67
|
+
options.maxAge = maxAge;
|
|
68
|
+
}
|
|
69
|
+
res.cookie(refreshCookieName, token, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearRefreshCookie(res) {
|
|
73
|
+
res.clearCookie(refreshCookieName, { path: '/api/refresh' });
|
|
74
|
+
}
|
|
13
75
|
|
|
14
76
|
module.exports = {
|
|
15
|
-
init: function (_runtime, _secretCode, _tokenExpires) {
|
|
77
|
+
init: function (_runtime, _secretCode, _tokenExpires, _enableRefreshCookieAuth, _refreshTokenExpires) {
|
|
16
78
|
runtime = _runtime;
|
|
17
79
|
secretCode = _secretCode;
|
|
18
80
|
tokenExpiresIn = _tokenExpires;
|
|
81
|
+
enableRefreshCookieAuth = !!_enableRefreshCookieAuth;
|
|
82
|
+
if (_refreshTokenExpires) {
|
|
83
|
+
refreshTokenExpiresIn = _refreshTokenExpires;
|
|
84
|
+
}
|
|
19
85
|
},
|
|
20
86
|
app: function () {
|
|
21
87
|
var authApp = express();
|
|
@@ -35,7 +101,11 @@ module.exports = {
|
|
|
35
101
|
runtime.users.findOne(req.body).then(function (userInfo) {
|
|
36
102
|
if (userInfo && userInfo.length && userInfo[0].password) {
|
|
37
103
|
if (bcrypt.compareSync(req.body.password, userInfo[0].password)) {
|
|
38
|
-
const token =
|
|
104
|
+
const token = buildAccessToken(userInfo[0]);
|
|
105
|
+
if (enableRefreshCookieAuth) {
|
|
106
|
+
const refreshToken = buildRefreshToken(userInfo[0]);
|
|
107
|
+
setRefreshCookie(res, refreshToken);
|
|
108
|
+
}
|
|
39
109
|
res.json({
|
|
40
110
|
status: 'success',
|
|
41
111
|
message: 'user found!!!',
|
|
@@ -66,6 +136,74 @@ module.exports = {
|
|
|
66
136
|
});
|
|
67
137
|
});
|
|
68
138
|
|
|
139
|
+
/**
|
|
140
|
+
* POST Refresh
|
|
141
|
+
* Refresh access token using HttpOnly refresh cookie
|
|
142
|
+
*/
|
|
143
|
+
authApp.post('/api/refresh', async function (req, res, next) {
|
|
144
|
+
if (!runtime?.settings?.secureEnabled || !enableRefreshCookieAuth) {
|
|
145
|
+
return res.status(204).end();
|
|
146
|
+
}
|
|
147
|
+
const refreshToken = getCookieValue(req, refreshCookieName);
|
|
148
|
+
if (!refreshToken) {
|
|
149
|
+
return res.status(401).json({ status: 'error', message: 'Refresh token missing' });
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const decoded = jwt.verify(refreshToken, secretCode);
|
|
153
|
+
if (decoded?.type !== 'refresh') {
|
|
154
|
+
clearRefreshCookie(res);
|
|
155
|
+
return res.status(401).json({ status: 'error', message: 'Invalid refresh token' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let userData = null;
|
|
159
|
+
try {
|
|
160
|
+
const users = await runtime.users.getUsers({ username: decoded.id });
|
|
161
|
+
if (users && users.length) {
|
|
162
|
+
userData = users[0];
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
runtime.logger.error(`api refresh: user lookup failed ${err}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const user = {
|
|
169
|
+
username: decoded.id,
|
|
170
|
+
fullname: userData?.fullname,
|
|
171
|
+
groups: userData?.groups || decoded.groups,
|
|
172
|
+
info: userData?.info
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const newAccessToken = buildAccessToken(user);
|
|
176
|
+
const newRefreshToken = buildRefreshToken(user);
|
|
177
|
+
setRefreshCookie(res, newRefreshToken);
|
|
178
|
+
|
|
179
|
+
res.json({
|
|
180
|
+
status: 'success',
|
|
181
|
+
message: 'token refreshed',
|
|
182
|
+
data: {
|
|
183
|
+
username: user.username,
|
|
184
|
+
fullname: user.fullname,
|
|
185
|
+
groups: user.groups,
|
|
186
|
+
info: user.info,
|
|
187
|
+
token: newAccessToken
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
clearRefreshCookie(res);
|
|
192
|
+
res.status(401).json({ status: 'error', message: 'Invalid refresh token' });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* POST SignOut
|
|
198
|
+
* Clear refresh cookie
|
|
199
|
+
*/
|
|
200
|
+
authApp.post('/api/signout', function (req, res, next) {
|
|
201
|
+
if (enableRefreshCookieAuth) {
|
|
202
|
+
clearRefreshCookie(res);
|
|
203
|
+
}
|
|
204
|
+
res.status(204).end();
|
|
205
|
+
});
|
|
206
|
+
|
|
69
207
|
return authApp;
|
|
70
208
|
}
|
|
71
|
-
}
|
|
209
|
+
}
|
package/api/command/index.js
CHANGED
|
@@ -6,6 +6,7 @@ var express = require("express");
|
|
|
6
6
|
const authJwt = require('../jwt-helper');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const { resolveWithin } = require('../path-helper');
|
|
9
10
|
var runtime;
|
|
10
11
|
var secureFnc;
|
|
11
12
|
var checkGroupsFnc;
|
|
@@ -39,11 +40,16 @@ module.exports = {
|
|
|
39
40
|
} else {
|
|
40
41
|
if (req.query.cmd === CommanTypeEnum.reportDownload) {
|
|
41
42
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reportPath = path.join(process.cwd(), runtime.settings.reportsDir, fileName);
|
|
43
|
+
var reportBase = runtime.settings.reportsDir;
|
|
44
|
+
if (!fs.existsSync(reportBase)) {
|
|
45
|
+
reportBase = path.join(process.cwd(), runtime.settings.reportsDir);
|
|
46
46
|
}
|
|
47
|
+
const resolvedReport = resolveWithin(reportBase, req.query.name);
|
|
48
|
+
if (!resolvedReport) {
|
|
49
|
+
res.status(400).json({ error: "invalid_path", message: "Invalid report file." });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const reportPath = resolvedReport.resolvedTarget;
|
|
47
53
|
if (fs.existsSync(reportPath)) {
|
|
48
54
|
res.sendFile(reportPath, (err) => {
|
|
49
55
|
if (err) {
|
package/api/diagnose/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
var express = require("express");
|
|
8
8
|
const authJwt = require('../jwt-helper');
|
|
9
|
+
const { normalizeRelativePath, resolveWithin } = require('../path-helper');
|
|
9
10
|
var runtime;
|
|
10
11
|
var secureFnc;
|
|
11
12
|
var checkGroupsFnc;
|
|
@@ -67,14 +68,21 @@ module.exports = {
|
|
|
67
68
|
runtime.logger.error("api get logs: Unauthorized!");
|
|
68
69
|
} else {
|
|
69
70
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const requestedName = req.query.file ? normalizeRelativePath(req.query.file) : 'fuxa.log';
|
|
72
|
+
if (!requestedName) {
|
|
73
|
+
res.status(400).json({ error: "invalid_path", message: "Invalid log file." });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
72
76
|
var logPath = runtime.logger.logDir();
|
|
73
77
|
if (!fs.existsSync(logPath)) {
|
|
74
78
|
logPath = path.join(process.cwd(), runtime.logger.logDir());
|
|
75
79
|
}
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
const resolvedLog = resolveWithin(logPath, requestedName);
|
|
81
|
+
if (!resolvedLog) {
|
|
82
|
+
res.status(400).json({ error: "invalid_path", message: "Invalid log file." });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let logFile = resolvedLog.resolvedTarget;
|
|
78
86
|
res.header('Content-Type', 'text/plain; charset=utf-8');
|
|
79
87
|
res.download(logFile, (err) => {
|
|
80
88
|
if (err) {
|
package/api/index.js
CHANGED
|
@@ -39,8 +39,11 @@ function init(_server, _runtime) {
|
|
|
39
39
|
return new Promise(function (resolve, reject) {
|
|
40
40
|
if (runtime.settings.disableServer !== false) {
|
|
41
41
|
apiApp = express();
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
if (runtime.settings.logApiLevel !== 'none') {
|
|
44
|
+
apiApp.use(morgan(['combined', 'common', 'dev', 'short', 'tiny'].
|
|
45
|
+
includes(runtime.settings.logApiLevel) ? runtime.settings.logApiLevel : 'combined'));
|
|
46
|
+
}
|
|
44
47
|
|
|
45
48
|
var maxApiRequestSize = runtime.settings.apiMaxLength || '100mb';
|
|
46
49
|
apiApp.use(bodyParser.json({limit:maxApiRequestSize}));
|
|
@@ -53,7 +56,7 @@ function init(_server, _runtime) {
|
|
|
53
56
|
apiApp.use(usersApi.app());
|
|
54
57
|
alarmsApi.init(runtime, authMiddleware, verifyGroups);
|
|
55
58
|
apiApp.use(alarmsApi.app());
|
|
56
|
-
authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn);
|
|
59
|
+
authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn, runtime.settings.enableRefreshCookieAuth, runtime.settings.refreshTokenExpiresIn);
|
|
57
60
|
apiApp.use(authApi.app());
|
|
58
61
|
pluginsApi.init(runtime, authMiddleware, verifyGroups);
|
|
59
62
|
apiApp.use(pluginsApi.app());
|
|
@@ -76,7 +79,9 @@ function init(_server, _runtime) {
|
|
|
76
79
|
|
|
77
80
|
const limiter = rateLimit({
|
|
78
81
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
79
|
-
max: 100 // limit each IP to 100 requests per windowMs
|
|
82
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
83
|
+
// Keep lightweight health/version checks unthrottled
|
|
84
|
+
skip: (req) => req.path === '/api/version'
|
|
80
85
|
});
|
|
81
86
|
|
|
82
87
|
// apply to all requests
|
|
@@ -141,18 +146,31 @@ function init(_server, _runtime) {
|
|
|
141
146
|
if (!req.body.secretCode && runtime.settings.secretCode) {
|
|
142
147
|
req.body.secretCode = runtime.settings.secretCode;
|
|
143
148
|
}
|
|
149
|
+
if (req.body.secureEnabled && !req.body.secretCode) {
|
|
150
|
+
req.body.secretCode = utils.generateSecretCode();
|
|
151
|
+
runtime.logger.warn('Generated random JWT secret because secureEnabled=true and no secretCode was provided.');
|
|
152
|
+
}
|
|
144
153
|
const prevAuth = {
|
|
145
154
|
secureEnabled: runtime.settings.secureEnabled,
|
|
146
155
|
tokenExpiresIn: runtime.settings.tokenExpiresIn,
|
|
156
|
+
enableRefreshCookieAuth: runtime.settings.enableRefreshCookieAuth,
|
|
157
|
+
refreshTokenExpiresIn: runtime.settings.refreshTokenExpiresIn,
|
|
147
158
|
secretCode: runtime.settings.secretCode
|
|
148
159
|
};
|
|
160
|
+
if (req.body.nodeRedEnabled === true &&
|
|
161
|
+
utils.isNullOrUndefined(req.body.nodeRedAuthMode) &&
|
|
162
|
+
runtime.settings.nodeRedEnabled === false) {
|
|
163
|
+
req.body.nodeRedAuthMode = 'secure';
|
|
164
|
+
}
|
|
149
165
|
fs.writeFileSync(runtime.settings.userSettingsFile, JSON.stringify(req.body, null, 4));
|
|
150
166
|
mergeUserSettings(req.body);
|
|
151
167
|
if (prevAuth.secureEnabled !== runtime.settings.secureEnabled ||
|
|
152
168
|
prevAuth.tokenExpiresIn !== runtime.settings.tokenExpiresIn ||
|
|
169
|
+
prevAuth.enableRefreshCookieAuth !== runtime.settings.enableRefreshCookieAuth ||
|
|
170
|
+
prevAuth.refreshTokenExpiresIn !== runtime.settings.refreshTokenExpiresIn ||
|
|
153
171
|
prevAuth.secretCode !== runtime.settings.secretCode) {
|
|
154
172
|
authJwt.init(runtime.settings.secureEnabled, runtime.settings.secretCode, runtime.settings.tokenExpiresIn);
|
|
155
|
-
authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn);
|
|
173
|
+
authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn, runtime.settings.enableRefreshCookieAuth, runtime.settings.refreshTokenExpiresIn);
|
|
156
174
|
}
|
|
157
175
|
runtime.restart(true).then(function(result) {
|
|
158
176
|
res.end();
|
|
@@ -168,11 +186,11 @@ function init(_server, _runtime) {
|
|
|
168
186
|
* GET Heartbeat to check token
|
|
169
187
|
*/
|
|
170
188
|
apiApp.post('/api/heartbeat', authMiddleware, function (req, res) {
|
|
189
|
+
|
|
171
190
|
if (!runtime.settings.secureEnabled) {
|
|
172
|
-
res.end();
|
|
173
|
-
} else if (res.statusCode === 403) {
|
|
174
|
-
runtime.logger.error("api post heartbeat: Tocken Expired");
|
|
191
|
+
return res.end();
|
|
175
192
|
}
|
|
193
|
+
|
|
176
194
|
if (req.body.params) {
|
|
177
195
|
|
|
178
196
|
if (!req.isAuthenticated) {
|
|
@@ -196,6 +214,7 @@ function init(_server, _runtime) {
|
|
|
196
214
|
token: authJwt.getGuestToken()
|
|
197
215
|
});
|
|
198
216
|
}
|
|
217
|
+
|
|
199
218
|
return res.end();
|
|
200
219
|
});
|
|
201
220
|
|
|
@@ -215,12 +234,26 @@ function mergeUserSettings(settings) {
|
|
|
215
234
|
runtime.settings.logFull = settings.logFull;
|
|
216
235
|
runtime.settings.userRole = settings.userRole;
|
|
217
236
|
runtime.settings.nodeRedEnabled = settings.nodeRedEnabled;
|
|
237
|
+
if (!utils.isNullOrUndefined(settings.nodeRedAuthMode)) {
|
|
238
|
+
runtime.settings.nodeRedAuthMode = settings.nodeRedAuthMode;
|
|
239
|
+
}
|
|
240
|
+
if (!utils.isNullOrUndefined(settings.enableRefreshCookieAuth)) {
|
|
241
|
+
runtime.settings.enableRefreshCookieAuth = settings.enableRefreshCookieAuth;
|
|
242
|
+
}
|
|
243
|
+
if (!utils.isNullOrUndefined(settings.refreshTokenExpiresIn)) {
|
|
244
|
+
runtime.settings.refreshTokenExpiresIn = settings.refreshTokenExpiresIn;
|
|
245
|
+
}
|
|
246
|
+
if (!utils.isNullOrUndefined(settings.nodeRedUnsafeModules)) {
|
|
247
|
+
runtime.settings.nodeRedUnsafeModules = settings.nodeRedUnsafeModules;
|
|
248
|
+
}
|
|
218
249
|
runtime.settings.swaggerEnabled = settings.swaggerEnabled;
|
|
219
250
|
if (settings.secretCode) {
|
|
220
251
|
runtime.settings.secretCode = settings.secretCode;
|
|
221
252
|
}
|
|
222
253
|
if (settings.secureEnabled) {
|
|
223
254
|
runtime.settings.tokenExpiresIn = settings.tokenExpiresIn;
|
|
255
|
+
runtime.settings.enableRefreshCookieAuth = settings.enableRefreshCookieAuth;
|
|
256
|
+
runtime.settings.refreshTokenExpiresIn = settings.refreshTokenExpiresIn;
|
|
224
257
|
}
|
|
225
258
|
if (settings.smtp) {
|
|
226
259
|
runtime.settings.smtp = settings.smtp;
|
package/api/jwt-helper.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const jwt = require('jsonwebtoken');
|
|
4
|
+
const utils = require('../runtime/utils');
|
|
4
5
|
|
|
5
6
|
var secureEnabled = false;
|
|
6
|
-
|
|
7
|
+
// Runtime fallback secret used only when no persistent secret is configured.
|
|
8
|
+
var secretCode = utils.generateSecretCode();
|
|
7
9
|
var tokenExpiresIn = 60 * 60; // 60 minutes
|
|
8
10
|
const adminGroups = [-1, 255];
|
|
9
11
|
|
|
@@ -132,6 +134,16 @@ function getGuestToken() {
|
|
|
132
134
|
return token;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
function isGuestUser(userId, userGroups) {
|
|
138
|
+
if (userId === 'guest') {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(userGroups) && userGroups.includes('guest')) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
function haveAdminPermission(permission) {
|
|
136
148
|
if (permission === null || permission === undefined) {
|
|
137
149
|
return false;
|
|
@@ -156,5 +168,6 @@ module.exports = {
|
|
|
156
168
|
get secretCode() { return secretCode },
|
|
157
169
|
get tokenExpiresIn() { return tokenExpiresIn },
|
|
158
170
|
haveAdminPermission: haveAdminPermission,
|
|
159
|
-
adminGroups: adminGroups
|
|
171
|
+
adminGroups: adminGroups,
|
|
172
|
+
isGuestUser: isGuestUser
|
|
160
173
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function normalizeRelativePath(input) {
|
|
4
|
+
if (typeof input !== 'string' || !input) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (input.indexOf('\0') !== -1) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (path.isAbsolute(input)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const normalized = path.normalize(input).replace(/^[\\/]+/, '');
|
|
14
|
+
if (!normalized || normalized === '.' || normalized === path.sep) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const parts = normalized.split(path.sep);
|
|
18
|
+
if (parts.includes('..')) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveWithin(baseDir, targetPath) {
|
|
25
|
+
const normalized = normalizeRelativePath(targetPath);
|
|
26
|
+
if (!normalized) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const resolvedBase = path.resolve(baseDir);
|
|
30
|
+
const resolvedTarget = path.resolve(resolvedBase, normalized);
|
|
31
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
32
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return { resolvedTarget, normalized };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
normalizeRelativePath,
|
|
40
|
+
resolveWithin
|
|
41
|
+
};
|
package/api/projects/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const authJwt = require('../jwt-helper');
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
+
const { normalizeRelativePath, resolveWithin } = require('../path-helper');
|
|
10
11
|
|
|
11
12
|
var runtime;
|
|
12
13
|
var secureFnc;
|
|
@@ -207,41 +208,53 @@ module.exports = {
|
|
|
207
208
|
let encoding = {};
|
|
208
209
|
// let basedata = file.data.replace(/^data:.*,/, '');
|
|
209
210
|
// let basedata = file.data.replace(/^data:image\/png;base64,/, "");
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
const rawFileName = typeof file.name === 'string' ? file.name : '';
|
|
212
|
+
const safeFileName = normalizeRelativePath(rawFileName);
|
|
213
|
+
const safeFullPath = normalizeRelativePath(file.fullPath || rawFileName);
|
|
214
|
+
const relativePath = safeFullPath || safeFileName;
|
|
215
|
+
if (!relativePath) {
|
|
216
|
+
res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
214
219
|
|
|
215
220
|
if (file.type !== 'svg') {
|
|
216
221
|
basedata = file.data.replace(/^data:.*,/, '');
|
|
217
222
|
encoding = {encoding: 'base64'};
|
|
218
223
|
}
|
|
219
|
-
|
|
224
|
+
const resolvedUpload = resolveWithin(runtime.settings.uploadFileDir, relativePath);
|
|
225
|
+
if (!resolvedUpload) {
|
|
226
|
+
res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let filePath = resolvedUpload.resolvedTarget;
|
|
220
230
|
if (destination) {
|
|
221
231
|
const baseDir = process.versions.electron
|
|
222
232
|
? (process.env.userDir || path.join(os.homedir(), '.fuxa'))
|
|
223
233
|
: runtime.settings.appDir;
|
|
224
|
-
const normalizedDestination =
|
|
225
|
-
|
|
226
|
-
const hasTraversal = destinationParts.includes('..');
|
|
227
|
-
if (!normalizedDestination || hasTraversal || path.isAbsolute(destination)) {
|
|
234
|
+
const normalizedDestination = normalizeRelativePath(destination);
|
|
235
|
+
if (!normalizedDestination) {
|
|
228
236
|
res.status(400).json({error:"invalid_destination", message: "Invalid destination path."});
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (destinationDir !== resolvedBase && !destinationDir.startsWith(resolvedBase + path.sep)) {
|
|
239
|
+
const resolvedDestination = resolveWithin(baseDir, `_${normalizedDestination}`);
|
|
240
|
+
if (!resolvedDestination) {
|
|
234
241
|
res.status(400).json({error:"invalid_destination", message: "Invalid destination path."});
|
|
235
242
|
return;
|
|
236
243
|
}
|
|
237
|
-
|
|
244
|
+
const destinationDir = resolvedDestination.resolvedTarget;
|
|
245
|
+
const resolvedFile = resolveWithin(destinationDir, relativePath);
|
|
246
|
+
if (!resolvedFile) {
|
|
247
|
+
res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
filePath = resolvedFile.resolvedTarget;
|
|
238
251
|
const dir = path.dirname(filePath);
|
|
239
252
|
if (!fs.existsSync(dir)) {
|
|
240
253
|
fs.mkdirSync(dir, { recursive: true });
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
256
|
fs.writeFileSync(filePath, basedata, encoding);
|
|
244
|
-
let result = {'location': '/' + runtime.settings.httpUploadFileStatic + '/' +
|
|
257
|
+
let result = {'location': '/' + runtime.settings.httpUploadFileStatic + '/' + relativePath };
|
|
245
258
|
res.json(result);
|
|
246
259
|
} catch (err) {
|
|
247
260
|
if (err && err.code) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const authJwt = require('../api/jwt-helper');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { resolveWithin } = require('../api/path-helper');
|
|
5
6
|
import express, { Request, Response } from 'express';
|
|
6
7
|
|
|
7
8
|
export class ReportsApiService {
|
|
@@ -121,7 +122,16 @@ export class ReportsApiService {
|
|
|
121
122
|
if (!fs.existsSync(reportPath)) {
|
|
122
123
|
reportPath = path.join(process.cwd(), this.runtime.settings.reportsDir, req.params);
|
|
123
124
|
}
|
|
124
|
-
const
|
|
125
|
+
const resolvedFile = resolveWithin(reportPath, req.body.params?.fileName);
|
|
126
|
+
if (!resolvedFile) {
|
|
127
|
+
res.status(400).json({ error: "invalid_path", message: "Invalid report file." });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const filePath = resolvedFile.resolvedTarget;
|
|
131
|
+
if (!fs.existsSync(filePath)) {
|
|
132
|
+
res.status(404).json({ error: "not_found", message: "Report file not found!" });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
125
135
|
fs.unlinkSync(filePath);
|
|
126
136
|
this.runtime.logger.info(`Report file '${filePath}' deleted!`, true);
|
|
127
137
|
res.end();
|
|
@@ -170,4 +180,4 @@ export interface ReportFile {
|
|
|
170
180
|
fileName: string;
|
|
171
181
|
reportName: string;
|
|
172
182
|
created: Date;
|
|
173
|
-
}
|
|
183
|
+
}
|
package/api/resources/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const authJwt = require('../jwt-helper');
|
|
|
9
9
|
const Report = require('../../runtime/jobs/report');
|
|
10
10
|
const fontkit = require('fontkit');
|
|
11
11
|
const os = require('os');
|
|
12
|
+
const { resolveWithin } = require('../path-helper');
|
|
12
13
|
|
|
13
14
|
var runtime;
|
|
14
15
|
var secureFnc;
|
|
@@ -116,8 +117,12 @@ module.exports = {
|
|
|
116
117
|
runtime.logger.error("api post remove resource: Unauthorized");
|
|
117
118
|
} else {
|
|
118
119
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const resolvedFile = resolveWithin(runtime.settings.resourcesFileDir, req.body.file);
|
|
121
|
+
if (!resolvedFile) {
|
|
122
|
+
res.status(400).json({ error: "invalid_path", message: "Invalid resource path." });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const filePath = resolvedFile.resolvedTarget;
|
|
121
126
|
if (fs.existsSync(filePath)) {
|
|
122
127
|
fs.unlinkSync(filePath);
|
|
123
128
|
}
|
|
@@ -270,10 +275,13 @@ module.exports = {
|
|
|
270
275
|
var group = { name: resourcesDirs[i], items: [] };
|
|
271
276
|
var dirPath = path.resolve(runtime.settings.widgetsFileDir, resourcesDirs[i]);
|
|
272
277
|
var wwwSubDir = path.join('_widgets', resourcesDirs[i]);
|
|
273
|
-
var files =
|
|
278
|
+
var files = getFilesRecursive(dirPath, ['.svg']);
|
|
274
279
|
for (var x = 0; x < files.length; x++) {
|
|
275
280
|
var filename = files[x];
|
|
276
|
-
group.items.push({
|
|
281
|
+
group.items.push({
|
|
282
|
+
path: path.join(wwwSubDir, files[x]).split(path.sep).join(path.posix.sep),
|
|
283
|
+
name: filename
|
|
284
|
+
});
|
|
277
285
|
}
|
|
278
286
|
result.groups.push(group);
|
|
279
287
|
}
|
|
@@ -302,7 +310,6 @@ module.exports = {
|
|
|
302
310
|
}
|
|
303
311
|
try {
|
|
304
312
|
let relPath = req.body?.path;
|
|
305
|
-
relPath = relPath.replace(new RegExp('\\.\\.\/', 'g'), '');
|
|
306
313
|
if (!relPath || typeof relPath !== 'string') {
|
|
307
314
|
return res.status(400).json({ error: "invalid_path", message: "Missing or invalid widget path." });
|
|
308
315
|
}
|
|
@@ -310,12 +317,12 @@ module.exports = {
|
|
|
310
317
|
if (process.versions.electron) {
|
|
311
318
|
basePath = process.env.userDir || path.join(os.homedir(), '.fuxa');
|
|
312
319
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
runtime.logger.error("api resources/widgets: security_violation " + fullPath);
|
|
320
|
+
const resolvedWidget = resolveWithin(basePath, relPath);
|
|
321
|
+
if (!resolvedWidget) {
|
|
322
|
+
runtime.logger.error("api resources/widgets: security_violation " + relPath);
|
|
317
323
|
return res.status(403).json({ error: 'security_violation', message: 'Invalid path' });
|
|
318
324
|
}
|
|
325
|
+
const fullPath = resolvedWidget.resolvedTarget;
|
|
319
326
|
|
|
320
327
|
if (!fs.existsSync(fullPath)) {
|
|
321
328
|
return res.status(404).json({ error: "not_found", message: "Widget file not found." });
|
|
@@ -354,3 +361,17 @@ function getFiles(pathDir, extensions) {
|
|
|
354
361
|
.filter((item) => extensions.indexOf(path.extname(item).toLowerCase()) !== -1);
|
|
355
362
|
return filesInDIrectory;
|
|
356
363
|
}
|
|
364
|
+
|
|
365
|
+
function getFilesRecursive(pathDir, extensions, baseDir = pathDir) {
|
|
366
|
+
const entries = fs.readdirSync(pathDir, { withFileTypes: true });
|
|
367
|
+
const files = [];
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
const entryPath = path.join(pathDir, entry.name);
|
|
370
|
+
if (entry.isDirectory()) {
|
|
371
|
+
files.push(...getFilesRecursive(entryPath, extensions, baseDir));
|
|
372
|
+
} else if (extensions.indexOf(path.extname(entry.name).toLowerCase()) !== -1) {
|
|
373
|
+
files.push(path.relative(baseDir, entryPath));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return files;
|
|
377
|
+
}
|
package/api/scheduler/index.js
CHANGED
|
@@ -51,6 +51,16 @@ module.exports = {
|
|
|
51
51
|
|
|
52
52
|
// POST scheduler data
|
|
53
53
|
schedulerApp.post("/api/scheduler", secureFnc, function(req, res) {
|
|
54
|
+
if (res.statusCode === 403) {
|
|
55
|
+
runtime.logger.error("api post scheduler: Tocken Expired");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const isGuest = authJwt.isGuestUser(req.userId, req.userGroups);
|
|
59
|
+
if (runtime.settings?.secureEnabled && isGuest) {
|
|
60
|
+
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
|
|
61
|
+
runtime.logger.error("api post scheduler: Unauthorized guest");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
54
64
|
try {
|
|
55
65
|
if (req.body && req.body.id && req.body.data !== undefined) {
|
|
56
66
|
var schedulerId = req.body.id;
|
|
@@ -90,6 +100,16 @@ module.exports = {
|
|
|
90
100
|
|
|
91
101
|
// DELETE scheduler data
|
|
92
102
|
schedulerApp.delete("/api/scheduler", secureFnc, function(req, res) {
|
|
103
|
+
if (res.statusCode === 403) {
|
|
104
|
+
runtime.logger.error("api delete scheduler: Tocken Expired");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const isGuest = authJwt.isGuestUser(req.userId, req.userGroups);
|
|
108
|
+
if (runtime.settings?.secureEnabled && isGuest) {
|
|
109
|
+
res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
|
|
110
|
+
runtime.logger.error("api delete scheduler: Unauthorized guest");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
93
113
|
try {
|
|
94
114
|
if (req.query && req.query.id) {
|
|
95
115
|
var schedulerId = req.query.id;
|
|
@@ -181,4 +201,4 @@ function validateSchedulerData(data) {
|
|
|
181
201
|
}
|
|
182
202
|
|
|
183
203
|
return { valid: true };
|
|
184
|
-
}
|
|
204
|
+
}
|