From 15c882f24ed62fe7e21d8b67d8af98401899da07 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 31 Aug 2022 11:28:36 -0700 Subject: [PATCH] Partial support for device paging support on the server side, allows a user to view only a subset of the devices. --- db.js | 51 +++++++++++--------- meshuser.js | 132 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 139 insertions(+), 44 deletions(-) diff --git a/db.js b/db.js index 8da7e9b4..a323c27b 100644 --- a/db.js +++ b/db.js @@ -1329,20 +1329,21 @@ module.exports.CreateDB = function (parent, func) { func(err, performTypedRecordDecrypt(docs)); }); }; - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { + if (limit == 0) { limit = -1; } // In SQLite, no limit is -1 if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); } else { if (extrasids == null) { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); } else { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); @@ -1550,7 +1551,7 @@ module.exports.CreateDB = function (parent, func) { func(null, common.aceUnEscapeAllFieldNames(docs)); }); } - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (meshes.length == 0) { func(null, []); return; } var query = obj.file.query('meshcentral').take(999999).filter('type', '==', type).filter('domain', '==', domain); if (id) { query = query.filter('_id', '==', id); } @@ -1780,14 +1781,15 @@ module.exports.CreateDB = function (parent, func) { obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); }; - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { + if (limit == 0) { limit = 0xFFFFFFFF; } if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } else { if (extrasids == null) { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true); + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true); } else { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } } }; @@ -1947,13 +1949,14 @@ module.exports.CreateDB = function (parent, func) { obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); }; - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { + if (limit == 0) { limit = 0xFFFFFFFF; } if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } else { - sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } }; obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) { @@ -2177,15 +2180,21 @@ module.exports.CreateDB = function (parent, func) { obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }; obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }; obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }; - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (extrasids == null) { - var x = { type: type, domain: domain, meshid: { $in: meshes } }; + const x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } - obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); + var f = obj.file.find(x, { type: 0 }); + if (skip > 0) f = f.skip(skip); // Skip records + if (limit > 0) f = f.limit(limit); // Limit records + f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } else { - var x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] }; + const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] }; if (id) { x._id = id; } - obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); + var f = obj.file.find(x, { type: 0 }); + if (skip > 0) f = f.skip(skip); // Skip records + if (limit > 0) f = f.limit(limit); // Limit records + f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } }; obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) { @@ -2409,18 +2418,18 @@ module.exports.CreateDB = function (parent, func) { obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }; obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); }; obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }; - //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { + //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) { //var x = { type: type, domain: domain, meshid: { $in: meshes } }; //if (id) { x._id = id; } //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); //}; - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (extrasids == null) { - var x = { type: type, domain: domain, meshid: { $in: meshes } }; + const x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } else { - var x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] }; + const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] }; if (id) { x._id = id; } obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } diff --git a/meshuser.js b/meshuser.js index 8972acf3..18515363 100644 --- a/meshuser.js +++ b/meshuser.js @@ -105,6 +105,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use obj.domain = domain; obj.ws = ws; + // Information related to the current page the user is looking at + obj.deviceSkip = 0; // How many devices to skip + obj.deviceLimit = 0; // How many devices to view + obj.visibleDevices = null; // An object of visible nodeid's if the user is in paging mode + // Check if we are a cross-domain administrator if (parent.parent.config.settings.managecrossdomain && (parent.parent.config.settings.managecrossdomain.indexOf(user._id) >= 0)) { obj.crossDomain = true; } @@ -414,6 +419,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // If this session is logged in using a loginToken and the token is removed, disconnect. if ((req.session.loginToken != null) && (typeof event == 'object') && (event.action == 'loginTokenChanged') && (event.removed != null) && (event.removed.indexOf(req.session.loginToken) >= 0)) { delete req.session; obj.close(); return; } + // If this user is not viewing all devices and paging, check if this event is in the current page + if (isEventWithinPage(ids) == false) return; + // Normally, only allow this user to receive messages from it's own domain. // If the user is a cross domain administrator, allow some select messages from different domains. if ((event.domain == null) || (event.domain == domain.id) || ((obj.crossDomain === true) && (allowedCrossDomainMessages.indexOf(event.action) >= 0))) { @@ -706,7 +714,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } // Request a list of all nodes - db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, docs) { + db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, obj.deviceSkip, obj.deviceLimit, function (err, docs) { //console.log(err, docs, links, extraids, domain.id, 'node', command.id); @@ -714,10 +722,14 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.common.unEscapeAllLinksFieldName(docs); var r = {}; + if (obj.visibleDevices != null) { obj.visibleDevices = {}; } for (i in docs) { // Check device links, if a link points to an unknown user, remove it. parent.cleanDevice(docs[i]); + // If we are paging, add the device to the page here + if (obj.visibleDevices != null) { obj.visibleDevices[docs[i]._id] = 1; } + // Remove any connectivity and power state information, that should not be in the database anyway. // TODO: Find why these are sometimes saved in the db. if (docs[i].conn != null) { delete docs[i].conn; } @@ -788,7 +800,14 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use r[meshid].push(docs[i]); } - try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag })); } catch (ex) { } + const response = { action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag }; + if (obj.visibleDevices != null) { + // If in paging mode, report back the skip and limit values + response.skip = obj.deviceSkip; + response.limit = obj.deviceLimit; + // TODO: Add total device count + } + try { ws.send(JSON.stringify(response)); } catch (ex) { } }); break; } @@ -6128,14 +6147,30 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } function serverCommandLastConnects(command) { - const links = parent.GetAllMeshIdWithRights(user); - const extraids = getUserExtraIds(); - db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, function (err, docs) { - if (docs == null) return; + if (obj.visibleDevices == null) { + // If we are not paging, get all devices visible to this user + const links = parent.GetAllMeshIdWithRights(user); + const extraids = getUserExtraIds(); + db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) { + if (docs == null) return; + // Create a list of node ids for this user and query them for last device connection time + const ids = [] + for (var i in docs) { ids.push('lc' + docs[i]._id); } + + // Pull list of last connections only for device owned by this user + db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) { + if (docs == null) return; + const response = {}; + for (var j in docs) { response[docs[j]._id.substring(2)] = docs[j].time; } + obj.send({ action: 'lastconnects', lastconnects: response, tag: command.tag }); + }); + }); + } else { + // If we are paging, we know what devices the user is look at // Create a list of node ids for this user and query them for last device connection time const ids = [] - for (var i in docs) { ids.push('lc' + docs[i]._id); } + for (var i in obj.visibleDevices) { ids.push('lc' + i); } // Pull list of last connections only for device owned by this user db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) { @@ -6144,7 +6179,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use for (var j in docs) { response[docs[j]._id.substring(2)] = docs[j].time; } obj.send({ action: 'lastconnects', lastconnects: response, tag: command.tag }); }); - }); + } } function serverCommandLoginCookie(command) { @@ -7505,22 +7540,62 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Return detailed information about all nodes this user has access to function getAllDeviceDetailedInfo(type, func) { - // Get all device groups this user has access to - var links = parent.GetAllMeshIdWithRights(user); + // If we are not paging, get all devices visible to this user + if (obj.visibleDevices == null) { - // Add any nodes with direct rights or any nodes with user group direct rights - var extraids = getUserExtraIds(); + // Get all device groups this user has access to + var links = parent.GetAllMeshIdWithRights(user); - // Request a list of all nodes - db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, function (err, docs) { - if (docs == null) { docs = []; } - parent.common.unEscapeAllLinksFieldName(docs); + // Add any nodes with direct rights or any nodes with user group direct rights + var extraids = getUserExtraIds(); - var results = [], resultPendingCount = 0; - for (i in docs) { - // Check device links, if a link points to an unknown user, remove it. - parent.cleanDevice(docs[i]); + // Request a list of all nodes + db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) { + if (docs == null) { docs = []; } + parent.common.unEscapeAllLinksFieldName(docs); + var results = [], resultPendingCount = 0; + for (i in docs) { + // Check device links, if a link points to an unknown user, remove it. + parent.cleanDevice(docs[i]); + + // Fetch the node from the database + resultPendingCount++; + const getNodeFunc = function (node, rights, visible) { + if ((node != null) && (visible == true)) { + const getNodeSysInfoFunc = function (err, docs) { + const getNodeNetInfoFunc = function (err, docs) { + var netinfo = null; + if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; } + resultPendingCount--; + getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo }); + if (resultPendingCount == 0) { func(getNodeFunc.results, type); } + } + getNodeNetInfoFunc.results = getNodeSysInfoFunc.results; + getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid; + getNodeNetInfoFunc.node = getNodeSysInfoFunc.node; + if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; } + + // Query the database for network information + db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc); + } + getNodeSysInfoFunc.results = getNodeFunc.results; + getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid; + getNodeSysInfoFunc.node = node; + + // Query the database for system information + db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc); + } else { resultPendingCount--; } + if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); } + } + getNodeFunc.results = results; + getNodeFunc.nodeid = docs[i]._id; + parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc); + } + }); + } else { + // If we are paging, we know what devices the user is look at + for (var id in obj.visibleDevices) { // Fetch the node from the database resultPendingCount++; const getNodeFunc = function (node, rights, visible) { @@ -7551,10 +7626,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); } } getNodeFunc.results = results; - getNodeFunc.nodeid = docs[i]._id; - parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc); + getNodeFunc.nodeid = id; + parent.GetNodeWithRights(domain, user, id, getNodeFunc); } - }); + } } // Display a notification message for this session only. @@ -7721,5 +7796,16 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use return authFactorCount; } + // Return true if the event is for a device that is part of the currently visible page + function isEventWithinPage(ids) { + if (obj.visibleDevices == null) return true; // Add devices are visible + var r = true; + for (var i in ids) { + // If the event is for a visible device, return true + if (ids[i].startsWith('node/')) { r = false; if (obj.visibleDevices[ids[i]] != null) return true; } + } + return r; // If this event is not for any specific device, return true + } + return obj; };