Skip to content

Commit a0cb967

Browse files
Implement security features and enhance UI interactions
- Added a new security management system, allowing for admin password setup and UI locking mechanisms. - Introduced a security tab in the settings for configuring lock UI styles and managing admin access. - Updated the controller script to handle security status checks and UI updates based on security state. - Enhanced the message list and upload log widgets to support additional metadata, including descriptions. - Improved CSS styles for the controller and lock icons to enhance visual feedback and user interaction. - Updated server routes to include security management, ensuring proper access control for configuration changes.
1 parent 4afd102 commit a0cb967

15 files changed

Lines changed: 2124 additions & 59 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,5 @@ uploads/
277277
*.pem
278278
ssl/
279279
scripts/.DS_Store
280+
281+
.plan/

defaultConfig.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
"sslCertFile": "/ssl/cert.pem"
2121
},
2222
"mdns": { "enabled": false },
23-
"sspd": { "enabled": false },
24-
"services": {}
23+
"sspd": { "enabled": false }
2524
},
2625
"services": {
2726
"protocol": "http://",
@@ -42,6 +41,12 @@
4241
"sendLogMessages": false
4342
}
4443
},
44+
"security": {
45+
"enabled": false,
46+
"lockUiStyle": "LOCKED_VISIBLE",
47+
"adminPasswordSalt": "",
48+
"adminPasswordHash": ""
49+
},
4550
"dashboard": { "startPage": "index.html" },
4651
"log": {
4752
"app": {

pages/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454

5555
<link id="cssref_intellibrite" rel="stylesheet" type="text/css" href="themes/intellibrite.css" />
5656
<link id="cssref_widgets" rel="stylesheet" type="text/css" href="themes/widgets.css" />
57+
<link id="cssref_controller" rel="stylesheet" type="text/css" href="themes/controller.css" />
5758
<link id="cssref_config" rel="stylesheet" type="text/css" href="themes/configPage.css" />
5859
<link id="cssref_dash" rel="stylesheet" type="text/css" href="themes/dashboard.css" />
5960
<script type="text/javascript">

pages/messageManager.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<link rel="stylesheet" type="text/css" href="jquery-ui/jquery-ui.theme.css" />
2828
<link rel="stylesheet" type="text/css" href="font-awesome/css/all.css" />
2929
<link rel="stylesheet" type="text/css" href="themes/widgets.css" />
30+
<link rel="stylesheet" type="text/css" href="themes/controller.css" />
3031
<link rel="stylesheet" type="text/css" href="themes/vlist.css" />
3132
<link rel="stylesheet" type="text/css" href="themes/messageManager.css" />
3233
<!-- Blank stylesheet for message responses -->

scripts/controller.js

Lines changed: 357 additions & 24 deletions
Large diffs are not rendered by default.

scripts/messages/messageList/messageList.widget.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(function ($) {
22
$.widget("pic.messageList", {
33
options: {
4-
receivingMessages: false, pinScrolling: false, changesOnly: false, messageKeys: {}, contexts: {}, messages: {}, portFilters: [], filters: [], rowIds: [], ports: [], expandedRows: {}, loadedFilename: null
4+
receivingMessages: false, pinScrolling: false, changesOnly: false, messageKeys: {}, contexts: {}, messages: {}, portFilters: [], filters: [], rowIds: [], ports: [], expandedRows: {}, loadedFilename: null, loadedDescription: null
55
},
66
_create: function () {
77
var self = this, o = self.options, el = self.element;
@@ -1496,7 +1496,11 @@
14961496
var self = this, o = self.options, el = self.element;
14971497
var titleSpan = el.find('div.picMessageListTitle:first > span');
14981498
if (o.loadedFilename) {
1499-
titleSpan.text('Messages - Loaded from ' + o.loadedFilename);
1499+
var title = 'Messages - Loaded from ' + o.loadedFilename;
1500+
if (o.loadedDescription) {
1501+
title += ' - ' + o.loadedDescription;
1502+
}
1503+
titleSpan.text(title);
15001504
}
15011505
else {
15021506
titleSpan.text('Messages');
@@ -1509,8 +1513,9 @@
15091513
if (o.receivingMessages) {
15101514
el.find('div.picStartLogs > i').removeClass('far').addClass('fas');
15111515
el.find('div.picStartLogs').addClass('selected');
1512-
// Clear the loaded filename when starting live logging
1516+
// Clear the loaded filename and description when starting live logging
15131517
o.loadedFilename = null;
1518+
o.loadedDescription = null;
15141519
self._updateTitle();
15151520
}
15161521
else {

scripts/messages/messageList/uploadLog.widget.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
$('<hr></hr>').appendTo(line);
1515
line = $('<div></div>').appendTo(div);
1616
var fset = $('<fieldset></fieldset>').appendTo(line);
17-
$('<legend></legend>').appendTo(fset).text('Options');
17+
$('<legend></legend>').appendTo(fset).text('Description');
1818
line = $('<div></div>').appendTo(fset);
19+
$('<div></div>').appendTo(line).inputField({ binding: 'description', labelText: '', inputAttrs: { style: { width: '100%' } } });
1920
//$('<div></div>').appendTo(line).pickList({
2021
// displayColumn:0,
2122
// labelText: 'Playback To', binding: 'playbackTo',
@@ -61,10 +62,11 @@
6162
}
6263
else {
6364
msgList.clear();
64-
// Store the filename and update the title
65+
// Store the filename, description and update the title
6566
var msgListWidget = $('div.picMessages:first').data('pic-messageList');
6667
if (msgListWidget && filename) {
6768
msgListWidget.options.loadedFilename = filename;
69+
msgListWidget.options.loadedDescription = opts.description || '';
6870
msgListWidget._updateTitle();
6971
}
7072
self._processNextMessage(msgList, progress[0], data);

scripts/widgets.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,21 @@ function makeBool(val) {
264264
}
265265
return false;
266266
}
267+
function _redactForLog(url, data) {
268+
try {
269+
var u = (url || '').toString().toLowerCase();
270+
// Redact any security-related payloads (passwords, pins, etc.)
271+
if (u.indexOf('/security/') !== -1) {
272+
return { redacted: true };
273+
}
274+
// Redact common sensitive keys if present
275+
var s = (typeof data === 'string') ? data : JSON.stringify(data);
276+
if (typeof s === 'string' && s.match(/password|pin|secret|token/i)) {
277+
return { redacted: true };
278+
}
279+
} catch (e) { }
280+
return data;
281+
}
267282
// PUT and Delete for ReST calls.
268283
jQuery.each(["put", "delete"], function (i, method) {
269284
jQuery[method] = function (url, data, callback, type) {
@@ -343,13 +358,15 @@ jQuery.each(['get', 'put', 'delete', 'post'], function (i, method) {
343358
successCallback = $.mergeCallbacks(successCallback, cbShowSuccess);
344359
errorCallback = $.mergeCallbacks(errorCallback, cbShowError);
345360
completeCallback = $.mergeCallbacks(completeCallback, cbComplete);
346-
console.log({ method: method, url: url, data: typeof data === 'string' ? data : JSON.stringify(data) });
361+
console.log({ method: method, url: url, data: _redactForLog(url, (typeof data === 'string' ? data : JSON.stringify(data))) });
362+
// Treat null like undefined (prevents "?null" from being appended on GET requests)
363+
if (data === null) data = undefined;
347364
return jQuery.ajax({
348365
url: serviceUrl,
349366
type: method,
350367
dataType: 'json',
351368
contentType: 'application/json; charset=utf-8',
352-
data: typeof data === 'string' ? data : JSON.stringify(data),
369+
data: typeof data === 'undefined' ? undefined : (typeof data === 'string' ? data : JSON.stringify(data)),
353370
error: errorCallback,
354371
success: successCallback,
355372
complete: completeCallback
@@ -421,13 +438,15 @@ jQuery.each(['get', 'put', 'delete', 'post'], function (i, method) {
421438
successCallback = $.mergeCallbacks(successCallback, cbShowSuccess);
422439
errorCallback = $.mergeCallbacks(errorCallback, cbShowError);
423440
completeCallback = $.mergeCallbacks(completeCallback, cbComplete);
424-
console.log({ method: method, url: url, data: typeof data === 'string' ? data : JSON.stringify(data) });
441+
console.log({ method: method, url: url, data: _redactForLog(url, (typeof data === 'string' ? data : JSON.stringify(data))) });
442+
// Treat null like undefined (prevents "?null" from being appended on GET requests)
443+
if (data === null) data = undefined;
425444
return jQuery.ajax({
426445
url: serviceUrl,
427446
type: method,
428447
dataType: 'json',
429448
contentType: 'application/json; charset=utf-8',
430-
data: typeof data === 'string' ? data : JSON.stringify(data),
449+
data: typeof data === 'undefined' ? undefined : (typeof data === 'string' ? data : JSON.stringify(data)),
431450
error: errorCallback,
432451
success: successCallback,
433452
complete: completeCallback
@@ -482,14 +501,16 @@ jQuery.each(['get', 'put', 'delete', 'post'], function (i, method) {
482501
successCallback = $.mergeCallbacks(successCallback, cbShowSuccess);
483502
errorCallback = $.mergeCallbacks(errorCallback, cbShowError);
484503
completeCallback = $.mergeCallbacks(completeCallback, cbComplete);
485-
console.log({ method: method, url: url, data: typeof data === 'string' ? data : JSON.stringify(data) });
504+
console.log({ method: method, url: url, data: _redactForLog(url, (typeof data === 'string' ? data : JSON.stringify(data))) });
505+
// Treat null like undefined (prevents "?null" from being appended on GET requests)
506+
if (data === null) data = undefined;
486507
return jQuery.ajax({
487508
url: serviceUrl,
488509
type: method,
489510
dataType: 'binary',
490511
processData: false,
491512
contentType: 'application/json; charset=utf-8',
492-
data: typeof data === 'string' ? data : JSON.stringify(data),
513+
data: typeof data === 'undefined' ? undefined : (typeof data === 'string' ? data : JSON.stringify(data)),
493514
cache: false,
494515
xhrFields: { responseType: 'blob' },
495516
error: errorCallback,
@@ -4333,6 +4354,24 @@ $.pic.modalDialog.closeDialog = function (el) {
43334354
return dlg;
43344355
};
43354356
$.pic.modalDialog.createApiError = function (err, options) {
4357+
try {
4358+
// For guest-facing security flows, don't show stack traces.
4359+
// Example: /security/unlock invalid password should show a simple message.
4360+
if (err && err.httpCode === 401 && err.error && typeof err.error.message === 'string' &&
4361+
err.error.message.toLowerCase().indexOf('invalid admin password') !== -1) {
4362+
return $.pic.modalDialog.createConfirm('dlgIncorrectPassword', {
4363+
autoOpen: false,
4364+
height: 'auto',
4365+
width: '22rem',
4366+
modal: true,
4367+
title: 'Incorrect Password',
4368+
message: '<div class="info-message">Incorrect password.</div>',
4369+
buttons: [
4370+
{ text: 'Close', icon: '<i class="far fa-window-close"></i>', click: function () { $.pic.modalDialog.closeDialog(this); } }
4371+
]
4372+
});
4373+
}
4374+
} catch (e) { }
43364375
var opt = typeof options !== 'undefined' && options !== null ? options : {
43374376
autoOpen: false,
43384377
height: 'auto',

server/Server.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { UploadRoute, BackgroundUpload } from "./upload/upload";
1515
import { setTimeout } from "timers";
1616
import { ConfigRoute } from "./api/Config";
1717
import { MessagesRoute } from "./api/Messages";
18+
import { SecurityRoute } from "./api/Security";
1819
import { Timestamp, utils } from "./Constants";
1920
import { RelayRoute, njsPCRelay } from "./relay/relayRoute";
2021
import { Namespace, RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
@@ -155,10 +156,16 @@ export class HttpServer extends ProtoServer {
155156
this.app.use('/jquery-ui', express.static(path.join(process.cwd(), '/node_modules/jquery-ui-dist/'), { maxAge: '60d' }));
156157
this.app.use('/jquery-ui-touch-punch', express.static(path.join(process.cwd(), '/node_modules/jquery-ui-touch-punch-c/'), { maxAge: '60d' }));
157158
this.app.use('/font-awesome', express.static(path.join(process.cwd(), '/node_modules/@fortawesome/fontawesome-free/'), { maxAge: '60d' }));
158-
this.app.use('/scripts', express.static(path.join(process.cwd(), '/scripts/'), { maxAge: '1d' }));
159+
// Do not aggressively cache app scripts; we ship updates by replacing files in-place.
160+
this.app.use('/scripts', (req, res, next) => {
161+
res.setHeader('Cache-Control', 'no-store');
162+
next();
163+
});
164+
this.app.use('/scripts', express.static(path.join(process.cwd(), '/scripts/'), { maxAge: 0 }));
159165
this.app.use('/themes', express.static(path.join(process.cwd(), '/themes/'), { maxAge: '1d' }));
160166
this.app.use('/icons', express.static(path.join(process.cwd(), '/themes/icons'), { maxAge: '1d' }));
161167
RelayRoute.initRoutes(this.app);
168+
SecurityRoute.initRoutes(this.app);
162169
ConfigRoute.initRoutes(this.app);
163170
MessagesRoute.initRoutes(this.app);
164171
UploadRoute.initRoutes(this.app);
@@ -304,6 +311,7 @@ export class HttpsServer extends HttpServer {
304311
return value;
305312
});
306313

314+
SecurityRoute.initRoutes(this.app);
307315
ConfigRoute.initRoutes(this.app);
308316
// StateRoute.initRoutes(this.app);
309317
// UtilitiesRoute.initRoutes(this.app);
@@ -317,10 +325,16 @@ export class HttpsServer extends HttpServer {
317325
this.app.use('/jquery-ui', express.static(path.join(process.cwd(), '/node_modules/jquery-ui-dist/'), { maxAge: '60d' }));
318326
this.app.use('/jquery-ui-touch-punch', express.static(path.join(process.cwd(), '/node_modules/jquery-ui-touch-punch-c/'), { maxAge: '60d' }));
319327
this.app.use('/font-awesome', express.static(path.join(process.cwd(), '/node_modules/@fortawesome/fontawesome-free/'), { maxAge: '60d' }));
320-
this.app.use('/scripts', express.static(path.join(process.cwd(), '/scripts/'), { maxAge: '1d' }));
328+
// Do not aggressively cache app scripts; we ship updates by replacing files in-place.
329+
this.app.use('/scripts', (req, res, next) => {
330+
res.setHeader('Cache-Control', 'no-store');
331+
next();
332+
});
333+
this.app.use('/scripts', express.static(path.join(process.cwd(), '/scripts/'), { maxAge: 0 }));
321334
this.app.use('/themes', express.static(path.join(process.cwd(), '/themes/'), { maxAge: '1d' }));
322335
this.app.use('/icons', express.static(path.join(process.cwd(), '/themes/icons'), { maxAge: '1d' }));
323336
RelayRoute.initRoutes(this.app);
337+
SecurityRoute.initRoutes(this.app);
324338
ConfigRoute.initRoutes(this.app);
325339
MessagesRoute.initRoutes(this.app);
326340
UploadRoute.initRoutes(this.app);

server/api/Config.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { config } from "../config/Config";
88
import { logger } from "../logger/Logger";
99
import { versionCheck } from '../config/VersionCheck';
1010
import { njsPCRelay } from "../relay/relayRoute";
11+
import { securityService } from "../security/SecurityService";
1112
export class ConfigRoute {
1213
public static initRoutes(app: express.Application) {
1314
app.get('/config/serviceUri', (req, res, next) => {
@@ -22,6 +23,7 @@ export class ConfigRoute {
2223
});
2324
app.put('/config/serviceUri', async (req, res, next) => {
2425
try {
26+
securityService.requireUnlocked();
2527
let srv = extend(true, {}, config.getSection('web.services'), req.body);
2628
config.setSection('web.services', srv);
2729
njsPCRelay.init();
@@ -97,11 +99,24 @@ export class ConfigRoute {
9799
catch (err) { console.log(err); return res.status(500).send(err); }
98100
});
99101
app.get('/config/:section', (req, res) => { return res.status(200).send(config.getSection(req.params.section)); });
100-
app.put('/config/:section', (req, res) => {
102+
app.put('/config/:section', (req, res, next) => {
101103
try {
102-
config.setSection(req.params.section, req.body);
104+
securityService.requireUnlocked();
105+
// Protect critical nested config sections from accidental clobbering.
106+
// Example: /config/web.services should not allow deleting protocol/ip/port by sending a partial object.
107+
if (req.params.section === 'web.services') {
108+
const merged = extend(true, {}, config.getSection('web.services'), req.body);
109+
config.setSection('web.services', merged);
110+
}
111+
else if (req.params.section === 'security') {
112+
const merged = extend(true, {}, config.getSection('security'), req.body);
113+
config.setSection('security', merged);
114+
}
115+
else {
116+
config.setSection(req.params.section, req.body);
117+
}
103118
}
104-
catch (err) { return res.status(400).send(new Error(err)); }
119+
catch (err) { return next(err); }
105120
return res.status(200).send(config.getSection(req.params.section));
106121
});
107122
app.get('/options', (req, res) => {

0 commit comments

Comments
 (0)