Lots of progress on security user group UI.
Ylian Saint-Hilaire committed
Dec 29, 2019 at 18:10 UTC
fbbc6193647b4997a0cfe2aab63b9fe7b76924c8
9 files changed
+383
-20
meshcentral.js
+2
-1
@@ -123,7 +123,7 @@ function CreateMeshCentralServer(config, args) {
123
try { require('./pass').hash('test', function () { }, 0); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
124
125
// Check for invalid arguments
126
- var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats'];
126
+ var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showusergroups', 'shownodes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats'];
127
for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
128
if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
129
for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
@@ -437,6 +437,7 @@ function CreateMeshCentralServer(config, args) {
437
if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
438
if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(docs); process.exit(); }); return; }
439
if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(docs); process.exit(); }); return; }
440
+ if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(docs); process.exit(); }); return; }
441
if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(docs); process.exit(); }); return; }
442
if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(docs); process.exit(); }); return; }
443
if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(docs); process.exit(); }); return; }
meshmail.js
+8
-5
@@ -139,7 +139,7 @@ module.exports.CreateMeshMail = function (parent) {
139
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, invitation not possible.
140
141
// Set all the options.
142
- var options = { username: username, accountname: accountname, email: email, servername: domain.title, password: password };
142
+ var options = { username: username, accountname: accountname, email: email, servername: domain.title ? domain.title : 'MeshCentral', password: password };
143
144
// Send the email
145
obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) });
@@ -152,7 +152,7 @@ module.exports.CreateMeshMail = function (parent) {
152
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, no reset possible.
153
154
// Set all the options.
155
- var options = { username: username, email: email, servername: domain.title };
155
+ var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
156
options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username.toLowerCase(), e: email, a: 1 }, obj.mailCookieEncryptionKey);
157
158
// Send the email
@@ -166,7 +166,7 @@ module.exports.CreateMeshMail = function (parent) {
166
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, don't validate the email address.
167
168
// Set all the options.
169
- var options = { username: username, email: email, servername: domain.title };
169
+ var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
170
options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }, obj.mailCookieEncryptionKey);
171
172
// Send the email
@@ -180,7 +180,7 @@ module.exports.CreateMeshMail = function (parent) {
180
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, don't validate the email address.
181
182
// Set all the template replacement options and generate the final email text (both in txt and html formats).
183
- var options = { username: username, name: name, email: email, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title };
183
+ var options = { username: username, name: name, email: email, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' };
184
options.windows = ((os == 0) || (os == 1)) ? 1 : 0;
185
options.linux = ((os == 0) || (os == 2)) ? 1 : 0;
186
options.osx = ((os == 0) || (os == 3)) ? 1 : 0;
@@ -220,7 +220,10 @@ module.exports.CreateMeshMail = function (parent) {
220
if (err == null) {
221
console.log('SMTP mail server ' + parent.config.smtp.host + ' working as expected.');
222
} else {
223
- console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err));
223
+ // Remove all non-object types from error to avoid a JSON stringify error.
224
+ var err2 = {};
225
+ for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } }
226
+ console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err2));
227
}
228
});
229
};
meshuser.js
+102
-4
@@ -1472,16 +1472,114 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1472
}
1473
break;
1474
}
1475
+ case 'usergroups':
1476
+ {
1477
+ if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1478
+
1479
+ // Request a list of all user groups this user as rights to
1480
+ db.GetAllTypeNoTypeField('ugrp', domain.id, function (err, docs) {
1481
+ try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: docs, tag: command.tag })); } catch (ex) { }
1482
+ });
1483
+ break;
1484
+ }
1485
case 'createusergroup':
1486
{
1477
- // TODO
1478
- //console.log(command);
1487
+ var err = null;
1488
+ try {
1489
+ // Check if we have new group restriction
1490
+ if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; }
1491
+
1492
+ // In some situations, we need a verified email address to create a device group.
1493
+ else if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != 0xFFFFFFFF)) { err = 'Email verification required'; } // User must verify it's email first.
1494
+
1495
+ // Create user group
1496
+ else if (common.validateString(command.name, 1, 64) == false) { err = 'Invalid group name'; } // User group name is between 1 and 64 characters
1497
+ else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // User group description is between 0 and 1024 characters
1498
+ } catch (ex) { err = 'Validation exception: ' + ex; }
1499
+
1500
+ // Handle any errors
1501
+ if (err != null) {
1502
+ if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
1503
+ break;
1504
+ }
1505
+
1506
+ // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
1507
+ parent.crypto.randomBytes(48, function (err, buf) {
1508
+ // Create new device group identifier
1509
+ var ugrpid = 'ugrp/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1510
+
1511
+ // Create the new device group
1512
+ var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: domain.id, links: {} };
1513
+ db.Set(common.escapeLinksFieldName(ugrp));
1514
+ //parent.meshes[ugrpid] = ugrp;
1515
+
1516
+ // Event the device group creation
1517
+ var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrpid, name: command.name, desc: command.desc, action: 'createusergroup', links: links, msg: 'User group created: ' + command.name, domain: domain.id };
1518
+ parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
1519
+
1520
+ try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: 'ok', ugrpid: ugrpid, links: links })); } catch (ex) { }
1521
+ });
1522
break;
1523
}
1524
case 'deleteusergroup':
1525
{
1483
- // TODO
1484
- //console.log(command);
1526
+ if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1527
+
1528
+ // Change the name or description of a user group
1529
+ if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
1530
+ var ugroupidsplit = command.ugrpid.split('/');
1531
+ if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
1532
+
1533
+ db.Get(command.ugrpid, function (err, groups) {
1534
+ if ((err != null) || (groups.length != 1)) return;
1535
+ var group = groups[0];
1536
+ db.Remove(group._id);
1537
+ var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: domain.id };
1538
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1539
+ parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1540
+ });
1541
+ break;
1542
+ }
1543
+ case 'editusergroup':
1544
+ {
1545
+ if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1546
+
1547
+ // Change the name or description of a user group
1548
+ if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
1549
+ var ugroupidsplit = command.ugrpid.split('/');
1550
+ if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
1551
+
1552
+ db.Get(command.ugrpid, function (err, groups) {
1553
+ if ((err != null) || (groups.length != 1)) return;
1554
+ var group = groups[0], change = '';
1555
+
1556
+ if ((common.validateString(command.name, 1, 64) == true) && (command.name != group.name)) { change = 'User group name changed from "' + group.name + '" to "' + command.name + '"'; group.name = command.name; }
1557
+ if ((common.validateString(command.desc, 0, 1024) == true) && (command.desc != group.desc)) { if (change != '') change += ' and description changed'; else change += 'User group "' + group.name + '" description changed'; group.desc = command.desc; }
1558
+ if (change != '') {
1559
+ db.Set(common.escapeLinksFieldName(group));
1560
+ var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msg: change, domain: domain.id };
1561
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1562
+ parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1563
+ }
1564
+ });
1565
+ break;
1566
+ }
1567
+ case 'addusertousergroup':
1568
+ {
1569
+ if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1570
+
1571
+ // Change the name or description of a user group
1572
+ if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
1573
+ var ugroupidsplit = command.ugrpid.split('/');
1574
+ if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
1575
+
1576
+ db.Get(command.ugrpid, function (err, groups) {
1577
+ if ((err != null) || (groups.length != 1)) return;
1578
+ var group = groups[0];
1579
+
1580
+ // TODO
1581
+ console.log(command);
1582
+ });
1583
break;
1584
}
1585
case 'changemeshnotify':
public/images/images16.png
Binary files a/public/images/images16.png and b/public/images/images16.png differ
public/images/webp/group-256.webp
Binary files /dev/null and b/public/images/webp/group-256.webp differ
public/scripts/common-0.0.1.js
+1
-1
@@ -16,7 +16,7 @@ function QE(x, y) { try { Q(x).disabled = !y; } catch (x) { } }
16
function QV(x, y) { try { QS(x).display = (y ? '' : 'none'); } catch (x) { } } // "Q" visible
17
function QA(x, y) { Q(x).innerHTML += y; } // "Q" append
18
function QH(x, y) { Q(x).innerHTML = y; } // "Q" html
19
-function QC(x) { try { return Q(x).classList; } catch (x) { } } // "Q" class
19
+function QC(x) { try { return Q(x).classList; } catch (x) { } } // "Q" class
20
21
// Move cursor to end of input box
22
function inputBoxFocus(x) { Q(x).focus(); var v = Q(x).value; Q(x).value = ''; Q(x).value = v; }
public/styles/style.css
+8
@@ -1363,6 +1363,14 @@ a {
1363
float: left;
1364
}
1365
1366
+.m4 {
1367
+ background: url(../images/images16.png) -128px 0px;
1368
+ height: 16px;
1369
+ width: 16px;
1370
+ border: none;
1371
+ float: left;
1372
+}
1373
+
1374
.si1 {
1375
background: url(../images/icons16.png) 0px 0px;
1376
height: 16px;
views/default.handlebars
+261
-8
@@ -959,6 +959,29 @@
959
</table>
960
<div id="p50groups"></div>
961
</div>
962
+ <div id=p51 style="display:none">
963
+ <table style="width:100%" cellpadding="0" cellspacing="0">
964
+ <tr>
965
+ <td style=width:auto valign=top>
966
+ <div id="p30title">
967
+ <div style="float:left"><div class="backButton" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"><div class="backButtonEx"></div></div></div>
968
+ <h1>User Group - <span id=p51groupName></span></h1>
969
+ </div>
970
+ <div id=p51group></div>
971
+ </td>
972
+ <td style=width:20px></td>
973
+ <td style=width:200px>
974
+ <picture id=MainUserImage style=border-width:0px;height:200px;width:200px;float:right>
975
+ <source type="image/webp" width=200 height=200 srcset="images/webp/group-256.webp" />
976
+ <img alt="" width=200 height=200 src=images/group-256.png />
977
+ </picture>
978
+ <div style="width:100%;text-align:center"><strong><span id=MainUserState></span></strong></div>
979
+ </td>
980
+ </tr>
981
+ </table>
982
+ <div id=p51group2></div>
983
+ <br />
984
+ </div>
985
<div id=p19 style="display:none">
986
<h1>Plugins - <span id=p19deviceName></span></h1>
987
<div id="p19headers"></div>
@@ -1101,6 +1124,7 @@
1124
var meshes = {};
1125
var meshcount = 0;
1126
var nodes = null;
1127
+ var usergroups = {};
1128
var filetree = {};
1129
var userinfo = null;
1130
var serverinfo = null;
@@ -1272,7 +1296,7 @@
1296
if (pluginHandler != null) pluginHandler.callHook('onWebUIStartupEnd');
1297
1298
// Deleted non-english style and fix all topbar titles
1275
- if (Q('MainDevDesktop').innerHTML != 'Desktop') { QC('body').add('nonenglish'); }
1299
+ if (('{{{lang}}}' != 'en') && ('{{{lang}}}' != '')) { QC('body').add('nonenglish'); }
1300
var elements = document.getElementsByClassName('topbar_td');
1301
for (var i in elements) { if (elements[i].innerHTML) { elements[i].innerHTML = elements[i].innerHTML.split(' ').join(' '); } }
1302
}
@@ -1476,6 +1500,7 @@
1500
} else if (state == 2) {
1501
// Fetch list of meshes, nodes, files
1502
meshserver.send({ action: 'meshes' });
1503
+ meshserver.send({ action: 'usergroups' });
1504
meshserver.send({ action: 'nodes', id: '{{currentNode}}' });
1505
if (pluginHandler != null) { meshserver.send({ action: 'plugins' }); }
1506
if ('{{currentNode}}' == '') { meshserver.send({ action: 'files' }); }
@@ -1566,6 +1591,7 @@
1591
if (updateNaggleFlags & 1024) { deviceEventsUpdate(); }
1592
if (updateNaggleFlags & 2048) { userEventsUpdate(); }
1593
if (updateNaggleFlags & 4096) { p20updateMesh(); }
1594
+ if (updateNaggleFlags & 8192) { updateUserGroups(); }
1595
updateNaggleTimer = null;
1596
updateNaggleFlags = 0;
1597
}, 150);
@@ -1694,6 +1720,12 @@
1720
masterUpdate(4 + 128);
1721
break;
1722
}
1723
+ case 'usergroups': {
1724
+ usergroups = {};
1725
+ for (var i in message.ugroups) { usergroups[message.ugroups[i]._id] = message.ugroups[i]; }
1726
+ masterUpdate(8192);
1727
+ break;
1728
+ }
1729
case 'files': {
1730
filetree = setupBackPointers(message.filetree);
1731
updateFiles();
@@ -2182,6 +2214,27 @@
2214
updateUsers();
2215
break;
2216
}
2217
+ case 'createusergroup':
2218
+ case 'usergroupchange': {
2219
+ // User group changed
2220
+ var ugroup = usergroups[message.event.ugrpid];
2221
+ if (ugroup == null) {
2222
+ // This is a new user group for us
2223
+ usergroups[message.event.ugrpid] = { _id: message.event.ugrpid, name: message.event.name, desc: message.event.desc, domain: message.event.domain, links: message.event.links };
2224
+ } else {
2225
+ // This is an existing user group
2226
+ ugroup.name = message.event.name;
2227
+ ugroup.desc = message.event.desc;
2228
+ ugroup.links = message.event.links;
2229
+ }
2230
+ masterUpdate(8192);
2231
+ break;
2232
+ }
2233
+ case 'deleteusergroup': {
2234
+ // User group removed
2235
+ if (usergroups[message.event.ugrpid] != null) { delete usergroups[message.event.ugrpid]; masterUpdate(8192); }
2236
+ break;
2237
+ }
2238
case 'createmesh': {
2239
// A new mesh was created
2240
if ((meshes[message.event.meshid] == null) && (message.event.links[userinfo._id] != null)) { // Check if this is a mesh create for a mesh we own. If site administrator, we get all messages so need to ignore some.
@@ -3720,6 +3773,7 @@
3773
if (!skipsave) { putstore('sort', sort); }
3774
}
3775
3776
+ function nameSort(a, b) { var aa = a.name.toLowerCase(), bb = b.name.toLowerCase(); if (aa > bb) return 1; if (aa < bb) return -1; return 0; }
3777
function meshSort(a, b) { if (a.meshnamel > b.meshnamel) return 1; if (a.meshnamel < b.meshnamel) return -1; if (a.meshid == b.meshid) { if (showRealNames == true) { if (a.rnamel > b.rnamel) return 1; if (a.rnamel < b.rnamel) return -1; return 0; } else { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; return 0; } } return 0; }
3778
function powerSort(a, b) { var ap = a.pwr?a.pwr:0; var bp = b.pwr?b.pwr:0; if (ap > bp) return -1; if (ap < bp) return 1; if (ap == bp) { if (showRealNames == true) { if (a.rnamel > b.rnamel) return 1; if (a.rnamel < b.rnamel) return -1; return 0; } else { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; return 0; } } return 0; }
3779
function deviceSort(a, b) { if (a.namel > b.namel) return 1; if (a.namel < b.namel) return -1; return 0; }
@@ -8509,6 +8563,7 @@
8563
x += '<table class=p3eventsTable cellpadding=0 cellspacing=0><tr><td colspan=4 class=DevSt>' + dateHeader + '</td></tr>';
8564
}
8565
var icon = 'si3';
8566
+ if (event.etype == 'ugrp') icon = 'm4';
8567
if (event.etype == 'user') icon = 'm2';
8568
if (event.etype == 'server') icon = 'si3';
8569
@@ -8978,7 +9033,33 @@
9033
//
9034
9035
function updateUserGroups() {
9036
+ // Sort the list of group names
9037
+ var sortedGroups = [];
9038
+ for (var i in usergroups) { sortedGroups.push(usergroups[i]); }
9039
+ sortedGroups.sort(nameSort);
9040
9041
+ // Display the groups using the sorted list
9042
+ var x = '<table class=p3usersTable cellpadding=0 cellspacing=0>', addHeader = true;
9043
+ x += '<th>' + "Name" + '<th style=width:80px>' + "Users";
9044
+ for (var i in sortedGroups) { x += addUserGroupHtml(sortedGroups[i]); }
9045
+ x += '</table>';
9046
+
9047
+ if (sortedGroups.length == 0) { x += '<br />' + "No groups found." + '<br />'; }
9048
+ QH('p50groups', x);
9049
+
9050
+ // Update current user panel if needed
9051
+ if ((currentUserGroup != null) && (xxcurrentView == 51)) { gotoUserGroup(encodeURIComponent(currentUserGroup._id), true); }
9052
+ }
9053
+
9054
+ function addUserGroupHtml(group) {
9055
+ var usercount = 0;
9056
+ if (group.links) { for (var i in group.links) { usercount++; } }
9057
+ var x = '<tr tabindex=0 onmouseover=userMouseHover(this,1) onmouseout=userMouseHover(this,0) onkeypress="if (event.key==\'Enter\') gotoUserGroup(\'' + encodeURIComponent(group._id) + '\')"><td style=cursor:pointer onclick=gotoUserGroup(\"' + encodeURIComponent(group._id) + '\")>';
9058
+ x += '<div class=bar style=width:100%>';
9059
+ x += '<div class=baricon><div class="m4"></div></div>';
9060
+ x += '<div class=g1></div><div class=g2></div>';
9061
+ x += '<div><span style=font-size:16px>' + group.name + '</span></div></div><td style=text-align:center>' + usercount;
9062
+ return x;
9063
}
9064
9065
function showCreateUserGroupDialog() {
@@ -8999,6 +9080,178 @@
9080
meshserver.send({ action: 'createusergroup', name: Q('p4name').value, desc: Q('p4desc').value });
9081
}
9082
9083
+ //
9084
+ // MY USER GROUP
9085
+ //
9086
+
9087
+ var currentUserGroup = null;
9088
+ function gotoUserGroup(groupid, force) {
9089
+ if (xxdialogMode && !force) return;
9090
+ var group = currentUserGroup = usergroups[decodeURIComponent(groupid)];
9091
+ if (group == null) { if (xxcurrentView == 51) { setDialogMode(0); go(50); } return; }
9092
+ QH('p51groupName', group.name);
9093
+
9094
+ var desc = group.desc;
9095
+ if ((desc == null) || (desc == '')) { desc = '<i>' + "None" + '<i>'; } else { desc = EscapeHtml(desc); }
9096
+
9097
+ var x = '<div style=min-height:80px><table style=width:100%>';
9098
+ if ((userinfo.siteadmin & 256) != 0) {
9099
+ x += addDeviceAttribute("Name", '<span onclick=p51editgroup(1) style=cursor:pointer>' + EscapeHtml(group.name) + ' <img class=hoverButton src="images/link5.png" /></span>');
9100
+ x += addDeviceAttribute("Description", '<span onclick=p51editgroup(2) style=cursor:pointer>' + desc + ' <img class=hoverButton src="images/link5.png" /></span>');
9101
+ } else {
9102
+ x += addDeviceAttribute("Name", EscapeHtml(group.name));
9103
+ x += addDeviceAttribute("Description", desc);
9104
+ }
9105
+ x += '</table></div><br />';
9106
+
9107
+ // Setup the panel
9108
+ QH('p51group', x);
9109
+
9110
+ if ((userinfo.siteadmin & 256) != 0) {
9111
+ x = '<a href=# onclick="return p51showAddUserDialog()" style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "Add Users" + '</a>';
9112
+ }
9113
+ x += '<table style="color:black;background-color:#EEE;border-color:#AAA;border-width:1px;border-style:solid;border-collapse:collapse" border=0 cellpadding=2 cellspacing=0 width=100%><tbody><tr style=background-color:#AAAAAA;font-weight:bold><th scope=col style=text-align:left;width:430px>' + "Group Members" + '</th><th scope=col style=text-align:left></th></tr>';
9114
+
9115
+ // Sort the users for this mesh
9116
+ var count = 1, sortedusers = [];
9117
+ for (var i in currentUserGroup.links) {
9118
+ var uname = i.split('/')[2];
9119
+ if (currentUserGroup.links[i].name) { uname = currentUserGroup.links[i].name; }
9120
+ if (i == userinfo._id) { uname = userinfo.name; }
9121
+ sortedusers.push({ id: i, name: uname, rights: currentUserGroup.links[i].rights });
9122
+ }
9123
+ sortedusers.sort(function(a, b) { if (a.name > b.name) return 1; if (a.name < b.name) return -1; return 0; });
9124
+
9125
+ // Display all users for this mesh
9126
+ for (var i in sortedusers) {
9127
+ var trash = '<a href=# onclick=\'return p51deleteUser(event,"' + encodeURIComponent(sortedusers[i].id) + '")\' title=\"' + "Remove user rights to this device group" + '\" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>';
9128
+ x += '<tr tabindex=0 onclick=p20viewuser("' + encodeURIComponent(sortedusers[i].id) + '") onkeypress="if (event.key==\'Enter\') p20viewuser(\'' + encodeURIComponent(sortedusers[i].id) + '\')" style=cursor:pointer' + (((count % 2) == 0) ? ';background-color:#DDD' : '') + '><td><div title=\"' + "User" + '\" class=m2></div><div> ' + EscapeHtml(decodeURIComponent(sortedusers[i].name)) + '<div></div></div></td><td><div style=float:right>' + trash + '</div></td></tr>';
9129
+ ++count;
9130
+ }
9131
+
9132
+ if (count == 1) { x += '<tr><td><div style=padding:6px> <i>' + "No Members" + '</i><div></div></div></td><td></td></tr>'; }
9133
+
9134
+ x += '</tbody></table>';
9135
+ if ((userinfo.siteadmin & 256) != 0) {
9136
+ x += '<div style=font-size:x-small;text-align:right><span><a href=# onclick=p51showDeleteUserGroupDialog() style=cursor:pointer>' + "Delete User Group" + '</a></span></div>';
9137
+ }
9138
+
9139
+ QH('p51group2', x);
9140
+ go(51);
9141
+ }
9142
+
9143
+ function p51editgroup(focus) {
9144
+ if (xxdialogMode) return;
9145
+ var x = addHtmlValue("Name", '<input id=dp51name style=width:230px maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) />');
9146
+ x += addHtmlValue("Description", '<div style=width:230px;margin:0;padding:0><textarea id=dp51desc maxlength=1024 style=width:100%;resize:none></textarea></div>');
9147
+ setDialogMode(2, "Edit User Group", 3, p51editgroupEx, x);
9148
+ Q('dp51name').value = currentUserGroup.name;
9149
+ if (currentUserGroup.desc) Q('dp51desc').value = currentUserGroup.desc;
9150
+ p51editgroupValidate();
9151
+ if (focus == 2) { Q('dp51desc').focus(); } else { Q('dp51name').focus(); }
9152
+ }
9153
+
9154
+ function p51editgroupEx() {
9155
+ meshserver.send({ action: 'editusergroup', ugrpid: currentUserGroup._id, name: Q('dp51name').value, desc: Q('dp51desc').value });
9156
+ }
9157
+
9158
+ function p51editgroupValidate(e) {
9159
+ QE('idx_dlgOkButton', (Q('dp51name').value.length > 0) && (Q('dp51name').value.indexOf(' ') == -1));
9160
+ if (e && e.key == 'Enter') { Q('dp51desc').focus(); }
9161
+ }
9162
+
9163
+ function p51showDeleteUserGroupDialog() {
9164
+ if (xxdialogMode) return false;
9165
+ var x = format("Delete user group {0}?", EscapeHtml(currentUserGroup.name)) + '<br /><br />';
9166
+ x += '<label><input id=p51check type=checkbox onchange=p51validateDeleteGroupDialog() />' + "Confirm" + '</label>';
9167
+ setDialogMode(2, "Delete User Group", 3, p51showDeleteUserGroupDialogEx, x);
9168
+ p51validateDeleteGroupDialog();
9169
+ return false;
9170
+ }
9171
+
9172
+ function p51validateDeleteGroupDialog() {
9173
+ QE('idx_dlgOkButton', Q('p51check').checked);
9174
+ }
9175
+
9176
+ function p51showDeleteUserGroupDialogEx(buttons, tag) {
9177
+ meshserver.send({ action: 'deleteusergroup', ugrpid: currentUserGroup._id });
9178
+ }
9179
+
9180
+ function p51deleteUser(e, id) {
9181
+ console.log('p51deleteUser', id);
9182
+ }
9183
+
9184
+
9185
+
9186
+
9187
+ function p51showAddUserDialog() {
9188
+ if (xxdialogMode) return false;
9189
+ var x = "Allow users to manage this device group and devices in this group.";
9190
+ if (features & 0x00080000) { x += " Users need to login to this server once before they can be added to a device group." }
9191
+ x += '<br /><br /><div style=\'position:relative\'>';
9192
+ x += addHtmlValue("User Names", '<input id=dp51username style=width:230px maxlength=32 onchange=p51validateAddUserDialog() onkeyup=p51validateAddUserDialog() placeholder="user1, user2, user3" />');
9193
+ x += '<div id=dp51usersuggest class=suggestionBox style=\'top:30px;left:130px;display:none\'></div>';
9194
+ x += '</div><br>';
9195
+ setDialogMode(2, "Add Users to User Group", 3, p51showAddUserDialogEx, x);
9196
+ Q('dp51username').focus();
9197
+ p51validateAddUserDialog();
9198
+ return false;
9199
+ }
9200
+
9201
+ function p51setname(name) {
9202
+ name = decodeURIComponent(name);
9203
+ var xusers = Q('dp51username').value.split(',');
9204
+ for (var i in xusers) { xusers[i] = xusers[i].trim(); }
9205
+ xusers[xusers.length - 1] = name;
9206
+ Q('dp51username').value = xusers.join(', ');
9207
+ p51validateAddUserDialog();
9208
+ return false;
9209
+ }
9210
+
9211
+ function p51validateAddUserDialog() {
9212
+ var meshrights = GetMeshRights(currentMesh);
9213
+ var ok = true;
9214
+ if (Q('dp51username')) {
9215
+ var xusers = Q('dp51username').value.split(',');
9216
+ for (var i in xusers) {
9217
+ var xuser = xusers[i] = xusers[i].trim();
9218
+ if (xuser.length == 0) { ok = false; } else if (xuser.indexOf('"') >= 0) { ok = false; }
9219
+ }
9220
+
9221
+ // Fill the suggestion box
9222
+ var showsuggestbox = false, exactMatch = false;
9223
+ if (users != null) {
9224
+ var lastuser = xusers[xusers.length - 1].trim(), lastuserl = lastuser.toLowerCase(), matchingUsers = [];
9225
+ if (lastuser.length > 0) {
9226
+ for (var i in users) {
9227
+ if (users[i].name === lastuser) { exactMatch = true; break; }
9228
+ if (users[i].name.toLowerCase().indexOf(lastuserl) >= 0) { matchingUsers.push(users[i].name); if (matchingUsers.length >= 8) break; }
9229
+ }
9230
+ if ((exactMatch == false) && (matchingUsers.length > 0)) {
9231
+ var x = '';
9232
+ for (var i in matchingUsers) { x += '<a href=# onclick=\'p51setname("' + encodeURIComponent(matchingUsers[i]) + '")\'>' + matchingUsers[i] + '</a><br />'; }
9233
+ QH('dp51usersuggest', x);
9234
+ showsuggestbox = true;
9235
+ }
9236
+ }
9237
+ }
9238
+ QV('dp51usersuggest', showsuggestbox);
9239
+ }
9240
+ QE('idx_dlgOkButton', ok);
9241
+ }
9242
+
9243
+ function p51showAddUserDialogEx(b, t) {
9244
+ if (t == null) {
9245
+ var users = Q('dp51username').value.split(','), users2 = [];
9246
+ for (var i in users) { users2.push(users[i].trim()); }
9247
+ meshserver.send({ action: 'addusertousergroup', meshid: currentUserGroup._id, usernames: users2 });
9248
+ } else {
9249
+ meshserver.send({ action: 'addusertousergroup', meshid: currentUserGroup._id, usernames: [ t.split('/')[2] ] });
9250
+ }
9251
+ }
9252
+
9253
+
9254
+
9255
//
9256
// MY USER GENERAL
9257
//
@@ -9050,9 +9303,9 @@
9303
9304
// Administrative Realms
9305
if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
9053
- var userGroups = '<i>' + "None" + '</i>';
9054
- if (user.groups) { userGroups = ''; for (var i in user.groups) { userGroups += '<span class="tagSpan">' + user.groups[i] + '</span>'; } }
9055
- x += addDeviceAttribute("Admin Realms", addLinkConditional(userGroups, 'showUserGroupDialog(event,\"' + userid + '\")', (userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.groups == null) && (userinfo._id != user._id) && (user.siteadmin != 0xFFFFFFFF))));
9306
+ var xuserGroups = '<i>' + "None" + '</i>';
9307
+ if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + user.groups[i] + '</span>'; } }
9308
+ x += addDeviceAttribute("Admin Realms", addLinkConditional(xuserGroups, 'showUserGroupDialog(event,\"' + userid + '\")', (userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.groups == null) && (userinfo._id != user._id) && (user.siteadmin != 0xFFFFFFFF))));
9309
}
9310
9311
var multiFactor = 0;
@@ -9856,11 +10109,11 @@
10109
// Set the goback stack, if going to top-level view, clear the stack.
10110
if ((xxcurrentView < 0) || (x < 10)) { goBackStack = []; } else {
10111
// Do not push into the back stack if we are changing tabs at the same level.
9859
- if (Math.floor(xxcurrentView / 10) != Math.floor(x / 10)) { goBackStack.push(xxcurrentView); }
10112
+ if ((xxcurrentView == 50) || (Math.floor(xxcurrentView / 10) != Math.floor(x / 10))) { goBackStack.push(xxcurrentView); }
10113
}
10114
10115
// Edit this line when adding a new screen
9863
- for (var i = 0; i < 51; i++) { QV('p' + i, i == x); }
10116
+ for (var i = 0; i < 52; i++) { QV('p' + i, i == x); }
10117
xxcurrentView = x;
10118
10119
// Remove top bar selection
@@ -9896,7 +10149,7 @@
10149
// My Events
10150
QC('MainMenuMyEvents').add(mainMenuActiveClass);
10151
QC('LeftMenuMyEvents').add(leftMenuActiveClass);
9899
- } else if (x == 4 || (x >= 30 && x < 40) || x == 50) {
10152
+ } else if (x == 4 || (x >= 30 && x < 40) || (x == 50) || (x == 51)) {
10153
// My Users
10154
QC('MainMenuMyUsers').add(mainMenuActiveClass);
10155
QC('LeftMenuMyUsers').add(leftMenuActiveClass);
@@ -9919,7 +10172,7 @@
10172
if ((x == 0) && (webPageFullScreen)) { QC('body').add('arg_hide'); }
10173
10174
QV('MainSubMenuSpan', (x >= 10) && (x < 20));
9922
- QV('UserDummyMenuSpan', (x < 10) && (x != 6) && webPageFullScreen);
10175
+ QV('UserDummyMenuSpan', (x == 51) || ((x < 10) && (x != 6) && webPageFullScreen));
10176
QV('MeshSubMenuSpan', (x >= 20) && (x < 30));
10177
QV('UserSubMenuSpan', (x >= 30) && (x < 40));
10178
QV('ServerSubMenuSpan', x == 6 || x == 115 || x == 40 || x == 41 || x == 42 || x == 43);
webserver.js
+1
-1
@@ -4185,7 +4185,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4185
if (fileOptions[acceptLanguages[i]] != null) {
4186
// Found a match. If the file no longer exists, default to English.
4187
obj.fs.exists(fileOptions[acceptLanguages[i]] + '.handlebars', function (exists) {
4188
- if (exists) { res.render(fileOptions[acceptLanguages[i]], args); } else { res.render(filename, args); }
4188
+ if (exists) { args.lang = acceptLanguages[i]; res.render(fileOptions[acceptLanguages[i]], args); } else { res.render(filename, args); }
4189
});
4190
return;
4191
}