diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj
index 697f5c3d..566e0376 100644
--- a/MeshCentralServer.njsproj
+++ b/MeshCentralServer.njsproj
@@ -108,6 +108,7 @@
+
diff --git a/agents/meshcore.js b/agents/meshcore.js
index c56b0b67..833e874b 100644
--- a/agents/meshcore.js
+++ b/agents/meshcore.js
@@ -280,7 +280,7 @@ function createMeshCore(agent) {
*/
// MeshAgent JavaScript Core Module. This code is sent to and running on the mesh agent.
- var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ? ('MeshCore CRC[' + crc32c(require('MeshAgent').coreHash) + ']') : ('MeshCore v6')), caps: 14 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent
+ var meshCoreObj = { action: 'coreinfo', value: (require('MeshAgent').coreHash ? ('MeshCore CRC-' + crc32c(require('MeshAgent').coreHash)) : ('MeshCore v6')), caps: 14 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent
// Get the operating system description string
@@ -1995,7 +1995,7 @@ function createMeshCore(agent) {
var response = null;
switch (cmd) {
case 'help': { // Displays available commands
- var fin = '', f = '', availcommands = 'startupoptions,alert,agentsize,version,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt,wallpaper';
+ var fin = '', f = '', availcommands = 'startupoptions,alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt,wallpaper';
if (process.platform == 'win32') { availcommands += ',safemode,wpfhwacceleration'; }
if (require('MeshAgent').maxKvmTileSize != null) { availcommands += ',kvmmode'; }
@@ -2059,9 +2059,6 @@ function createMeshCore(agent) {
} else { response = "Agent Size: " + actualSize + " kb"; }
} else { response = "Agent Size: " + actualSize + " kb"; }
break;
- case 'version':
- response = "Mesh Agent Version: " + process.versions.meshAgent;
- break;
case 'versions':
response = JSON.stringify(process.versions, null, ' ');
break;
diff --git a/emails/sms-messages.txt b/emails/sms-messages.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/sms-messages.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_cs.txt b/emails/translations/sms-messages_cs.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_cs.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_de.txt b/emails/translations/sms-messages_de.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_de.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_es.txt b/emails/translations/sms-messages_es.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_es.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_fr.txt b/emails/translations/sms-messages_fr.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_fr.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_hi.txt b/emails/translations/sms-messages_hi.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_hi.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_ja.txt b/emails/translations/sms-messages_ja.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_ja.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_ko.txt b/emails/translations/sms-messages_ko.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_ko.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_nl.txt b/emails/translations/sms-messages_nl.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_nl.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_pt.txt b/emails/translations/sms-messages_pt.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_pt.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_ru.txt b/emails/translations/sms-messages_ru.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_ru.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/emails/translations/sms-messages_zh-chs.txt b/emails/translations/sms-messages_zh-chs.txt
new file mode 100644
index 00000000..cffc6c48
--- /dev/null
+++ b/emails/translations/sms-messages_zh-chs.txt
@@ -0,0 +1,2 @@
+[[0]] verification code is: [[1]]
+[[0]] access token is: [[1]]
diff --git a/meshcentral.js b/meshcentral.js
index 4f9b927c..a24b3ccf 100644
--- a/meshcentral.js
+++ b/meshcentral.js
@@ -30,6 +30,7 @@ function CreateMeshCentralServer(config, args) {
obj.mqttbroker = null;
obj.swarmserver = null;
obj.mailserver = null;
+ obj.smsserver = null;
obj.amtEventHandler = null;
obj.pluginHandler = null;
obj.amtScanner = null;
@@ -1323,6 +1324,15 @@ function CreateMeshCentralServer(config, args) {
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); }
}
+ // Setup SMS gateway
+ if (config.sms != null) {
+ obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
+ if (obj.smsserver != null) {
+ //obj.smsserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMS gateway has limited use in LAN mode."); }
+ }
+ }
+
// Start periodic maintenance
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
@@ -2560,7 +2570,7 @@ function mainStart() {
}
// SMS support
- if ((config.sms != null) && (config.sms.provider == 'twilio') && (typeof config.sms.sid == 'string') && (typeof config.sms.auth == 'string')) { modules.push('twilio'); }
+ if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); }
// Syslog support
if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog'); }
diff --git a/meshmail.js b/meshmail.js
index 7227ec9e..87ae89f5 100644
--- a/meshmail.js
+++ b/meshmail.js
@@ -160,7 +160,7 @@ module.exports.CreateMeshMail = function (parent) {
var template = getTemplate('account-login', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
- parent.debug('email', "Error: Failed to get mail template."); // Not email template found
+ parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
@@ -187,7 +187,7 @@ module.exports.CreateMeshMail = function (parent) {
var template = getTemplate('account-invite', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
- parent.debug('email', "Error: Failed to get mail template."); // Not email template found
+ parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
@@ -214,7 +214,7 @@ module.exports.CreateMeshMail = function (parent) {
var template = getTemplate('account-check', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
- parent.debug('email', "Error: Failed to get mail template."); // Not email template found
+ parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
@@ -242,7 +242,7 @@ module.exports.CreateMeshMail = function (parent) {
var template = getTemplate('account-reset', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
- parent.debug('email', "Error: Failed to get mail template."); // Not email template found
+ parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
@@ -270,7 +270,7 @@ module.exports.CreateMeshMail = function (parent) {
var template = getTemplate('mesh-invite', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
- parent.debug('email', "Error: Failed to get mail template."); // Not email template found
+ parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
diff --git a/meshsms.js b/meshsms.js
new file mode 100644
index 00000000..fecb209e
--- /dev/null
+++ b/meshsms.js
@@ -0,0 +1,120 @@
+/**
+* @description MeshCentral SMS gateway communication module
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2020
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+// Construct a MeshAgent object, called upon connection
+module.exports.CreateMeshSMS = function (parent) {
+ var obj = {};
+ obj.parent = parent;
+ obj.provider = null;
+
+ // SMS gateway provider setup
+ switch (parent.config.sms.provider) {
+ case 'twilio': {
+ // Validate Twilio configuration values
+ if (typeof parent.config.sms.sid != 'string') { console.log('Invalid or missing SMS gateway provider sid.'); return null; }
+ if (typeof parent.config.sms.auth != 'string') { console.log('Invalid or missing SMS gateway provider auth.'); return null; }
+ if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; }
+
+ // Setup Twilio
+ var Twilio = require('twilio');
+ obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth);
+ break;
+ }
+ default: {
+ // Unknown SMS gateway provider
+ console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider);
+ return null;
+ }
+ }
+
+ // Send an SMS message
+ obj.sendSMS = function (to, msg, func) {
+ parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg);
+ console.log({ from: parent.config.sms.from, to: to, body: msg });
+ if (parent.config.sms.provider == 'twilio') {
+ obj.provider.messages.create({
+ from: parent.config.sms.from,
+ to: to,
+ body: msg
+ }, function (err, result) {
+ if (err != null) { parent.debug('email', 'SMS error: ' + JSON.stringify(err)); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); }
+ if (func != null) { func((err == null) && (result.status == 'queued'), err, result); }
+ });
+ }
+ }
+
+ // Get the correct SMS template
+ function getTemplate(templateNumber, domain, lang) {
+ parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang);
+ if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given.
+
+ var r = {}, emailsPath = null;
+ if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; }
+ else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; }
+ else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; }
+ if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null }
+
+ // Get the non-english email if needed
+ var txtfile = null;
+ if ((lang != null) && (lang != 'en')) {
+ var translationsPath = obj.parent.path.join(emailsPath, 'translations');
+ var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', 'sms-messages_' + lang + '.txt');
+ if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) {
+ txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
+ }
+ }
+
+ // Get the english email
+ if ((htmlfile == null) || (txtfile == null)) {
+ var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt');
+ if (obj.parent.fs.existsSync(pathTxt)) {
+ txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
+ }
+ }
+
+ // No email templates
+ if (txtfile == null) { return null; }
+
+ // Decode the TXT file
+ lines = txtfile.split('\r\n').join('\n').split('\n')
+ if (lines.length >= templateNumber) return null;
+
+ return lines[templateNumber];
+ }
+
+ // Send phone number verification SMS
+ obj.sendPhoneCheck = function (domain, phoneNumber, verificationCode, language, func) {
+ parent.debug('email', "Sending verification SMS to " + phoneNumber);
+
+ var template = getTemplate(0, domain, language);
+ if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
+ parent.debug('email', "Error: Failed to get SMS template"); // No SMS template found
+ return;
+ }
+
+ // Setup the template
+ template.split("[[0]]").join(domain.title ? domain.title : 'MeshCentral');
+ template.split("[[1]]").join(verificationCode);
+
+ // Send the SMS
+ obj.sendSMS(phoneNumber, template, func);
+ };
+
+ return obj;
+};
+
+// +18632703894
+// SMS 5032700426 "This is a test"
diff --git a/meshuser.js b/meshuser.js
index 41b89a40..0507a711 100644
--- a/meshuser.js
+++ b/meshuser.js
@@ -755,6 +755,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
break;
}
+ case 'sms': {
+ if (parent.parent.smsserver == null) {
+ r = "No SMS gateway in use.";
+ } else {
+ if (cmdargs['_'].length != 2) {
+ r = "Usage: SMS \"PhoneNumber\" \"Message\".";
+ } else {
+ parent.parent.smsserver.sendSMS(cmdargs['_'][0], cmdargs['_'][1], function (status) {
+ try { ws.send(JSON.stringify({ action: 'serverconsole', value: status?'Success':'Failed', tag: command.tag })); } catch (ex) { }
+ });
+ }
+ }
+ break;
+ }
case 'le': {
if (parent.parent.letsencrypt == null) {
r = "Let's Encrypt not in use.";
@@ -3695,6 +3709,55 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
delete obj.hardwareKeyRegistrationRequest;
break;
}
+ case 'verifyPhone': {
+ if (parent.parent.smsserver == null) return;
+ if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length
+ if (command.phone.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) == false) break; // Check phone
+ var code = getRandomEightDigitInteger();
+ //console.log(code);
+
+ // TODO: We need to tie this cookie to this session and limit how many times we can guess the code
+ const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone });
+
+ ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: true })); // DEBUG
+ /*
+ parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) {
+ ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success }));
+ });
+ */
+ break;
+ }
+ case 'confirmPhone': {
+ if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'number')) break; // Input checks
+ var cookie = parent.parent.decodeCookie(command.cookie);
+ if (cookie == null) break; // Invalid cookie
+ if (cookie.c != command.code) { ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true })); break; } // Code does not match
+
+ // Set the user's phone
+ user.phone = cookie.p;
+ db.SetUser(user);
+
+ // Event the change
+ var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msg: 'Verified phone number 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);
+
+ break;
+ }
+ case 'removePhone': {
+ if (user.phone == null) break;
+
+ // Clear the user's phone
+ delete user.phone;
+ db.SetUser(user);
+
+ // Event the change
+ var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msg: 'Removed phone number 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);
+
+ break;
+ }
case 'getClip': {
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
diff --git a/package.json b/package.json
index 2779fc5d..a86397f3 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
],
"dependencies": {
"archiver": "^3.0.0",
+ "archiver-zip-encrypted": "^1.0.8",
"body-parser": "^1.19.0",
"cbor": "^4.1.5",
"compression": "^1.7.4",
diff --git a/public/images/phone80.png b/public/images/phone80.png
new file mode 100644
index 00000000..4aa55046
Binary files /dev/null and b/public/images/phone80.png differ
diff --git a/translate/translate.js b/translate/translate.js
index 3abc0811..2492fb01 100644
--- a/translate/translate.js
+++ b/translate/translate.js
@@ -41,7 +41,8 @@ var meshCentralSourceFiles = [
"../emails/account-invite.txt",
"../emails/account-login.txt",
"../emails/account-reset.txt",
- "../emails/mesh-invite.txt"
+ "../emails/mesh-invite.txt",
+ "../emails/sms-messages.txt"
];
var minifyMeshCentralSourceFiles = [
diff --git a/translate/translate.json b/translate/translate.json
index 42c5d182..cd076b7e 100644
--- a/translate/translate.json
+++ b/translate/translate.json
@@ -4846,7 +4846,6 @@
},
{
"en": "Change Email Address",
- "en": "E-mailadres wijzigen",
"xloc": [
"login-mobile.handlebars->container->page_content->column_l->1->1->0->1->checkemailpanel->1->checkCheckOperations->1->2->1->1",
"login.handlebars->container->column_l->centralTable->1->0->logincell->checkemailpanel->1->checkCheckOperations->1->2->1->1"
@@ -9004,6 +9003,9 @@
"default.handlebars->29->889"
]
},
+ {
+ "en": "E-mailadres wijzigen"
+ },
{
"cs": "CHYBA: ",
"de": "FEHLER:",
@@ -27935,6 +27937,18 @@
"default.handlebars->29->1039"
]
},
+ {
+ "en": "[[0]] access token is: [[1]]",
+ "xloc": [
+ "sms-messages.txt"
+ ]
+ },
+ {
+ "en": "[[0]] verification code is: [[1]]",
+ "xloc": [
+ "sms-messages.txt"
+ ]
+ },
{
"cs": "[[[SERVERNAME]]]",
"de": "[[[SERVERNAME]]]",
@@ -29682,4 +29696,4 @@
]
}
]
-}
+}
\ No newline at end of file
diff --git a/views/default.handlebars b/views/default.handlebars
index 494b65eb..bf3aecec 100644
--- a/views/default.handlebars
+++ b/views/default.handlebars
@@ -306,6 +306,7 @@
Account security
+
@@ -1608,6 +1609,7 @@
// Update account actions
QV('p2AccountSecurity', ((features & 4) == 0) && (serverinfo.domainauth == false) && ((features & 4096) != 0)); // Hide Account Security if in single user mode, domain authentication to 2 factor auth not supported.
+ QV('managePhoneNumber', features & 0x02000000);
QV('manageEmail2FA', features & 0x00800000);
QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == 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
@@ -1679,6 +1681,7 @@
QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
QV('manageOtp', (userinfo.otpsecret == 1) || (userinfo.otphkeys > 0));
+ QV('authPhoneNumberCheck', (userinfo.phone != null));
QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
QV('authAppSetupCheck', userinfo.otpsecret == 1);
QV('authKeySetupCheck', userinfo.otphkeys > 0);
@@ -2182,6 +2185,16 @@
}, 100);
break;
}
+ case 'verifyPhone': {
+ if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
+ var x = '
';
+ setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie);
+ Q('d2phoneCodeInput').focus();
+ account_managePhoneCodeValidate();
+ break;
+ }
case 'event': {
if (!message.event.nolog) {
if (currentNode && (message.event.nodeid == currentNode._id)) {
@@ -7871,6 +7884,33 @@
// MY ACCOUNT
//
+ function account_managePhone() {
+ if (xxdialogMode || ((features & 0x02000000) == 0)) return;
+ var x;
+ if (userinfo.phone != null) {
+ x = '
';
+ x += ' | ' + "Verified phone number" + ' ' + userinfo.phone + ' ';
+ x += '';
+ setDialogMode(2, "Phone Notifications", 3, account_managePhoneRemove, x);
+ account_managePhoneRemoveValidate();
+ } else {
+ x = '';
+ setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone');
+ Q('d2phoneinput').focus();
+ account_managePhoneValidate();
+ }
+ }
+
+ function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) }
+ function account_managePhoneValidate(x) { var ok = isPhoneNumber(Q('d2phoneinput').value); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
+ function account_managePhoneCodeValidate(x) { var ok = Q('d2phoneCodeInput').value.match(/[0-9]/); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
+ function account_managePhoneConfirm(b, tag) { meshserver.send({ action: 'confirmPhone', code: parseInt(Q('d2phoneCodeInput').value), cookie: tag }); }
+ function account_managePhoneAdd() { if (isPhoneNumber(Q('d2phoneinput').value) == false) return; QE('d2phoneinput', false); meshserver.send({ action: 'verifyPhone', phone: Q('d2phoneinput').value }); }
+ function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } }
+ function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); }
+
function account_manageAuthEmail() {
if (xxdialogMode || ((features & 0x00800000) == 0)) return;
var emailU2Fenabled = ((userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
@@ -10564,6 +10604,7 @@
} else {
x += addDeviceAttribute("Email", everify + email + ' ');
}
+ if (user.phone != null) { x += addDeviceAttribute("Phone Number", user.phone); }
x += addDeviceAttribute("Server Rights", premsg + '' + msg.join(', ') + '');
if (user.quota) x += addDeviceAttribute("Server Quota", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
x += addDeviceAttribute("Creation", printDateTime(new Date(user.creation * 1000)));
diff --git a/webserver.js b/webserver.js
index 079da913..409fb7f9 100644
--- a/webserver.js
+++ b/webserver.js
@@ -1838,6 +1838,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
+ if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
// Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey);
|