diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 532cf25cd8..cfd70d9fa4 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -113,6 +113,7 @@ + diff --git a/meshmessaging.js b/meshmessaging.js index 3c7a8f0222..9f990a6b9c 100644 --- a/meshmessaging.js +++ b/meshmessaging.js @@ -117,8 +117,8 @@ module.exports.CreateServer = function (parent) { return lines[templateNumber]; } - // Send phone number verification SMS - obj.sendPhoneCheck = function (domain, to, verificationCode, language, func) { + // Send messaging account verification + obj.sendMessagingCheck = function (domain, to, verificationCode, language, func) { parent.debug('email', "Sending verification message to " + to); var sms = getTemplate(0, domain, language); @@ -128,11 +128,11 @@ module.exports.CreateServer = function (parent) { sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); sms = sms.split('[[1]]').join(verificationCode); - // Send the SMS - obj.sendSMS(to, sms, func); + // Send the message + obj.sendMessage(to, sms, func); }; - // Send phone number verification SMS + // Send 2FA verification obj.sendToken = function (domain, to, verificationCode, language, func) { parent.debug('email', "Sending login token message to " + to); @@ -143,8 +143,8 @@ module.exports.CreateServer = function (parent) { sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral'); sms = sms.split('[[1]]').join(verificationCode); - // Send the SMS - obj.sendSMS(to, sms, func); + // Send the message + obj.sendMessage(to, sms, func); }; return obj; diff --git a/meshuser.js b/meshuser.js index 80c3fd6d2a..7b855f8b14 100644 --- a/meshuser.js +++ b/meshuser.js @@ -5233,6 +5233,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'changelang': serverCommandChangeLang, 'close': serverCommandClose, 'confirmPhone': serverCommandConfirmPhone, + 'confirmMessaging': serverCommandConfirmMessaging, 'emailuser': serverCommandEmailUser, 'files': serverCommandFiles, 'getClip': serverCommandGetClip, @@ -5261,6 +5262,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'serverversion': serverCommandServerVersion, 'setClip': serverCommandSetClip, 'smsuser': serverCommandSmsUser, + 'msguser': serverCommandMsgUser, 'trafficdelta': serverCommandTrafficDelta, 'trafficstats': serverCommandTrafficStats, 'updateAgents': serverCommandUpdateAgents, @@ -5268,7 +5270,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'urlargs': serverCommandUrlArgs, 'users': serverCommandUsers, 'verifyemail': serverCommandVerifyEmail, - 'verifyPhone': serverCommandVerifyPhone + 'verifyPhone': serverCommandVerifyPhone, + 'verifyMessaging': serverCommandVerifyMessaging }; const serverUserCommands = { @@ -6026,6 +6029,35 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); } + function serverCommandConfirmMessaging(command) { + // Do not allow this command when logged in using a login token + if (req.session.loginToken != null) return; + + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here. + if ((parent.parent.msgserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'string') || (obj.failedMsgCookieCheck == 1)) return; // Input checks + var cookie = parent.parent.decodeCookie(command.cookie); + if (cookie == null) return; // Invalid cookie + if (cookie.s != ws.sessionId) return; // Invalid session + if (cookie.c != command.code) { + obj.failedMsgCookieCheck = 1; + // Code does not match, delay the response to limit how many guesses we can make and don't allow more than 1 guess at any given time. + setTimeout(function () { + ws.send(JSON.stringify({ action: 'verifyMessaging', cookie: command.cookie, success: true })); + delete obj.failedMsgCookieCheck; + }, 2000 + (parent.crypto.randomBytes(2).readUInt16BE(0) % 4095)); + return; + } + + // Set the user's messaging handle + user.msghandle = cookie.p; + db.SetUser(user); + + // Event the change + var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 156, msgArgs: [user.name], msg: 'Verified messaging account of user ' + EscapeHtml(user.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(['*', 'server-users', user._id], obj, event); + } + function serverCommandEmailUser(command) { var errMsg = null, emailuser = null; if (domain.mailserver == null) { errMsg = 'Email server not enabled'; } @@ -6498,6 +6530,29 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); } + function serverCommandMsgUser(command) { + var errMsg = null, msguser = null; + if ((parent.parent.msgserver == null) || (parent.parent.msgserver.providers == 0)) { errMsg = "Messaging server not enabled"; } + else if ((user.siteadmin & 2) == 0) { errMsg = "No user management rights"; } + else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = "Invalid username"; } + else if (common.validateString(command.msg, 1, 160) == false) { errMsg = "Invalid message"; } + else { + msguser = parent.users[command.userid]; + if (msguser == null) { errMsg = "Invalid username"; } + else if (msguser.msghandle == null) { errMsg = "No messaging service configured for this user"; } + } + + if (errMsg != null) { displayNotificationMessage(errMsg); return; } + + parent.parent.msgserver.sendMessage(msguser.msghandle, command.msg, function (success, msg) { + if (success) { + displayNotificationMessage("Message succesfuly sent.", null, null, null, 32); + } else { + if (typeof msg == 'string') { displayNotificationMessage("Messaging error: " + msg, null, null, null, 34, [msg]); } else { displayNotificationMessage("Messaging error", null, null, null, 33); } + } + }); + } + function serverCommandTrafficDelta(command) { const stats = parent.getTrafficDelta(obj.trafficStats); obj.trafficStats = stats.current; @@ -6606,6 +6661,27 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); } + function serverCommandVerifyMessaging(command) { + // Do not allow this command when logged in using a login token + if (req.session.loginToken != null) return; + + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here. + if (parent.parent.msgserver == null) return; + if (common.validateString(command.handle, 1, 64) == false) return; // Check handle length + + // Setup the handle for the right messaging service + var handle = null; + if ((command.service == 1) && ((parent.parent.msgserver.providers & 1) != 0)) { handle = 'telegram:@' + command.handle; } + if (handle == null) return; + + // Send a verification message + const code = common.zeroPad(getRandomSixDigitInteger(), 6); + const messagingCookie = parent.parent.encodeCookie({ a: 'verifyMessaging', c: code, p: handle, s: ws.sessionId }); + parent.parent.msgserver.sendMessagingCheck(domain, handle, code, parent.getLanguageCodes(req), function (success) { + ws.send(JSON.stringify({ action: 'verifyMessaging', cookie: messagingCookie, success: success })); + }); + } + function serverUserCommandHelp(cmdData) { var fin = '', f = '', availcommands = []; for (var i in serverUserCommands) { availcommands.push(i); } diff --git a/public/images/messaging40.png b/public/images/messaging40.png new file mode 100644 index 0000000000..a826e90052 Binary files /dev/null and b/public/images/messaging40.png differ diff --git a/views/default.handlebars b/views/default.handlebars index bcba66230d..ad714baf05 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -425,6 +425,7 @@ +
View previous logins
@@ -432,6 +433,7 @@

Account actions

+ Localization Settings
@@ -2139,6 +2141,8 @@ QV('p2AccountActions', !accountSettingsLocked) QV('managePhoneNumber1', (features & 0x02000000) && (features & 0x04000000) && (serverinfo.lock2factor != true)); QV('managePhoneNumber2', (features & 0x02000000) && !(features & 0x04000000) && (serverinfo.lock2factor != true)); + QV('manageMessaging1', (features2 & 0x02000000) && (features2 & 0x04000000) && (serverinfo.lock2factor != true)); + QV('manageMessaging2', (features2 & 0x02000000) && !(features2 & 0x04000000) && (serverinfo.lock2factor != true)); QV('manageEmail2FA', (features & 0x00800000) && (serverinfo.lock2factor != true)); QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false) && (userinfo != null) && (userinfo._id.split('/')[2].startsWith('~') == false)); // Hide Account Actions if in single user mode or domain authentication //QV('p2AccountImage', ((features & 4) == 0) && (serverinfo.domainauth == false)); // If account actions are not visible, also remove the image on that panel @@ -2238,6 +2242,7 @@ QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true) && (accountSettingsLocked == false)); QV('manageOtp', (serverinfo.lock2factor != true) && (authFactorCount > 0) && ((features2 & 0x40000) == 0)); QV('authPhoneNumberCheck', (userinfo.phone != null)); + QV('authMessagingCheck', (userinfo.msghandle != null)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authAppSetupCheck', userinfo.otpsecret == 1); QV('manageAuthApp', (serverinfo.lock2factor != true) && ((userinfo.otpsecret == 1) || ((features2 & 0x00020000) == 0))); @@ -2953,6 +2958,16 @@ account_managePhoneCodeValidate(); break; } + case 'verifyMessaging': { + if (xxdialogMode && (xxdialogTag != 'verifyMessaging')) return; + var x = '
'; + x += '' + "Check your messaging application and enter the verification code."; + x += '

' + "Verification code:" + '
'; + setDialogMode(2, "Messaging Notifications", 3, account_manageMessagingConfirm, x, message.cookie); + Q('d2phoneCodeInput').focus(); + account_managePhoneCodeValidate(); + break; + } case 'fileoperation': { // View the file in the dialog box var p5editSaveBack = function(b, tag) { @@ -11920,7 +11935,7 @@ var x; if (userinfo.phone != null) { x = '
'; - x += '
' + "Verified phone number" + '
' + userinfo.phone + '
'; + x += '
' + "Verified phone number" + '
' + EscapeHtml(userinfo.phone) + '
'; x += '
'; setDialogMode(2, "Phone Notifications", 3, account_managePhoneRemove, x); account_managePhoneRemoveValidate(); @@ -11942,6 +11957,35 @@ function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } } function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); } + function account_manageMessaging() { + if (xxdialogMode || ((features2 & 0x02000000) == 0)) return; + var x; + if (userinfo.msghandle != null) { + x = '
'; + x += '
' + "Verified handle" + '
' + EscapeHtml(userinfo.msghandle) + '
'; + x += '
'; + setDialogMode(2, "Messaging Notifications", 3, account_managePhoneRemove, x); + account_managePhoneRemoveValidate(); + } else { + x = '
'; + x += '' + "Enter your messaging service and handle. Once verified, this server can send you login verification and other notifications." + '

'; + var y = ''; + x += '
' + "Service" + '' + y; + x += '
' + "Handle" + ''; + x += '
'; + setDialogMode(2, "Messaging Notifications", 3, account_manageMessagingAdd, x, 'verifyMessaging'); + Q('d2handleinput').focus(); + account_manageMessagingValidate(); + } + } + + function account_manageMessagingValidate(x) { var ok = (Q('d2handleinput').value.length > 0); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } } + function account_manageMessagingAdd() { if (Q('d2handleinput').value.length == 0) return; QE('d2handleinput', false); meshserver.send({ action: 'verifyMessaging', service: Q('d2serviceselect').value, handle: Q('d2handleinput').value }); } + function account_manageMessagingConfirm(b, tag) { meshserver.send({ action: 'confirmMessaging', code: Q('d2phoneCodeInput').value, cookie: tag }); } + function account_manageAuthEmail() { if (xxdialogMode || ((features & 0x00800000) == 0)) return; var emailU2Fenabled = ((userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); @@ -14279,7 +14323,9 @@ 152: "No longer a relay for \"{0}\".", 153: "Is a relay for \"{0}\".", 154: "Account changed to sync with LDAP data.", - 155: "Denied user login from {0}, {1}, {2}" + 155: "Denied user login from {0}, {1}, {2}", + 156: "Verified messaging account of user {0}", + 157: "Removed messaging account of user {0}" }; var eventsShortMessageId = { @@ -14723,6 +14769,15 @@ function showSendSMSValidate() { QE('idx_dlgOkButton', Q('d2smsText').value.length > 0); } function showSendSMSEx(b, tag) { if (Q('d2smsText').value.length > 0) { meshserver.send({ action: 'smsuser', userid: decodeURIComponent(tag), msg: Q('d2smsText').value }); } } + function showSendMessage(userid) { + if (xxdialogMode) return; + setDialogMode(2, "Send Message", 3, showSendMessageEx, '', userid); + Q('d2smsText').focus(); + showSendSMSValidate(); + } + + function showSendMessageEx(b, tag) { if (Q('d2smsText').value.length > 0) { meshserver.send({ action: 'msguser', userid: decodeURIComponent(tag), msg: Q('d2smsText').value }); } } + function showSendEmail(userid) { if (xxdialogMode) return; var x = ''; @@ -15630,6 +15685,10 @@ x += addDeviceAttribute("Phone Number", (user.phone?user.phone:('' + "None" + '')) + ' '); } + if ((features2 & 0x02000000) || (user.msghandle != null)) { // If user messaging is enabled on the server or user has a messaging handle + x += addDeviceAttribute("Messaging", (user.msghandle?user.msghandle:('' + "None" + '')) + ' '); + } + // Display features var userFeatures = []; if ((serverinfo.usersSessionRecording == 1) && (user.flags) && (user.flags & 2)) { userFeatures.push("Record Sessions"); } @@ -15706,6 +15765,7 @@ // Add action buttons x += ''; if (user.phone && (features & 0x02000000)) { x += ''; } + if (user.msghandle && (features2 & 0x02000000)) { x += ''; } if ((typeof user.email == 'string') && (user.emailVerified === true) && (features & 0x00000040)) { x += ''; } if (!self && ((activeSessions > 0) || ((features2 & 8) && (user.webpush)))) { x += ''; @@ -15771,6 +15831,16 @@ p30editPhoneValidate(); } + function p30editMessaging() { // TODO + if (xxdialogMode) return; + var x = '
'; + x += '' + "SMS capable phone number for this user." + '
' + "Leave blank for none."; + x += '

' + "Phone number:" + '
'; + setDialogMode(2, "Phone Notifications", 3, p30editPhoneEx, x, 'verifyPhone'); + Q('d2phoneinput').focus(); + p30editPhoneValidate(); + } + function p20edituserfeatures() { if (xxdialogMode) return; var flags = (currentUser.flags)?currentUser.flags:0, x = ''; // Flags: 1 = Account Image, 2 = Session Recording @@ -16866,10 +16936,14 @@ "No user management rights", "Invalid SMS message", "No phone number for this user", - "SMS succesfuly sent.", + "SMS succesfully sent.", "SMS error", "SMS error: {0}", - "Email domain \"{0}\" is not allowed. Only ({1}) are allowed" // 30 + "Email domain \"{0}\" is not allowed. Only ({1}) are allowed", // 30, + "Invalid message", + "Message succesfully sent.", + "Message error", + "Message error: {0}" ]; if (typeof n.titleid == 'number') { try { n.title = translatedTitles[n.titleid]; } catch (ex) {} } if (typeof n.msgid == 'number') { try { n.text = translatedMessages[n.msgid]; if (Array.isArray(n.args)) { n.text = format(n.text, n.args[0], n.args[1], n.args[2], n.args[3], n.args[4], n.args[5]); } } catch (ex) { } }