Fixed desktop multiplexor race condition, web application improvements.

This commit is contained in:
Ylian Saint-Hilaire 2020-04-30 18:26:53 -07:00
parent ea9edefad1
commit b09efc7df5
5 changed files with 1659 additions and 1598 deletions

View File

@ -203,6 +203,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
// Clean up ourselves // Clean up ourselves
function dispose() { function dispose() {
if (obj.viewers == null) return;
//console.log('dispose', obj.nodeid); //console.log('dispose', obj.nodeid);
delete obj.viewers; delete obj.viewers;
delete obj.imagesCounters; delete obj.imagesCounters;
@ -226,6 +227,8 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
parent.parent.DispatchEvent(['*', obj.nodeid], obj, event); // TODO: Add Node MeshID to targets parent.parent.DispatchEvent(['*', obj.nodeid], obj, event); // TODO: Add Node MeshID to targets
obj.startTime = null; obj.startTime = null;
} }
parent.parent.debug('relay', 'DesktopRelay: Disposing desktop multiplexor');
} }
// Send data to the agent or queue it up for sending // Send data to the agent or queue it up for sending
@ -715,7 +718,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
} }
// If there is no authentication, drop this connection // If there is no authentication, drop this connection
if ((obj.id != null) && (obj.user == null) && (obj.ruserid == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with no authentication (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } return; } if ((obj.id != null) && (obj.user == null) && (obj.ruserid == null)) { try { ws.close(); parent.parent.debug('relay', 'DesktopRelay: Connection with no authentication (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } return; }
// Relay session count (we may remove this in the future) // Relay session count (we may remove this in the future)
obj.relaySessionCounted = true; obj.relaySessionCounted = true;
@ -748,8 +751,8 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
if (obj.ws == null) return; // Already closed. if (obj.ws == null) return; // Already closed.
// Close the connection // Close the connection
if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Soft disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'DesktopRelay: Soft disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket
if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'Relay: Hard disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'DesktopRelay: Hard disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; } if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; }
if (obj.deskDecoder != null) { if (obj.deskDecoder.removePeer(obj) == true) { delete parent.desktoprelays[obj.nodeid]; } } if (obj.deskDecoder != null) { if (obj.deskDecoder.removePeer(obj) == true) { delete parent.desktoprelays[obj.nodeid]; } }
@ -826,9 +829,9 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } } catch (ex) { } try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } } catch (ex) { }
} }
function performRelay() { function performRelay(retryCount) {
if (obj.id == null) { try { obj.close(); } catch (e) { } return null; } // Attempt to connect without id, drop this. if ((obj.id == null) || (retryCount > 20)) { try { obj.close(); } catch (e) { } return null; } // Attempt to connect without id, drop this.
ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive if (retryCount == 0) { ws._socket.setKeepAlive(true, 240000); } // Set TCP keep alive
/* /*
// Validate that the id is valid, we only need to do this on non-authenticated sessions. // Validate that the id is valid, we only need to do this on non-authenticated sessions.
@ -843,13 +846,19 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
} }
*/ */
if (retryCount == 0) {
// Setup the agent PING/PONG timers // Setup the agent PING/PONG timers
if ((typeof parent.parent.args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, parent.parent.args.agentping * 1000); } if ((typeof parent.parent.args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, parent.parent.args.agentping * 1000); }
else if ((typeof parent.parent.args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, parent.parent.args.agentpong * 1000); } else if ((typeof parent.parent.args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, parent.parent.args.agentpong * 1000); }
parent.parent.debug('relay', 'DesktopRelay: Connection (' + cleanRemoteAddr(obj.req.ip) + ')');
}
// Create if needed and add this peer to the desktop multiplexor // Create if needed and add this peer to the desktop multiplexor
obj.deskDecoder = parent.desktoprelays[obj.nodeid]; obj.deskDecoder = parent.desktoprelays[obj.nodeid];
if (obj.deskDecoder == null) { if (obj.deskDecoder == null) {
parent.desktoprelays[obj.nodeid] = 1; // Indicate that the creating of the desktop multiplexor is pending.
parent.parent.debug('relay', 'DesktopRelay: Creating new desktop multiplexor');
CreateDesktopMultiplexor(parent, domain, obj.nodeid, function (deskDecoder) { CreateDesktopMultiplexor(parent, domain, obj.nodeid, function (deskDecoder) {
obj.deskDecoder = deskDecoder; obj.deskDecoder = deskDecoder;
parent.desktoprelays[obj.nodeid] = obj.deskDecoder; parent.desktoprelays[obj.nodeid] = obj.deskDecoder;
@ -857,8 +866,14 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
ws._socket.resume(); // Release the traffic ws._socket.resume(); // Release the traffic
}); });
} else { } else {
if (obj.deskDecoder == 1) {
// The multiplexor is being created, hold a little and try again. This is to prevent a possible race condition.
setTimeout(function () { performRelay(++retryCount); }, 50);
} else {
// Hook up this peer to the multiplexor and release the traffic
obj.deskDecoder.addPeer(obj); obj.deskDecoder.addPeer(obj);
ws._socket.resume(); // Release the traffic ws._socket.resume();
}
} }
} }
@ -902,7 +917,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr }; const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr };
parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command));
if (obj.sendAgentMessage(command, user._id, cookie.domainid) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); } if (obj.sendAgentMessage(command, user._id, cookie.domainid) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); }
performRelay(); performRelay(0);
}); });
return obj; return obj;
} else if ((obj.req.query.nodeid != null) && ((obj.req.query.tcpport != null) || (obj.req.query.udpport != null))) { } else if ((obj.req.query.nodeid != null) && ((obj.req.query.tcpport != null) || (obj.req.query.udpport != null))) {
@ -927,14 +942,14 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
parent.parent.debug('relay', 'Relay: Sending agent UDP tunnel command: ' + JSON.stringify(command)); parent.parent.debug('relay', 'Relay: Sending agent UDP tunnel command: ' + JSON.stringify(command));
if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); } if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); }
} }
performRelay(); performRelay(0);
}); });
return obj; return obj;
} }
} }
// If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session. // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
performRelay(); performRelay(0);
return obj; return obj;
}; };

View File

@ -1666,6 +1666,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if ((command.emailVerified === true || command.emailVerified === false) && (chguser.emailVerified != command.emailVerified)) { chguser.emailVerified = command.emailVerified; change = 1; } if ((command.emailVerified === true || command.emailVerified === false) && (chguser.emailVerified != command.emailVerified)) { chguser.emailVerified = command.emailVerified; change = 1; }
if ((common.validateInt(command.quota, 0) || command.quota == null) && (command.quota != chguser.quota)) { chguser.quota = command.quota; if (chguser.quota == null) { delete chguser.quota; } change = 1; } if ((common.validateInt(command.quota, 0) || command.quota == null) && (command.quota != chguser.quota)) { chguser.quota = command.quota; if (chguser.quota == null) { delete chguser.quota; } change = 1; }
if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { chguser.consent = command.consent; } change = 1; } if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { chguser.consent = command.consent; } change = 1; }
if ((command.phone != null) && (typeof command.phone == 'string') && ((command.phone == '') || isPhoneNumber(command.phone))) { if (command.phone == '') { delete chguser.phone; } else { chguser.phone = command.phone; } change = 1; }
// Site admins can change any server rights, user managers can only change AccountLock, NoMeshCmd and NoNewGroups // Site admins can change any server rights, user managers can only change AccountLock, NoMeshCmd and NoNewGroups
if (chguser._id !== user._id) { // We can't change our own siteadmin permissions. if (chguser._id !== user._id) { // We can't change our own siteadmin permissions.
@ -4202,6 +4203,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Split a string taking into account the quoats. Used for command line parsing // Split a string taking into account the quoats. Used for command line parsing
function splitArgs(str) { var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi; do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null); return myArray; } function splitArgs(str) { var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi; do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null); return myArray; }
function toNumberIfNumber(x) { if ((typeof x == 'string') && (+parseInt(x) === x)) { x = parseInt(x); } return x; } function toNumberIfNumber(x) { if ((typeof x == 'string') && (+parseInt(x) === x)) { x = parseInt(x); } return x; }
function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3,4})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) }
function removeAllUnderScore(obj) { function removeAllUnderScore(obj) {
if (typeof obj != 'object') return obj; if (typeof obj != 'object') return obj;

View File

@ -1,6 +1,6 @@
{ {
"name": "meshcentral", "name": "meshcentral",
"version": "0.5.18", "version": "0.5.19",
"keywords": [ "keywords": [
"Remote Management", "Remote Management",
"Intel AMT", "Intel AMT",

File diff suppressed because it is too large Load Diff

View File

@ -2213,7 +2213,7 @@
case 'verifyPhone': { case 'verifyPhone': {
if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return; if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>'; var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>';
x += '<td>Check your phone and enter the verification code.'; x += '<td>' + "Check your phone and enter the verification code.";
x += '<br /><br /><div style=width:100%;text-align:center>' + "Verification code:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>'; x += '<br /><br /><div style=width:100%;text-align:center>' + "Verification code:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=6 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie); setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie);
Q('d2phoneCodeInput').focus(); Q('d2phoneCodeInput').focus();
@ -7943,7 +7943,7 @@
account_managePhoneRemoveValidate(); account_managePhoneRemoveValidate();
} else { } else {
x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>'; x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
x += '<td>Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.'; x += '<td>' + "Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.";
x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>'; x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>';
setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone'); setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone');
Q('d2phoneinput').focus(); Q('d2phoneinput').focus();
@ -9839,10 +9839,10 @@
if (user.email != null) { if (user.email != null) {
if (((features & 0x200000) == 0) || (user.email.toLowerCase() != user.name.toLowerCase())) { if (((features & 0x200000) == 0) || (user.email.toLowerCase() != user.name.toLowerCase())) {
// Username & email are different // Username & email are different
username += ', <a href=# onclick=\'return doemail(event,\"' + user.email + '\")\'>' + user.email + '</a>' + emailVerified; username += ', <a href="mailto:' + user.email + '" \'>' + user.email + '</a>' + emailVerified;
} else { } else {
// Username & email are the same // Username & email are the same
username += ' <a href=# onclick=\'return doemail(event,\"' + user.email + '\")\'><img src="images/mail12.png" height=9 width=12 title="' + "Send email to user" + '" style="margin-top:2px" /></a>' + emailVerified; username += ' <a href="mailto:' + user.email + '" \'><img src="images/mail12.png" height=9 width=12 title="' + "Send email to user" + '" style="margin-top:2px" /></a>' + emailVerified;
} }
} }
@ -9851,7 +9851,7 @@
if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { username += ' <img src="images/padlock12.png" height=12 width=8 title="' + "Account is locked" + '" style="margin-top:2px" />'; } if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) { username += ' <img src="images/padlock12.png" height=12 width=8 title="' + "Account is locked" + '" style="margin-top:2px" />'; }
x += '<tr tabindex=0 onmouseover=userMouseHover(this,1) onmouseout=userMouseHover(this,0) onkeypress="if (event.key==\'Enter\') gotoUser(\'' + encodeURIComponent(user._id) + '\')"><td>'; x += '<tr tabindex=0 onmouseover=userMouseHover(this,1) onmouseout=userMouseHover(this,0) onkeypress="if (event.key==\'Enter\') gotoUser(\'' + encodeURIComponent(user._id) + '\')"><td>';
x += '<div class=bar>'; x += '<div class=bar>';
x += '<div class=baricon><input class=UserCheckbox value=' + encodeURIComponent(user._id) + ' onclick=p3updateInfo() type=checkbox' + ((user._id == userinfo._id)?' disabled':'') + '></div><div style=cursor:pointer onclick=gotoUser(\"' + encodeURIComponent(user._id) + '\")>'; x += '<div class=baricon><input class=UserCheckbox value=' + encodeURIComponent(user._id) + ' onclick=p3updateInfo() type=checkbox' + ((user._id == userinfo._id)?' disabled':'') + '></div><div style=cursor:pointer onclick=gotoUser(\"' + encodeURIComponent(user._id) + '\",false,event)>';
x += '<div class=baricon><div class="' + icon + gray + '"></div></div>'; x += '<div class=baricon><div class="' + icon + gray + '"></div></div>';
x += '<div class=g1></div><div class=g2></div><div>'; x += '<div class=g1></div><div class=g2></div><div>';
x += '<div><span>' + username + '</span>' + msg + '</div></div><td style=text-align:center>' + groups + '<td style=text-align:center>' + lastAccess + '<td style=text-align:center>' + permissions; x += '<div><span>' + username + '</span>' + msg + '</div></div><td style=text-align:center>' + groups + '<td style=text-align:center>' + lastAccess + '<td style=text-align:center>' + permissions;
@ -9975,13 +9975,6 @@
function showUserAlertDialogEx(button, userid) { meshserver.send({ action: 'notifyuser', userid: decodeURIComponent(userid), msg: Q('d2notifyText').value }); } function showUserAlertDialogEx(button, userid) { meshserver.send({ action: 'notifyuser', userid: decodeURIComponent(userid), msg: Q('d2notifyText').value }); }
function doemail(e, addr) {
if (xxdialogMode) return false;
haltEvent(e);
window.open('mailto:' + addr);
return false;
}
function p4batchAccountCreate() { function p4batchAccountCreate() {
if (xxdialogMode) return; if (xxdialogMode) return;
var x = "Create many accounts at once by importing a JSON file with the following format:" + '<br /><pre>[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]</pre><input style=width:370px type=file id=d4importFile accept=".json" onchange=p4batchAccountCreateValidate() />'; var x = "Create many accounts at once by importing a JSON file with the following format:" + '<br /><pre>[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]</pre><input style=width:370px type=file id=d4importFile accept=".json" onchange=p4batchAccountCreateValidate() />';
@ -10633,8 +10626,9 @@
// //
var currentUser = null; var currentUser = null;
function gotoUser(userid, force) { function gotoUser(userid, force, event) {
if (xxdialogMode && !force) return; if (xxdialogMode && !force) return;
if ((event != null) && (event.originalTarget != null) && (event.originalTarget.href != null)) return;
var user = currentUser = users[decodeURIComponent(userid)]; var user = currentUser = users[decodeURIComponent(userid)];
if (user == null) { setDialogMode(0); go(4); return; } if (user == null) { setDialogMode(0); go(4); return; }
QH('p30userName', user.name); QH('p30userName', user.name);
@ -10657,13 +10651,17 @@
var email = user.email?EscapeHtml(user.email):'<i>' + "Not set" + '</i>', everify = ''; var email = user.email?EscapeHtml(user.email):'<i>' + "Not set" + '</i>', everify = '';
if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title=\"' + "Email is verified" + '\">&#x2713</b> ' : '<b style=color:red;cursor:pointer title=\"' + "Email not verified" + '\">&#x2717;</b> '); } if (serverinfo.emailcheck) { everify = ((user.emailVerified == true) ? '<b style=color:green;cursor:pointer title=\"' + "Email is verified" + '\">&#x2713</b> ' : '<b style=color:red;cursor:pointer title=\"' + "Email not verified" + '\">&#x2717;</b> '); }
if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("User Identifier", user._id.split('/')[2]); } if (user.name.toLowerCase() != user._id.split('/')[2]) { x += addDeviceAttribute("User Identifier", user._id.split('/')[2]); }
if (((features & 0x200000) == 0) && ((user.siteadmin != 0xFFFFFFFF) || (userinfo.siteadmin == 0xFFFFFFFF))) { // If we are not site admin, we can't change a admin email. if (((user.siteadmin != 0xFFFFFFFF) || (userinfo.siteadmin == 0xFFFFFFFF))) { // If we are not site admin, we can't change a admin email.
x += addDeviceAttribute("Email", everify + '<a href=# style=cursor:pointer onclick=p30showUserEmailChangeDialog(event,\"' + userid + '\")>' + email + '</a> <a href=# style=cursor:pointer onclick=\'return doemail(event,\"' + user.email + '\")\'><img class=hoverButton src="images/link1.png" /></a>'); x += addDeviceAttribute("Email", everify + email + ' <a href="mailto:' + user.email + '" \'><img class=hoverButton src="images/link1.png" /></a>' + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30showUserEmailChangeDialog(event,\"' + userid + '\") />');
} else { } else {
x += addDeviceAttribute("Email", everify + email + ' <a href=# style=cursor:pointer onclick=\'return doemail(event,\"' + user.email + '\")\'><img class=hoverButton src="images/link1.png" /></a>'); x += addDeviceAttribute("Email", everify + email + ' <a href="mailto:' + user.email + '" \'><img class=hoverButton src="images/link1.png" /></a>');
} }
if (user.phone != null) { x += addDeviceAttribute("Phone Number", user.phone); }
x += addDeviceAttribute("Server Rights", premsg + '<a href=# style=cursor:pointer onclick=\'return showUserAdminDialog(event,\"' + userid + '\")\'>' + msg.join(', ') + '</a>'); if ((features & 0x02000000) || (user.phone != null)) { // If SMS is enabled on the server or user has a phone number
x += addDeviceAttribute("Phone Number", (user.phone?user.phone:('<i>' + "None" + '</i>')) + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30editPhone() />');
}
x += addDeviceAttribute("Server Rights", premsg + msg.join(', ') + ' <img style=cursor:pointer class=hoverButton onclick=\'return showUserAdminDialog(event,\"' + userid + '\")\' src="images/link5.png" />');
if (user.quota) x += addDeviceAttribute("Server Quota", EscapeHtml(parseInt(user.quota) / 1024) + ' k'); if (user.quota) x += addDeviceAttribute("Server Quota", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
x += addDeviceAttribute("Creation", printDateTime(new Date(user.creation * 1000))); x += addDeviceAttribute("Creation", printDateTime(new Date(user.creation * 1000)));
if (user.login) x += addDeviceAttribute("Last Login", printDateTime(new Date(user.login * 1000))); if (user.login) x += addDeviceAttribute("Last Login", printDateTime(new Date(user.login * 1000)));
@ -10756,6 +10754,25 @@
} }
} }
function p30editPhone() {
if (xxdialogMode) return;
var x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
x += '<td style=width:100%;text-align:center>' + "SMS capable phone number for this user." + '<br />' + "Leave blank for none.";
x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]" autocomplete="tel" value="' + (currentUser.phone?currentUser.phone:'') + '" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=p30editPhoneValidate() onkeypress="if (event.key==\'Enter\') p30editPhoneValidate(1)"></div></table>';
setDialogMode(2, "Phone Notifications", 3, p30editPhoneEx, x, 'verifyPhone');
Q('d2phoneinput').focus();
p30editPhoneValidate();
}
function p30editPhoneValidate(x) {
var ok = (Q('d2phoneinput').value == '') || (isPhoneNumber(Q('d2phoneinput').value));
QE('idx_dlgOkButton', ok);
if ((x == 1) && ok) { dialogclose(1); }
}
// Send to the server the user's new phone number
function p30editPhoneEx() { meshserver.send({ action: 'edituser', id: currentUser._id, phone: Q('d2phoneinput').value }); }
// Display the user's email change dialog box // Display the user's email change dialog box
function p30showUserEmailChangeDialog(event) { function p30showUserEmailChangeDialog(event) {
if (xxdialogMode) return false; if (xxdialogMode) return false;