diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj
index 532cf25c..cfd70d9f 100644
--- a/MeshCentralServer.njsproj
+++ b/MeshCentralServer.njsproj
@@ -113,6 +113,7 @@
+
diff --git a/meshmessaging.js b/meshmessaging.js
index 3c7a8f02..9f990a6b 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 80c3fd6d..7b855f8b 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 00000000..a826e900
Binary files /dev/null and b/public/images/messaging40.png differ
diff --git a/views/default.handlebars b/views/default.handlebars
index bcba6623..ad714baf 100644
--- a/views/default.handlebars
+++ b/views/default.handlebars
@@ -425,6 +425,7 @@
+
View previous logins
@@ -432,6 +433,7 @@
Account actions
Manage phone number
+ Manage messaging
Verify email
Enable web notifications
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 = '
';
+ 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 += '';
+ 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 = '';
+ 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) { } }
|
|
|