More work on SMS support.

Ylian Saint-Hilaire committed Apr 22, 2020 at 01:33 UTC 200acde9c875e945c501f6c0bab57a6326e84b85
24 files changed +287 -14
MeshCentralServer.njsproj
+1
@@ -108,6 +108,7 @@
108 <Compile Include="meshaccelerator.js" />
109 <Compile Include="meshctrl.js" />
110 <Compile Include="meshmail.js" />
111 + <Compile Include="meshsms.js" />
112 <Compile Include="meshscanner.js" />
113 <Compile Include="certoperations.js" />
114 <Compile Include="common.js" />
agents/meshcore.js
+2 -5
@@ -280,7 +280,7 @@ function createMeshCore(agent) {
280 */
281
282 // MeshAgent JavaScript Core Module. This code is sent to and running on the mesh agent.
283 - 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
283 + 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
284
285
286 // Get the operating system description string
@@ -1995,7 +1995,7 @@ function createMeshCore(agent) {
1995 var response = null;
1996 switch (cmd) {
1997 case 'help': { // Displays available commands
1998 - 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';
1998 + 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';
1999 if (process.platform == 'win32') { availcommands += ',safemode,wpfhwacceleration'; }
2000 if (require('MeshAgent').maxKvmTileSize != null) { availcommands += ',kvmmode'; }
2001
@@ -2059,9 +2059,6 @@ function createMeshCore(agent) {
2059 } else { response = "Agent Size: " + actualSize + " kb"; }
2060 } else { response = "Agent Size: " + actualSize + " kb"; }
2061 break;
2062 - case 'version':
2063 - response = "Mesh Agent Version: " + process.versions.meshAgent;
2064 - break;
2062 case 'versions':
2063 response = JSON.stringify(process.versions, null, ' ');
2064 break;
emails/sms-messages.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_cs.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_de.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_es.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_fr.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_hi.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_ja.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_ko.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_nl.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_pt.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_ru.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
emails/translations/sms-messages_zh-chs.txt new
+2
@@ -0,0 +1,2 @@
1 +[[0]] verification code is: [[1]]
2 +[[0]] access token is: [[1]]
meshcentral.js
+11 -1
@@ -30,6 +30,7 @@ function CreateMeshCentralServer(config, args) {
30 obj.mqttbroker = null;
31 obj.swarmserver = null;
32 obj.mailserver = null;
33 + obj.smsserver = null;
34 obj.amtEventHandler = null;
35 obj.pluginHandler = null;
36 obj.amtScanner = null;
@@ -1323,6 +1324,15 @@ function CreateMeshCentralServer(config, args) {
1324 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); }
1325 }
1326
1327 + // Setup SMS gateway
1328 + if (config.sms != null) {
1329 + obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
1330 + if (obj.smsserver != null) {
1331 + //obj.smsserver.verify();
1332 + if (obj.args.lanonly == true) { addServerWarning("SMS gateway has limited use in LAN mode."); }
1333 + }
1334 + }
1335 +
1336 // Start periodic maintenance
1337 obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
1338
@@ -2560,7 +2570,7 @@ function mainStart() {
2570 }
2571
2572 // SMS support
2563 - if ((config.sms != null) && (config.sms.provider == 'twilio') && (typeof config.sms.sid == 'string') && (typeof config.sms.auth == 'string')) { modules.push('twilio'); }
2573 + if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); }
2574
2575 // Syslog support
2576 if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog'); }
meshmail.js
+5 -5
@@ -160,7 +160,7 @@ module.exports.CreateMeshMail = function (parent) {
160
161 var template = getTemplate('account-login', domain, language);
162 if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
163 - parent.debug('email', "Error: Failed to get mail template."); // Not email template found
163 + parent.debug('email', "Error: Failed to get mail template."); // No email template found
164 return;
165 }
166
@@ -187,7 +187,7 @@ module.exports.CreateMeshMail = function (parent) {
187
188 var template = getTemplate('account-invite', domain, language);
189 if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
190 - parent.debug('email', "Error: Failed to get mail template."); // Not email template found
190 + parent.debug('email', "Error: Failed to get mail template."); // No email template found
191 return;
192 }
193
@@ -214,7 +214,7 @@ module.exports.CreateMeshMail = function (parent) {
214
215 var template = getTemplate('account-check', domain, language);
216 if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
217 - parent.debug('email', "Error: Failed to get mail template."); // Not email template found
217 + parent.debug('email', "Error: Failed to get mail template."); // No email template found
218 return;
219 }
220
@@ -242,7 +242,7 @@ module.exports.CreateMeshMail = function (parent) {
242
243 var template = getTemplate('account-reset', domain, language);
244 if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
245 - parent.debug('email', "Error: Failed to get mail template."); // Not email template found
245 + parent.debug('email', "Error: Failed to get mail template."); // No email template found
246 return;
247 }
248
@@ -270,7 +270,7 @@ module.exports.CreateMeshMail = function (parent) {
270
271 var template = getTemplate('mesh-invite', domain, language);
272 if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
273 - parent.debug('email', "Error: Failed to get mail template."); // Not email template found
273 + parent.debug('email', "Error: Failed to get mail template."); // No email template found
274 return;
275 }
276
meshsms.js new
+120
@@ -0,0 +1,120 @@
1 +/**
2 +* @description MeshCentral SMS gateway communication module
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2020
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +/*xjslint node: true */
10 +/*xjslint plusplus: true */
11 +/*xjslint maxlen: 256 */
12 +/*jshint node: true */
13 +/*jshint strict: false */
14 +/*jshint esversion: 6 */
15 +"use strict";
16 +
17 +// Construct a MeshAgent object, called upon connection
18 +module.exports.CreateMeshSMS = function (parent) {
19 + var obj = {};
20 + obj.parent = parent;
21 + obj.provider = null;
22 +
23 + // SMS gateway provider setup
24 + switch (parent.config.sms.provider) {
25 + case 'twilio': {
26 + // Validate Twilio configuration values
27 + if (typeof parent.config.sms.sid != 'string') { console.log('Invalid or missing SMS gateway provider sid.'); return null; }
28 + if (typeof parent.config.sms.auth != 'string') { console.log('Invalid or missing SMS gateway provider auth.'); return null; }
29 + if (typeof parent.config.sms.from != 'string') { console.log('Invalid or missing SMS gateway provider from.'); return null; }
30 +
31 + // Setup Twilio
32 + var Twilio = require('twilio');
33 + obj.provider = new Twilio(parent.config.sms.sid, parent.config.sms.auth);
34 + break;
35 + }
36 + default: {
37 + // Unknown SMS gateway provider
38 + console.log('Unknown SMS gateway provider: ' + parent.config.sms.provider);
39 + return null;
40 + }
41 + }
42 +
43 + // Send an SMS message
44 + obj.sendSMS = function (to, msg, func) {
45 + parent.debug('email', 'Sending SMS to: ' + to + ': ' + msg);
46 + console.log({ from: parent.config.sms.from, to: to, body: msg });
47 + if (parent.config.sms.provider == 'twilio') {
48 + obj.provider.messages.create({
49 + from: parent.config.sms.from,
50 + to: to,
51 + body: msg
52 + }, function (err, result) {
53 + if (err != null) { parent.debug('email', 'SMS error: ' + JSON.stringify(err)); } else { parent.debug('email', 'SMS result: ' + JSON.stringify(result)); }
54 + if (func != null) { func((err == null) && (result.status == 'queued'), err, result); }
55 + });
56 + }
57 + }
58 +
59 + // Get the correct SMS template
60 + function getTemplate(templateNumber, domain, lang) {
61 + parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang);
62 + if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given.
63 +
64 + var r = {}, emailsPath = null;
65 + if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; }
66 + else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; }
67 + else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; }
68 + if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null }
69 +
70 + // Get the non-english email if needed
71 + var txtfile = null;
72 + if ((lang != null) && (lang != 'en')) {
73 + var translationsPath = obj.parent.path.join(emailsPath, 'translations');
74 + var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', 'sms-messages_' + lang + '.txt');
75 + if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) {
76 + txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
77 + }
78 + }
79 +
80 + // Get the english email
81 + if ((htmlfile == null) || (txtfile == null)) {
82 + var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt');
83 + if (obj.parent.fs.existsSync(pathTxt)) {
84 + txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
85 + }
86 + }
87 +
88 + // No email templates
89 + if (txtfile == null) { return null; }
90 +
91 + // Decode the TXT file
92 + lines = txtfile.split('\r\n').join('\n').split('\n')
93 + if (lines.length >= templateNumber) return null;
94 +
95 + return lines[templateNumber];
96 + }
97 +
98 + // Send phone number verification SMS
99 + obj.sendPhoneCheck = function (domain, phoneNumber, verificationCode, language, func) {
100 + parent.debug('email', "Sending verification SMS to " + phoneNumber);
101 +
102 + var template = getTemplate(0, domain, language);
103 + if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
104 + parent.debug('email', "Error: Failed to get SMS template"); // No SMS template found
105 + return;
106 + }
107 +
108 + // Setup the template
109 + template.split("[[0]]").join(domain.title ? domain.title : 'MeshCentral');
110 + template.split("[[1]]").join(verificationCode);
111 +
112 + // Send the SMS
113 + obj.sendSMS(phoneNumber, template, func);
114 + };
115 +
116 + return obj;
117 +};
118 +
119 +// +18632703894
120 +// SMS 5032700426 "This is a test"
meshuser.js
+63
@@ -755,6 +755,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
755 }
756 break;
757 }
758 + case 'sms': {
759 + if (parent.parent.smsserver == null) {
760 + r = "No SMS gateway in use.";
761 + } else {
762 + if (cmdargs['_'].length != 2) {
763 + r = "Usage: SMS \"PhoneNumber\" \"Message\".";
764 + } else {
765 + parent.parent.smsserver.sendSMS(cmdargs['_'][0], cmdargs['_'][1], function (status) {
766 + try { ws.send(JSON.stringify({ action: 'serverconsole', value: status?'Success':'Failed', tag: command.tag })); } catch (ex) { }
767 + });
768 + }
769 + }
770 + break;
771 + }
772 case 'le': {
773 if (parent.parent.letsencrypt == null) {
774 r = "Let's Encrypt not in use.";
@@ -3695,6 +3709,55 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3709 delete obj.hardwareKeyRegistrationRequest;
3710 break;
3711 }
3712 + case 'verifyPhone': {
3713 + if (parent.parent.smsserver == null) return;
3714 + if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length
3715 + if (command.phone.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) == false) break; // Check phone
3716 + var code = getRandomEightDigitInteger();
3717 + //console.log(code);
3718 +
3719 + // TODO: We need to tie this cookie to this session and limit how many times we can guess the code
3720 + const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone });
3721 +
3722 + ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: true })); // DEBUG
3723 + /*
3724 + parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) {
3725 + ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success }));
3726 + });
3727 + */
3728 + break;
3729 + }
3730 + case 'confirmPhone': {
3731 + if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'number')) break; // Input checks
3732 + var cookie = parent.parent.decodeCookie(command.cookie);
3733 + if (cookie == null) break; // Invalid cookie
3734 + if (cookie.c != command.code) { ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true })); break; } // Code does not match
3735 +
3736 + // Set the user's phone
3737 + user.phone = cookie.p;
3738 + db.SetUser(user);
3739 +
3740 + // Event the change
3741 + 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 };
3742 + 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.
3743 + parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
3744 +
3745 + break;
3746 + }
3747 + case 'removePhone': {
3748 + if (user.phone == null) break;
3749 +
3750 + // Clear the user's phone
3751 + delete user.phone;
3752 + db.SetUser(user);
3753 +
3754 + // Event the change
3755 + 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 };
3756 + 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.
3757 + parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
3758 +
3759 + break;
3760 + }
3761 case 'getClip': {
3762 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
3763
package.json
+1
@@ -30,6 +30,7 @@
30 ],
31 "dependencies": {
32 "archiver": "^3.0.0",
33 + "archiver-zip-encrypted": "^1.0.8",
34 "body-parser": "^1.19.0",
35 "cbor": "^4.1.5",
36 "compression": "^1.7.4",
public/images/phone80.png
Binary files /dev/null and b/public/images/phone80.png differ
translate/translate.js
+2 -1
@@ -41,7 +41,8 @@ var meshCentralSourceFiles = [
41 "../emails/account-invite.txt",
42 "../emails/account-login.txt",
43 "../emails/account-reset.txt",
44 - "../emails/mesh-invite.txt"
44 + "../emails/mesh-invite.txt",
45 + "../emails/sms-messages.txt"
46 ];
47
48 var minifyMeshCentralSourceFiles = [
translate/translate.json
+16 -2
@@ -4846,7 +4846,6 @@
4846 },
4847 {
4848 "en": "Change Email Address",
4849 - "en": "E-mailadres wijzigen",
4849 "xloc": [
4850 "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->checkemailpanel->1->checkCheckOperations->1->2->1->1",
4851 "login.handlebars->container->column_l->centralTable->1->0->logincell->checkemailpanel->1->checkCheckOperations->1->2->1->1"
@@ -9004,6 +9003,9 @@
9003 "default.handlebars->29->889"
9004 ]
9005 },
9006 + {
9007 + "en": "E-mailadres wijzigen"
9008 + },
9009 {
9010 "cs": "CHYBA: ",
9011 "de": "FEHLER:",
@@ -27935,6 +27937,18 @@
27937 "default.handlebars->29->1039"
27938 ]
27939 },
27940 + {
27941 + "en": "[[0]] access token is: [[1]]",
27942 + "xloc": [
27943 + "sms-messages.txt"
27944 + ]
27945 + },
27946 + {
27947 + "en": "[[0]] verification code is: [[1]]",
27948 + "xloc": [
27949 + "sms-messages.txt"
27950 + ]
27951 + },
27952 {
27953 "cs": "[[[SERVERNAME]]]",
27954 "de": "[[[SERVERNAME]]]",
@@ -29682,4 +29696,4 @@
29696 ]
29697 }
29698 ]
29685 -}
29699 +}
\ No newline at end of file
views/default.handlebars
+41
@@ -306,6 +306,7 @@
306 <div id="p2AccountSecurity" style="display:none">
307 <p><strong>Account security</strong></p>
308 <div style="margin-left:25px">
309 + <div id="managePhoneNumber"><div class="p2AccountActions"><span id="authPhoneNumberCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_managePhone()">Manage phone number</a><br /></span></div>
310 <div id="manageEmail2FA"><div class="p2AccountActions"><span id="authEmailSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthEmail()">Manage email authentication</a><br /></span></div>
311 <div id="manageAuthApp"><div class="p2AccountActions"><span id="authAppSetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageAuthApp()">Manage authenticator app</a><br /></span></div>
312 <div id="manageHardwareOtp"><div class="p2AccountActions"><span id="authKeySetupCheck"><strong>&#x2713;</strong></span></div><span><a href=# onclick="return account_manageHardwareOtp(0)">Manage security keys</a><br /></span></div>
@@ -1608,6 +1609,7 @@
1609
1610 // Update account actions
1611 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.
1612 + QV('managePhoneNumber', features & 0x02000000);
1613 QV('manageEmail2FA', features & 0x00800000);
1614 QV('p2AccountPassActions', ((features & 4) == 0) && (serverinfo.domainauth == false)); // Hide Account Actions if in single user mode or domain authentication
1615 //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 @@
1681 QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1682 QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1683 QV('manageOtp', (userinfo.otpsecret == 1) || (userinfo.otphkeys > 0));
1684 + QV('authPhoneNumberCheck', (userinfo.phone != null));
1685 QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
1686 QV('authAppSetupCheck', userinfo.otpsecret == 1);
1687 QV('authKeySetupCheck', userinfo.otphkeys > 0);
@@ -2182,6 +2185,16 @@
2185 }, 100);
2186 break;
2187 }
2188 + case 'verifyPhone': {
2189 + if (xxdialogMode && (xxdialogTag != 'verifyPhone')) return;
2190 + var x = '<table><tr><td><img src="images/phone80.png" style=padding:8px>';
2191 + x += '<td>Check your phone and enter the verification code.';
2192 + x += '<br /><br /><div style=width:100%;text-align:center>' + "Verification code:" + ' <input type=tel pattern="[0-9]" inputmode="number" maxlength=8 id=d2phoneCodeInput onKeyUp=account_managePhoneCodeValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneCodeValidate(1)"></div></table>';
2193 + setDialogMode(2, "Phone Notifications", 3, account_managePhoneConfirm, x, message.cookie);
2194 + Q('d2phoneCodeInput').focus();
2195 + account_managePhoneCodeValidate();
2196 + break;
2197 + }
2198 case 'event': {
2199 if (!message.event.nolog) {
2200 if (currentNode && (message.event.nodeid == currentNode._id)) {
@@ -7871,6 +7884,33 @@
7884 // MY ACCOUNT
7885 //
7886
7887 + function account_managePhone() {
7888 + if (xxdialogMode || ((features & 0x02000000) == 0)) return;
7889 + var x;
7890 + if (userinfo.phone != null) {
7891 + x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
7892 + x += '<td style=text-align:center><div style=padding:6px>' + "Verified phone number" + '</div><div style=font-size:20px>' + userinfo.phone + '</div>';
7893 + x += '<div style=margin:10px><label><input id=d2delPhone type=checkbox onclick=account_managePhoneRemoveValidate() />' + "Remove phone number" + '</label></div>';
7894 + setDialogMode(2, "Phone Notifications", 3, account_managePhoneRemove, x);
7895 + account_managePhoneRemoveValidate();
7896 + } else {
7897 + x = '<table style=width:100%><tr><td style=width:56px><img src="images/phone80.png" style=padding:8px>';
7898 + x += '<td>Enter your SMS capable phone number. Once verified, the number may be used for login verification and other notifications.';
7899 + x += '<br /><br /><div style=width:100%;text-align:center>' + "Phone number:" + ' <input type=tel pattern="[0-9]{9}" autocomplete="tel" inputmode="tel" maxlength=18 id=d2phoneinput onKeyUp=account_managePhoneValidate() onkeypress="if (event.key==\'Enter\') account_managePhoneValidate(1)"></div></table>';
7900 + setDialogMode(2, "Phone Notifications", 3, account_managePhoneAdd, x, 'verifyPhone');
7901 + Q('d2phoneinput').focus();
7902 + account_managePhoneValidate();
7903 + }
7904 + }
7905 +
7906 + function isPhoneNumber(x) { return x.match(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/) }
7907 + function account_managePhoneValidate(x) { var ok = isPhoneNumber(Q('d2phoneinput').value); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
7908 + function account_managePhoneCodeValidate(x) { var ok = Q('d2phoneCodeInput').value.match(/[0-9]/); QE('idx_dlgOkButton', ok); if ((x == 1) && ok) { dialogclose(1); } }
7909 + function account_managePhoneConfirm(b, tag) { meshserver.send({ action: 'confirmPhone', code: parseInt(Q('d2phoneCodeInput').value), cookie: tag }); }
7910 + function account_managePhoneAdd() { if (isPhoneNumber(Q('d2phoneinput').value) == false) return; QE('d2phoneinput', false); meshserver.send({ action: 'verifyPhone', phone: Q('d2phoneinput').value }); }
7911 + function account_managePhoneRemove() { if (Q('d2delPhone').checked) { meshserver.send({ action: 'removePhone' }); } }
7912 + function account_managePhoneRemoveValidate() { QE('idx_dlgOkButton', Q('d2delPhone').checked); }
7913 +
7914 function account_manageAuthEmail() {
7915 if (xxdialogMode || ((features & 0x00800000) == 0)) return;
7916 var emailU2Fenabled = ((userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
@@ -10564,6 +10604,7 @@
10604 } else {
10605 x += addDeviceAttribute("Email", everify + email + ' <a href=# style=cursor:pointer onclick=\'return doemail(event,\"' + user.email + '\")\'><img class=hoverButton src="images/link1.png" /></a>');
10606 }
10607 + if (user.phone != null) { x += addDeviceAttribute("Phone Number", user.phone); }
10608 x += addDeviceAttribute("Server Rights", premsg + '<a href=# style=cursor:pointer onclick=\'return showUserAdminDialog(event,\"' + userid + '\")\'>' + msg.join(', ') + '</a>');
10609 if (user.quota) x += addDeviceAttribute("Server Quota", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
10610 x += addDeviceAttribute("Creation", printDateTime(new Date(user.creation * 1000)));
webserver.js
+1
@@ -1838,6 +1838,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1838 if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
1839 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
1840 if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
1841 + if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
1842
1843 // Create a authentication cookie
1844 const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey);