More user groups improvements.
This commit is contained in:
parent
8a77c78ebb
commit
7307152dbb
30
db.js
30
db.js
|
@ -349,19 +349,21 @@ module.exports.CreateDB = function (parent, func) {
|
||||||
if (typeof obj.file.watch != 'function') {
|
if (typeof obj.file.watch != 'function') {
|
||||||
console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
|
console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
|
||||||
} else {
|
} else {
|
||||||
obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
|
obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
|
||||||
obj.fileChangeStream.on('change', function (change) {
|
obj.fileChangeStream.on('change', function (change) {
|
||||||
if (change.operationType == 'update') {
|
if (change.operationType == 'update') {
|
||||||
switch (change.fullDocument.type) {
|
switch (change.fullDocument.type) {
|
||||||
case 'node': { dbNodeChange(change, false); break; } // A node has changed
|
case 'node': { dbNodeChange(change, false); break; } // A node has changed
|
||||||
case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
|
case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
|
||||||
case 'user': { dbUserChange(change, false); break; } // A user account has changed
|
case 'user': { dbUserChange(change, false); break; } // A user account has changed
|
||||||
|
case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
|
||||||
}
|
}
|
||||||
} else if (change.operationType == 'insert') {
|
} else if (change.operationType == 'insert') {
|
||||||
switch (change.fullDocument.type) {
|
switch (change.fullDocument.type) {
|
||||||
case 'node': { dbNodeChange(change, true); break; } // A node has added
|
case 'node': { dbNodeChange(change, true); break; } // A node has added
|
||||||
case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
|
case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
|
||||||
case 'user': { dbUserChange(change, true); break; } // A user account has created
|
case 'user': { dbUserChange(change, true); break; } // A user account has created
|
||||||
|
case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
|
||||||
}
|
}
|
||||||
} else if (change.operationType == 'delete') {
|
} else if (change.operationType == 'delete') {
|
||||||
var splitId = change.documentKey._id.split('/');
|
var splitId = change.documentKey._id.split('/');
|
||||||
|
@ -372,7 +374,7 @@ module.exports.CreateDB = function (parent, func) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mesh': {
|
case 'mesh': {
|
||||||
parent.DispatchEvent(['*', node.meshid], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
|
parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'user': {
|
case 'user': {
|
||||||
|
@ -380,6 +382,10 @@ module.exports.CreateDB = function (parent, func) {
|
||||||
//parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
|
//parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ugrp': {
|
||||||
|
parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1096,5 +1102,25 @@ module.exports.CreateDB = function (parent, func) {
|
||||||
parent.DispatchEvent(targets, obj, { etype: 'user', username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
|
parent.DispatchEvent(targets, obj, { etype: 'user', username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when a user group has changed
|
||||||
|
function dbUGrpChange(ugrpChange, added) {
|
||||||
|
if (parent.webserver == null) return;
|
||||||
|
common.unEscapeLinksFieldName(ugrpChange.fullDocument);
|
||||||
|
const usergroup = ugrpChange.fullDocument;
|
||||||
|
|
||||||
|
// Update the user group object in memory
|
||||||
|
const uusergroup = parent.webserver.usergroups[usergroup._id];
|
||||||
|
for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
|
||||||
|
for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
|
||||||
|
|
||||||
|
// Send the user group update
|
||||||
|
usergroup.action = (added ? 'createusergroup' : 'usergroupchange');
|
||||||
|
usergroup.ugrpid = usergroup._id;
|
||||||
|
usergroup.nolog = 1;
|
||||||
|
delete usergroup.type;
|
||||||
|
delete usergroup._id;
|
||||||
|
parent.DispatchEvent(['*', usergroup.ugrpid], obj, usergroup);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
58
meshuser.js
58
meshuser.js
|
@ -1513,6 +1513,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
// Create the new device group
|
// Create the new device group
|
||||||
var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: domain.id, links: {} };
|
var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: domain.id, links: {} };
|
||||||
db.Set(common.escapeLinksFieldName(ugrp));
|
db.Set(common.escapeLinksFieldName(ugrp));
|
||||||
|
if (db.changeStream == false) { parent.userGroups[ugrpid] = ugrp; }
|
||||||
|
|
||||||
// Event the device group creation
|
// Event the device group creation
|
||||||
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 };
|
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 };
|
||||||
|
@ -1555,6 +1556,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
|
|
||||||
// Remove the user group from the database
|
// Remove the user group from the database
|
||||||
db.Remove(group._id);
|
db.Remove(group._id);
|
||||||
|
if (db.changeStream == false) { delete parent.userGroups[group._id]; }
|
||||||
|
|
||||||
// Event the user group being removed
|
// Event the user group being removed
|
||||||
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: domain.id };
|
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: domain.id };
|
||||||
|
@ -2059,7 +2061,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
}
|
}
|
||||||
case 'editmesh':
|
case 'editmesh':
|
||||||
{
|
{
|
||||||
// Change the name or description of a mesh
|
// Change the name or description of a device group (mesh)
|
||||||
if (common.validateString(command.meshid, 1, 1024) == false) break; // Check the meshid
|
if (common.validateString(command.meshid, 1, 1024) == false) break; // Check the meshid
|
||||||
mesh = parent.meshes[command.meshid];
|
mesh = parent.meshes[command.meshid];
|
||||||
change = '';
|
change = '';
|
||||||
|
@ -2084,11 +2086,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
}
|
}
|
||||||
case 'addmeshuser':
|
case 'addmeshuser':
|
||||||
{
|
{
|
||||||
|
if (typeof command.userid == 'string') { command.userids = [ command.userid ]; }
|
||||||
var err = null;
|
var err = null;
|
||||||
try {
|
try {
|
||||||
if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid
|
if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid
|
||||||
else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer
|
else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer
|
||||||
else if (common.validateStrArray(command.usernames, 1, 64) == false) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters
|
else if ((common.validateStrArray(command.usernames, 1, 64) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters
|
||||||
else {
|
else {
|
||||||
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
|
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
|
||||||
mesh = parent.meshes[command.meshid];
|
mesh = parent.meshes[command.meshid];
|
||||||
|
@ -2104,26 +2107,46 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert user names to userid's
|
||||||
|
if (command.userids == null) {
|
||||||
|
command.userids = [];
|
||||||
|
for (var i in command.usernames) { command.userids.push('user/' + domain.id + '/' + command.usernames[i].toLowerCase()); }
|
||||||
|
}
|
||||||
|
|
||||||
var unknownUsers = [], removedCount = 0, failCount = 0;
|
var unknownUsers = [], removedCount = 0, failCount = 0;
|
||||||
for (var i in command.usernames) {
|
for (var i in command.userids) {
|
||||||
// Check if the user exists
|
// Check if the user exists
|
||||||
var newuserid = 'user/' + domain.id + '/' + command.usernames[i].toLowerCase(), newuser = parent.users[newuserid];
|
var newuserid = command.userids[i], newuser = null;
|
||||||
if (newuserid == obj.user._id) { continue; } // Can't add or modify self
|
if (newuserid.startsWith('user/')) { newuser = parent.users[newuserid]; }
|
||||||
|
else if (newuserid.startsWith('ugrp/')) { newuser = parent.userGroups[newuserid]; }
|
||||||
|
|
||||||
if (newuser != null) {
|
if (newuser != null) {
|
||||||
// Add mesh to user
|
// Can't add or modify self
|
||||||
|
if (newuserid == obj.user._id) { continue; }
|
||||||
|
|
||||||
|
// Add mesh to user or user group
|
||||||
if (newuser.links == null) newuser.links = {};
|
if (newuser.links == null) newuser.links = {};
|
||||||
if (newuser.links[command.meshid]) { newuser.links[command.meshid].rights = command.meshadmin; } else { newuser.links[command.meshid] = { rights: command.meshadmin }; }
|
if (newuser.links[command.meshid]) { newuser.links[command.meshid].rights = command.meshadmin; } else { newuser.links[command.meshid] = { rights: command.meshadmin }; }
|
||||||
db.SetUser(newuser);
|
if (newuserid.startsWith('user/')) { db.SetUser(newuser); }
|
||||||
|
else if (newuserid.startsWith('ugrp/')) { db.Set(newuser); }
|
||||||
parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe');
|
parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe');
|
||||||
|
|
||||||
|
if (newuserid.startsWith('user/')) {
|
||||||
// Notify user change
|
// Notify user change
|
||||||
var targets = ['*', 'server-users', user._id, newuser._id];
|
var targets = ['*', 'server-users', user._id, newuser._id];
|
||||||
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(newuser), action: 'accountchange', msg: 'Device group membership changed: ' + newuser.name, domain: domain.id };
|
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(newuser), action: 'accountchange', msg: 'Device group membership changed: ' + newuser.name, domain: domain.id };
|
||||||
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
||||||
parent.parent.DispatchEvent(targets, obj, event);
|
parent.parent.DispatchEvent(targets, obj, event);
|
||||||
|
} else if (newuserid.startsWith('ugrp/')) {
|
||||||
|
// Notify user group change
|
||||||
|
var targets = ['*', 'server-ugroups', user._id, newuser._id];
|
||||||
|
var event = { etype: 'ugrp', username: user.name, ugrpid: newuser._id, name: newuser.name, desc: newuser.desc, action: 'usergroupchange', links: newuser.links, msg: 'User group changed: ' + newuser.name, domain: domain.id };
|
||||||
|
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
||||||
|
parent.parent.DispatchEvent(targets, obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
// Add a user to the mesh
|
// Add userid to the mesh
|
||||||
mesh.links[newuserid] = { userid: newuser.id, name: newuser.name, rights: command.meshadmin };
|
mesh.links[newuserid] = { name: newuser.name, rights: command.meshadmin };
|
||||||
db.Set(common.escapeLinksFieldName(mesh));
|
db.Set(common.escapeLinksFieldName(mesh));
|
||||||
|
|
||||||
// Notify mesh change
|
// Notify mesh change
|
||||||
|
@ -2132,7 +2155,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
parent.parent.DispatchEvent(['*', mesh._id, user._id, newuserid], obj, event);
|
parent.parent.DispatchEvent(['*', mesh._id, user._id, newuserid], obj, event);
|
||||||
removedCount++;
|
removedCount++;
|
||||||
} else {
|
} else {
|
||||||
unknownUsers.push(command.usernames[i]);
|
unknownUsers.push(command.userids[i]);
|
||||||
failCount++;
|
failCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2170,21 +2193,32 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user exists - Just in case we need to delete a mesh right for a non-existant user, we do it this way. Technically, it's not possible, but just in case.
|
// Check if the user exists - Just in case we need to delete a mesh right for a non-existant user, we do it this way. Technically, it's not possible, but just in case.
|
||||||
var deluserid = command.userid, deluser = parent.users[deluserid];
|
var deluserid = command.userid, deluser = null;
|
||||||
|
if (deluserid.startsWith('user/')) { deluser = parent.users[deluserid]; }
|
||||||
|
else if (deluserid.startsWith('ugrp/')) { deluser = parent.userGroups[deluserid]; }
|
||||||
if (deluser != null) {
|
if (deluser != null) {
|
||||||
// Remove mesh from user
|
// Remove mesh from user
|
||||||
if (deluser.links != null && deluser.links[command.meshid] != null) {
|
if (deluser.links != null && deluser.links[command.meshid] != null) {
|
||||||
var delmeshrights = deluser.links[command.meshid].rights;
|
var delmeshrights = deluser.links[command.meshid].rights;
|
||||||
if ((delmeshrights == 0xFFFFFFFF) && (mesh.links[deluserid].rights != 0xFFFFFFFF)) return; // A non-admin can't kick out an admin
|
if ((delmeshrights == 0xFFFFFFFF) && (mesh.links[deluserid].rights != 0xFFFFFFFF)) return; // A non-admin can't kick out an admin
|
||||||
delete deluser.links[command.meshid];
|
delete deluser.links[command.meshid];
|
||||||
db.Set(deluser);
|
if (deluserid.startsWith('user/')) { db.SetUser(deluser); }
|
||||||
|
else if (deluserid.startsWith('ugrp/')) { db.Set(deluser); }
|
||||||
parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
|
parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
|
||||||
|
|
||||||
|
if (deluserid.startsWith('user/')) {
|
||||||
// Notify user change
|
// Notify user change
|
||||||
var targets = ['*', 'server-users', user._id, deluser._id];
|
var targets = ['*', 'server-users', user._id, deluser._id];
|
||||||
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(deluser), action: 'accountchange', msg: 'Device group membership changed: ' + deluser.name, domain: domain.id };
|
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(deluser), action: 'accountchange', msg: 'Device group membership changed: ' + deluser.name, domain: domain.id };
|
||||||
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
||||||
parent.parent.DispatchEvent(targets, obj, event);
|
parent.parent.DispatchEvent(targets, obj, event);
|
||||||
|
} else if (deluserid.startsWith('ugrp/')) {
|
||||||
|
// Notify user group change
|
||||||
|
var targets = ['*', 'server-ugroups', user._id, deluser._id];
|
||||||
|
var event = { etype: 'ugrp', username: user.name, ugrpid: deluser._id, name: deluser.name, desc: deluser.desc, action: 'usergroupchange', links: deluser.links, msg: 'User group changed: ' + deluser.name, domain: domain.id };
|
||||||
|
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
||||||
|
parent.parent.DispatchEvent(targets, obj, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7648,7 +7648,14 @@
|
||||||
if (meshrights & 1) { x += '<br><input type=button value=' + "Notes" + ' title=\"' + "View notes about this device group" + '\" onclick=showNotes(false,"' + encodeURIComponent(currentMesh._id) + '") />'; }
|
if (meshrights & 1) { x += '<br><input type=button value=' + "Notes" + ' title=\"' + "View notes about this device group" + '\" onclick=showNotes(false,"' + encodeURIComponent(currentMesh._id) + '") />'; }
|
||||||
|
|
||||||
x += '<br style=clear:both><br>';
|
x += '<br style=clear:both><br>';
|
||||||
if (meshrights & 2) { x += '<a href=# onclick="return p20showAddMeshUserDialog()" style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "Add Users" + '</a>'; }
|
if (meshrights & 2) {
|
||||||
|
x += '<a href=# onclick="return p20showAddMeshUserDialog()" style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "Add Users" + '</a>';
|
||||||
|
if ((userinfo.siteadmin & 256) != 0) {
|
||||||
|
var userGroupCount = 0, newUserGroup = false;
|
||||||
|
for (var i in usergroups) { userGroupCount++; if ((currentMesh.links == null) || (currentMesh.links[i] == null)) { newUserGroup = true; } }
|
||||||
|
if ((userGroupCount > 0) && (newUserGroup)) { x += '<a href=# onclick="return p20showAddMeshUserDialog(2)" style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "Add User Group" + '</a>'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (meshrights & 4) {
|
if (meshrights & 4) {
|
||||||
if (currentMesh.mtype == 1) {
|
if (currentMesh.mtype == 1) {
|
||||||
|
@ -7680,10 +7687,11 @@
|
||||||
|
|
||||||
// Display all users for this mesh
|
// Display all users for this mesh
|
||||||
for (var i in sortedusers) {
|
for (var i in sortedusers) {
|
||||||
var trash = '', rights = "Partial Device Group Rights", r = sortedusers[i].rights;
|
var trash = '', rights = "Partial Device Group Rights", r = sortedusers[i].rights, icon = 2;
|
||||||
if (r == 0xFFFFFFFF) rights = "Full Device Group Administrator"; else if (r == 0) rights = "No Rights";
|
if (r == 0xFFFFFFFF) rights = "Full Device Group Administrator"; else if (r == 0) rights = "No Rights";
|
||||||
if ((sortedusers[i].id != userinfo._id) && (meshrights == 0xFFFFFFFF || (((meshrights & 2) != 0)))) { trash = '<a href=# onclick=\'return p20deleteUser(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>'; }
|
if ((sortedusers[i].id != userinfo._id) && (meshrights == 0xFFFFFFFF || (((meshrights & 2) != 0)))) { trash = '<a href=# onclick=\'return p20deleteUser(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>'; }
|
||||||
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><div>' + rights + '</div></td></tr>';
|
if (sortedusers[i].id.startsWith('ugrp/')) { icon = 4; }
|
||||||
|
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=m' + icon + '></div><div> ' + EscapeHtml(decodeURIComponent(sortedusers[i].name)) + '<div></div></div></td><td><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7878,13 +7886,27 @@
|
||||||
var y = '';
|
var y = '';
|
||||||
for (var i in meshes) { if ((currentUser.links == null) || (currentUser.links[i] == null)) { y += '<option value=' + encodeURIComponent(i) + '>' + EscapeHtml(meshes[i].name) + '</option>'; } }
|
for (var i in meshes) { if ((currentUser.links == null) || (currentUser.links[i] == null)) { y += '<option value=' + encodeURIComponent(i) + '>' + EscapeHtml(meshes[i].name) + '</option>'; } }
|
||||||
x += addHtmlValue("Device Group", '<div style=width:230px;margin:0;padding:0><select onchange=p20validateAddMeshUserDialog() id=dp2groupid style=width:100%>' + y + '</select></div>');
|
x += addHtmlValue("Device Group", '<div style=width:230px;margin:0;padding:0><select onchange=p20validateAddMeshUserDialog() id=dp2groupid style=width:100%>' + y + '</select></div>');
|
||||||
|
} else if (userid === 2) {
|
||||||
|
if ((userinfo.siteadmin & 256) == 0) return;
|
||||||
|
var y = '';
|
||||||
|
for (var i in usergroups) { if ((currentMesh.links == null) || (currentMesh.links[i] == null)) { y += '<option value=' + encodeURIComponent(i) + '>' + EscapeHtml(usergroups[i].name) + '</option>'; } }
|
||||||
|
x += addHtmlValue("User Group", '<div style=width:230px;margin:0;padding:0><select onchange=p20validateAddMeshUserDialog() id=dp2groupid style=width:100%>' + y + '</select></div>');
|
||||||
|
} else if (userid === 3) {
|
||||||
|
var y = '';
|
||||||
|
for (var i in meshes) { if ((currentUserGroup.links == null) || (currentUserGroup.links[i] == null)) { y += '<option value=' + encodeURIComponent(i) + '>' + EscapeHtml(meshes[i].name) + '</option>'; } }
|
||||||
|
x += addHtmlValue("Device Group", '<div style=width:230px;margin:0;padding:0><select onchange=p20validateAddMeshUserDialog() id=dp2groupid style=width:100%>' + y + '</select></div>');
|
||||||
} else {
|
} else {
|
||||||
userid = decodeURIComponent(userid);
|
userid = decodeURIComponent(userid);
|
||||||
var uname = userid.split('/')[2];
|
var uname = userid.split('/')[2];
|
||||||
if (users && users[userid]) { uname = users[userid].name; }
|
if (users && users[userid]) { uname = users[userid].name; }
|
||||||
|
if (usergroups && usergroups[userid]) { uname = usergroups[userid].name; }
|
||||||
if (userinfo._id == userid) { uname = userinfo.name; }
|
if (userinfo._id == userid) { uname = userinfo.name; }
|
||||||
|
if (userid.startsWith('ugrp/')) {
|
||||||
|
x += format("Group permissions for {0}.", uname) + '<br /><br />';
|
||||||
|
} else {
|
||||||
x += format("Group permissions for user {0}.", uname) + '<br /><br />';
|
x += format("Group permissions for user {0}.", uname) + '<br /><br />';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
x += '<div style="height:120px;overflow-y:scroll;border:1px solid gray">';
|
x += '<div style="height:120px;overflow-y:scroll;border:1px solid gray">';
|
||||||
x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20fulladmin>' + "Full Administrator" + '</label><br>';
|
x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20fulladmin>' + "Full Administrator" + '</label><br>';
|
||||||
x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Edit Device Group" + '</label><br>';
|
x += '<label><input type=checkbox onchange=p20validateAddMeshUserDialog() id=p20editmesh>' + "Edit Device Group" + '</label><br>';
|
||||||
|
@ -7909,8 +7931,16 @@
|
||||||
Q('dp20username').focus();
|
Q('dp20username').focus();
|
||||||
} else if (userid === 1) {
|
} else if (userid === 1) {
|
||||||
setDialogMode(2, "Add Device Group", 3, p20showAddMeshUserDialogEx, x, userid);
|
setDialogMode(2, "Add Device Group", 3, p20showAddMeshUserDialogEx, x, userid);
|
||||||
|
} else if (userid === 2) {
|
||||||
|
setDialogMode(2, "Add User Group", 3, p20showAddMeshUserDialogEx, x, userid);
|
||||||
|
} else if (userid === 3) {
|
||||||
|
setDialogMode(2, "Add Device Group", 3, p20showAddMeshUserDialogEx, x, userid);
|
||||||
|
} else {
|
||||||
|
if (userid.startsWith('ugrp/')) {
|
||||||
|
setDialogMode(2, "Edit Device Group Permissions", 7, p20showAddMeshUserDialogEx, x, userid);
|
||||||
} else {
|
} else {
|
||||||
setDialogMode(2, "Edit User Device Group Permissions", 7, p20showAddMeshUserDialogEx, x, userid);
|
setDialogMode(2, "Edit User Device Group Permissions", 7, p20showAddMeshUserDialogEx, x, userid);
|
||||||
|
}
|
||||||
var cmeshrights = GetMeshRights(currentMesh), meshrights = GetMeshRights(currentMesh, userid);
|
var cmeshrights = GetMeshRights(currentMesh), meshrights = GetMeshRights(currentMesh, userid);
|
||||||
if (meshrights == 0xFFFFFFFF) {
|
if (meshrights == 0xFFFFFFFF) {
|
||||||
Q('p20fulladmin').checked = true;
|
Q('p20fulladmin').checked = true;
|
||||||
|
@ -8031,14 +8061,20 @@
|
||||||
|
|
||||||
if (t === 1) {
|
if (t === 1) {
|
||||||
var meshid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[meshid];
|
var meshid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[meshid];
|
||||||
if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: meshid, meshname: mesh.name, usernames: [ currentUser._id.split('/')[2] ], meshadmin: meshadmin }); }
|
if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: meshid, meshname: mesh.name, userids: [ currentUser._id ], meshadmin: meshadmin }); }
|
||||||
|
} else if (t === 2) {
|
||||||
|
var ugrpid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[currentMesh._id];
|
||||||
|
if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: ugrpid, meshadmin: meshadmin }); }
|
||||||
|
} else if (t === 3) {
|
||||||
|
var meshid = decodeURIComponent(Q('dp2groupid').value), mesh = meshes[meshid];
|
||||||
|
if (mesh != null) { meshserver.send({ action: 'addmeshuser', meshid: meshid, meshname: mesh.name, userids: [ currentUserGroup._id ], meshadmin: meshadmin }); }
|
||||||
} else {
|
} else {
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
var users = Q('dp20username').value.split(','), users2 = [];
|
var users = Q('dp20username').value.split(','), users2 = [];
|
||||||
for (var i in users) { users2.push(users[i].trim()); }
|
for (var i in users) { users2.push(users[i].trim()); }
|
||||||
meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: users2, meshadmin: meshadmin });
|
meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: users2, meshadmin: meshadmin });
|
||||||
} else {
|
} else {
|
||||||
meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: [ t.split('/')[2] ], meshadmin: meshadmin });
|
meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userids: [ t ], meshadmin: meshadmin });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8087,6 +8123,7 @@
|
||||||
if (button != 2) return;
|
if (button != 2) return;
|
||||||
var uname = userid.split('/')[2];
|
var uname = userid.split('/')[2];
|
||||||
if (users && users[userid]) { uname = users[userid].name; }
|
if (users && users[userid]) { uname = users[userid].name; }
|
||||||
|
if (usergroups && usergroups[userid]) { uname = usergroups[userid].name; }
|
||||||
if (userinfo._id == userid) { uname = userinfo.name; }
|
if (userinfo._id == userid) { uname = userinfo.name; }
|
||||||
setDialogMode(2, "Remote Mesh User", 3, p20viewuserEx2, format("Confirm removal of user {0}?", EscapeHtml(decodeURIComponent(uname))), userid);
|
setDialogMode(2, "Remote Mesh User", 3, p20viewuserEx2, format("Confirm removal of user {0}?", EscapeHtml(decodeURIComponent(uname))), userid);
|
||||||
}
|
}
|
||||||
|
@ -9056,7 +9093,7 @@
|
||||||
|
|
||||||
// Display the groups using the sorted list
|
// Display the groups using the sorted list
|
||||||
var x = '<table class=p3usersTable cellpadding=0 cellspacing=0>', addHeader = true;
|
var x = '<table class=p3usersTable cellpadding=0 cellspacing=0>', addHeader = true;
|
||||||
x += '<th>' + "Name" + '<th style=width:80px>' + "Users";
|
x += '<th>' + "Name" + '<th style=width:80px>' + "Device Groups" + '<th style=width:80px>' + "Users";
|
||||||
for (var i in sortedGroups) { x += addUserGroupHtml(sortedGroups[i]); }
|
for (var i in sortedGroups) { x += addUserGroupHtml(sortedGroups[i]); }
|
||||||
x += '</table>';
|
x += '</table>';
|
||||||
|
|
||||||
|
@ -9068,13 +9105,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUserGroupHtml(group) {
|
function addUserGroupHtml(group) {
|
||||||
var usercount = 0;
|
var usercount = 0, meshcount = 0;
|
||||||
if (group.links) { for (var i in group.links) { usercount++; } }
|
if (group.links) { for (var i in group.links) { if (i.startsWith('user/')) { usercount++; } if (i.startsWith('mesh/')) { meshcount++; } } }
|
||||||
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) + '\")>';
|
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) + '\")>';
|
||||||
x += '<div class=bar style=width:100%>';
|
x += '<div class=bar style=width:100%>';
|
||||||
x += '<div class=baricon><div class="m4"></div></div>';
|
x += '<div class=baricon><div class="m4"></div></div>';
|
||||||
x += '<div class=g1></div><div class=g2></div>';
|
x += '<div class=g1></div><div class=g2></div>';
|
||||||
x += '<div><span style=font-size:16px>' + group.name + '</span></div></div><td style=text-align:center>' + usercount;
|
x += '<div><span style=font-size:16px>' + group.name + '</span></div></div><td style=text-align:center>' + meshcount + '<td style=text-align:center>' + usercount;
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9106,7 +9143,8 @@
|
||||||
var group = currentUserGroup = usergroups[decodeURIComponent(groupid)];
|
var group = currentUserGroup = usergroups[decodeURIComponent(groupid)];
|
||||||
if (group == null) { if (xxcurrentView == 51) { setDialogMode(0); go(50); } return; }
|
if (group == null) { if (xxcurrentView == 51) { setDialogMode(0); go(50); } return; }
|
||||||
QH('p51groupName', group.name);
|
QH('p51groupName', group.name);
|
||||||
|
var usercount = 0, meshcount = 0;
|
||||||
|
if (group.links) { for (var i in group.links) { if (i.startsWith('user/')) { usercount++; } if (i.startsWith('mesh/')) { meshcount++; } } }
|
||||||
var desc = group.desc;
|
var desc = group.desc;
|
||||||
if ((desc == null) || (desc == '')) { desc = '<i>' + "None" + '<i>'; } else { desc = EscapeHtml(desc); }
|
if ((desc == null) || (desc == '')) { desc = '<i>' + "None" + '<i>'; } else { desc = EscapeHtml(desc); }
|
||||||
|
|
||||||
|
@ -9118,6 +9156,9 @@
|
||||||
x += addDeviceAttribute("Name", EscapeHtml(group.name));
|
x += addDeviceAttribute("Name", EscapeHtml(group.name));
|
||||||
x += addDeviceAttribute("Description", desc);
|
x += addDeviceAttribute("Description", desc);
|
||||||
}
|
}
|
||||||
|
x += addDeviceAttribute("Users", usercount);
|
||||||
|
x += addDeviceAttribute("Device Groups", meshcount);
|
||||||
|
|
||||||
x += '</table></div><br />';
|
x += '</table></div><br />';
|
||||||
|
|
||||||
if ((userinfo.siteadmin & 256) != 0) {
|
if ((userinfo.siteadmin & 256) != 0) {
|
||||||
|
@ -9127,14 +9168,16 @@
|
||||||
// Setup the panel
|
// Setup the panel
|
||||||
QH('p51group', x);
|
QH('p51group', x);
|
||||||
|
|
||||||
|
x = '<br />';
|
||||||
if ((userinfo.siteadmin & 256) != 0) {
|
if ((userinfo.siteadmin & 256) != 0) {
|
||||||
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>';
|
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>';
|
||||||
}
|
}
|
||||||
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>';
|
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>';
|
||||||
|
|
||||||
// Sort the users for this mesh
|
// Sort the users for this mesh
|
||||||
var count = 1, sortedusers = [];
|
var count = 1, sortedusers = [];
|
||||||
for (var i in currentUserGroup.links) {
|
for (var i in currentUserGroup.links) {
|
||||||
|
if (i.startsWith('user/') == false) continue;
|
||||||
var uname = i.split('/')[2];
|
var uname = i.split('/')[2];
|
||||||
if (currentUserGroup.links[i].name) { uname = currentUserGroup.links[i].name; }
|
if (currentUserGroup.links[i].name) { uname = currentUserGroup.links[i].name; }
|
||||||
if (i == userinfo._id) { uname = userinfo.name; }
|
if (i == userinfo._id) { uname = userinfo.name; }
|
||||||
|
@ -9151,7 +9194,29 @@
|
||||||
|
|
||||||
if (count == 1) { x += '<tr><td><div style=padding:6px> <i>' + "No Members" + '</i><div></div></div></td><td></td></tr>'; }
|
if (count == 1) { x += '<tr><td><div style=padding:6px> <i>' + "No Members" + '</i><div></div></div></td><td></td></tr>'; }
|
||||||
|
|
||||||
|
x += '</tbody></table><br />';
|
||||||
|
|
||||||
|
count = 1;
|
||||||
|
var deviceGroupCount = 0, newDeviceGroup = false;
|
||||||
|
for (var i in meshes) { deviceGroupCount++; if ((currentUserGroup.links == null) || (currentUserGroup.links[i] == null)) { newDeviceGroup = true; } }
|
||||||
|
if ((deviceGroupCount > 0) && (newDeviceGroup)) { x += '<a href=# onclick="return p20showAddMeshUserDialog(3)" style=cursor:pointer;margin-right:10px><img src=images/icon-addnew.png border=0 height=12 width=12> ' + "Add Device Group" + '</a>'; }
|
||||||
|
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>' + "Common Device Groups" + '</th><th scope=col style=text-align:left></th></tr>';
|
||||||
|
if (currentUserGroup.links) {
|
||||||
|
for (var i in currentUserGroup.links) {
|
||||||
|
if (i.startsWith('mesh/')) {
|
||||||
|
var cr = 0, r = currentUserGroup.links[i].rights, mesh = meshes[i], trash = '', rights = "Partial Device Group Rights";
|
||||||
|
if (mesh == null) { continue; }
|
||||||
|
if ((userinfo.links) && (userinfo.links[i] != null) && (userinfo.links[i].rights != null)) { cr = userinfo.links[i].rights; }
|
||||||
|
var meshname = mesh?EscapeHtml(mesh.name):('<i>' + "Unknown Device Group" + '</i>');
|
||||||
|
if (r == 0xFFFFFFFF) rights = "Full Device Group Administrator"; else if (r == 0) rights = "No Rights";
|
||||||
|
if ((cr & 2) != 0) { trash = '<a href=# onclick=\'return p51removeMeshFromUserGroup(event,"' + encodeURIComponent(mesh._id) + '")\' title=\"' + "Remove user group rights to this device group" + '\" style=cursor:pointer><img src=images/trash.png border=0 height=10 width=10></a>'; }
|
||||||
|
x += '<tr ' + (((++count % 2) == 0) ? 'style=background-color:#DDD' : '') + '><td><div title=\"' + "Device Group" + '\" class=m99></div><div> ' + meshname + '<div></div></div></td><td><div style=float:right>' + trash + '</div><div>' + rights + '</div></td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 1) { x += '<tr><td><div style=padding:6px> <i>' + "No device groups in common" + '</i><div></div></div></td><td></td></tr>'; }
|
||||||
x += '</tbody></table>';
|
x += '</tbody></table>';
|
||||||
|
|
||||||
if ((userinfo.siteadmin & 256) != 0) {
|
if ((userinfo.siteadmin & 256) != 0) {
|
||||||
x += '<div style=font-size:x-small;text-align:right><span><a href=# onclick=p51showDeleteUserGroupDialog() style=cursor:pointer>' + "Delete User Group" + '</a></span></div>';
|
x += '<div style=font-size:x-small;text-align:right><span><a href=# onclick=p51showDeleteUserGroupDialog() style=cursor:pointer>' + "Delete User Group" + '</a></span></div>';
|
||||||
}
|
}
|
||||||
|
@ -9160,6 +9225,17 @@
|
||||||
go(51);
|
go(51);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function p51removeMeshFromUserGroup(e, meshid) {
|
||||||
|
if (xxdialogMode) return;
|
||||||
|
var mesh = meshes[decodeURIComponent(meshid)];
|
||||||
|
if (mesh == null) return;
|
||||||
|
setDialogMode(2, "Remove Device Group", 3, p51removeMeshFromUserGroupEx, format("Confirm removal of device group {0}?", mesh.name), mesh._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function p51removeMeshFromUserGroupEx(b, meshid) {
|
||||||
|
meshserver.send({ action: 'removemeshuser', meshid: meshid, userid: currentUserGroup._id });
|
||||||
|
}
|
||||||
|
|
||||||
function p51editgroup(focus) {
|
function p51editgroup(focus) {
|
||||||
if (xxdialogMode) return;
|
if (xxdialogMode) return;
|
||||||
var x = addHtmlValue("Name", '<input id=dp51name style=width:230px maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) />');
|
var x = addHtmlValue("Name", '<input id=dp51name style=width:230px maxlength=32 onchange=p51editgroupValidate() onkeyup=p51editgroupValidate(event) />');
|
||||||
|
|
11
webserver.js
11
webserver.js
|
@ -77,6 +77,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
obj.args = args;
|
obj.args = args;
|
||||||
obj.users = {}; // UserID --> User
|
obj.users = {}; // UserID --> User
|
||||||
obj.meshes = {}; // MeshID --> Mesh (also called device group)
|
obj.meshes = {}; // MeshID --> Mesh (also called device group)
|
||||||
|
obj.userGroups = {}; // UGrpID --> User Group
|
||||||
obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
|
obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
|
||||||
obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
|
obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
|
||||||
obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
|
obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
|
||||||
|
@ -211,15 +212,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all meshes from the database, keep this in memory
|
// Fetch all device groups (meshes) from the database, keep this in memory
|
||||||
obj.db.GetAllType('mesh', function (err, docs) {
|
obj.db.GetAllType('mesh', function (err, docs) {
|
||||||
obj.common.unEscapeAllLinksFieldName(docs);
|
obj.common.unEscapeAllLinksFieldName(docs);
|
||||||
for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
|
for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
|
||||||
|
|
||||||
// We loaded the users and mesh state, start the server
|
// Fetch all user groups from the database, keep this in memory
|
||||||
|
obj.db.GetAllType('ugrp', function (err, docs) {
|
||||||
|
obj.common.unEscapeAllLinksFieldName(docs);
|
||||||
|
for (var i in docs) { obj.userGroups[docs[i]._id] = docs[i]; } // Get all user groups
|
||||||
|
|
||||||
|
// We loaded the users, device groups and suer group state, start the server
|
||||||
serverStart();
|
serverStart();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Return statistics about this web server
|
// Return statistics about this web server
|
||||||
obj.getStats = function () {
|
obj.getStats = function () {
|
||||||
|
|
Loading…
Reference in New Issue