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('&nbsp;'); } }
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>&nbsp;' + 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>&nbsp;<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 }